dataceen-client 0.0.1__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 (73) hide show
  1. dataceen_client-0.0.1/.env.example +17 -0
  2. dataceen_client-0.0.1/CHANGELOG.md +58 -0
  3. dataceen_client-0.0.1/LICENSE +16 -0
  4. dataceen_client-0.0.1/MANIFEST.in +14 -0
  5. dataceen_client-0.0.1/PKG-INFO +160 -0
  6. dataceen_client-0.0.1/README.md +124 -0
  7. dataceen_client-0.0.1/docs/decisions.md +26 -0
  8. dataceen_client-0.0.1/docs/dsl-comparison.md +125 -0
  9. dataceen_client-0.0.1/docs/plan.md +143 -0
  10. dataceen_client-0.0.1/docs/porting-guide.md +119 -0
  11. dataceen_client-0.0.1/docs/progress.md +359 -0
  12. dataceen_client-0.0.1/docs/release.md +130 -0
  13. dataceen_client-0.0.1/protos/dataceenevent.proto +85 -0
  14. dataceen_client-0.0.1/pyproject.toml +67 -0
  15. dataceen_client-0.0.1/setup.cfg +4 -0
  16. dataceen_client-0.0.1/src/dataceen_client/__init__.py +48 -0
  17. dataceen_client-0.0.1/src/dataceen_client/config.py +105 -0
  18. dataceen_client-0.0.1/src/dataceen_client/dataceen_client.py +230 -0
  19. dataceen_client-0.0.1/src/dataceen_client/errors.py +36 -0
  20. dataceen_client-0.0.1/src/dataceen_client/http_transport.py +137 -0
  21. dataceen_client-0.0.1/src/dataceen_client/py.typed +0 -0
  22. dataceen_client-0.0.1/src/dataceen_client/runtime/__init__.py +48 -0
  23. dataceen_client-0.0.1/src/dataceen_client/runtime/aggregations.py +54 -0
  24. dataceen_client-0.0.1/src/dataceen_client/runtime/fields.py +78 -0
  25. dataceen_client-0.0.1/src/dataceen_client/runtime/filter.py +130 -0
  26. dataceen_client-0.0.1/src/dataceen_client/runtime/operations.py +199 -0
  27. dataceen_client-0.0.1/src/dataceen_client/runtime/order_by.py +29 -0
  28. dataceen_client-0.0.1/src/dataceen_client/subscriptions/__init__.py +36 -0
  29. dataceen_client-0.0.1/src/dataceen_client/subscriptions/_pb/__init__.py +15 -0
  30. dataceen_client-0.0.1/src/dataceen_client/subscriptions/_pb/dataceenevent_pb2.py +52 -0
  31. dataceen_client-0.0.1/src/dataceen_client/subscriptions/_pb/dataceenevent_pb2.pyi +130 -0
  32. dataceen_client-0.0.1/src/dataceen_client/subscriptions/_pb/dataceenevent_pb2_grpc.py +100 -0
  33. dataceen_client-0.0.1/src/dataceen_client/subscriptions/grpc_client.py +75 -0
  34. dataceen_client-0.0.1/src/dataceen_client/subscriptions/subscription.py +75 -0
  35. dataceen_client-0.0.1/src/dataceen_client/subscriptions/subscription_manager.py +367 -0
  36. dataceen_client-0.0.1/src/dataceen_client/subscriptions/types.py +103 -0
  37. dataceen_client-0.0.1/src/dataceen_client/token_provider.py +68 -0
  38. dataceen_client-0.0.1/src/dataceen_client/types.py +62 -0
  39. dataceen_client-0.0.1/src/dataceen_client.egg-info/PKG-INFO +160 -0
  40. dataceen_client-0.0.1/src/dataceen_client.egg-info/SOURCES.txt +71 -0
  41. dataceen_client-0.0.1/src/dataceen_client.egg-info/dependency_links.txt +1 -0
  42. dataceen_client-0.0.1/src/dataceen_client.egg-info/requires.txt +12 -0
  43. dataceen_client-0.0.1/src/dataceen_client.egg-info/top_level.txt +1 -0
  44. dataceen_client-0.0.1/tests/__init__.py +0 -0
  45. dataceen_client-0.0.1/tests/test_aggregations.py +18 -0
  46. dataceen_client-0.0.1/tests/test_codegen_aggregation.py +146 -0
  47. dataceen_client-0.0.1/tests/test_codegen_emit.py +155 -0
  48. dataceen_client-0.0.1/tests/test_codegen_parse.py +299 -0
  49. dataceen_client-0.0.1/tests/test_dataceen_client.py +133 -0
  50. dataceen_client-0.0.1/tests/test_examples_smoke.py +58 -0
  51. dataceen_client-0.0.1/tests/test_fields.py +52 -0
  52. dataceen_client-0.0.1/tests/test_filter.py +72 -0
  53. dataceen_client-0.0.1/tests/test_http_transport.py +148 -0
  54. dataceen_client-0.0.1/tests/test_operations.py +163 -0
  55. dataceen_client-0.0.1/tests/test_order_by.py +27 -0
  56. dataceen_client-0.0.1/tests/test_subscriptions.py +368 -0
  57. dataceen_client-0.0.1/tools/__init__.py +0 -0
  58. dataceen_client-0.0.1/tools/codegen/__init__.py +0 -0
  59. dataceen_client-0.0.1/tools/codegen/src/__init__.py +0 -0
  60. dataceen_client-0.0.1/tools/codegen/src/cli.py +107 -0
  61. dataceen_client-0.0.1/tools/codegen/src/emit_aggregation.py +143 -0
  62. dataceen_client-0.0.1/tools/codegen/src/emit_all_client.py +38 -0
  63. dataceen_client-0.0.1/tools/codegen/src/emit_client.py +184 -0
  64. dataceen_client-0.0.1/tools/codegen/src/emit_create.py +84 -0
  65. dataceen_client-0.0.1/tools/codegen/src/emit_fields.py +57 -0
  66. dataceen_client-0.0.1/tools/codegen/src/emit_filter.py +91 -0
  67. dataceen_client-0.0.1/tools/codegen/src/emit_model.py +80 -0
  68. dataceen_client-0.0.1/tools/codegen/src/emit_plan.py +142 -0
  69. dataceen_client-0.0.1/tools/codegen/src/emit_update.py +115 -0
  70. dataceen_client-0.0.1/tools/codegen/src/emit_util.py +146 -0
  71. dataceen_client-0.0.1/tools/codegen/src/fetch_schema.py +147 -0
  72. dataceen_client-0.0.1/tools/codegen/src/introspection_query.py +82 -0
  73. dataceen_client-0.0.1/tools/codegen/src/parse_schema.py +402 -0
