durablestack 0.1.0b1__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 (75) hide show
  1. durablestack-0.1.0b1/.github/workflows/ci.yml +39 -0
  2. durablestack-0.1.0b1/.github/workflows/publish-pypi.yml +85 -0
  3. durablestack-0.1.0b1/.gitignore +11 -0
  4. durablestack-0.1.0b1/ARCHITECTURE.md +58 -0
  5. durablestack-0.1.0b1/CONTRACTS.md +155 -0
  6. durablestack-0.1.0b1/PKG-INFO +120 -0
  7. durablestack-0.1.0b1/PROVIDER_PARITY.md +45 -0
  8. durablestack-0.1.0b1/README.md +86 -0
  9. durablestack-0.1.0b1/RELEASING.md +54 -0
  10. durablestack-0.1.0b1/pyproject.toml +80 -0
  11. durablestack-0.1.0b1/src/durablestack/__init__.py +39 -0
  12. durablestack-0.1.0b1/src/durablestack/core/__init__.py +1 -0
  13. durablestack-0.1.0b1/src/durablestack/core/abstractions.py +255 -0
  14. durablestack-0.1.0b1/src/durablestack/core/constants.py +59 -0
  15. durablestack-0.1.0b1/src/durablestack/core/cron.py +25 -0
  16. durablestack-0.1.0b1/src/durablestack/core/event_sink.py +24 -0
  17. durablestack-0.1.0b1/src/durablestack/core/models.py +137 -0
  18. durablestack-0.1.0b1/src/durablestack/core/options.py +131 -0
  19. durablestack-0.1.0b1/src/durablestack/core/processor.py +420 -0
  20. durablestack-0.1.0b1/src/durablestack/core/registry.py +34 -0
  21. durablestack-0.1.0b1/src/durablestack/core/utils.py +52 -0
  22. durablestack-0.1.0b1/src/durablestack/mysql/__init__.py +15 -0
  23. durablestack-0.1.0b1/src/durablestack/mysql/migrator.py +132 -0
  24. durablestack-0.1.0b1/src/durablestack/mysql/runtime.py +40 -0
  25. durablestack-0.1.0b1/src/durablestack/mysql/store.py +945 -0
  26. durablestack-0.1.0b1/src/durablestack/mysql/table_names.py +34 -0
  27. durablestack-0.1.0b1/src/durablestack/mysql/types.py +17 -0
  28. durablestack-0.1.0b1/src/durablestack/observability/__init__.py +1 -0
  29. durablestack-0.1.0b1/src/durablestack/observability/http.py +28 -0
  30. durablestack-0.1.0b1/src/durablestack/observability/ingestion.py +264 -0
  31. durablestack-0.1.0b1/src/durablestack/observability/runtime_control.py +401 -0
  32. durablestack-0.1.0b1/src/durablestack/observability/url_validation.py +22 -0
  33. durablestack-0.1.0b1/src/durablestack/postgres/__init__.py +12 -0
  34. durablestack-0.1.0b1/src/durablestack/postgres/migrator.py +155 -0
  35. durablestack-0.1.0b1/src/durablestack/postgres/runtime.py +42 -0
  36. durablestack-0.1.0b1/src/durablestack/postgres/store.py +741 -0
  37. durablestack-0.1.0b1/src/durablestack/postgres/table_names.py +34 -0
  38. durablestack-0.1.0b1/src/durablestack/postgres/types.py +17 -0
  39. durablestack-0.1.0b1/src/durablestack/providers/__init__.py +1 -0
  40. durablestack-0.1.0b1/src/durablestack/providers/inmemory.py +527 -0
  41. durablestack-0.1.0b1/src/durablestack/py.typed +0 -0
  42. durablestack-0.1.0b1/src/durablestack/runtime/__init__.py +5 -0
  43. durablestack-0.1.0b1/src/durablestack/runtime/factory.py +338 -0
  44. durablestack-0.1.0b1/src/durablestack/sqlite/__init__.py +15 -0
  45. durablestack-0.1.0b1/src/durablestack/sqlite/migrator.py +139 -0
  46. durablestack-0.1.0b1/src/durablestack/sqlite/runtime.py +40 -0
  47. durablestack-0.1.0b1/src/durablestack/sqlite/store.py +879 -0
  48. durablestack-0.1.0b1/src/durablestack/sqlite/table_names.py +34 -0
  49. durablestack-0.1.0b1/src/durablestack/sqlite/types.py +20 -0
  50. durablestack-0.1.0b1/src/durablestack/sqlserver/__init__.py +15 -0
  51. durablestack-0.1.0b1/src/durablestack/sqlserver/migrator.py +205 -0
  52. durablestack-0.1.0b1/src/durablestack/sqlserver/runtime.py +40 -0
  53. durablestack-0.1.0b1/src/durablestack/sqlserver/store.py +986 -0
  54. durablestack-0.1.0b1/src/durablestack/sqlserver/table_names.py +34 -0
  55. durablestack-0.1.0b1/src/durablestack/sqlserver/types.py +17 -0
  56. durablestack-0.1.0b1/src/durablestack/validators/__init__.py +1 -0
  57. durablestack-0.1.0b1/src/durablestack/validators/contracts.py +324 -0
  58. durablestack-0.1.0b1/tests/fixtures/runtime-control-sync-request.golden.json +28 -0
  59. durablestack-0.1.0b1/tests/fixtures/runtime-control-sync-response.golden.json +23 -0
  60. durablestack-0.1.0b1/tests/fixtures/telemetry-batch-request.golden.json +30 -0
  61. durablestack-0.1.0b1/tests/test_constants.py +32 -0
  62. durablestack-0.1.0b1/tests/test_contract_validators.py +162 -0
  63. durablestack-0.1.0b1/tests/test_inmemory_store_phase1.py +49 -0
  64. durablestack-0.1.0b1/tests/test_mysql_integration.py +138 -0
  65. durablestack-0.1.0b1/tests/test_observability_ingestion.py +172 -0
  66. durablestack-0.1.0b1/tests/test_options.py +37 -0
  67. durablestack-0.1.0b1/tests/test_postgres_integration.py +139 -0
  68. durablestack-0.1.0b1/tests/test_postgres_scaffold.py +18 -0
  69. durablestack-0.1.0b1/tests/test_provider_contracts.py +279 -0
  70. durablestack-0.1.0b1/tests/test_provider_scaffold_phase4.py +72 -0
  71. durablestack-0.1.0b1/tests/test_runtime_control_service.py +175 -0
  72. durablestack-0.1.0b1/tests/test_runtime_factory_hardening.py +130 -0
  73. durablestack-0.1.0b1/tests/test_runtime_phase1.py +323 -0
  74. durablestack-0.1.0b1/tests/test_sqlite_integration.py +121 -0
  75. durablestack-0.1.0b1/tests/test_sqlserver_integration.py +138 -0
