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.
- durablestack-0.1.0b1/.github/workflows/ci.yml +39 -0
- durablestack-0.1.0b1/.github/workflows/publish-pypi.yml +85 -0
- durablestack-0.1.0b1/.gitignore +11 -0
- durablestack-0.1.0b1/ARCHITECTURE.md +58 -0
- durablestack-0.1.0b1/CONTRACTS.md +155 -0
- durablestack-0.1.0b1/PKG-INFO +120 -0
- durablestack-0.1.0b1/PROVIDER_PARITY.md +45 -0
- durablestack-0.1.0b1/README.md +86 -0
- durablestack-0.1.0b1/RELEASING.md +54 -0
- durablestack-0.1.0b1/pyproject.toml +80 -0
- durablestack-0.1.0b1/src/durablestack/__init__.py +39 -0
- durablestack-0.1.0b1/src/durablestack/core/__init__.py +1 -0
- durablestack-0.1.0b1/src/durablestack/core/abstractions.py +255 -0
- durablestack-0.1.0b1/src/durablestack/core/constants.py +59 -0
- durablestack-0.1.0b1/src/durablestack/core/cron.py +25 -0
- durablestack-0.1.0b1/src/durablestack/core/event_sink.py +24 -0
- durablestack-0.1.0b1/src/durablestack/core/models.py +137 -0
- durablestack-0.1.0b1/src/durablestack/core/options.py +131 -0
- durablestack-0.1.0b1/src/durablestack/core/processor.py +420 -0
- durablestack-0.1.0b1/src/durablestack/core/registry.py +34 -0
- durablestack-0.1.0b1/src/durablestack/core/utils.py +52 -0
- durablestack-0.1.0b1/src/durablestack/mysql/__init__.py +15 -0
- durablestack-0.1.0b1/src/durablestack/mysql/migrator.py +132 -0
- durablestack-0.1.0b1/src/durablestack/mysql/runtime.py +40 -0
- durablestack-0.1.0b1/src/durablestack/mysql/store.py +945 -0
- durablestack-0.1.0b1/src/durablestack/mysql/table_names.py +34 -0
- durablestack-0.1.0b1/src/durablestack/mysql/types.py +17 -0
- durablestack-0.1.0b1/src/durablestack/observability/__init__.py +1 -0
- durablestack-0.1.0b1/src/durablestack/observability/http.py +28 -0
- durablestack-0.1.0b1/src/durablestack/observability/ingestion.py +264 -0
- durablestack-0.1.0b1/src/durablestack/observability/runtime_control.py +401 -0
- durablestack-0.1.0b1/src/durablestack/observability/url_validation.py +22 -0
- durablestack-0.1.0b1/src/durablestack/postgres/__init__.py +12 -0
- durablestack-0.1.0b1/src/durablestack/postgres/migrator.py +155 -0
- durablestack-0.1.0b1/src/durablestack/postgres/runtime.py +42 -0
- durablestack-0.1.0b1/src/durablestack/postgres/store.py +741 -0
- durablestack-0.1.0b1/src/durablestack/postgres/table_names.py +34 -0
- durablestack-0.1.0b1/src/durablestack/postgres/types.py +17 -0
- durablestack-0.1.0b1/src/durablestack/providers/__init__.py +1 -0
- durablestack-0.1.0b1/src/durablestack/providers/inmemory.py +527 -0
- durablestack-0.1.0b1/src/durablestack/py.typed +0 -0
- durablestack-0.1.0b1/src/durablestack/runtime/__init__.py +5 -0
- durablestack-0.1.0b1/src/durablestack/runtime/factory.py +338 -0
- durablestack-0.1.0b1/src/durablestack/sqlite/__init__.py +15 -0
- durablestack-0.1.0b1/src/durablestack/sqlite/migrator.py +139 -0
- durablestack-0.1.0b1/src/durablestack/sqlite/runtime.py +40 -0
- durablestack-0.1.0b1/src/durablestack/sqlite/store.py +879 -0
- durablestack-0.1.0b1/src/durablestack/sqlite/table_names.py +34 -0
- durablestack-0.1.0b1/src/durablestack/sqlite/types.py +20 -0
- durablestack-0.1.0b1/src/durablestack/sqlserver/__init__.py +15 -0
- durablestack-0.1.0b1/src/durablestack/sqlserver/migrator.py +205 -0
- durablestack-0.1.0b1/src/durablestack/sqlserver/runtime.py +40 -0
- durablestack-0.1.0b1/src/durablestack/sqlserver/store.py +986 -0
- durablestack-0.1.0b1/src/durablestack/sqlserver/table_names.py +34 -0
- durablestack-0.1.0b1/src/durablestack/sqlserver/types.py +17 -0
- durablestack-0.1.0b1/src/durablestack/validators/__init__.py +1 -0
- durablestack-0.1.0b1/src/durablestack/validators/contracts.py +324 -0
- durablestack-0.1.0b1/tests/fixtures/runtime-control-sync-request.golden.json +28 -0
- durablestack-0.1.0b1/tests/fixtures/runtime-control-sync-response.golden.json +23 -0
- durablestack-0.1.0b1/tests/fixtures/telemetry-batch-request.golden.json +30 -0
- durablestack-0.1.0b1/tests/test_constants.py +32 -0
- durablestack-0.1.0b1/tests/test_contract_validators.py +162 -0
- durablestack-0.1.0b1/tests/test_inmemory_store_phase1.py +49 -0
- durablestack-0.1.0b1/tests/test_mysql_integration.py +138 -0
- durablestack-0.1.0b1/tests/test_observability_ingestion.py +172 -0
- durablestack-0.1.0b1/tests/test_options.py +37 -0
- durablestack-0.1.0b1/tests/test_postgres_integration.py +139 -0
- durablestack-0.1.0b1/tests/test_postgres_scaffold.py +18 -0
- durablestack-0.1.0b1/tests/test_provider_contracts.py +279 -0
- durablestack-0.1.0b1/tests/test_provider_scaffold_phase4.py +72 -0
- durablestack-0.1.0b1/tests/test_runtime_control_service.py +175 -0
- durablestack-0.1.0b1/tests/test_runtime_factory_hardening.py +130 -0
- durablestack-0.1.0b1/tests/test_runtime_phase1.py +323 -0
- durablestack-0.1.0b1/tests/test_sqlite_integration.py +121 -0
- 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,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.
|