@@ -0,0 +1,17 @@
1
+ # Copy to .env and fill in real values.
2
+ # Mirrors the C# example's appsettings.json for CandyShopModel.
3
+
4
+ DATACEEN_TENANT_ID=97dd913c-931e-44cf-96fd-a36270f7a465
5
+ DATACEEN_CLIENT_ID=62e0cda5-a254-4498-b660-e00b98e83225
6
+ DATACEEN_CLIENT_SECRET=replace-me
7
+
8
+ DATACEEN_DOMAIN=Thomas
9
+ DATACEEN_MODEL=CandyShopModel
10
+ DATACEEN_SCOPE=All
11
+
12
+ DATACEEN_API_URL=https://api.dataceen.com
13
+ DATACEEN_API_PATH=/graphql
14
+ DATACEEN_API_SCOPE=api://413b24f0-e191-4cf2-839d-3c9c75465ba2/.default
15
+
16
+ DATACEEN_SUBSCRIPTION_URL=https://subscription.dataceen.com
17
+ DATACEEN_SUBSCRIPTION_SCOPE=api://6982ac36-9f3e-4b60-9f17-6b078fd65ead/.default
@@ -0,0 +1,58 @@
1
+ # Changelog
2
+
3
+ All notable changes to `dataceen-client` (Python) are documented here. Format
4
+ follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
+ project loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.0.1] — 2026-05-01
10
+
11
+ Initial alpha — feature-parity with C# `DataceenClient` and TypeScript
12
+ `@dataceen/client`, verified against the same model fixtures.
13
+
14
+ ### Added
15
+
16
+ - **Core runtime** (`dataceen_client.*`)
17
+ - `DataceenConfig`, `DataceenClientOptions`, `load_config_from_env()`
18
+ - `DataceenError` — single error type for HTTP, GraphQL, parse, and server-side `ResponseCode != 0`
19
+ - `DataceenClient.execute_query()` (default retry on transient) and `execute_mutation()` (no retry by default)
20
+ - `TokenProvider` — Azure AD client-credentials via `msal`, with `force_refresh` simulated by `remove_tokens_for_client()`
21
+ - `HttpTransport` — `httpx.AsyncClient` POST with 401-refresh + transient retry (408/429/5xx, exponential backoff)
22
+ - Result `TypedDict`s: `FindResult[T]`, `SearchResult[T]`, `MutationResult`, …
23
+
24
+ - **Runtime DSL helpers** (`dataceen_client.runtime.*`)
25
+ - Pure-dict DSL — `filter_to_graphql`, `order_by_to_graphql`, `collect_fields`
26
+ - `StringOps`, `NumberOps`, `BooleanOps`, `DateOps` — typed comparison-operator dicts
27
+ - `aggregation_fragment(depth)` — recursive Aggregations result expansion
28
+ - `do_find` / `do_search` / `do_create` / `do_create_bulk` / `do_update` / `do_update_bulk` / `do_delete` / `do_create_relation` / `do_create_relation_bulk`
29
+
30
+ - **Codegen** (`tools/codegen/`)
31
+ - GraphQL introspection fetch + cache (`fetch_schema.py`, `parse_schema.py`)
32
+ - Per-entity emitters: read TypedDict, Create TypedDict, Update class with `__setattr__` change-tracking, Filter / SearchFilter, Fields, per-entity Client, AllClient
33
+ - Aggregation emitter — typed `Search<Entity>_aggregation` + `<Entity>AggregationDetails`
34
+ - CLI: `python -m tools.codegen.src.cli {fetch,emit ROOT,emit-all}` (also installable as `dataceen-codegen`)
35
+
36
+ - **gRPC subscriptions** (`dataceen_client.subscriptions.*`)
37
+ - `Subscription` class with typed + low-level handlers, `asyncio.Event`-based cancel
38
+ - `grpc.aio` async stream open + Bearer-metadata auth
39
+ - Subscription manager with baseload semantics, exponential reconnect (max 10), synthetic CONNECTION_LOST/RECONNECTED events, slow-handler timing, central exception catch
40
+ - Pre-compiled proto stubs in `_pb/` (regenerable via `scripts/regenerate_proto.py`)
41
+
42
+ - **Example app** — `examples/candy_shop/` ports the TS port's `examples/candy-shop/`. Read-only flows always run; create/update/delete gated by `RUN_DESTRUCTIVE=1`.
43
+
44
+ - **Docs** — `docs/{plan,decisions,progress,dsl-comparison,porting-guide,release}.md`.
45
+
46
+ ### Test coverage
47
+
48
+ 88 unit tests, hermetic (~1.7s). Live-backend integration is opt-in via
49
+ `RUN_INTEGRATION=1` once `.env` is configured.
50
+
51
+ ### Known gaps for v0.1
52
+
53
+ - No live-backend smoke run yet (needs valid `.env`).
54
+ - No published wheel — `python -m build` produces it locally.
55
+ - Sphinx/MkDocs site not built; docstrings + `docs/*.md` are the source of truth.
56
+
57
+ [Unreleased]: https://github.com/dataceen/dataceen-client-python/compare/v0.0.1...HEAD
58
+ [0.0.1]: https://github.com/dataceen/dataceen-client-python/releases/tag/v0.0.1
@@ -0,0 +1,16 @@
1
+ Copyright (c) 2026 Dataceen AB.
2
+
3
+ All rights reserved.
4
+
5
+ This software is proprietary and confidential. Unauthorized copying,
6
+ modification, distribution, or use of this software, via any medium, is
7
+ strictly prohibited without the express written permission of the copyright
8
+ holder.
9
+
10
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
11
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
12
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE
13
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER
14
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING
15
+ FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
16
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,14 @@
1
+ include README.md
2
+ include CHANGELOG.md
3
+ include LICENSE
4
+ include .env.example
5
+ recursive-include docs *.md
6
+ recursive-include protos *.proto
7
+ recursive-include src *.py *.pyi py.typed
8
+ recursive-include tests *.py
9
+ recursive-include tools *.py
10
+ prune .venv
11
+ prune dist
12
+ prune build
13
+ global-exclude __pycache__
14
+ global-exclude *.py[cod]
@@ -0,0 +1,160 @@
1
+ Metadata-Version: 2.4
2
+ Name: dataceen-client
3
+ Version: 0.0.1
4
+ Summary: Python client for Dataceen GraphQL + gRPC subscription APIs
5
+ Author: Dataceen
6
+ License: Proprietary
7
+ Project-URL: Source, https://github.com/dataceen/dataceen-client-python
8
+ Project-URL: Issues, https://github.com/dataceen/dataceen-client-python/issues
9
+ Keywords: dataceen,graphql,grpc,client,azure-ad
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: Other/Proprietary License
13
+ Classifier: Operating System :: OS Independent
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: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: httpx>=0.27
25
+ Requires-Dist: msal>=1.28
26
+ Requires-Dist: grpcio>=1.60
27
+ Requires-Dist: protobuf>=4.25
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
31
+ Requires-Dist: respx>=0.21; extra == "dev"
32
+ Requires-Dist: python-dotenv>=1.0; extra == "dev"
33
+ Requires-Dist: grpcio-tools>=1.60; extra == "dev"
34
+ Requires-Dist: build>=1.2; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ # dataceen-client (Python)
38
+
39
+ Python client for the Dataceen GraphQL + gRPC subscription APIs. Parallel to
40
+ the C# (`DataceenClient/`) and TypeScript (`DataceenClientTs/`) reference
41
+ clients — feature-parity verified against the same `Thomas/CandyShopModel/All`
42
+ fixtures.
43
+
44
+ > **Status:** 0.0.1 alpha. 88 unit tests green. Not yet on PyPI.
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ python -m venv .venv
50
+ .venv\Scripts\activate # PowerShell
51
+ pip install -e ".[dev]"
52
+ ```
53
+
54
+ Requires Python 3.11+ (`typing.NotRequired`, generic `TypedDict`).
55
+
56
+ ## Configure
57
+
58
+ Copy `.env.example` to `.env` at the repo root and fill in real Azure AD
59
+ credentials. The example matches the C# `appsettings.json`'s `AllClient`
60
+ section.
61
+
62
+ ## Quick start
63
+
64
+ ```python
65
+ import asyncio
66
+ from dataceen_client import DataceenClient, load_config_from_env
67
+ from dataceen_client.runtime import do_find
68
+
69
+ async def main():
70
+ cfg = load_config_from_env()
71
+ async with DataceenClient(cfg) as client:
72
+ result = await do_find(
73
+ client,
74
+ "Customer",
75
+ size=5,
76
+ filter={"CustomerId": {"like": "cst"}},
77
+ fields={
78
+ "_id": True,
79
+ "CustomerId": True,
80
+ "CustomerPlacedOrder": {"Order": {"OrderId": True}},
81
+ },
82
+ order_by=[{"CustomerId": "Ascending"}],
83
+ )
84
+ for item in result["Items"]:
85
+ print(item)
86
+
87
+ asyncio.run(main())
88
+ ```
89
+
90
+ After running codegen, replace `do_find(client, "Customer", ...)` with the
91
+ typed `AllClient(client).Customer.find(...)`.
92
+
93
+ ## Subscriptions
94
+
95
+ ```python
96
+ async with DataceenClient(cfg) as client:
97
+ sub = client.create_subscription(
98
+ topics=["Customer"],
99
+ baseload_topics=["Customer"],
100
+ start_mode="POSITION_BEGINNING",
101
+ include_complete=True,
102
+ )
103
+
104
+ async def on_customer(evt):
105
+ if evt.event_type == "BASELOAD_EVENT":
106
+ ... # evt.complete is parsed JSON
107
+ elif evt.event_type == "SUBSCRIPTION_EVENT":
108
+ ... # live event
109
+
110
+ sub.on("Customer", on_customer)
111
+ await client.subscribe(sub) # blocks until sub.cancel()
112
+ ```
113
+
114
+ ## Codegen
115
+
116
+ ```sh
117
+ python -m tools.codegen.src.cli fetch # cache schema.json
118
+ python -m tools.codegen.src.cli emit Customer # Customer + everything it references
119
+ python -m tools.codegen.src.cli emit-all # whole model
120
+ ```
121
+
122
+ Output → `src/dataceen_client/generated/` (gitignored). Per entity you get
123
+ typed `TypedDict`s for read/create/filter/fields, a change-tracked `Update`
124
+ class, and an `<Entity>Client` wrapping the runtime helpers; `AllClient`
125
+ aggregates the top-level entities.
126
+
127
+ Codegen runs from a source checkout — it is intentionally not part of the
128
+ published wheel (only the runtime `dataceen_client` package is shipped).
129
+
130
+ ## Tests
131
+
132
+ ```sh
133
+ pytest # 88 unit tests, ~1.7s
134
+ RUN_INTEGRATION=1 pytest # also run live-backend integration tests (needs .env)
135
+ ```
136
+
137
+ ## Example app
138
+
139
+ End-to-end example against the live backend:
140
+
141
+ ```sh
142
+ python -m examples.candy_shop.find
143
+ python -m examples.candy_shop.search_and_aggregate
144
+ python -m examples.candy_shop.subscriber
145
+ python -m examples.candy_shop.program # full read-only orchestrator
146
+ RUN_DESTRUCTIVE=1 python -m examples.candy_shop.program # also create/update/delete
147
+ ```
148
+
149
+ ## Docs
150
+
151
+ - `docs/plan.md` — phase plan
152
+ - `docs/decisions.md` — decisions and motivation
153
+ - `docs/progress.md` — live status
154
+ - `docs/dsl-comparison.md` — why pure-dict DSL for Python
155
+ - `docs/porting-guide.md` — Python-specific amendments to the canonical guide
156
+ - `docs/release.md` — version-bump and publish runbook
157
+
158
+ ## License
159
+
160
+ Proprietary — see [LICENSE](LICENSE).
@@ -0,0 +1,124 @@
1
+ # dataceen-client (Python)
2
+
3
+ Python client for the Dataceen GraphQL + gRPC subscription APIs. Parallel to
4
+ the C# (`DataceenClient/`) and TypeScript (`DataceenClientTs/`) reference
5
+ clients — feature-parity verified against the same `Thomas/CandyShopModel/All`
6
+ fixtures.
7
+
8
+ > **Status:** 0.0.1 alpha. 88 unit tests green. Not yet on PyPI.
9
+
10
+ ## Install
11
+
12
+ ```sh
13
+ python -m venv .venv
14
+ .venv\Scripts\activate # PowerShell
15
+ pip install -e ".[dev]"
16
+ ```
17
+
18
+ Requires Python 3.11+ (`typing.NotRequired`, generic `TypedDict`).
19
+
20
+ ## Configure
21
+
22
+ Copy `.env.example` to `.env` at the repo root and fill in real Azure AD
23
+ credentials. The example matches the C# `appsettings.json`'s `AllClient`
24
+ section.
25
+
26
+ ## Quick start
27
+
28
+ ```python
29
+ import asyncio
30
+ from dataceen_client import DataceenClient, load_config_from_env
31
+ from dataceen_client.runtime import do_find
32
+
33
+ async def main():
34
+ cfg = load_config_from_env()
35
+ async with DataceenClient(cfg) as client:
36
+ result = await do_find(
37
+ client,
38
+ "Customer",
39
+ size=5,
40
+ filter={"CustomerId": {"like": "cst"}},
41
+ fields={
42
+ "_id": True,
43
+ "CustomerId": True,
44
+ "CustomerPlacedOrder": {"Order": {"OrderId": True}},
45
+ },
46
+ order_by=[{"CustomerId": "Ascending"}],
47
+ )
48
+ for item in result["Items"]:
49
+ print(item)
50
+
51
+ asyncio.run(main())
52
+ ```
53
+
54
+ After running codegen, replace `do_find(client, "Customer", ...)` with the
55
+ typed `AllClient(client).Customer.find(...)`.
56
+
57
+ ## Subscriptions
58
+
59
+ ```python
60
+ async with DataceenClient(cfg) as client:
61
+ sub = client.create_subscription(
62
+ topics=["Customer"],
63
+ baseload_topics=["Customer"],
64
+ start_mode="POSITION_BEGINNING",
65
+ include_complete=True,
66
+ )
67
+
68
+ async def on_customer(evt):
69
+ if evt.event_type == "BASELOAD_EVENT":
70
+ ... # evt.complete is parsed JSON
71
+ elif evt.event_type == "SUBSCRIPTION_EVENT":
72
+ ... # live event
73
+
74
+ sub.on("Customer", on_customer)
75
+ await client.subscribe(sub) # blocks until sub.cancel()
76
+ ```
77
+
78
+ ## Codegen
79
+
80
+ ```sh
81
+ python -m tools.codegen.src.cli fetch # cache schema.json
82
+ python -m tools.codegen.src.cli emit Customer # Customer + everything it references
83
+ python -m tools.codegen.src.cli emit-all # whole model
84
+ ```
85
+
86
+ Output → `src/dataceen_client/generated/` (gitignored). Per entity you get
87
+ typed `TypedDict`s for read/create/filter/fields, a change-tracked `Update`
88
+ class, and an `<Entity>Client` wrapping the runtime helpers; `AllClient`
89
+ aggregates the top-level entities.
90
+
91
+ Codegen runs from a source checkout — it is intentionally not part of the
92
+ published wheel (only the runtime `dataceen_client` package is shipped).
93
+
94
+ ## Tests
95
+
96
+ ```sh
97
+ pytest # 88 unit tests, ~1.7s
98
+ RUN_INTEGRATION=1 pytest # also run live-backend integration tests (needs .env)
99
+ ```
100
+
101
+ ## Example app
102
+
103
+ End-to-end example against the live backend:
104
+
105
+ ```sh
106
+ python -m examples.candy_shop.find
107
+ python -m examples.candy_shop.search_and_aggregate
108
+ python -m examples.candy_shop.subscriber
109
+ python -m examples.candy_shop.program # full read-only orchestrator
110
+ RUN_DESTRUCTIVE=1 python -m examples.candy_shop.program # also create/update/delete
111
+ ```
112
+
113
+ ## Docs
114
+
115
+ - `docs/plan.md` — phase plan
116
+ - `docs/decisions.md` — decisions and motivation
117
+ - `docs/progress.md` — live status
118
+ - `docs/dsl-comparison.md` — why pure-dict DSL for Python
119
+ - `docs/porting-guide.md` — Python-specific amendments to the canonical guide
120
+ - `docs/release.md` — version-bump and publish runbook
121
+
122
+ ## License
123
+
124
+ Proprietary — see [LICENSE](LICENSE).
@@ -0,0 +1,26 @@
1
+ # Beslutade defaults — Python-port
2
+
3
+ Logg av Python-specifika beslut. Server-imposed beslut (operatorernas casing,
4
+ PascalCase-svar, `like`-semantik etc.) ärvs från
5
+ `DataceenClientTs/docs/porting-guide.md` #19–47 och repeteras inte här.
6
+
7
+ ## 2026-05-01 — Projektstart
8
+
9
+ | # | Beslut | Motivering |
10
+ |---|---|---|
11
+ | 1 | **Python 3.10+ som minimum** | `X | Y` union-syntax, `dict[str, T]`-generics, `match` om vi behöver. 3.10 är välsupporterat överallt. |
12
+ | 2 | **Async-first API** (`httpx.AsyncClient`, `async def`) | Subscriptions är inneboende async (gRPC stream). Att blanda sync HTTP + async gRPC ger två kodvägar. Sync-wrapper kan adderas senare med `asyncio.run`. |
13
+ | 3 | **`httpx` istället för `requests` + `aiohttp`** | Modern, sync+async i samma paket, native HTTP/2. Default-timeout, AbortController-motsvarighet via `httpx.Timeout`. |
14
+ | 4 | **`msal` för Azure AD** | Officiell Microsoft-SDK, samma autentiseringsflöde som C# och TS. Har inbyggd token-cache. |
15
+ | 5 | **DSL-strategi A (pure dict)** | Se `docs/dsl-comparison.md`. Python's `__getattr__`-proxies ger ingen typsäkerhet och försämrar IDE-stöd; idiomatisk Python är `TypedDict` + `dict`. |
16
+ | 6 | **PascalCase i wire-typer** (`FindResult.Items`, `Customer._id`) | Matchar server-format 1:1. Att konvertera till snake_case skapar friktion vid paritet med C#/TS-dokumentation och GraphQL-strängar. Python-konsumenter får acceptera PascalCase-fält. |
17
+ | 7 | **`pytest` + `pytest-asyncio` + `respx`** | De facto-standard för Python-testning. `respx` för HTTP-mockning matchar Vitest/MSW-rollen i TS-porten. |
18
+ | 8 | **src-layout (`src/dataceen_client/`)** | Standardpraxis för moderna Python-paket; tvingar editable-installs att gå via paket-import istället för repo-rot. |
19
+ | 9 | **`force_refresh` via `app.remove_tokens_for_client()` före re-acquire** | MSAL Python's `acquire_token_for_client` har ingen `skip_cache`-parameter. Cleanest workaround: rensa app-only-cachen före nytt anrop på 401. |
20
+ | 10 | **Result-typer som `TypedDict` (total=False)** | PascalCase-nycklar bevarade. Pydantic kan komma som opt-in senare; `TypedDict` är dependency-free och räcker för read-modeller. |
21
+
22
+ ## Öppna frågor
23
+
24
+ - Sync-wrapper-API (`DataceenClientSync`)? Beslut tas i fas 6 om exempelappen visar att det behövs.
25
+ - `dataclass` vs `TypedDict` vs `pydantic.BaseModel` för genererade entitet-typer? Tas i fas 3.
26
+ - gRPC-stub-generering: `grpc_tools.protoc` mot `dataceenevent.proto` i build-steg eller checkat in? Tas i fas 4.
@@ -0,0 +1,125 @@
1
+ # DSL-jämförelse — Python-port
2
+
3
+ **Datum:** 2026-05-01
4
+ **Beslut:** Strategi A (pure dict / objekt-literal) — för **alla tre** delar
5
+ (filter, fields, orderBy).
6
+
7
+ Detta avviker från TS-porten som valde C (hybrid: dict-filter, proxy-fields).
8
+ Motiveringen följer.
9
+
10
+ ---
11
+
12
+ ## Use-case
13
+
14
+ Hämta 10 kunder vars `CustomerId` matchar `cst` OCH vars status är antingen
15
+ `Active` eller `Premium`. Returnera `_id`, `CustomerId`, `ExternalReference`
16
+ plus nästlad relation `CustomerPlacedOrder -> Order -> OrderId`. Sortera på
17
+ `CustomerId Ascending, ExternalReference Descending`.
18
+
19
+ ```python
20
+ await api.Customer.find(
21
+ size=10,
22
+ filter={
23
+ "and": [
24
+ {"CustomerId": {"like": "cst"}},
25
+ {
26
+ "or": [
27
+ {"Status": {"eq": "Active"}},
28
+ {"Status": {"eq": "Premium"}},
29
+ ],
30
+ },
31
+ ],
32
+ },
33
+ fields={
34
+ "_id": True,
35
+ "CustomerId": True,
36
+ "ExternalReference": True,
37
+ "CustomerPlacedOrder": {
38
+ "Order": {"OrderId": True},
39
+ },
40
+ },
41
+ order_by=[
42
+ {"CustomerId": "Ascending"},
43
+ {"ExternalReference": "Descending"},
44
+ ],
45
+ )
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Varför inte proxy-lambda för fields (som TS)?
51
+
52
+ Två separata problem på Python-sidan.
53
+
54
+ ### 1. Typchecker-stöd är sämre
55
+
56
+ `__getattr__` returnerar `Any` som default. mypy/pyright kan inte härleda
57
+ strukturen `f.CustomerPlacedOrder(lambda r: r.Order(...))` utan att vi skriver
58
+ overloadade `__getattr__`-stubbar per fält per entitet — vilket gör att hela
59
+ "proxy-elegansen" är beroende av codegen som ändå producerar lika mycket TS-typ-kod.
60
+
61
+ I TS funkar `FieldsProxy<T>` mapped types med konditionella inferens som faktiskt
62
+ ger autocomplete. Pythons typsystem har inga conditional types av motsvarande styrka.
63
+
64
+ ### 2. Statement-as-expression-mönstret är inte idiomatiskt
65
+
66
+ TS-porten skriver `f._id; f.CustomerId;` som hängande identifiers. Python tillåter
67
+ det syntaktiskt men är ovanligt; rinter och kollegor kommer att fråga "är det
68
+ en bug?". Lambdor på flera rader är dessutom inte tillåtet utan att gå via en
69
+ named function — vilket eliminerar lambda-syntaxens läsbarhetsfördel.
70
+
71
+ ---
72
+
73
+ ## Varför inte builders med method-chaining?
74
+
75
+ ```python
76
+ # Avfärdat
77
+ filter = (FilterBuilder[Customer]()
78
+ .where("CustomerId").like("cst")
79
+ .and_(FilterBuilder().where("Status").eq("Active")
80
+ .or_(FilterBuilder().where("Status").eq("Premium"))))
81
+ ```
82
+
83
+ - Verbose i Python (måste returnera `self` på varje metod).
84
+ - Ger inte bättre typstöd än dict + `TypedDict`.
85
+ - Lägger till en abstraktion som måste serialiseras tillbaka till samma struktur
86
+ som dict-input ändå producerar.
87
+
88
+ ---
89
+
90
+ ## Vad pure dict ger
91
+
92
+ - **Typsäkert via `TypedDict`** (genereras i fas 3): `CustomerFilter` är en
93
+ `TypedDict` per fält → IDE flaggar tryckfel direkt.
94
+ - **Triviell serialisering:** `filter_to_graphql(d)` är ren rekursion över strukturen.
95
+ - **Trivial codegen:** generatorn emit:ar bara typaliasar — inga runtime-byggare per entitet.
96
+ - **Lätt att kopiera mellan språk:** samma struktur fungerar i JSON, vilket är
97
+ praktiskt vid debug.
98
+
99
+ ---
100
+
101
+ ## Korrelationer mot porting-guide
102
+
103
+ `DataceenClientTs/docs/porting-guide.md` rekommenderar för Python:
104
+ > Object-literal (dict) for everything. `__getattr__`-proxies are ergonomic
105
+ > but trip static type checkers.
106
+
107
+ Detta beslut följer rekommendationen.
108
+
109
+ ---
110
+
111
+ ## Konsekvenser för fas 3 (generator)
112
+
113
+ Per entitet emit:ar generatorn bara *typer* (`TypedDict`):
114
+
115
+ 1. `Customer` (read-model)
116
+ 2. `CustomerCreate` (create-input)
117
+ 3. `CustomerUpdate` (klass med `__setattr__`-tracking)
118
+ 4. `CustomerFilter` (filter-input)
119
+ 5. `CustomerSearchFilter` (separat — inkluderar relationer)
120
+ 6. `CustomerOrderBy = list[dict[Literal[...field names...], OrderByDirection]]`
121
+ 7. `CustomerFields = TypedDict` (rekursivt)
122
+
123
+ Runtime-helpers (`filter_to_graphql`, `order_by_to_graphql`, `collect_fields`,
124
+ `do_find`, `do_search`, `do_create`, …) delas mellan alla entiteter — skrivs
125
+ en gång i `dataceen_client/runtime/`.
@@ -0,0 +1,143 @@
1
+ # Plan: portera Dataceen-klienten till Python
2
+
3
+ Parallell-klient-strategi: Python-klienten samexisterar med C#- och TS-klienterna,
4
+ ersätter dem inte. Tredje officiella språkporten efter `DataceenClient/` (C#) och
5
+ `DataceenClientTs/` (TypeScript).
6
+
7
+ ## Förutsättningar (beslutade defaults)
8
+
9
+ | Fråga | Beslut |
10
+ |---|---|
11
+ | Målversion | Python 3.10+ (typ-syntax `X | Y`, `dict[...]`) |
12
+ | Beroenden | `httpx` (HTTP), `msal` (Azure AD), `grpcio` + `protobuf` (subscriptions, optional) |
13
+ | Async-modell | `asyncio` — primär API är `async def` |
14
+ | Test-ramverk | `pytest` + `pytest-asyncio` + `respx` (HTTP mocking) |
15
+ | Repo-struktur | Eget paket under `C:\Source\Dataceen\DataceenClientPython\` |
16
+ | PyPI-namn | `dataceen-client` (tentativt) |
17
+ | Källlayout | `src/dataceen_client/` (src-layout) |
18
+ | Auth | Client-credentials via `msal.ConfidentialClientApplication` |
19
+ | DSL-strategi | **A — pure dict (objekt-literal)**. Se `docs/dsl-comparison.md`. |
20
+ | Typing | `TypedDict` + Pydantic v2 valbart i framtida fas; v1 levererar `dict`-baserade signaturer |
21
+
22
+ ## Komponentmappning C#/TS → Python
23
+
24
+ | C# / TS-komponent | Python-motsvarighet | Svårighet |
25
+ |---|---|---|
26
+ | `DataceenClient` (GraphQL över HTTP) | `httpx.AsyncClient` | Lätt |
27
+ | `DataceenClient` subscriptions (gRPC) | `grpcio` (async stub) | Medel |
28
+ | `TokenProvider` (MSAL) | `msal.ConfidentialClientApplication` | Lätt |
29
+ | `FilterBuilder<T>` / object-literal-filter | `dict` med typade `TypedDict`-aliaser | Lätt |
30
+ | `OrderByBuilder<T>` / array | `list[dict[str, Literal["Ascending","Descending"]]]` | Trivialt |
31
+ | `FieldsBuilder<T>` / proxy-lambda | Nested `dict`: `{"_id": True, "Order": {"OrderId": True}}` | Lätt |
32
+ | `CustomerUpdate` med ändringsspårning | Klass med `__setattr__`-hook + `_changed: set[str]` | Medel |
33
+ | `FindResult<T>`, `SearchResult<T>`, `MutationResult` | `TypedDict` (PascalCase för wire-paritet) | Trivialt |
34
+ | Genererad `AllClient` | Codegen i fas 3+ — emit:ar `.py`-moduler från GraphQL-introspection | Stort men rakframt |
35
+ | `EventHandlerList` + typade delegates | `dict[type, list[Callable]]` | Lätt |
36
+ | `PostGraphQLAsync` (retry/auth) | Custom retry-loop runt `httpx` | Lätt |
37
+
38
+ ## Fasplan
39
+
40
+ Följer samma faser som TS-porten enligt `DataceenClientTs/docs/porting-guide.md`.
41
+
42
+ ### Fas 0 — Spike (1–2 dagar)
43
+ - Token via `msal.ConfidentialClientApplication.acquire_token_for_client`
44
+ - GraphQL POST mot `apiUrl`
45
+ - gRPC subscription-stream mot `subscriptionUrl`
46
+ - Introspection POST → cacha `tools/codegen/cache/schema.json`
47
+
48
+ ### Fas 1 — Kärn-runtime (3–5 dagar) — **denna sessionen**
49
+ Generisk `DataceenClient`-kärna utan typgenerering.
50
+
51
+ ```
52
+ src/dataceen_client/
53
+ __init__.py
54
+ config.py # DataceenConfig dataclass + load_config_from_env()
55
+ errors.py # DataceenError
56
+ types.py # FindResult, SearchResult, MutationResult (TypedDict)
57
+ token_provider.py # TokenProvider (MSAL)
58
+ http_transport.py # HttpTransport.post_graphql() — 401-refresh + retry
59
+ dataceen_client.py # DataceenClient.execute_query/execute_mutation
60
+ runtime/
61
+ filter.py # filter_to_graphql()
62
+ order_by.py # order_by_to_graphql()
63
+ fields.py # collect_fields()
64
+ aggregations.py # aggregation_fragment(depth)
65
+ operations.py # do_find / do_search / do_create / do_update / do_delete
66
+ ```
67
+
68
+ ### Fas 2 — DSL-prototyper + beslut (1 dag)
69
+ **Beslut redan taget:** strategi A (pure dict). Python's typcheckare har svårt med
70
+ proxy-attribute-access utan att tappa typsäkerhet. Dict + `TypedDict` är det
71
+ idiomatiska valet. Prototyparbete behövs alltså inte — kort dokumentation i
72
+ `docs/dsl-comparison.md`.
73
+
74
+ ### Fas 3 — Kodgenerator v1 (1–2 veckor)
75
+ Generera typade `.py`-filer per entitet från GraphQL-introspection.
76
+
77
+ ```
78
+ tools/codegen/
79
+ src/
80
+ fetch_schema.py
81
+ parse_schema.py
82
+ emit_model.py
83
+ emit_filter.py
84
+ emit_create.py
85
+ emit_update.py
86
+ emit_client.py
87
+ cli.py
88
+ ```
89
+
90
+ Output i `src/dataceen_client/generated/`.
91
+
92
+ ### Fas 4 — Subscriptions (3–5 dagar)
93
+ gRPC-baserad subscription-klient med baseload-semantik.
94
+
95
+ ```
96
+ src/dataceen_client/subscriptions/
97
+ __init__.py
98
+ types.py
99
+ subscription.py
100
+ subscription_manager.py
101
+ grpc_client.py
102
+ ```
103
+
104
+ ### Fas 5 — Full modell (3–5 dagar)
105
+ Alla 9 noder + 18 relationer + ~12 sub-entiteter för CandyShopModel. Compound
106
+ mutations (`Create{From}{Rel}{To}`) + aggregations.
107
+
108
+ ### Fas 6 — Exempel-app (3–5 dagar)
109
+ Port av `examples/candy-shop/` från TS:
110
+
111
+ ```
112
+ examples/candy_shop/
113
+ config.py
114
+ program.py
115
+ find.py
116
+ search_and_aggregate.py
117
+ subscriber.py
118
+ create.py / update.py / delete.py
119
+ handlers/
120
+ ```
121
+
122
+ ### Fas 7 — Paketering & release (2–3 dagar)
123
+ - `dataceen-client` på PyPI
124
+ - Sphinx-dokumentation (eller MkDocs)
125
+ - CI: GitHub Actions
126
+
127
+ ## Kritiska beslutpunkter
128
+
129
+ | # | Beslut | Status |
130
+ |---|---|---|
131
+ | 1 | Parallell eller ersättande klient? | Parallell ✅ |
132
+ | 2 | Sync- eller async-API? | Async-first (`httpx.AsyncClient`); sync-wrapper kan läggas till senare |
133
+ | 3 | DSL-strategi | A (pure dict) ✅ — Python type-checkers fungerar bäst med strukturerad data |
134
+ | 4 | Schemakälla för generator | GraphQL introspection (samma som TS) |
135
+ | 5 | Validering av inputs | Lazy: `dict` skickas direkt; runtime-fel kommer från servern. Pydantic-overlay valbart senare. |
136
+ | 6 | Pydantic-modeller? | Optional opt-in i fas 3 — kan emit:as parallellt med `TypedDict` |
137
+
138
+ ## Referensfiler
139
+
140
+ - `C:\Source\Dataceen\DataceenClient\` — C#-klienten (kanonisk)
141
+ - `C:\Source\Dataceen\DataceenClientTs\` — TS-klienten (verifierad mot live)
142
+ - `C:\Source\Dataceen\DataceenClientTs\docs\porting-guide.md` — kanonisk porting-guide
143
+ - `C:\Source\Dataceen\DataceenEvent\Protos\dataceenevent.proto` — gRPC-kontrakt