diff --git a/RELEASE.md b/RELEASE.md
index e7b9a3985e..157b1b55c9 100644
--- a/RELEASE.md
+++ b/RELEASE.md
@@ -1,13 +1,17 @@
-# Product Taxonomy Model - Adding "CFTC" value in TaxonomySourceEnum
+# *Technical Change - Update Python generation to utilize the new process*
-_Background_
+## Background
-A gap has been identified in the model when capturing taxonomy values for commodity underlyer assets as defined by CFTC regulation. Introducing `CFTC` as a taxonomy source is necessary to properly map these values within the model and to support population of the **"Commodity Underlyer ID"** fields in DRR.
+The upcoming release of CDM 7 will standardize the serialization format (Issue #3236) and include a new Python library generated by updated tooling.
-_What is being released?_
+## What is being released?
-The contribution is the addition of a new `CFTC` value to the `TaxonomySourceEnum` in order to represent the Commodity Futures Trading Commission as a taxonomy source, enabling support for **Commodity Underlyer ID** rules under CFTC jurisdiction in DRR.
+The Python library will use updated tooling that supports metadata. Python support for functions will arrive in a subsequent update.
-_Review Directions_
+Note that the new Python library for CDM 6.x will use the new serialization format that is not backward compatible with data produced by the old Python libraries. Before standardization, cross-language data exchange (e.g., between Java and Python) was not guaranteed. The new Python tooling facilitates such compatibility by implementing the new format.
-Changes can be reviewed in PR: https://github.com/finos/common-domain-model/pull/4111
\ No newline at end of file
+Detail on the changes can be found at [Issue #3878](https://github.com/finos/common-domain-model/issues/3878)
+
+## Review Directions
+
+Changes can be reviewed in PR: [#4125](https://github.com/finos/common-domain-model/pull/4125)
diff --git a/cdm-python/test/__init__.py b/cdm-python/test/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/cdm-python/test/semantics/test_cardinality.py b/cdm-python/test/semantics/test_cardinality.py
deleted file mode 100644
index 3a9c75e9c1..0000000000
--- a/cdm-python/test/semantics/test_cardinality.py
+++ /dev/null
@@ -1,37 +0,0 @@
-'''testing cardinality enforcement'''
-import datetime
-import pytest
-from cdm.base.datetime.DateList import DateList
-from rosetta.runtime.utils import ConditionViolationError
-
-
-def test_1_many_fail():
- '''DateList cannot be empty'''
- dl = DateList(date=[])
- with pytest.raises(ConditionViolationError):
- dl.validate_conditions()
-
-
-def test_1_many_fail_empty_constructor():
- '''DateList cannot be empty'''
- dl = DateList()
- with pytest.raises(ConditionViolationError):
- dl.validate_conditions()
-
-
-def test_1_many_pass():
- '''Valid DateList'''
- dl = DateList(date=[datetime.date(2020, 1, 1)])
- dl.validate_conditions()
-
-
-if __name__ == "__main__":
- print("test_1_many_pass")
- test_1_many_pass()
- print("test_1_many_fail")
- test_1_many_fail()
- print("test_1_many_fail_empty_constructor")
- test_1_many_fail_empty_constructor()
-
-
-# EOF
diff --git a/cdm-python/test/semantics/test_conditions.py b/cdm-python/test/semantics/test_conditions.py
deleted file mode 100644
index d4fa93cb58..0000000000
--- a/cdm-python/test/semantics/test_conditions.py
+++ /dev/null
@@ -1,50 +0,0 @@
-'''Full attribute validation - pydantic and constraints'''
-import pytest
-from pydantic import ValidationError
-from rosetta.runtime.utils import ConditionViolationError
-from cdm.base.math.NonNegativeQuantity import NonNegativeQuantity
-from cdm.base.math.UnitType import UnitType
-from cdm.base.datetime.Frequency import Frequency
-from cdm.base.datetime.PeriodExtendedEnum import PeriodExtendedEnum
-
-
-def test_recursive_conditions_base_fail():
- '''condition_0_AmountOnlyExists violation'''
- unit = UnitType(currency='EUR')
- mq = NonNegativeQuantity(unit=unit)
- with pytest.raises(ConditionViolationError):
- mq.validate_model()
-
-
-def test_recursive_conditions_direct_fail():
- '''Negative quantity condition violation'''
- unit = UnitType(currency='EUR')
- mq = NonNegativeQuantity(value=-10, unit=unit)
- with pytest.raises(ConditionViolationError):
- mq.validate_model()
-
-
-def test_bad_attrib_validation():
- '''Invalid attribute assigned'''
- unit = UnitType(currency='EUR')
- mq = NonNegativeQuantity(value=10, unit=unit)
- mq.frequency = 'Blah'
- with pytest.raises(ValidationError):
- mq.validate_model()
-
-
-def test_correct_attrib_validation():
- '''Valid attribute assigned'''
- unit = UnitType(currency='EUR')
- mq = NonNegativeQuantity(value=10, unit=unit)
- mq.frequency = Frequency(periodMultiplier=1, period=PeriodExtendedEnum.M)
- mq.validate_model()
-
-
-if __name__ == "__main__":
- test_recursive_conditions_base_fail()
- test_recursive_conditions_direct_fail()
- test_bad_attrib_validation()
- test_correct_attrib_validation()
-
-# EOF
diff --git a/cdm-python/test/semantics/test_if_cond.py b/cdm-python/test/semantics/test_if_cond.py
deleted file mode 100644
index dc1409db4b..0000000000
--- a/cdm-python/test/semantics/test_if_cond.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import pytest
-from rosetta.runtime.utils import ConditionViolationError
-from rosetta.runtime.utils import if_cond
-from cdm.base.math.QuantitySchedule import QuantitySchedule
-from cdm.base.math.UnitType import UnitType
-
-
-def test_if_cond_literals():
- class T:
- def __init__(self):
- self.cleared = 'Y'
- self.counterparty1FinancialEntityIndicator = None
- self.counterparty1FinancialEntityIndicator = None
- self.actionType = "NEWT"
- self.eventType = "CLRG"
- self.originalSwapUTI = 1
- self.originalSwapUSI = 'OKI'
- self = T()
-
- res = if_cond(
- ((self.cleared == "N") or (self.cleared == "I")),
- '((self.counterparty1FinancialEntityIndicator) is not None)',
- 'if_cond((self.cleared == "Y"), \'((self.counterparty1FinancialEntityIndicator) == "NO")\', \'True\', self)',
- self)
- assert not res
-
- res = if_cond((((self.cleared == "Y") and (self.actionType == "NEWT")) and (self.eventType == "CLRG")),
- 'if_cond(((self.originalSwapUTI) is None), \'((self.originalSwapUSI) is not None)\', \'if_cond(((self.originalSwapUTI) is not None), \\\'((self.originalSwapUSI) == "OKI")\\\', \\\'True\\\', self)\', self)',
- '((self.originalSwapUTI) is None)',
- self)
- assert res
-
-def test_if_direct():
- class T:
- def __init__(self):
- self.x, self.y = 1, 2
-
- assert not if_cond(True,
- 'if_cond(True, \'self.x > self.y\', \'True\', self)', 'True', T())
- assert not if_cond(True,
- 'if_cond(True, \'if_cond(True, \\\'self.x > self.y\\\', \\\'True\\\', self)\', \'True\', self)',
- 'True', T())
- assert not if_cond(True,
- 'if_cond(True, \'if_cond(True, \\\'if_cond(True, \\\\\\\'self.x > self.y\\\\\\\', \\\\\\\'True\\\\\\\', self)\\\', \\\'True\\\', self)\', \'True\', self)',
- 'True', T())
-
-if __name__ == "__main__":
- print('test_if_cond_literals',end='')
- test_if_cond_literals()
- print('...passed\ntest_if_direct', end='')
- test_if_direct()
- print('...passed')
-
-# EOF
diff --git a/cdm-python/test/semantics/test_pydantic_simple.py b/cdm-python/test/semantics/test_pydantic_simple.py
deleted file mode 100644
index 8aeedcb430..0000000000
--- a/cdm-python/test/semantics/test_pydantic_simple.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# pylint: disable=unused-import,missing-function-docstring,invalid-name
-from datetime import date
-from cdm.event.common.Trade import Trade
-from cdm.event.common.TradeIdentifier import TradeIdentifier
-from cdm.product.template.TradableProduct import TradableProduct
-from cdm.product.template.Product import Product
-
-
-def test_trade():
- product = Product()
- tradableProduct = TradableProduct(product=product)
- tradeIdentifier=[TradeIdentifier(issuer='Acme Corp')]
-
- t = Trade(
- tradeDate=date(2023, 1, 1),
- tradableProduct=tradableProduct,
- tradeIdentifier=tradeIdentifier
- )
- print(t.model_dump())
- print('Done!')
-
-
-if __name__ == '__main__':
- test_trade()
-
-# EOF
diff --git a/cdm-python/test/semantics/test_validation.py b/cdm-python/test/semantics/test_validation.py
deleted file mode 100644
index 287ba3f73c..0000000000
--- a/cdm-python/test/semantics/test_validation.py
+++ /dev/null
@@ -1,83 +0,0 @@
-''' Tests related to validating models created on the fly or ingested from a
- json file.
-'''
-import os
-import sys
-from datetime import date
-import logging
-from pathlib import Path
-from cdm.event.common.Trade import Trade
-from cdm.event.common.TradeIdentifier import TradeIdentifier
-from cdm.product.template.TradableProduct import TradableProduct
-from cdm.product.template.Product import Product
-from cdm.product.template.TradeLot import TradeLot
-from cdm.observable.asset.PriceQuantity import PriceQuantity
-from cdm.base.staticdata.party.Party import Party
-from cdm.base.staticdata.party.PartyIdentifier import PartyIdentifier
-from cdm.base.staticdata.party.Counterparty import Counterparty
-from cdm.base.staticdata.party.CounterpartyRoleEnum import CounterpartyRoleEnum
-from cdm.base.staticdata.asset.common.Index import Index
-from cdm.base.staticdata.identifier.AssignedIdentifier import AssignedIdentifier
-from cdm.event.common.TradeState import TradeState
-
-sys.path.append(
- os.path.abspath(
- os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)))
-# pylint:disable=wrong-import-position,import-error
-from test_helpers.config import CDM_JSON_SAMPLE_SOURCE
-
-
-def test_trade():
- '''Constructs a simple Trade in memory and validates the model.'''
- price_quantity = PriceQuantity()
- trade_lot = TradeLot(priceQuantity=[price_quantity])
- product = Product(index=Index())
- counterparty = [
- Counterparty(role=CounterpartyRoleEnum.PARTY_1,
- partyReference=Party(
- partyId=[PartyIdentifier(identifier='Acme Corp')])),
- Counterparty(
- role=CounterpartyRoleEnum.PARTY_2,
- partyReference=Party(
- partyId=[PartyIdentifier(identifier='Wile E. Coyote')]))
- ]
- tradable_product = TradableProduct(product=product,
- tradeLot=[trade_lot],
- counterparty=counterparty)
- assigned_identifier = AssignedIdentifier(identifier='BIG DEAL!')
- trade_identifier = [
- TradeIdentifier(issuer='Acme Corp',
- assignedIdentifier=[assigned_identifier])
- ]
-
- t = Trade(tradeDate=date(2023, 1, 1),
- tradableProduct=tradable_product,
- tradeIdentifier=trade_identifier)
- exceptions = t.validate_model(raise_exc=False)
- assert not exceptions
-
-
-def test_rates():
- ''' The below sample json is conform to CDM 5.8.0, the python library
- generated for earlier or newer versions of CDM might fail to parse
- it correctly.
- '''
- path = os.path.join(os.path.dirname(__file__),
- CDM_JSON_SAMPLE_SOURCE,
- 'rates',
- 'bond-option-uti.json')
- json_str = Path(path).read_text(encoding='utf8')
- ts = TradeState.model_validate_json(json_str)
- print(repr(ts))
-
- exceptions = ts.validate_model(raise_exc=False)
- assert not exceptions
-
-
-if __name__ == '__main__':
- logging.getLogger().setLevel(logging.DEBUG)
- test_trade()
- test_rates()
- print('Done!')
-
-# EOF
diff --git a/cdm-python/test/serialization/__init__.py b/cdm-python/test/serialization/__init__.py
deleted file mode 100644
index f76aff80ce..0000000000
--- a/cdm-python/test/serialization/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__all__ = ["cdm_comparison_test", "dict_comp"]
\ No newline at end of file
diff --git a/cdm-python/test/serialization/cdm_comparison_test.py b/cdm-python/test/serialization/cdm_comparison_test.py
deleted file mode 100644
index de8189d1ba..0000000000
--- a/cdm-python/test/serialization/cdm_comparison_test.py
+++ /dev/null
@@ -1,37 +0,0 @@
-''' Compare CDM / JSON deserialization and serialization'''
-import json
-from pathlib import Path
-import sys
-import os
-from pydantic import ValidationError
-from cdm.version import __build_time__
-from cdm.event.common.TradeState import TradeState
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)))
-from serialization.dict_comp import dict_comp
-from test_helpers.config import CDM_JSON_SAMPLE_SOURCE
-
-def cdm_comparison_test_from_file(path, class_name):
- '''loads the json from a file and runs the comparison'''
- print('testing: ',
- path,
- ' with className: ',
- class_name.__name__,
- ' using CDM built at: ',
- __build_time__)
- json_str = Path(path).read_text()
- json_dict = json.loads (json_str)
- print('json_dict["trade"]["tradeDate"]: ' + json.dumps(json_dict["trade"]["tradeDate"]))
- try:
- print('raw parse from json_str')
- cdm_object = class_name.model_validate_json(json_str)
- trade = cdm_object.trade
- print('trade.tradeDate:', str(trade.tradeDate))
- json_data_out = cdm_object.model_dump_json(indent=4, exclude_defaults=True)
- generated_dict = json.loads(json_data_out)
- assert dict_comp(json_dict, generated_dict), "Failed corrected dict comparison"
- print('passed: dicts matched')
- except ValidationError as e:
- print('failed to parse')
- print(e)
-
-# EOF
diff --git a/cdm-python/test/serialization/dict_comp.py b/cdm-python/test/serialization/dict_comp.py
deleted file mode 100644
index e20d199443..0000000000
--- a/cdm-python/test/serialization/dict_comp.py
+++ /dev/null
@@ -1,32 +0,0 @@
-'''dict/list recursive comparison'''
-def dict_comp(d1, d2, prefix=''):
- '''compare recursively a dictionary/list'''
- if d1 == d2:
- return True
- if type(d1) != type(d2): # pylint: disable=unidiomatic-typecheck
- if (isinstance(d1, (float, int)) and isinstance(d2, str)
- or isinstance(d1, str)
- and isinstance(d2, (float, int))) and float(d1) == float(d2):
- return True
- print(f'Types differ for path {prefix} - d1: {type(d1)}, d2: {type(d2)}')
- return False
- if isinstance(d1, dict) and isinstance(d2, dict):
- if d1.keys() != d2.keys():
- print(f'Keys for path {prefix} differ: d1: {d1.keys()}, d2: {d2.keys()}')
- return False
- for k, v in d1.items():
- if not dict_comp(v, d2[k], f'{prefix}.{k}'):
- return False
- return True
- if isinstance(d1, list) and isinstance(d2, list):
- if len(d1) != len(d2):
- print(f'Lists at {prefix} are of different length d1: {len(d1)}, d2: {len(d2)}')
- return False
- for i, (e1, e2) in enumerate(zip(d1, d2)):
- if not dict_comp(e1, e2, f'{prefix}[{i}]'):
- return False
- return True
- print(f'Elements for path {prefix} differ: d1: {d1}, d2 {d2}')
- return False
-
-# EOF
diff --git a/cdm-python/test/serialization/test_trade_state_product.py b/cdm-python/test/serialization/test_trade_state_product.py
deleted file mode 100644
index deecbdb765..0000000000
--- a/cdm-python/test/serialization/test_trade_state_product.py
+++ /dev/null
@@ -1,21 +0,0 @@
-'''read and validate trade state specified by cdm_sample'''
-import sys
-import os
-import inspect
-from cdm.event.common.TradeState import TradeState
-sys.path.append(os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), os.pardir)))
-from test_helpers.config import CDM_JSON_SAMPLE_SOURCE
-from serialization.cdm_comparison_test import cdm_comparison_test_from_file
-
-
-def test_trade_state (cdm_sample_in=None):
- '''test trade state'''
- dir_path = os.path.dirname(__file__)
- if cdm_sample_in is None:
- sys.path.append(os.path.join(dir_path))
- cdm_sample_in = os.path.join(dir_path, CDM_JSON_SAMPLE_SOURCE, 'rates', 'EUR-Vanilla-account.json')
- cdm_comparison_test_from_file (cdm_sample_in, TradeState)
-
-if __name__ == "__main__":
- cdm_sample = sys.argv[1] if len(sys.argv) > 1 else None
- test_trade_state(cdm_sample)
\ No newline at end of file
diff --git a/cdm-python/test/test_helpers/__init__.py b/cdm-python/test/test_helpers/__init__.py
deleted file mode 100644
index dd1e446f1c..0000000000
--- a/cdm-python/test/test_helpers/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-'''init for config'''
-__all__ = ["config"]
diff --git a/cdm-python/test/test_helpers/config.py b/cdm-python/test/test_helpers/config.py
deleted file mode 100644
index 482ca43e86..0000000000
--- a/cdm-python/test/test_helpers/config.py
+++ /dev/null
@@ -1,12 +0,0 @@
-'''used to establish configuration shared across the tests'''
-import pathlib
-
-CDM_JSON_SAMPLE_SOURCE = str(pathlib.Path(pathlib.Path().parent.absolute(),
- 'rosetta-source',
- 'src',
- 'main',
- 'resources',
- 'result-json-files',
- 'fpml-5-10',
- 'products'))
-# EOF
diff --git a/cdm-python/test/test_import_trade_state.py b/cdm-python/test/test_import_trade_state.py
new file mode 100644
index 0000000000..1b2b9f45a5
--- /dev/null
+++ b/cdm-python/test/test_import_trade_state.py
@@ -0,0 +1,10 @@
+'''test the generated Python'''
+# pylint: disable=import-outside-toplevel at top, unused-import
+import pytest
+
+def test_import_tradestate():
+ '''confirm that tradestate can be imported'''
+ try:
+ from cdm.event.common.TradeState import TradeState
+ except ImportError:
+ pytest.fail("Importing cdm.event.common.TradeState failed")
\ No newline at end of file
diff --git a/codefresh.yml b/codefresh.yml
index e6b75fa588..13383b248b 100644
--- a/codefresh.yml
+++ b/codefresh.yml
@@ -130,19 +130,12 @@ steps:
stage: 'build'
title: Python build
fail_fast: false
- image: python:3.10-alpine
+ image: maven:3.9.9-eclipse-temurin-21-alpine
working_directory: ./
- shell: sh
+ shell: bash
commands:
- - export PYTHONDONTWRITEBYTECODE=1
- - python3 -m pip install pydantic
- - python3 -m pip install jsonpickle
- - python3 -m pip install ./rosetta-source/target/classes/cdm/python/runtime/rosetta_runtime-2.1.0-py3-none-any.whl
- - |-
- python3 -m pip wheel --no-deps --only-binary :all: --wheel-dir ./rosetta-source/target/classes/cdm/python ./rosetta-source/target/classes/cdm/python
- - python3 -m pip install ./rosetta-source/target/classes/cdm/python/python_cdm-*-py3-none-any.whl
- - python3 -m pip install pytest
-# - pytest -p no:cacheprovider ./cdm-python/test/
+ - apk add --no-cache python3 py3-pip wget curl bash
+ - rosetta-source/src/main/resources/build-resources/python/build-cdm-python.sh
DeployDaml:
stage: 'build'
@@ -253,8 +246,8 @@ steps:
commands:
- apk add --no-cache python3
- bash -c "${{GPG_IMPORT_COMMAND}}"
- - cd target/classes/cdm/python
- - tar -cvzf cdm-python-${{RELEASE_NAME}}.tar.gz python_cdm-*-py3-none-any.whl runtime/rosetta_runtime-*-py3-none-any.whl
+ - cd src/generated/python
+ - tar -cvzf cdm-python-${{RELEASE_NAME}}.tar.gz python_cdm-*-py3-none-any.whl rune_runtime-*-py3-none-any.whl
- python3 ${{GEN_DEPLOY_POM_PY}} cdm-python ${{RELEASE_NAME}} tar.gz ${{CF_VOLUME_PATH}}/${{CF_REPO_NAME}}/rosetta-source/src/main/resources/build-resources/python/python-developer.txt
- ${{MVN_DEPLOY_FILE_COMMAND}}
diff --git a/rosetta-source/src/main/resources/build-resources/python/Dockerfile b/rosetta-source/src/main/resources/build-resources/python/Dockerfile
new file mode 100644
index 0000000000..1b596ac095
--- /dev/null
+++ b/rosetta-source/src/main/resources/build-resources/python/Dockerfile
@@ -0,0 +1,24 @@
+# docker build -t rosetta-python-gen -f rosetta-source/src/main/resources/build-resources/python/Dockerfile .
+# docker run --rm rosetta-python-gen
+
+# to run with results saved
+# docker run --rm -v "$(pwd)":/mnt/common-domain-model -w /mnt/common-domain-model rosetta-python-gen
+
+FROM maven:3.9.9-eclipse-temurin-21-alpine
+
+# Set working directory to /mnt (where the repo will be copied)
+WORKDIR /mnt
+
+# Install required tools
+RUN apk add --no-cache python3 py3-pip wget curl bash
+
+# Copy the entire repo into /mnt
+COPY . /mnt/common-domain-model/
+
+WORKDIR /mnt/common-domain-model/
+
+# Make the entrypoint script executable
+RUN chmod +x rosetta-source/src/main/resources/build-resources/python/build-cdm-python.sh
+
+# Set the entrypoint
+ENTRYPOINT ["rosetta-source/src/main/resources/build-resources/python/build-cdm-python.sh"]
\ No newline at end of file
diff --git a/rosetta-source/src/main/resources/build-resources/python/build-cdm-python.sh b/rosetta-source/src/main/resources/build-resources/python/build-cdm-python.sh
new file mode 100755
index 0000000000..43c16162e7
--- /dev/null
+++ b/rosetta-source/src/main/resources/build-resources/python/build-cdm-python.sh
@@ -0,0 +1,79 @@
+#!/bin/bash
+set -euo pipefail
+set -x
+IFS=$'\n\t'
+
+export PROJECT_ROOT=$(pwd)
+export CDM_ROSETTA="rosetta-source/src/main/rosetta"
+export PYTHON_TARGET="rosetta-source/src/generated/python"
+
+# Extract and set ROSETTA_CODE_GEN_VERSION to the rosetta.dsl.version in the parent POM
+export ROSETTA_CODE_GEN_VERSION=$(mvn help:evaluate -Dexpression=rosetta.dsl.version -q -DforceStdout)
+echo "rosetta.code-gen.version: ${ROSETTA_CODE_GEN_VERSION}"
+
+# Find the latest release tag that matches the DSL version (x.y.z.n)
+DSL_VERSION="${ROSETTA_CODE_GEN_VERSION}"
+GENERATOR_REPO="finos/rune-python-generator"
+echo "Looking for latest generator release matching DSL version: ${DSL_VERSION} in ${GENERATOR_REPO}"
+echo "Fetching tags from GitHub API..."
+RAW_TAGS=$(curl -s "https://api.github.com/repos/${GENERATOR_REPO}/tags?per_page=100")
+if [[ -z "${RAW_TAGS}" ]]; then
+ echo "ERROR: No response from GitHub API"
+ exit 1
+fi
+
+LATEST_TAG=$(echo "${RAW_TAGS}" \
+ | grep '"name":' \
+ | cut -d '"' -f 4 \
+ | grep -E "^${DSL_VERSION}\.[0-9]+$" \
+ | sort -t. -k4 -n \
+ | tail -n 1) || true
+
+echo "Latest matching generator tag: ${LATEST_TAG}"
+
+if [[ -z "${LATEST_TAG}" ]]; then
+ echo "ERROR: No generator release found for DSL version ${DSL_VERSION}"
+ exit 1
+fi
+
+GENERATOR_JAR="python-${LATEST_TAG}.jar"
+GENERATOR_URL="https://github.com/${GENERATOR_REPO}/releases/download/${LATEST_TAG}/${GENERATOR_JAR}"
+echo "Attempting to download ${GENERATOR_URL}"
+
+if ! wget -q --spider "${GENERATOR_URL}"; then
+ echo "ERROR: Generator jar ${GENERATOR_JAR} not found for tag ${LATEST_TAG}"
+ exit 1
+fi
+
+wget -O "/tmp/${GENERATOR_JAR}" "${GENERATOR_URL}" || { echo "Failed to download generator JAR"; exit 1; }
+
+# Run the generator
+java -cp "/tmp/${GENERATOR_JAR}" com.regnosys.rosetta.generator.python.PythonCodeGeneratorCLI -s "${CDM_ROSETTA}" -t "${PYTHON_TARGET}"
+
+export PYTHONDONTWRITEBYTECODE=1
+python3 -m venv /tmp/.pyenv
+source /tmp/.pyenv/bin/activate
+python3 -m pip install --upgrade pip
+
+cd "${PYTHON_TARGET}"
+
+# Download the latest rune-python-runtime wheel from GitHub releases
+export RUNTIME_WHEEL_URL=$(curl -s https://api.github.com/repos/finos/rune-python-runtime/releases/latest | grep browser_download_url | grep whl | cut -d '"' -f 4)
+export RUNTIME_WHEEL_NAME=$(basename "${RUNTIME_WHEEL_URL}")
+
+echo "Downloading latest runtime wheel: ${RUNTIME_WHEEL_NAME}"
+wget -O "${RUNTIME_WHEEL_NAME}" "${RUNTIME_WHEEL_URL}" || { echo "Failed to download runtime wheel"; exit 1; }
+python3 -m pip install "${RUNTIME_WHEEL_NAME}"
+
+# Build and install the generated Python package
+python3 -m pip wheel --no-deps --only-binary :all: --wheel-dir . .
+WHEEL_FILE=$(ls ./python_cdm-*-py3-none-any.whl | head -n 1)
+if [[ ! -f "${WHEEL_FILE}" ]]; then
+ echo "Wheel file not found!"
+ exit 1
+fi
+python3 -m pip install "${WHEEL_FILE}"
+python3 -m pip install pytest
+
+# Run unit tests (output will be visible in Docker logs)
+pytest -p no:cacheprovider ${PROJECT_ROOT}/cdm-python/test/
\ No newline at end of file
diff --git a/rosetta-source/src/main/resources/build-resources/python/create-deploy-pom.py b/rosetta-source/src/main/resources/build-resources/python/create-deploy-pom.py
new file mode 100755
index 0000000000..7332701cd7
--- /dev/null
+++ b/rosetta-source/src/main/resources/build-resources/python/create-deploy-pom.py
@@ -0,0 +1,142 @@
+import sys
+from string import Template
+import os.path
+
+def main():
+ # build parameters from the command line arguments
+ params_dict = {
+ 'ARTIFACT_ID': sys.argv[1],
+ 'RELEASE_NAME': sys.argv[2],
+ 'PACKAGING': sys.argv[3],
+ 'GROUP_ID': 'org.finos.cdm'
+ }
+
+ # add in additional developers if any
+ if len(sys.argv) > 4 and os.path.isfile(sys.argv[4]):
+ with open(sys.argv[4], 'r') as developers_file:
+ params_dict['DEVELOPERS'] = developers_file.read()
+ else:
+ params_dict['DEVELOPERS'] = '''
+ minesh-s-patel
+ Minesh Patel
+ infra@regnosys.com
+ http://github.com/minesh-s-patel
+ REGnosys
+ https://regnosys.com
+ +1
+
+ Maintainer
+ Developer
+
+
+
+ hugohills-regnosys
+ Hugo Hills
+ infra@regnosys.com
+ http://github.com/hugohills-regnosys
+ REGnosys
+ https://regnosys.com
+ +1
+
+ Maintainer
+ Developer
+
+ '''
+
+ deploy_pom_text = '''
+ 4.0.0
+ $GROUP_ID
+ $ARTIFACT_ID
+ $RELEASE_NAME
+ pom
+
+ $ARTIFACT_ID
+
+
+ org.finos
+ finos
+ 7
+
+
+ https://www.finos.org/common-domain-model
+
+
+ scm:git:https://github.com/finos/common-domain-model
+ scm:git:git://github.com/finos/common-domain-model.git
+ HEAD
+ https://github.com/finos/common-domain-model
+
+
+ The FINOS Common Domain Model (CDM) is a standardised, machine-readable and machine-executable blueprint for how financial products are traded and managed across the transaction lifecycle. It is represented as a domain model and distributed in open source.
+
+
+ FINOS
+ https://finos.org
+
+
+
+
+ Community Specification License 1.0
+ https://github.com/finos/common-domain-model/blob/master/LICENSE.md
+
+
+
+
+$DEVELOPERS
+
+
+
+
+ release
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+ 3.3.0
+
+
+ attach-artifacts
+ package
+
+ attach-artifact
+
+
+
+
+ ${project.basedir}/$ARTIFACT_ID-${project.version}.$PACKAGING
+ $PACKAGING
+
+
+
+
+
+
+
+
+ org.sonatype.central
+ central-publishing-maven-plugin
+ 0.7.0
+
+
+ publish-release
+ deploy
+
+ publish
+
+
+
+
+
+
+
+
+'''
+
+ deploy_pom_text = Template(deploy_pom_text).safe_substitute(params_dict)
+ with open('pom.xml', "w") as deploy_pom_file:
+ deploy_pom_file.write(deploy_pom_text)
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file