framework-m 0.12.2__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {framework_m-0.12.2 → framework_m-0.14.0}/CHANGELOG.md +16 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/PKG-INFO +3 -3
- {framework_m-0.12.2 → framework_m-0.14.0}/pyproject.toml +4 -4
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_jobs_events_webhooks.py +0 -36
- framework_m-0.14.0/tests/integration/test_nats_deduplication.py +143 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/.gitignore +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/LICENSE +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/README.md +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/alembic/README +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/alembic/env.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/alembic/script.py.mako +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/alembic.ini +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/__init__.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/cli/__init__.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/cli/main.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/core/services/__init__.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/core/services/correction.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/py.typed +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/adapters/auth/test_oauth_adapter.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/adapters/read_model/test_projector.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/architecture/fixtures/ci_dummy_index.html +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/architecture/test_frontend_bootstrap.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/architecture/test_package_separation.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/conftest.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/core/interfaces/test_read_model.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/core/interfaces/test_report_engine.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/core/test_correction_service.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/helpers/__init__.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/__init__.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_api_endpoints.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_app_ports.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_child_table_integration.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_crud_flow.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_migration_flow.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_override_migration.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_postgres_flow.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_real_http_client.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_rfc_0008_audit.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_rfc_0008_correction.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_rfc_0008_versioning.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_workflow_lifecycle.py +0 -0
- {framework_m-0.12.2 → framework_m-0.14.0}/tests/test_meta.py +0 -0
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## framework-m v0.14.0
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
- add commit scope linting and CI validator (96cb12a)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- correct workflow transitions, enhance UI select, support SDK subroutes, and resolve CVEs (34fbc7a)
|
|
10
|
+
|
|
11
|
+
## framework-m v0.13.0
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- Implement dual-mode migrations for Pyodide/WASM support (42a8650)
|
|
16
|
+
|
|
1
17
|
## framework-m v0.12.2
|
|
2
18
|
|
|
3
19
|
### Bug Fixes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: framework-m
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: A modular, metadata-driven business application framework
|
|
5
5
|
Project-URL: Homepage, https://gitlab.com/castlecraft/framework-m
|
|
6
6
|
Project-URL: Documentation, https://gitlab.com/castlecraft/framework-m#readme
|
|
@@ -18,8 +18,8 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Typing :: Typed
|
|
20
20
|
Requires-Python: >=3.12
|
|
21
|
-
Requires-Dist: framework-m-core>=0.
|
|
22
|
-
Requires-Dist: framework-m-standard>=0.
|
|
21
|
+
Requires-Dist: framework-m-core>=0.26.0
|
|
22
|
+
Requires-Dist: framework-m-standard>=0.24.0
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
# Framework M
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "framework-m"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.14.0"
|
|
4
4
|
description = "A modular, metadata-driven business application framework"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "Apache-2.0" }
|
|
@@ -21,8 +21,8 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
dependencies = [
|
|
24
|
-
"framework-m-core>=0.
|
|
25
|
-
"framework-m-standard>=0.
|
|
24
|
+
"framework-m-core>=0.26.0",
|
|
25
|
+
"framework-m-standard>=0.24.0",
|
|
26
26
|
]
|
|
27
27
|
|
|
28
28
|
[tool.uv.sources]
|
|
@@ -40,7 +40,7 @@ dev = [
|
|
|
40
40
|
"testcontainers>=4.14.2",
|
|
41
41
|
"httpx>=0.28.1",
|
|
42
42
|
"aiosqlite>=0.22.1",
|
|
43
|
-
"framework-m-studio>=0.
|
|
43
|
+
"framework-m-studio>=0.18.0",
|
|
44
44
|
# Release tools
|
|
45
45
|
"python-semantic-release>=10.5.3",
|
|
46
46
|
"build>=1.5.0",
|
|
@@ -96,42 +96,6 @@ class TestJobExecutionIntegration:
|
|
|
96
96
|
assert status.result == {"result": "success"}
|
|
97
97
|
|
|
98
98
|
|
|
99
|
-
# =============================================================================
|
|
100
|
-
# Test: Scheduled Jobs Integration
|
|
101
|
-
# =============================================================================
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
class TestScheduledJobsIntegration:
|
|
105
|
-
"""Integration tests for scheduled job execution."""
|
|
106
|
-
|
|
107
|
-
def test_cron_expression_builder(self) -> None:
|
|
108
|
-
"""build_cron_expression should create valid cron strings."""
|
|
109
|
-
from framework_m_standard.adapters.jobs.taskiq_adapter import (
|
|
110
|
-
build_cron_expression,
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
# Every hour at minute 0
|
|
114
|
-
expr = build_cron_expression(minute=0)
|
|
115
|
-
assert expr == "0 * * * *"
|
|
116
|
-
|
|
117
|
-
# Every day at 9am
|
|
118
|
-
expr = build_cron_expression(minute=0, hour=9)
|
|
119
|
-
assert expr == "0 9 * * *"
|
|
120
|
-
|
|
121
|
-
# Every Monday at 2pm
|
|
122
|
-
expr = build_cron_expression(minute=0, hour=14, day_of_week=1)
|
|
123
|
-
assert expr == "0 14 * * 1"
|
|
124
|
-
|
|
125
|
-
def test_cron_schedule_registration(self) -> None:
|
|
126
|
-
"""cron() should return schedule definition."""
|
|
127
|
-
from framework_m_standard.adapters.jobs.taskiq_adapter import cron
|
|
128
|
-
|
|
129
|
-
schedule = cron("daily_report", hour=6, minute=0)
|
|
130
|
-
|
|
131
|
-
assert schedule["task_name"] == "daily_report"
|
|
132
|
-
assert schedule["cron"] == "0 6 * * *"
|
|
133
|
-
|
|
134
|
-
|
|
135
99
|
# =============================================================================
|
|
136
100
|
# Test: Webhook Integration
|
|
137
101
|
# =============================================================================
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Integration tests for NATS JetStream deduplication.
|
|
2
|
+
|
|
3
|
+
These tests require a NATS server with JetStream to be running.
|
|
4
|
+
They will be skipped if a connection cannot be established.
|
|
5
|
+
|
|
6
|
+
To run these tests, first start a NATS server:
|
|
7
|
+
docker run -d --rm --name nats-test -p 4222:4222 nats:2.10-alpine --js
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import contextlib
|
|
12
|
+
import os
|
|
13
|
+
from collections.abc import AsyncIterator
|
|
14
|
+
from datetime import UTC, datetime
|
|
15
|
+
from typing import Any
|
|
16
|
+
from unittest.mock import AsyncMock
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from framework_m_core.cli.utility import console
|
|
20
|
+
from framework_m_core.interfaces.ticker import ScheduleDefinition
|
|
21
|
+
from framework_m_core.jobs_registry import get_global_registry
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture(scope="function", autouse=True)
|
|
25
|
+
async def check_nats_is_available():
|
|
26
|
+
"""Skips tests in this module if a NATS server is not available."""
|
|
27
|
+
import nats
|
|
28
|
+
|
|
29
|
+
nats_url = os.environ.get("NATS_URL", "nats://localhost:4222")
|
|
30
|
+
try:
|
|
31
|
+
# Use asyncio.wait_for for a robust timeout that doesn't depend on
|
|
32
|
+
# the nats.connect() keyword argument, which can cause issues.
|
|
33
|
+
nc = await asyncio.wait_for(nats.connect(nats_url), timeout=2)
|
|
34
|
+
await nc.close()
|
|
35
|
+
except Exception as e:
|
|
36
|
+
console.warning(f"NATS connection failed with: {e}")
|
|
37
|
+
pytest.skip(
|
|
38
|
+
f"NATS server not available at {nats_url}. Skipping NATS integration tests."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture(scope="module")
|
|
43
|
+
def nats_url() -> str:
|
|
44
|
+
"""Returns the NATS URL from the environment or a default."""
|
|
45
|
+
return os.environ.get("NATS_URL", "nats://localhost:4222")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.fixture
|
|
49
|
+
async def setup_nats_broker(nats_url: str) -> AsyncIterator[AsyncMock]:
|
|
50
|
+
"""Sets up a Taskiq broker and ensures the stream exists."""
|
|
51
|
+
import nats
|
|
52
|
+
from framework_m_standard.adapters.jobs.taskiq_adapter import create_taskiq_broker
|
|
53
|
+
from nats.js.errors import NotFoundError
|
|
54
|
+
|
|
55
|
+
# Ensure a clean state by deleting the stream before the test.
|
|
56
|
+
# This prevents issues where the stream exists from a previous run
|
|
57
|
+
# with a different configuration (e.g., without deduplication).
|
|
58
|
+
nc = await nats.connect(nats_url)
|
|
59
|
+
js = nc.jetstream()
|
|
60
|
+
with contextlib.suppress(NotFoundError):
|
|
61
|
+
await js.delete_stream(name="framework_m_jobs")
|
|
62
|
+
await asyncio.sleep(0.1) # Give NATS time to process deletion
|
|
63
|
+
|
|
64
|
+
await nc.close()
|
|
65
|
+
|
|
66
|
+
broker = create_taskiq_broker(nats_url=nats_url, subject="framework_m_jobs.default")
|
|
67
|
+
await broker.startup()
|
|
68
|
+
yield broker
|
|
69
|
+
await broker.shutdown()
|
|
70
|
+
|
|
71
|
+
# Teardown: Ensure the stream is deleted after the test
|
|
72
|
+
nc = await nats.connect(nats_url)
|
|
73
|
+
js = nc.jetstream()
|
|
74
|
+
with contextlib.suppress(NotFoundError):
|
|
75
|
+
await js.delete_stream(name="framework_m_jobs")
|
|
76
|
+
await nc.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class TestNatsDeduplication:
|
|
80
|
+
"""Integration tests for NATS JetStream deduplication."""
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_concurrent_ticker_deduplication(
|
|
84
|
+
self, setup_nats_broker: AsyncMock
|
|
85
|
+
) -> None:
|
|
86
|
+
"""Verify NATS deduplicates concurrent job enqueues from multiple tickers."""
|
|
87
|
+
from framework_m_standard.adapters.jobs.taskiq_adapter import (
|
|
88
|
+
TaskiqJobQueueAdapter,
|
|
89
|
+
TaskiqWorkerAdapter,
|
|
90
|
+
job,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# 1. Define a mock job that counts its invocations
|
|
94
|
+
invocation_count = 0
|
|
95
|
+
|
|
96
|
+
@job(name="dedup_test_job") # type: ignore[misc]
|
|
97
|
+
async def dedup_test_job(**kwargs: Any) -> None:
|
|
98
|
+
nonlocal invocation_count
|
|
99
|
+
invocation_count += 1
|
|
100
|
+
|
|
101
|
+
# Register the job
|
|
102
|
+
registry = get_global_registry()
|
|
103
|
+
registry.register("dedup_test_job", dedup_test_job)
|
|
104
|
+
|
|
105
|
+
worker = None
|
|
106
|
+
try:
|
|
107
|
+
# 2. Create and start a worker to consume the job
|
|
108
|
+
worker = TaskiqWorkerAdapter(broker=setup_nats_broker, concurrency=1)
|
|
109
|
+
await worker.start()
|
|
110
|
+
|
|
111
|
+
# 3. Create multiple job queues simulating multiple tickers/workers
|
|
112
|
+
schedule = ScheduleDefinition(job_name="dedup_test_job", cron="* * * * *")
|
|
113
|
+
# Use a single queue adapter to avoid concurrent broker startup issues.
|
|
114
|
+
job_queue = TaskiqJobQueueAdapter(broker=setup_nats_broker)
|
|
115
|
+
|
|
116
|
+
# 4. Concurrently enqueue the same job with the same deduplication_id
|
|
117
|
+
# This simulates what multiple tickers would do at the same minute.
|
|
118
|
+
fixed_now = datetime(2024, 1, 1, 10, 30, 15, tzinfo=UTC)
|
|
119
|
+
current_minute = fixed_now.replace(second=0, microsecond=0)
|
|
120
|
+
timestamp = int(current_minute.timestamp())
|
|
121
|
+
deduplication_id = f"tick:{schedule.job_name}:{timestamp}"
|
|
122
|
+
|
|
123
|
+
enqueue_tasks = [
|
|
124
|
+
job_queue.enqueue(
|
|
125
|
+
schedule.job_name,
|
|
126
|
+
deduplication_id=deduplication_id,
|
|
127
|
+
**schedule.kwargs,
|
|
128
|
+
)
|
|
129
|
+
for _ in range(3)
|
|
130
|
+
]
|
|
131
|
+
await asyncio.gather(*enqueue_tasks)
|
|
132
|
+
|
|
133
|
+
# Give the worker time to process the single message that gets through
|
|
134
|
+
await asyncio.sleep(0.5)
|
|
135
|
+
|
|
136
|
+
# 5. Assert that the job was invoked exactly once
|
|
137
|
+
assert invocation_count == 1
|
|
138
|
+
|
|
139
|
+
finally:
|
|
140
|
+
# 6. Cleanup
|
|
141
|
+
if worker:
|
|
142
|
+
await worker.stop()
|
|
143
|
+
registry.reset()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|