cosmodol 0.0.2__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.
- cosmodol-0.0.2/.claude/skills/cosmodol/SKILL.md +179 -0
- cosmodol-0.0.2/.github/workflows/ci.yml +234 -0
- cosmodol-0.0.2/.gitignore +138 -0
- cosmodol-0.0.2/LICENSE +201 -0
- cosmodol-0.0.2/PKG-INFO +65 -0
- cosmodol-0.0.2/README.md +45 -0
- cosmodol-0.0.2/cosmodol/__init__.py +100 -0
- cosmodol-0.0.2/cosmodol/base.py +175 -0
- cosmodol-0.0.2/cosmodol/connection.py +211 -0
- cosmodol-0.0.2/cosmodol/errors.py +143 -0
- cosmodol-0.0.2/cosmodol/recipes.py +122 -0
- cosmodol-0.0.2/cosmodol/stores.py +472 -0
- cosmodol-0.0.2/cosmodol/testing.py +308 -0
- cosmodol-0.0.2/cosmodol/tests/__init__.py +0 -0
- cosmodol-0.0.2/cosmodol/tests/test_errors.py +33 -0
- cosmodol-0.0.2/cosmodol/tests/test_integration_emulator.py +374 -0
- cosmodol-0.0.2/cosmodol/tests/test_stores.py +191 -0
- cosmodol-0.0.2/cosmodol/trees.py +277 -0
- cosmodol-0.0.2/misc/docs/architecture.md +252 -0
- cosmodol-0.0.2/misc/docs/cosmos_db_reference.md +204 -0
- cosmodol-0.0.2/misc/docs/design_decisions.md +206 -0
- cosmodol-0.0.2/pyproject.toml +168 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cosmodol
|
|
3
|
+
description: Use when developing, reviewing, or extending the cosmodol package (Azure Cosmos DB NoSQL/Core API as dol Mapping interfaces). Triggers on edits under i/cosmodol/, on imports of `cosmodol`, when adding new Cosmos store variants, codecs, or credential paths, when reasoning about partition keys / RU costs / consistency levels, when writing tests against the Cosmos DB emulator, and when answering "how do I use Cosmos DB from Python the dol way".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# cosmodol — Developer & Agent Skill
|
|
7
|
+
|
|
8
|
+
`cosmodol` exposes the **NoSQL (Core/SQL) API of Azure Cosmos DB** as `dol`-style
|
|
9
|
+
`Mapping` / `MutableMapping` interfaces. This skill is the working memory: when modifying
|
|
10
|
+
or extending the package, read this first, then dive into the
|
|
11
|
+
[misc/docs/](../../../misc/docs/) trio that this file distills.
|
|
12
|
+
|
|
13
|
+
**Source of truth** (always defer to these):
|
|
14
|
+
|
|
15
|
+
- `misc/docs/architecture.md` — the layered design and class hierarchy.
|
|
16
|
+
- `misc/docs/cosmos_db_reference.md` — Cosmos service + `azure-cosmos` SDK facts.
|
|
17
|
+
- `misc/docs/design_decisions.md` — every defaulted choice with rationale.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Mental model in one diagram
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
cosmodol.recipes ← cosmos_store(...) factory, codec layers
|
|
25
|
+
cosmodol.trees ← CosmosAccount, CosmosDatabase (mappings of children)
|
|
26
|
+
cosmodol.stores ← CosmosItems (one partition), CosmosPartitionedItems (whole container)
|
|
27
|
+
cosmodol.base ← free functions: point_get/upsert/replace/delete, query, batch
|
|
28
|
+
cosmodol.connection ← CosmosConnection: credential cascade, lazy CosmosClient
|
|
29
|
+
cosmodol.errors ← @translate_cosmos_errors decorator, custom KeyError subclasses
|
|
30
|
+
cosmodol.testing ← vNext emulator fixture, FakeContainerProxy for unit tests
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The two primary stores live in `stores.py`. Pick the right one:
|
|
34
|
+
|
|
35
|
+
| You have… | Use |
|
|
36
|
+
|---|---|
|
|
37
|
+
| A fixed partition-key value | `CosmosItems(container, partition_key_value=...)` — keys are id strings |
|
|
38
|
+
| The whole container | `CosmosPartitionedItems(container, partition_key_path=...)` — keys are `(pk_value, id)` tuples |
|
|
39
|
+
|
|
40
|
+
## Non-negotiable rules
|
|
41
|
+
|
|
42
|
+
1. **Scope is `azure-cosmos` (NoSQL API) only.** Cosmos-for-MongoDB accounts are not addressable through this SDK — route those users to `pymongo` + `mongodol`. The README must remain explicit about this.
|
|
43
|
+
2. **Class names are `Cosmos*` prefixed.** "Container" / "Database" collide with Blob's terminology and with generic Python — always say `CosmosContainer`-something or `CosmosDatabase`-something.
|
|
44
|
+
3. **`dol`-house base classes only.** Subclass `KvReader` / `KvPersister` from `dol.base`, never `Mapping` / `MutableMapping` directly.
|
|
45
|
+
4. **`__getitem__` and `__contains__` are ALWAYS point reads** (`read_item` with `partition_key=`). Never a query. ~1 RU vs ≥ 2.3 RU + result-size charge.
|
|
46
|
+
5. **`__len__` on `CosmosPartitionedItems` is NOT implemented by default.** Opt-in via `len_via_query=True`. `CosmosItems.__len__` (single-partition) is OK and implemented.
|
|
47
|
+
6. **`__iter__` on a full-container store emits a `UserWarning`** on first call (silence with `silent_full_scan=True`). Cross-partition scans are an economic land mine.
|
|
48
|
+
7. **All Cosmos-exception catches happen in `errors.py`.** `@translate_cosmos_errors` is the only path; auth/throttle errors never translate to "key absent".
|
|
49
|
+
8. **`__setitem__` auto-injects `id` (and `partition_key`) into the body.** If user-provided body already has those fields and they *disagree* with the inferred values, raise `KeyMismatchError` — never silently overwrite user dicts.
|
|
50
|
+
9. **Container/database creation is NEVER through `__setitem__`.** Use explicit `add_container(name, partition_key_path=..., throughput=..., ...)` and `add_database(name, throughput=...)`. Too parameter-rich for one-arg dict assignment.
|
|
51
|
+
10. **`del tree_store[name]` refuses non-empty children.** `tree_store.delete(name, force=True)` is the explicit purge.
|
|
52
|
+
11. **No silent destruction. No silent expensive ops. No silent partition crossing.** Every potentially expensive call has either a fixed-cost path (point read) or an explicit cost-aware opt-in flag.
|
|
53
|
+
12. **`id` is validated on writes** — string, ≤255 chars, no `/ \ ? #`. Cosmos may accept invalid ids but the item then becomes unreachable from the SDK.
|
|
54
|
+
|
|
55
|
+
## RU observability — required, not optional
|
|
56
|
+
|
|
57
|
+
Every Layer A function returns `(value, ResponseHeaders)` where `ResponseHeaders` carries
|
|
58
|
+
`request_charge` (RU) and `etag`. Every Layer B store exposes `store.last_request_charge`
|
|
59
|
+
(an `Optional[float]`) after each op and accepts a `record_ru` callback:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
items = CosmosItems(container, partition_key_value="t",
|
|
63
|
+
record_ru=lambda op, ru: prom.observe(op, ru))
|
|
64
|
+
items["k1"]
|
|
65
|
+
items.last_request_charge # → float (RU consumed)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If you add a new operation, **route it through the metal-layer free functions** so it
|
|
69
|
+
collects RU automatically. Don't call `container.read_item(...)` directly from the store.
|
|
70
|
+
|
|
71
|
+
## Partition-key playbook
|
|
72
|
+
|
|
73
|
+
The defining design fact in Cosmos. cosmodol exposes three idiomatic shapes; pick the
|
|
74
|
+
right one in your example/test:
|
|
75
|
+
|
|
76
|
+
| User has… | Recommended cosmodol class | Notes |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| One logical bucket of items (single tenant, single session) | `CosmosItems(container, partition_key_value="tenant-X")` | Simplest dict surface. Keys = `id`. |
|
|
79
|
+
| A multi-tenant container, each item is its own partition | `CosmosPartitionedItems(container, partition_key_path="/id")` | **Recommended default** for new containers. Best write distribution. Cross-partition queries needed for non-id WHERE clauses. |
|
|
80
|
+
| An existing container with a custom partition scheme | `CosmosPartitionedItems(container, partition_key_path="/whatever")` | Keys = `(pk_value, id)` tuples. |
|
|
81
|
+
| A view scoped to one partition of an existing partitioned container | `partitioned.partition(pk_value)` → `CosmosItems` | Zero round-trip narrowing. |
|
|
82
|
+
|
|
83
|
+
**Never** invent a 4th shape with a separator-joined string key (`"tenant42::user99"`) —
|
|
84
|
+
escaping the separator is more trouble than the tuple is worth. Users who want flat
|
|
85
|
+
strings compose `wrap_kvs` on top.
|
|
86
|
+
|
|
87
|
+
## Adding a new value codec / key transform
|
|
88
|
+
|
|
89
|
+
Layer C only. Never subclass `CosmosItems` to add a codec — use `dol`'s composition tools:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from dol import wrap_kvs, ValueCodecs
|
|
93
|
+
from cosmodol.stores import CosmosItems
|
|
94
|
+
|
|
95
|
+
# Right way: compose
|
|
96
|
+
CosmosBytesStore = wrap_kvs(
|
|
97
|
+
CosmosItems,
|
|
98
|
+
value_codec=ValueCodecs.pickle_b64_in_property('_blob'),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Wrong way: subclass
|
|
102
|
+
class CosmosBytesStore(CosmosItems): # don't do this
|
|
103
|
+
def __getitem__(self, k):
|
|
104
|
+
return base64.b64decode(super().__getitem__(k)['_blob'])
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
When the codec wraps the user's value into a Cosmos-shaped envelope `{"_blob": "..."}`,
|
|
108
|
+
make sure your `data_of_obj` does NOT overwrite `id` or the partition-key property —
|
|
109
|
+
those are injected by the store layer.
|
|
110
|
+
|
|
111
|
+
## Adding a new connection path / credential type
|
|
112
|
+
|
|
113
|
+
`cosmodol.connection.resolve_credential(...)` is the single place. Extend its cascade,
|
|
114
|
+
update `design_decisions.md` §14, and add a test in `tests/test_connection.py` that
|
|
115
|
+
exercises the new branch.
|
|
116
|
+
|
|
117
|
+
Document order of precedence in `architecture.md` Layer 0.
|
|
118
|
+
|
|
119
|
+
## Adding to the metal layer (when a new SDK feature lands)
|
|
120
|
+
|
|
121
|
+
Add a free function in `base.py` (not a method on a store). Wrap it with
|
|
122
|
+
`@translate_cosmos_errors` for any path that touches an item. Return
|
|
123
|
+
`(value, ResponseHeaders)` so RU charges are observable. Then surface it at Layer B if it's
|
|
124
|
+
genuinely Mapping-shaped, or as a top-level method on the relevant store otherwise.
|
|
125
|
+
|
|
126
|
+
## Testing protocol
|
|
127
|
+
|
|
128
|
+
1. **Unit tests** use `FakeContainerProxy` from `cosmodol.testing` — a dict-of-dicts-backed mock implementing the subset of `ContainerProxy` methods the metal layer uses. Fast, no Docker.
|
|
129
|
+
2. **Integration tests** use the Linux vNext emulator via the `cosmos_emulator` fixture (Docker-backed). Skipped with a clear message when Docker unavailable.
|
|
130
|
+
3. **Skip RU-charge assertions** when running against the emulator — the emulator does NOT populate `x-ms-request-charge`. The fixture sets a marker flag the tests can check.
|
|
131
|
+
4. **Doctests** in module docstrings stay runnable (`NORMALIZE_WHITESPACE` + `ELLIPSIS` enabled).
|
|
132
|
+
5. **Live-Azure tests** gated by env var `AZURE_COSMOS_LIVE_TEST_ENDPOINT`; never run in CI by default.
|
|
133
|
+
|
|
134
|
+
## Spinning up the Cosmos emulator locally (Mac/ARM friendly)
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
docker pull mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
|
|
138
|
+
docker run --detach \
|
|
139
|
+
--publish 8081:8081 --publish 8080:8080 --publish 1234:1234 \
|
|
140
|
+
mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:vnext-preview
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Well-known credentials (same on every emulator instance):
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
Endpoint: https://localhost:8081
|
|
147
|
+
Key: C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Use `--protocol http` if the HTTPS cert dance is painful.
|
|
151
|
+
|
|
152
|
+
Emulator **does not** support: stored procs/triggers/UDFs, parallel cross-partition
|
|
153
|
+
queries, offers/permissions/users, custom indexing policies (accepted but no-op), RU
|
|
154
|
+
accounting (header missing). Don't write tests that depend on those features.
|
|
155
|
+
|
|
156
|
+
## Common operations cheatsheet
|
|
157
|
+
|
|
158
|
+
| Want to... | Use |
|
|
159
|
+
|---|---|
|
|
160
|
+
| Dict over one partition | `CosmosItems(container, partition_key_value="X")` |
|
|
161
|
+
| Dict over the whole container, tuple keys | `CosmosPartitionedItems(container, partition_key_path="/_pk")` |
|
|
162
|
+
| Narrow a partitioned store to one partition | `partitioned.partition("X")` → `CosmosItems` |
|
|
163
|
+
| Narrow with a WHERE clause | `items.with_filter("c.status = 'active'")` |
|
|
164
|
+
| List databases | `for name in CosmosAccount(connection): ...` |
|
|
165
|
+
| List containers | `for name in CosmosDatabase(connection, "mydb"): ...` |
|
|
166
|
+
| Create a new container | `db_store.add_container("mycoll", partition_key_path="/id")` |
|
|
167
|
+
| Run a SQL query | `items.query("SELECT * FROM c WHERE c.age > @a", parameters=[{"name":"@a","value":18}])` |
|
|
168
|
+
| Transactional batch (one partition) | `items.batch([("upsert", {"id":"k","v":1}), ("delete", "k2"), ...])` |
|
|
169
|
+
| Conditional replace via ETag | `items.replace("k", new_body, etag=items.last_response_headers.etag)` |
|
|
170
|
+
| One-call factory | `recipes.cosmos_store(connection_string=..., database=..., container=..., partition_key_value=...)` |
|
|
171
|
+
|
|
172
|
+
## When in doubt
|
|
173
|
+
|
|
174
|
+
- Defaults always lean cost-visible (no auto-len, no implicit cross-partition).
|
|
175
|
+
- Defaults always lean Mapping-faithful (KeyError on missing, point reads for contains/get).
|
|
176
|
+
- Surface always leans `dol`-composable (wrap_kvs over subclasses, codecs over methods).
|
|
177
|
+
|
|
178
|
+
When changing a default, update `design_decisions.md` in the **same** PR. The doc IS the
|
|
179
|
+
contract.
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
name: Continuous Integration (uv)
|
|
2
|
+
# MIGRATION NOTE: Old CI uses pylint - new CI uses ruff for linting
|
|
3
|
+
# MIGRATION NOTE: PyPI auth changed: set secrets.PYPI_PASSWORD to a PyPI API token (uv publish uses UV_PUBLISH_TOKEN, mapped from PYPI_PASSWORD)
|
|
4
|
+
on: [push, pull_request]
|
|
5
|
+
|
|
6
|
+
# Workflow-level env vars from [tool.wads.ci.env] in pyproject.toml.
|
|
7
|
+
# Populated by wads-migrate / wads init via template substitution.
|
|
8
|
+
# Includes PROJECT_NAME, literal defaults from env.defaults, and secret-
|
|
9
|
+
# backed vars from required_envvars / test_envvars / extra_envvars,
|
|
10
|
+
# rendered as `KEY: secrets.KEY || ''` so missing secrets don't fail parsing.
|
|
11
|
+
env:
|
|
12
|
+
PROJECT_NAME: cosmodol
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
# First job: Read configuration from pyproject.toml
|
|
16
|
+
setup:
|
|
17
|
+
name: Read Configuration
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
outputs:
|
|
20
|
+
project-name: ${{ steps.config.outputs.project-name }}
|
|
21
|
+
python-versions: ${{ steps.config.outputs.python-versions }}
|
|
22
|
+
pytest-args: ${{ steps.config.outputs.pytest-args }}
|
|
23
|
+
coverage-enabled: ${{ steps.config.outputs.coverage-enabled }}
|
|
24
|
+
exclude-paths: ${{ steps.config.outputs.exclude-paths }}
|
|
25
|
+
test-on-windows: ${{ steps.config.outputs.test-on-windows }}
|
|
26
|
+
build-sdist: ${{ steps.config.outputs.build-sdist }}
|
|
27
|
+
build-wheel: ${{ steps.config.outputs.build-wheel }}
|
|
28
|
+
metrics-enabled: ${{ steps.config.outputs.metrics-enabled }}
|
|
29
|
+
metrics-config-path: ${{ steps.config.outputs.metrics-config-path }}
|
|
30
|
+
metrics-storage-branch: ${{ steps.config.outputs.metrics-storage-branch }}
|
|
31
|
+
metrics-python-version: ${{ steps.config.outputs.metrics-python-version }}
|
|
32
|
+
metrics-force-run: ${{ steps.config.outputs.metrics-force-run }}
|
|
33
|
+
ruff-enabled: ${{ steps.config.outputs.ruff-enabled }}
|
|
34
|
+
black-enabled: ${{ steps.config.outputs.black-enabled }}
|
|
35
|
+
mypy-enabled: ${{ steps.config.outputs.mypy-enabled }}
|
|
36
|
+
docs-enabled: ${{ steps.config.outputs.docs-enabled }}
|
|
37
|
+
|
|
38
|
+
steps:
|
|
39
|
+
- uses: actions/checkout@v6
|
|
40
|
+
|
|
41
|
+
- name: Set up uv
|
|
42
|
+
uses: astral-sh/setup-uv@v7
|
|
43
|
+
|
|
44
|
+
- name: Set up Python
|
|
45
|
+
run: uv python install 3.11
|
|
46
|
+
|
|
47
|
+
- name: Read CI Config
|
|
48
|
+
id: config
|
|
49
|
+
uses: i2mint/wads/actions/read-ci-config@master
|
|
50
|
+
with:
|
|
51
|
+
pyproject-path: .
|
|
52
|
+
|
|
53
|
+
# Second job: Validation using the config
|
|
54
|
+
validation:
|
|
55
|
+
name: Validation
|
|
56
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
|
|
57
|
+
needs: setup
|
|
58
|
+
runs-on: ubuntu-latest
|
|
59
|
+
strategy:
|
|
60
|
+
matrix:
|
|
61
|
+
python-version: ${{ fromJson(needs.setup.outputs.python-versions) }}
|
|
62
|
+
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v6
|
|
65
|
+
|
|
66
|
+
- name: Set up uv
|
|
67
|
+
uses: astral-sh/setup-uv@v7
|
|
68
|
+
with:
|
|
69
|
+
enable-cache: true
|
|
70
|
+
|
|
71
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
72
|
+
uses: i2mint/wads/actions/setup-python-uv@master
|
|
73
|
+
with:
|
|
74
|
+
python-version: ${{ matrix.python-version }}
|
|
75
|
+
|
|
76
|
+
- name: Install System Dependencies
|
|
77
|
+
uses: i2mint/wads/actions/install-system-deps@master
|
|
78
|
+
with:
|
|
79
|
+
pyproject-path: .
|
|
80
|
+
|
|
81
|
+
- name: Install Dependencies
|
|
82
|
+
uses: i2mint/wads/actions/install-deps-uv@master
|
|
83
|
+
|
|
84
|
+
- name: Format Source Code
|
|
85
|
+
if: needs.setup.outputs.ruff-enabled != 'false'
|
|
86
|
+
run: uvx ruff format .
|
|
87
|
+
|
|
88
|
+
- name: Format Source Code (black)
|
|
89
|
+
if: needs.setup.outputs.black-enabled == 'true'
|
|
90
|
+
run: uvx black .
|
|
91
|
+
|
|
92
|
+
- name: Lint Validation
|
|
93
|
+
if: needs.setup.outputs.ruff-enabled != 'false'
|
|
94
|
+
run: uvx ruff check --output-format=github ${{ needs.setup.outputs.project-name }}
|
|
95
|
+
|
|
96
|
+
- name: Type Check (mypy)
|
|
97
|
+
if: needs.setup.outputs.mypy-enabled == 'true'
|
|
98
|
+
run: uvx mypy ${{ needs.setup.outputs.project-name }}
|
|
99
|
+
|
|
100
|
+
- name: Run Tests
|
|
101
|
+
uses: i2mint/wads/actions/run-tests-uv@master
|
|
102
|
+
with:
|
|
103
|
+
root-dir: ${{ needs.setup.outputs.project-name }}
|
|
104
|
+
pytest-args: ${{ needs.setup.outputs.pytest-args }}
|
|
105
|
+
exclude-paths: ${{ needs.setup.outputs.exclude-paths }}
|
|
106
|
+
coverage: ${{ needs.setup.outputs.coverage-enabled }}
|
|
107
|
+
|
|
108
|
+
- name: Track Code Metrics
|
|
109
|
+
if: needs.setup.outputs.metrics-enabled == 'true'
|
|
110
|
+
uses: i2mint/umpyre/actions/track-metrics@master
|
|
111
|
+
continue-on-error: true
|
|
112
|
+
with:
|
|
113
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
114
|
+
config-path: ${{ needs.setup.outputs.metrics-config-path }}
|
|
115
|
+
storage-branch: ${{ needs.setup.outputs.metrics-storage-branch }}
|
|
116
|
+
python-version: ${{ needs.setup.outputs.metrics-python-version }}
|
|
117
|
+
force-run: ${{ needs.setup.outputs.metrics-force-run }}
|
|
118
|
+
|
|
119
|
+
# Optional Windows testing (if enabled in config)
|
|
120
|
+
windows-validation:
|
|
121
|
+
name: Windows Tests
|
|
122
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]') && needs.setup.outputs.test-on-windows == 'true'"
|
|
123
|
+
needs: setup
|
|
124
|
+
runs-on: windows-latest
|
|
125
|
+
continue-on-error: true
|
|
126
|
+
|
|
127
|
+
steps:
|
|
128
|
+
- uses: actions/checkout@v6
|
|
129
|
+
|
|
130
|
+
- name: Set up uv
|
|
131
|
+
uses: astral-sh/setup-uv@v7
|
|
132
|
+
with:
|
|
133
|
+
enable-cache: true
|
|
134
|
+
|
|
135
|
+
- name: Set up Python
|
|
136
|
+
uses: i2mint/wads/actions/setup-python-uv@master
|
|
137
|
+
with:
|
|
138
|
+
python-version: ${{ fromJson(needs.setup.outputs.python-versions)[0] }}
|
|
139
|
+
|
|
140
|
+
- name: Install System Dependencies
|
|
141
|
+
uses: i2mint/wads/actions/install-system-deps@master
|
|
142
|
+
with:
|
|
143
|
+
pyproject-path: .
|
|
144
|
+
|
|
145
|
+
- name: Install Dependencies
|
|
146
|
+
uses: i2mint/wads/actions/install-deps-uv@master
|
|
147
|
+
|
|
148
|
+
- name: Run Tests
|
|
149
|
+
uses: i2mint/wads/actions/run-tests-uv@master
|
|
150
|
+
with:
|
|
151
|
+
root-dir: ${{ needs.setup.outputs.project-name }}
|
|
152
|
+
pytest-args: ${{ needs.setup.outputs.pytest-args }}
|
|
153
|
+
exclude-paths: ${{ needs.setup.outputs.exclude-paths }}
|
|
154
|
+
|
|
155
|
+
# Publishing job
|
|
156
|
+
publish:
|
|
157
|
+
name: Publish
|
|
158
|
+
permissions:
|
|
159
|
+
contents: write
|
|
160
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]') && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')"
|
|
161
|
+
needs: [setup, validation]
|
|
162
|
+
runs-on: ubuntu-latest
|
|
163
|
+
|
|
164
|
+
steps:
|
|
165
|
+
- uses: actions/checkout@v6
|
|
166
|
+
with:
|
|
167
|
+
fetch-depth: 0
|
|
168
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
|
169
|
+
|
|
170
|
+
- name: Set up uv
|
|
171
|
+
uses: astral-sh/setup-uv@v7
|
|
172
|
+
|
|
173
|
+
- name: Set up Python
|
|
174
|
+
uses: i2mint/wads/actions/setup-python-uv@master
|
|
175
|
+
with:
|
|
176
|
+
python-version: ${{ fromJson(needs.setup.outputs.python-versions)[0] }}
|
|
177
|
+
create-venv: "false"
|
|
178
|
+
|
|
179
|
+
- name: Format Source Code
|
|
180
|
+
if: needs.setup.outputs.ruff-enabled != 'false'
|
|
181
|
+
run: uvx ruff format .
|
|
182
|
+
|
|
183
|
+
- name: Format Source Code (black)
|
|
184
|
+
if: needs.setup.outputs.black-enabled == 'true'
|
|
185
|
+
run: uvx black .
|
|
186
|
+
|
|
187
|
+
- name: Update Version Number
|
|
188
|
+
id: version
|
|
189
|
+
uses: i2mint/isee/actions/bump-version-number@master
|
|
190
|
+
|
|
191
|
+
- name: Build Distribution
|
|
192
|
+
uses: i2mint/wads/actions/build-dist-uv@master
|
|
193
|
+
with:
|
|
194
|
+
sdist: ${{ needs.setup.outputs.build-sdist }}
|
|
195
|
+
wheel: ${{ needs.setup.outputs.build-wheel }}
|
|
196
|
+
|
|
197
|
+
- name: Publish to PyPI
|
|
198
|
+
uses: i2mint/wads/actions/pypi-publish-uv@master
|
|
199
|
+
with:
|
|
200
|
+
pypi-token: ${{ secrets.PYPI_PASSWORD }}
|
|
201
|
+
|
|
202
|
+
- name: Force SSH for git remote
|
|
203
|
+
run: git remote set-url origin git@github.com:${{ github.repository }}.git
|
|
204
|
+
|
|
205
|
+
- name: Commit Changes
|
|
206
|
+
uses: i2mint/wads/actions/git-commit@master
|
|
207
|
+
with:
|
|
208
|
+
commit-message: "**CI** Formatted code + Updated version to ${{ env.VERSION }} [skip ci]"
|
|
209
|
+
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
|
|
210
|
+
push: true
|
|
211
|
+
|
|
212
|
+
- name: Tag Repository
|
|
213
|
+
uses: i2mint/wads/actions/git-tag@master
|
|
214
|
+
with:
|
|
215
|
+
tag: ${{ env.VERSION }}
|
|
216
|
+
message: "Release version ${{ env.VERSION }}"
|
|
217
|
+
push: true
|
|
218
|
+
|
|
219
|
+
# Optional GitHub Pages (skipped when [tool.wads.ci.docs].enabled = false)
|
|
220
|
+
github-pages:
|
|
221
|
+
name: Publish GitHub Pages
|
|
222
|
+
permissions:
|
|
223
|
+
contents: write
|
|
224
|
+
pages: write
|
|
225
|
+
id-token: write
|
|
226
|
+
if: "!contains(github.event.head_commit.message, '[skip ci]') && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && needs.setup.outputs.docs-enabled != 'false'"
|
|
227
|
+
needs: [setup, publish]
|
|
228
|
+
runs-on: ubuntu-latest
|
|
229
|
+
|
|
230
|
+
steps:
|
|
231
|
+
- uses: i2mint/epythet/actions/publish-github-pages@master
|
|
232
|
+
with:
|
|
233
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
234
|
+
ignore: "tests/,scrap/,examples/"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
pip-wheel-metadata/
|
|
24
|
+
share/python-wheels/
|
|
25
|
+
*.egg-info/
|
|
26
|
+
.installed.cfg
|
|
27
|
+
*.egg
|
|
28
|
+
MANIFEST
|
|
29
|
+
|
|
30
|
+
# PyInstaller
|
|
31
|
+
# Usually these files are written by a python script from a template
|
|
32
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
33
|
+
*.manifest
|
|
34
|
+
*.spec
|
|
35
|
+
|
|
36
|
+
# Installer logs
|
|
37
|
+
pip-log.txt
|
|
38
|
+
pip-delete-this-directory.txt
|
|
39
|
+
|
|
40
|
+
# Unit test / coverage reports
|
|
41
|
+
htmlcov/
|
|
42
|
+
.tox/
|
|
43
|
+
.nox/
|
|
44
|
+
.coverage
|
|
45
|
+
.coverage.*
|
|
46
|
+
.cache
|
|
47
|
+
nosetests.xml
|
|
48
|
+
coverage.xml
|
|
49
|
+
*.cover
|
|
50
|
+
*.py,cover
|
|
51
|
+
.hypothesis/
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
target/
|
|
76
|
+
|
|
77
|
+
# Jupyter Notebook
|
|
78
|
+
.ipynb_checkpoints
|
|
79
|
+
*.ipynb
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
.python-version
|
|
87
|
+
|
|
88
|
+
# pipenv
|
|
89
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
90
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
91
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
92
|
+
# install all needed dependencies.
|
|
93
|
+
#Pipfile.lock
|
|
94
|
+
|
|
95
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
|
96
|
+
__pypackages__/
|
|
97
|
+
|
|
98
|
+
# Celery stuff
|
|
99
|
+
celerybeat-schedule
|
|
100
|
+
celerybeat.pid
|
|
101
|
+
|
|
102
|
+
# SageMath parsed files
|
|
103
|
+
*.sage.py
|
|
104
|
+
|
|
105
|
+
# Environments
|
|
106
|
+
.env
|
|
107
|
+
.venv
|
|
108
|
+
env/
|
|
109
|
+
venv/
|
|
110
|
+
ENV/
|
|
111
|
+
env.bak/
|
|
112
|
+
venv.bak/
|
|
113
|
+
|
|
114
|
+
# Spyder project settings
|
|
115
|
+
.spyderproject
|
|
116
|
+
.spyproject
|
|
117
|
+
|
|
118
|
+
# Rope project settings
|
|
119
|
+
.ropeproject
|
|
120
|
+
|
|
121
|
+
# mkdocs documentation
|
|
122
|
+
/site
|
|
123
|
+
|
|
124
|
+
# mypy
|
|
125
|
+
.mypy_cache/
|
|
126
|
+
.dmypy.json
|
|
127
|
+
dmypy.json
|
|
128
|
+
|
|
129
|
+
# Pyre type checker
|
|
130
|
+
.pyre/
|
|
131
|
+
|
|
132
|
+
# IDE
|
|
133
|
+
.idea
|
|
134
|
+
|
|
135
|
+
# Azurite
|
|
136
|
+
__azurite*
|
|
137
|
+
__blobstorage__/
|
|
138
|
+
__queuestorage__/
|