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.
Files changed (42) hide show
  1. {framework_m-0.12.2 → framework_m-0.14.0}/CHANGELOG.md +16 -0
  2. {framework_m-0.12.2 → framework_m-0.14.0}/PKG-INFO +3 -3
  3. {framework_m-0.12.2 → framework_m-0.14.0}/pyproject.toml +4 -4
  4. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_jobs_events_webhooks.py +0 -36
  5. framework_m-0.14.0/tests/integration/test_nats_deduplication.py +143 -0
  6. {framework_m-0.12.2 → framework_m-0.14.0}/.gitignore +0 -0
  7. {framework_m-0.12.2 → framework_m-0.14.0}/LICENSE +0 -0
  8. {framework_m-0.12.2 → framework_m-0.14.0}/README.md +0 -0
  9. {framework_m-0.12.2 → framework_m-0.14.0}/alembic/README +0 -0
  10. {framework_m-0.12.2 → framework_m-0.14.0}/alembic/env.py +0 -0
  11. {framework_m-0.12.2 → framework_m-0.14.0}/alembic/script.py.mako +0 -0
  12. {framework_m-0.12.2 → framework_m-0.14.0}/alembic.ini +0 -0
  13. {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/__init__.py +0 -0
  14. {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/cli/__init__.py +0 -0
  15. {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/cli/main.py +0 -0
  16. {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/core/services/__init__.py +0 -0
  17. {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/core/services/correction.py +0 -0
  18. {framework_m-0.12.2 → framework_m-0.14.0}/src/framework_m/py.typed +0 -0
  19. {framework_m-0.12.2 → framework_m-0.14.0}/tests/adapters/auth/test_oauth_adapter.py +0 -0
  20. {framework_m-0.12.2 → framework_m-0.14.0}/tests/adapters/read_model/test_projector.py +0 -0
  21. {framework_m-0.12.2 → framework_m-0.14.0}/tests/architecture/fixtures/ci_dummy_index.html +0 -0
  22. {framework_m-0.12.2 → framework_m-0.14.0}/tests/architecture/test_frontend_bootstrap.py +0 -0
  23. {framework_m-0.12.2 → framework_m-0.14.0}/tests/architecture/test_package_separation.py +0 -0
  24. {framework_m-0.12.2 → framework_m-0.14.0}/tests/conftest.py +0 -0
  25. {framework_m-0.12.2 → framework_m-0.14.0}/tests/core/interfaces/test_read_model.py +0 -0
  26. {framework_m-0.12.2 → framework_m-0.14.0}/tests/core/interfaces/test_report_engine.py +0 -0
  27. {framework_m-0.12.2 → framework_m-0.14.0}/tests/core/test_correction_service.py +0 -0
  28. {framework_m-0.12.2 → framework_m-0.14.0}/tests/helpers/__init__.py +0 -0
  29. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/__init__.py +0 -0
  30. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_api_endpoints.py +0 -0
  31. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_app_ports.py +0 -0
  32. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_child_table_integration.py +0 -0
  33. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_crud_flow.py +0 -0
  34. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_migration_flow.py +0 -0
  35. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_override_migration.py +0 -0
  36. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_postgres_flow.py +0 -0
  37. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_real_http_client.py +0 -0
  38. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_rfc_0008_audit.py +0 -0
  39. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_rfc_0008_correction.py +0 -0
  40. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_rfc_0008_versioning.py +0 -0
  41. {framework_m-0.12.2 → framework_m-0.14.0}/tests/integration/test_workflow_lifecycle.py +0 -0
  42. {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.12.2
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.24.0
22
- Requires-Dist: framework-m-standard>=0.22.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.12.2"
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.24.0",
25
- "framework-m-standard>=0.22.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.17.2",
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