@@ -0,0 +1,39 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ python-version: ["3.11", "3.12", "3.13"]
16
+
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Setup Python
22
+ uses: actions/setup-python@v5
23
+ with:
24
+ python-version: ${{ matrix.python-version }}
25
+
26
+ - name: Install system dependencies
27
+ run: sudo apt-get update && sudo apt-get install -y --no-install-recommends unixodbc-dev
28
+
29
+ - name: Install package and dev dependencies
30
+ run: python -m pip install -U pip && python -m pip install -e ".[dev]"
31
+
32
+ - name: Ruff
33
+ run: python -m ruff check .
34
+
35
+ - name: Mypy
36
+ run: python -m mypy
37
+
38
+ - name: Pytest
39
+ run: python -m pytest -q
@@ -0,0 +1,85 @@
1
+ name: Publish PyPI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ paths:
8
+ - "pyproject.toml"
9
+ - "src/**"
10
+ - "README.md"
11
+ - ".github/workflows/publish-pypi.yml"
12
+ workflow_dispatch:
13
+
14
+ jobs:
15
+ publish:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ contents: read
19
+
20
+ steps:
21
+ - name: Checkout
22
+ uses: actions/checkout@v4
23
+
24
+ - name: Setup Python
25
+ uses: actions/setup-python@v5
26
+ with:
27
+ python-version: "3.13"
28
+
29
+ - name: Install system dependencies
30
+ run: sudo apt-get update && sudo apt-get install -y --no-install-recommends unixodbc-dev
31
+
32
+ - name: Install package and dev dependencies
33
+ run: python -m pip install -U pip && python -m pip install -e ".[dev]"
34
+
35
+ - name: Quality gates
36
+ run: python -m ruff check . && python -m mypy && python -m pytest -q
37
+
38
+ - name: Build artifacts
39
+ run: python -m build && python -m twine check dist/*
40
+
41
+ - name: Resolve package version
42
+ id: version
43
+ shell: pwsh
44
+ run: |
45
+ python -c "import pathlib, tomllib; data = tomllib.loads(pathlib.Path('pyproject.toml').read_text(encoding='utf-8')); print('version=' + data['project']['version'])" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
46
+
47
+ - name: Check if version exists on PyPI
48
+ id: pypi_check
49
+ shell: pwsh
50
+ run: |
51
+ $url = "https://pypi.org/pypi/durablestack/${{ steps.version.outputs.version }}/json"
52
+ $status = 0
53
+ try {
54
+ $response = Invoke-WebRequest -Uri $url -Method Get -ErrorAction Stop
55
+ $status = [int]$response.StatusCode
56
+ } catch {
57
+ if ($_.Exception.Response) {
58
+ $status = [int]$_.Exception.Response.StatusCode
59
+ } else {
60
+ throw
61
+ }
62
+ }
63
+ if ($status -eq 200) {
64
+ "exists=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
65
+ "Version already exists on PyPI; skipping publish."
66
+ } else {
67
+ "exists=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
68
+ "Version does not exist on PyPI; publishing."
69
+ }
70
+
71
+ - name: Publish to PyPI
72
+ if: steps.pypi_check.outputs.exists != 'true'
73
+ uses: pypa/gh-action-pypi-publish@release/v1
74
+ with:
75
+ packages-dir: dist/
76
+ password: ${{ secrets.PYPI_API_TOKEN }}
77
+
78
+ - name: Publish summary
79
+ shell: pwsh
80
+ run: |
81
+ if ('${{ steps.pypi_check.outputs.exists }}' -eq 'true') {
82
+ "No publish: durablestack ${{ steps.version.outputs.version }} already exists." | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
83
+ } else {
84
+ "Published durablestack ${{ steps.version.outputs.version }} to PyPI." | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
85
+ }
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+
5
+ .pytest_cache/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+
9
+ dist/
10
+ build/
11
+ *.egg-info/
@@ -0,0 +1,58 @@
1
+ # DurableStack Python Architecture
2
+
3
+ ## Goals
4
+
5
+ - Match DurableStack runtime semantics defined by the .NET runtime for run lifecycle, leasing, retries, recurring scheduling, and retention.
6
+ - Preserve platform-facing contract compatibility for telemetry/event ingestion and runtime-control sync.
7
+ - Provide a Python-idiomatic runtime that is async-first and production-safe.
8
+
9
+ ## Source of Truth and Compatibility Rules
10
+
11
+ - .NET behavior is canonical when there is any ambiguity.
12
+ - Node.js behavior is a secondary parity reference where it intentionally mirrors .NET.
13
+ - Cross-runtime worker execution is not supported: Python workers execute Python handlers only.
14
+ - Cross-runtime external contracts are required: payload fields, status values, and command semantics must match hosted platform expectations.
15
+
16
+ ## Runtime Shape (Phase 0 Contract)
17
+
18
+ - Runtime API surface is explicit and lifecycle-driven (`start`, `stop`, registration, enqueue/schedule/query/admin operations).
19
+ - Processor contract separates orchestration from storage provider specifics.
20
+ - Storage operations are abstracted via a provider interface that all durable backends must implement.
21
+ - Event sink contract is pluggable and independent of run-state persistence.
22
+ - Runtime-control sync contract is pluggable and uses durable command receipts.
23
+
24
+ ## Phase 0 Deliverables
25
+
26
+ - Canonical constants for:
27
+ - run statuses,
28
+ - event types and event version,
29
+ - runtime-control command and receipt status values.
30
+ - Options model and normalization entrypoint.
31
+ - Core abstraction layer (runtime, processor, client/admin/query services, job store, event sink).
32
+ - External DTO models and validators for telemetry and runtime-control payloads.
33
+ - Package scaffold and test scaffold.
34
+
35
+ ## Planned Runtime Semantics (Parity Targets)
36
+
37
+ 1. Run lifecycle states: `pending` -> `leased` -> `succeeded|failed`.
38
+ 2. Lease-based distributed claiming and heartbeat extension while executing.
39
+ 3. Lease-fenced completion writes.
40
+ 4. Retry scheduling with max-attempt terminal behavior.
41
+ 5. Recurring materialization with IANA timezone handling and slot uniqueness semantics.
42
+ 6. Retention that prunes only terminal runs.
43
+ 7. Runtime-control sync and command receipt lifecycle semantics.
44
+ 8. Event payload compatibility with hosted ingestion APIs.
45
+
46
+ ## Design Notes
47
+
48
+ - Runtime implementation will be `asyncio`-first.
49
+ - Handlers will support both async and sync callables (sync handlers executed via an executor).
50
+ - Startup must include schema migration and schema verification when durable providers are used.
51
+ - Runtime-specific schema separation is intentional. Shared DB instances are allowed; shared runtime tables are not.
52
+
53
+ ## Non-goals for Phase 0
54
+
55
+ - Durable provider implementations.
56
+ - Full runtime host loop and execution engine.
57
+ - Framework-specific integrations.
58
+ - Job autodiscovery.
@@ -0,0 +1,155 @@
1
+ # DurableStack Python Contracts (Phase 3)
2
+
3
+ ## Run Status Vocabulary
4
+
5
+ - `pending`
6
+ - `leased`
7
+ - `succeeded`
8
+ - `failed`
9
+
10
+ ## Event Types
11
+
12
+ - `job_claimed`
13
+ - `job_started`
14
+ - `job_succeeded`
15
+ - `job_failed`
16
+ - `job_retried`
17
+ - `retry_scheduled`
18
+ - `worker_heartbeat`
19
+
20
+ ## Event Version
21
+
22
+ - `2`
23
+
24
+ ## Runtime-Control Command Types
25
+
26
+ - `set_schedule_enabled`
27
+ - `run_schedule_now`
28
+ - `update_schedule_cron`
29
+
30
+ ## Runtime-Control Receipt Status Values
31
+
32
+ - `leased`
33
+ - `acknowledged`
34
+ - `succeeded`
35
+ - `failed`
36
+
37
+ ## External Payload Contract Policy
38
+
39
+ - Field names and value semantics align with current platform-facing .NET payloads.
40
+ - Validators are strict for required fields and permissive for nullable optional fields.
41
+ - Breaking payload changes require coordinated runtime + platform versioning.
42
+
43
+ ## Telemetry Ingestion DTOs
44
+
45
+ ### Telemetry Event DTO
46
+
47
+ Required fields:
48
+
49
+ - `eventType` (string, one of contract event types)
50
+ - `eventVersion` (int, current value `2`)
51
+ - `occurredAtUtc` (ISO-8601 UTC timestamp)
52
+ - `workerName` (non-empty string)
53
+ - `runtime` (non-empty string)
54
+ - `runtimeVersion` (non-empty string)
55
+
56
+ Conditionally required for run-scoped events:
57
+
58
+ - `runId` (non-empty string)
59
+ - `jobName` (non-empty string)
60
+ - `attempt` (int, >= 1)
61
+ - `maxAttempts` (int, >= 1)
62
+
63
+ Optional fields:
64
+
65
+ - `durationMs` (int >= 0)
66
+ - `errorType` (string)
67
+ - `errorMessage` (string)
68
+ - `payloadJson` (string)
69
+
70
+ ### Telemetry Batch Request DTO
71
+
72
+ Required fields:
73
+
74
+ - `tenantId` (non-empty string)
75
+ - `idempotencyKey` (non-empty string)
76
+ - `events` (non-empty array of telemetry event DTO)
77
+
78
+ Optional fields:
79
+
80
+ - `serviceName` (string)
81
+
82
+ ## Runtime-Control DTOs
83
+
84
+ ### Runtime-Control Sync Request DTO
85
+
86
+ Required fields:
87
+
88
+ - `tenantId` (non-empty string)
89
+ - `workerName` (non-empty string)
90
+ - `runtime` (non-empty string)
91
+ - `runtimeVersion` (non-empty string)
92
+ - `sentAtUtc` (ISO-8601 UTC timestamp)
93
+ - `snapshotItems` (array)
94
+ - `receipts` (array)
95
+
96
+ `snapshotItems` item fields:
97
+
98
+ - `jobName` (non-empty string)
99
+ - `cronExpression` (non-empty string)
100
+ - `timeZone` (non-empty string)
101
+ - `enabled` (boolean)
102
+ - `nextRunAtUtc` (ISO-8601 UTC timestamp)
103
+ - `maxAttempts` (int >= 1)
104
+ - `allowConcurrentRuns` (boolean)
105
+ - `lastSeenAtUtc` (ISO-8601 UTC timestamp)
106
+
107
+ `receipts` item fields:
108
+
109
+ - `commandId` (non-empty string)
110
+ - `status` (one of: `leased`, `acknowledged`, `succeeded`, `failed`)
111
+ - `recordedAtUtc` (ISO-8601 UTC timestamp)
112
+
113
+ Optional receipt fields:
114
+
115
+ - `completedAtUtc` (ISO-8601 UTC timestamp)
116
+ - `runId` (string)
117
+ - `errorCode` (string)
118
+ - `errorMessage` (string)
119
+
120
+ ### Runtime-Control Sync Response DTO
121
+
122
+ Top-level fields:
123
+
124
+ - `commands` or `Commands` (array of command envelope)
125
+
126
+ Command envelope fields (camelCase or PascalCase accepted by parser):
127
+
128
+ - `commandId`/`CommandId` (non-empty string)
129
+ - `commandType`/`CommandType` (one of runtime-control command types)
130
+ - `payloadJson`/`PayloadJson` (string JSON object)
131
+ - `issuedAtUtc`/`IssuedAtUtc` (ISO-8601 UTC timestamp)
132
+ - `expiresAtUtc`/`ExpiresAtUtc` (optional ISO-8601 UTC timestamp)
133
+
134
+ ### Runtime-Control Command Payload Shapes
135
+
136
+ - `set_schedule_enabled`
137
+ - required: `jobName` (or `JobName`), `enabled` (or `Enabled`)
138
+ - `run_schedule_now`
139
+ - required: `jobName` (or `JobName`)
140
+ - `update_schedule_cron`
141
+ - required: `jobName`/`JobName`, `cronExpression`/`CronExpression`, `timeZone`/`TimeZone`
142
+
143
+ ## Error/Retry Policy Contracts
144
+
145
+ - 2xx responses are treated as success.
146
+ - 401/403 are treated as terminal auth failures without retry.
147
+ - Transient statuses (`408`, `409`, `425`, `429`, `5xx`) are retried with bounded attempts and backoff.
148
+ - Non-transient non-2xx statuses are terminal for that sync attempt.
149
+ - Sync services include correlation ID headers and must include tenant/client-secret headers when credentials are configured.
150
+
151
+ ## Schema and Runtime Boundaries
152
+
153
+ - Runtime schemas are runtime-specific by design.
154
+ - Python and .NET runtimes may share a DB instance but must use separate tables/prefixes.
155
+ - Startup must verify that existing tables at the configured prefix match Python runtime schema expectations.
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.5
2
+ Name: durablestack
3
+ Version: 0.1.0b1
4
+ Summary: DurableStack Python runtime
5
+ Project-URL: Homepage, https://durablestack.com
6
+ Project-URL: Repository, https://github.com/durablestack/durablestack-python
7
+ Project-URL: Issues, https://github.com/durablestack/durablestack-python/issues
8
+ Author: DurableStack
9
+ License: MIT
10
+ Keywords: background-workers,distributed-systems,durable-jobs,job-queue,scheduling
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Software Development :: Libraries
19
+ Classifier: Topic :: System :: Distributed Computing
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: asyncpg>=0.30.0
22
+ Requires-Dist: croniter>=3.0.3
23
+ Requires-Dist: pymysql>=1.1.1
24
+ Requires-Dist: pyodbc>=5.1.0
25
+ Requires-Dist: tzdata>=2024.1
26
+ Provides-Extra: dev
27
+ Requires-Dist: build>=1.2.2; extra == 'dev'
28
+ Requires-Dist: mypy>=1.11.0; extra == 'dev'
29
+ Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
30
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
31
+ Requires-Dist: ruff>=0.6.0; extra == 'dev'
32
+ Requires-Dist: twine>=5.1.1; extra == 'dev'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # durablestack-python
36
+
37
+ DurableStack Python runtime (in active development): durable background jobs and recurring scheduling with platform-compatible contracts.
38
+
39
+ ## Status
40
+
41
+ This package has completed Phase 4 (provider parity).
42
+
43
+ Implemented so far:
44
+
45
+ - Phase 0: scaffold, contracts/constants, validators baseline, docs
46
+ - Phase 1: in-memory runtime/store with lease fencing, retries, recurring materialization, retention, parity tests
47
+ - Phase 2: PostgreSQL provider, migrations, schema verification, Postgres store/runtime factory, integration tests
48
+ - Phase 3: hosted ingestion + runtime-control sync services with contract validators and golden payload fixtures
49
+ - Phase 4: SQLite, MySQL, and SQL Server providers implemented with shared contract coverage
50
+
51
+ Observability note:
52
+
53
+ - We are explicitly applying lessons from the Node.js rollout around hosted API sync (tenant/client-secret auth, endpoint safety, retry policy, idempotency, shutdown flush). The Python ingestion sync foundation now includes these guardrails and dedicated tests.
54
+
55
+ ## Key documents
56
+
57
+ - `ARCHITECTURE.md`
58
+ - `CONTRACTS.md`
59
+ - `PROVIDER_PARITY.md`
60
+ - `RELEASING.md`
61
+
62
+ ## Local development
63
+
64
+ Python 3.11+ is required.
65
+
66
+ Install dev dependencies:
67
+
68
+ ```bash
69
+ pip install -e ".[dev]"
70
+ ```
71
+
72
+ Run tests:
73
+
74
+ ```bash
75
+ pytest
76
+ ```
77
+
78
+ Run lint + typing:
79
+
80
+ ```bash
81
+ ruff check .
82
+ mypy
83
+ ```
84
+
85
+ Run Postgres integration tests (optional):
86
+
87
+ ```bash
88
+ set DURABLESTACK_TEST_POSTGRES_DSN=postgresql://user:pass@localhost:5432/dbname
89
+ pytest -q tests/test_postgres_integration.py
90
+ ```
91
+
92
+ Run SQLite integration tests:
93
+
94
+ ```bash
95
+ pytest -q tests/test_sqlite_integration.py
96
+ ```
97
+
98
+ Run MySQL integration tests (optional):
99
+
100
+ ```bash
101
+ set DURABLESTACK_TEST_MYSQL_DSN=mysql://user:pass@localhost:3306/dbname
102
+ pytest -q tests/test_mysql_integration.py
103
+ ```
104
+
105
+ Run SQL Server integration tests (optional):
106
+
107
+ ```bash
108
+ set DURABLESTACK_TEST_SQLSERVER_DSN=Driver={ODBC Driver 18 for SQL Server};Server=localhost,1433;Database=durablestack;Uid=sa;Pwd=Your_password123;Encrypt=no;TrustServerCertificate=yes;
109
+ pytest -q tests/test_sqlserver_integration.py
110
+ ```
111
+
112
+ ## Provider matrix
113
+
114
+ | Provider | Runtime factory | Driver dependency | Integration env var | Integration test |
115
+ | --- | --- | --- | --- | --- |
116
+ | InMemory | `create_durable_stack(...)` | none | none | `tests/test_runtime_phase1.py` |
117
+ | PostgreSQL | `create_durable_stack_postgres(...)` | `asyncpg` | `DURABLESTACK_TEST_POSTGRES_DSN` | `tests/test_postgres_integration.py` |
118
+ | SQLite | `create_durable_stack_sqlite(...)` | stdlib `sqlite3` | none | `tests/test_sqlite_integration.py` |
119
+ | MySQL | `create_durable_stack_mysql(...)` | `pymysql` | `DURABLESTACK_TEST_MYSQL_DSN` | `tests/test_mysql_integration.py` |
120
+ | SQL Server | `create_durable_stack_sqlserver(...)` | `pyodbc` | `DURABLESTACK_TEST_SQLSERVER_DSN` | `tests/test_sqlserver_integration.py` |
@@ -0,0 +1,45 @@
1
+ # Provider Parity (Phase 4)
2
+
3
+ This document captures provider parity status and migration/locking expectations for the Python runtime.
4
+
5
+ ## Current status
6
+
7
+ - PostgreSQL: implemented with migrations, schema probes, and lease-fenced semantics.
8
+ - SQLite: implemented with migrations, schema probes, and single-writer transaction locking (`BEGIN IMMEDIATE`).
9
+ - MySQL: implemented with migrations, schema probes, and lease-fenced semantics.
10
+ - SQL Server: implemented with migrations, schema probes, and lease-fenced semantics.
11
+
12
+ ## Shared provider contract expectations
13
+
14
+ All providers must preserve these behaviors:
15
+
16
+ - Run lifecycle: `pending -> leased -> succeeded|failed`.
17
+ - Lease-fenced completion: stale worker completion writes are rejected.
18
+ - Retry semantics: failed runs can requeue to `pending`; terminal failure when max attempts reached.
19
+ - Recurring slot uniqueness: one run per (`job_name`, `schedule_slot_utc`) slot.
20
+ - Runtime-control receipts: receipt lease/ack/success/failure lifecycle with command-id dedupe.
21
+ - Retention cleanup: prune terminal runs only.
22
+
23
+ ## Locking and migration notes
24
+
25
+ - PostgreSQL:
26
+ - Migration lock: `pg_advisory_xact_lock` using stable key from table prefix.
27
+ - Claim path: `FOR UPDATE SKIP LOCKED` candidate selection + fenced update.
28
+ - SQLite:
29
+ - Migration lock: transactional lock via `BEGIN IMMEDIATE`.
30
+ - Claim/materialization paths: `BEGIN IMMEDIATE` guarded read-update sequences.
31
+ - Concurrency scope: safe for multi-worker processes sharing one DB file with SQLite single-writer model.
32
+ - MySQL:
33
+ - Migration lock: named lock via `GET_LOCK` / `RELEASE_LOCK` keyed by table prefix.
34
+ - Claim path: `FOR UPDATE SKIP LOCKED` candidate selection + fenced updates in transactions.
35
+ - SQL Server:
36
+ - Migration lock: `sp_getapplock` with transaction-scoped exclusive lock keyed by table prefix.
37
+ - Claim path: `UPDLOCK` + `READPAST` row selection and fenced updates.
38
+
39
+ ## Test coverage in repo
40
+
41
+ - Postgres integration: `tests/test_postgres_integration.py` (env-gated).
42
+ - SQLite integration: `tests/test_sqlite_integration.py`.
43
+ - MySQL integration: `tests/test_mysql_integration.py` (env-gated).
44
+ - SQL Server integration: `tests/test_sqlserver_integration.py` (env-gated).
45
+ - Phase 4 provider scaffolds: `tests/test_provider_scaffold_phase4.py`.
@@ -0,0 +1,86 @@
1
+ # durablestack-python
2
+
3
+ DurableStack Python runtime (in active development): durable background jobs and recurring scheduling with platform-compatible contracts.
4
+
5
+ ## Status
6
+
7
+ This package has completed Phase 4 (provider parity).
8
+
9
+ Implemented so far:
10
+
11
+ - Phase 0: scaffold, contracts/constants, validators baseline, docs
12
+ - Phase 1: in-memory runtime/store with lease fencing, retries, recurring materialization, retention, parity tests
13
+ - Phase 2: PostgreSQL provider, migrations, schema verification, Postgres store/runtime factory, integration tests
14
+ - Phase 3: hosted ingestion + runtime-control sync services with contract validators and golden payload fixtures
15
+ - Phase 4: SQLite, MySQL, and SQL Server providers implemented with shared contract coverage
16
+
17
+ Observability note:
18
+
19
+ - We are explicitly applying lessons from the Node.js rollout around hosted API sync (tenant/client-secret auth, endpoint safety, retry policy, idempotency, shutdown flush). The Python ingestion sync foundation now includes these guardrails and dedicated tests.
20
+
21
+ ## Key documents
22
+
23
+ - `ARCHITECTURE.md`
24
+ - `CONTRACTS.md`
25
+ - `PROVIDER_PARITY.md`
26
+ - `RELEASING.md`
27
+
28
+ ## Local development
29
+
30
+ Python 3.11+ is required.
31
+
32
+ Install dev dependencies:
33
+
34
+ ```bash
35
+ pip install -e ".[dev]"
36
+ ```
37
+
38
+ Run tests:
39
+
40
+ ```bash
41
+ pytest
42
+ ```
43
+
44
+ Run lint + typing:
45
+
46
+ ```bash
47
+ ruff check .
48
+ mypy
49
+ ```
50
+
51
+ Run Postgres integration tests (optional):
52
+
53
+ ```bash
54
+ set DURABLESTACK_TEST_POSTGRES_DSN=postgresql://user:pass@localhost:5432/dbname
55
+ pytest -q tests/test_postgres_integration.py
56
+ ```
57
+
58
+ Run SQLite integration tests:
59
+
60
+ ```bash
61
+ pytest -q tests/test_sqlite_integration.py
62
+ ```
63
+
64
+ Run MySQL integration tests (optional):
65
+
66
+ ```bash
67
+ set DURABLESTACK_TEST_MYSQL_DSN=mysql://user:pass@localhost:3306/dbname
68
+ pytest -q tests/test_mysql_integration.py
69
+ ```
70
+
71
+ Run SQL Server integration tests (optional):
72
+
73
+ ```bash
74
+ set DURABLESTACK_TEST_SQLSERVER_DSN=Driver={ODBC Driver 18 for SQL Server};Server=localhost,1433;Database=durablestack;Uid=sa;Pwd=Your_password123;Encrypt=no;TrustServerCertificate=yes;
75
+ pytest -q tests/test_sqlserver_integration.py
76
+ ```
77
+
78
+ ## Provider matrix
79
+
80
+ | Provider | Runtime factory | Driver dependency | Integration env var | Integration test |
81
+ | --- | --- | --- | --- | --- |
82
+ | InMemory | `create_durable_stack(...)` | none | none | `tests/test_runtime_phase1.py` |
83
+ | PostgreSQL | `create_durable_stack_postgres(...)` | `asyncpg` | `DURABLESTACK_TEST_POSTGRES_DSN` | `tests/test_postgres_integration.py` |
84
+ | SQLite | `create_durable_stack_sqlite(...)` | stdlib `sqlite3` | none | `tests/test_sqlite_integration.py` |
85
+ | MySQL | `create_durable_stack_mysql(...)` | `pymysql` | `DURABLESTACK_TEST_MYSQL_DSN` | `tests/test_mysql_integration.py` |
86
+ | SQL Server | `create_durable_stack_sqlserver(...)` | `pyodbc` | `DURABLESTACK_TEST_SQLSERVER_DSN` | `tests/test_sqlserver_integration.py` |
@@ -0,0 +1,54 @@
1
+ # Releasing DurableStack Python
2
+
3
+ This guide covers local pre-publish validation for beta and stable releases.
4
+
5
+ ## Versioning
6
+
7
+ - Use PEP 440 versioning in `pyproject.toml`.
8
+ - Beta example: `0.1.0b1`.
9
+ - Next beta: increment beta number (`0.1.0b2`, `0.1.0b3`, ...).
10
+
11
+ ## Pre-publish quality gates
12
+
13
+ Run from `durablestack-python/`:
14
+
15
+ ```bash
16
+ python -m ruff check .
17
+ python -m mypy
18
+ python -m pytest -q
19
+ ```
20
+
21
+ ## Build and package validation
22
+
23
+ Run from `durablestack-python/`:
24
+
25
+ ```bash
26
+ python -m build
27
+ python -m twine check dist/*
28
+ ```
29
+
30
+ Expected artifacts:
31
+
32
+ - `dist/durablestack-<version>-py3-none-any.whl`
33
+ - `dist/durablestack-<version>.tar.gz`
34
+
35
+ If `twine check` passes and tests are green, the package is ready for CI-driven publish.
36
+
37
+ ## CI-driven publish on `main`
38
+
39
+ This repo includes:
40
+
41
+ - `.github/workflows/ci.yml`: PR/push quality gates
42
+ - `.github/workflows/publish-pypi.yml`: publish pipeline for `main`
43
+
44
+ Required GitHub secret:
45
+
46
+ - `PYPI_API_TOKEN`: API token from PyPI account settings
47
+
48
+ Publish behavior:
49
+
50
+ 1. Push to `main` with an updated `project.version` in `pyproject.toml`.
51
+ 2. Workflow runs quality gates, build, and `twine check`.
52
+ 3. Workflow checks whether that exact version already exists on PyPI.
53
+ 4. If version is new, it publishes `dist/*` to PyPI.
54
+ 5. If version already exists, it exits without publishing.