cluebase-backend-sdk 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.
- cluebase_backend_sdk-0.0.1/PKG-INFO +193 -0
- cluebase_backend_sdk-0.0.1/README.md +136 -0
- cluebase_backend_sdk-0.0.1/pyproject.toml +180 -0
- cluebase_backend_sdk-0.0.1/setup.cfg +4 -0
- cluebase_backend_sdk-0.0.1/setup.py +20 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/__init__.py +484 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_cluebase_authored_attributes.py +37 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/__init__.py +11 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/correlation_attribute_keys.py +37 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/identity_pii_policy.py +62 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/ingest_privacy_tables.py +727 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/otel_resource_keys.py +62 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/sdk_hint_vocabulary.py +54 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/usage_governor_tables.py +52 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_generated/wire_credential_policy.py +24 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_init_options.py +156 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/__init__.py +10 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/__init__.py +10 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/_response_parsing.py +144 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/_span_attributes.py +249 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/_version.py +5 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/instrumentation/__init__.py +1 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/instrumentation/anthropic.py +526 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/instrumentation/base.py +543 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/instrumentation/openai.py +250 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/ai/observer.py +774 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/crewai/__init__.py +29 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/crewai/observer.py +393 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/django/__init__.py +43 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/django/django.py +85 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/django/install.py +154 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/fastapi/__init__.py +35 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/fastapi/fastapi.py +432 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/langchain/__init__.py +28 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/langchain/_run_tracker.py +127 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/langchain/callback.py +505 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/langchain/instrument.py +123 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/llamaindex/__init__.py +28 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/llamaindex/_run_tracker.py +124 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/llamaindex/event_handler.py +320 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/llamaindex/instrument.py +93 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/mcp/__init__.py +14 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/mcp/_method_observations.py +569 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/mcp/_observer_helpers.py +530 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/mcp/_tool_observation.py +346 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/mcp/middleware.py +103 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/mcp/observer.py +660 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_integrations/mcp/session.py +105 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_internal_log.py +51 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_safe_logger.py +32 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/_version.py +5 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/context/__init__.py +9 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/context/parameter_snapshot.py +183 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/__init__.py +10 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/client.py +574 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/collection_gate.py +217 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/contracts.py +577 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/deadline.py +36 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/delivery.py +509 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/delivery_runner.py +148 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/diagnostics.py +216 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/envelope.py +60 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/event_id.py +74 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/event_size.py +98 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/http_transport.py +262 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/lifecycle.py +91 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/observation_source_event_exporter.py +213 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/otel_bridge.py +192 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/pending.py +394 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/privacy.py +313 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/profile.py +149 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/sanitize.py +39 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/source_signal.py +299 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/source_signal_context.py +53 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/source_signal_privacy.py +88 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/source_signal_record_privacy.py +193 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/exporter/wire_credentials.py +33 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/__init__.py +10 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/_orm_django.py +188 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/_orm_spans.py +271 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/_orm_sqlalchemy.py +337 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/_orm_state.py +100 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/celery.py +539 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/http_extraction.py +158 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/httpx.py +101 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/orm.py +44 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/requests.py +254 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/server_request.py +104 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/instrumentation/span_attributes.py +71 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/__init__.py +10 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/_pipeline.py +286 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/_runtime_state.py +691 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/_runtime_telemetry.py +873 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/bootstrap.py +179 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/consent_span_processor.py +103 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/privacy_policy_store.py +115 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/resources.py +96 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/runtime.py +258 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/runtime_context.py +43 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/runtime_diagnostics.py +66 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk/provider/runtime_lifecycle.py +74 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk.egg-info/PKG-INFO +193 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk.egg-info/SOURCES.txt +104 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk.egg-info/dependency_links.txt +1 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk.egg-info/requires.txt +58 -0
- cluebase_backend_sdk-0.0.1/src/cluebase_backend_sdk.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cluebase-backend-sdk
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Cluebase Backend SDK (Python) — OTel-based observation-source event capture for Django, FastAPI, Celery, SQLAlchemy, LangChain, LlamaIndex, CrewAI, MCP, and AI providers.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: opentelemetry-api<2.0,>=1.41.1
|
|
8
|
+
Requires-Dist: opentelemetry-sdk<2.0,>=1.41.1
|
|
9
|
+
Requires-Dist: rfc8785<0.2,>=0.1.4
|
|
10
|
+
Provides-Extra: mutation
|
|
11
|
+
Requires-Dist: mutmut<4.0,>=3.2; extra == "mutation"
|
|
12
|
+
Provides-Extra: instrumentation-requests
|
|
13
|
+
Requires-Dist: opentelemetry-instrumentation-requests<1.0,>=0.63b1; extra == "instrumentation-requests"
|
|
14
|
+
Provides-Extra: instrumentation-httpx
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-httpx<1.0,>=0.63b1; extra == "instrumentation-httpx"
|
|
16
|
+
Provides-Extra: instrumentation-fastapi
|
|
17
|
+
Requires-Dist: fastapi<1.0,>=0.100; extra == "instrumentation-fastapi"
|
|
18
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi<1.0,>=0.63b1; extra == "instrumentation-fastapi"
|
|
19
|
+
Provides-Extra: instrumentation-asgi
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-asgi<1.0,>=0.63b1; extra == "instrumentation-asgi"
|
|
21
|
+
Provides-Extra: instrumentation-sqlalchemy
|
|
22
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy<1.0,>=0.63b1; extra == "instrumentation-sqlalchemy"
|
|
23
|
+
Provides-Extra: instrumentation-django
|
|
24
|
+
Requires-Dist: opentelemetry-instrumentation-django<1.0,>=0.63b1; extra == "instrumentation-django"
|
|
25
|
+
Provides-Extra: instrumentation-celery
|
|
26
|
+
Requires-Dist: opentelemetry-instrumentation-celery<1.0,>=0.63b1; extra == "instrumentation-celery"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-cov<7.0,>=5.0; extra == "test"
|
|
30
|
+
Requires-Dist: pyright<1.2,>=1.1.411; extra == "test"
|
|
31
|
+
Requires-Dist: build<2.0,>=1.2; extra == "test"
|
|
32
|
+
Requires-Dist: packaging<26,>=25; extra == "test"
|
|
33
|
+
Requires-Dist: twine<7.0,>=6.1; extra == "test"
|
|
34
|
+
Requires-Dist: opentelemetry-instrumentation-requests<1.0,>=0.63b1; extra == "test"
|
|
35
|
+
Requires-Dist: opentelemetry-instrumentation-httpx<1.0,>=0.63b1; extra == "test"
|
|
36
|
+
Requires-Dist: opentelemetry-instrumentation-fastapi<1.0,>=0.63b1; extra == "test"
|
|
37
|
+
Requires-Dist: opentelemetry-instrumentation-asgi<1.0,>=0.63b1; extra == "test"
|
|
38
|
+
Requires-Dist: opentelemetry-instrumentation-sqlalchemy<1.0,>=0.63b1; extra == "test"
|
|
39
|
+
Requires-Dist: opentelemetry-instrumentation-django<1.0,>=0.63b1; extra == "test"
|
|
40
|
+
Requires-Dist: opentelemetry-instrumentation-celery<1.0,>=0.63b1; extra == "test"
|
|
41
|
+
Requires-Dist: requests>=2.31; extra == "test"
|
|
42
|
+
Requires-Dist: httpx>=0.26; extra == "test"
|
|
43
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "test"
|
|
44
|
+
Requires-Dist: django<6.0,>=5.0; extra == "test"
|
|
45
|
+
Requires-Dist: fastapi<1.0,>=0.100; extra == "test"
|
|
46
|
+
Requires-Dist: celery<6.0,>=5.0; extra == "test"
|
|
47
|
+
Requires-Dist: langchain-core<1.0,>=0.3; extra == "test"
|
|
48
|
+
Requires-Dist: llama-index-core<1.0,>=0.12; extra == "test"
|
|
49
|
+
Requires-Dist: crewai<1.16,>=1.15.1; extra == "test"
|
|
50
|
+
Requires-Dist: openai==2.45.0; extra == "test"
|
|
51
|
+
Requires-Dist: anthropic==0.116.0; extra == "test"
|
|
52
|
+
Requires-Dist: opentelemetry-instrumentation-openai==0.62.1; extra == "test"
|
|
53
|
+
Requires-Dist: opentelemetry-instrumentation-anthropic==0.62.1; extra == "test"
|
|
54
|
+
Requires-Dist: aiosqlite==0.21.0; extra == "test"
|
|
55
|
+
Requires-Dist: setuptools<83,>=68; extra == "test"
|
|
56
|
+
Dynamic: requires-python
|
|
57
|
+
|
|
58
|
+
# cluebase-backend-sdk
|
|
59
|
+
|
|
60
|
+
Shared Python backend core for Cluebase SDK wrappers (Django, FastAPI,
|
|
61
|
+
Celery). Events flow through an OTel `TracerProvider` + `ConsentSpanProcessor` +
|
|
62
|
+
`ObservationSourceEventExporter` and the shared delivery queue to
|
|
63
|
+
`/api/v1/ingest/backend`.
|
|
64
|
+
|
|
65
|
+
## Minimal integration
|
|
66
|
+
|
|
67
|
+
The SDK exposes a single ``cluebase`` namespace with eight methods (``init`` /
|
|
68
|
+
``identify`` / ``group`` / ``reset`` / ``track`` / ``set_consent`` /
|
|
69
|
+
``flush`` / ``close``). This mirrors the cross-SDK contract documented in
|
|
70
|
+
``docs/contracts/sdk/public-api.md`` §1.6.
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
from cluebase_backend_sdk import cluebase
|
|
75
|
+
|
|
76
|
+
cluebase.init({
|
|
77
|
+
"endpoint": "https://api.example.com",
|
|
78
|
+
"project_key": "...",
|
|
79
|
+
"api_key": "...",
|
|
80
|
+
"service_key": "backend-api",
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
cluebase.identify("user_42", {"name": "Alice", "email": "alice@example.com"})
|
|
84
|
+
cluebase.group("organization", "org_7", {"name": "Acme Inc"})
|
|
85
|
+
cluebase.track("order_placed", {"product_name": "shirt", "amount": 100})
|
|
86
|
+
cluebase.set_consent("granted")
|
|
87
|
+
|
|
88
|
+
# Shutdown idiom — both are coroutines for cross-SDK parity.
|
|
89
|
+
asyncio.run(cluebase.flush())
|
|
90
|
+
asyncio.run(cluebase.close())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Do not pass ``environment`` or ``service_name`` in the MVP setup path.
|
|
94
|
+
Cluebase derives dev/prod from the project key prefix, and ``service_key`` is the
|
|
95
|
+
single literal customer-facing backend service label.
|
|
96
|
+
|
|
97
|
+
``cluebase.track`` custom event names must match
|
|
98
|
+
``^[A-Za-z0-9_.:-]{1,128}$``. Invalid names are ignored and do not emit
|
|
99
|
+
a source-signal custom track intent. ``cluebase.set_consent`` only records local
|
|
100
|
+
SDK state in the MVP backend SDK; it does not emit consent transition events.
|
|
101
|
+
|
|
102
|
+
Framework wrappers and runtime helpers are imported from their owning modules.
|
|
103
|
+
The package root is intentionally small: customer-facing lifecycle methods plus
|
|
104
|
+
stable setup helpers. Direct event builders are not public extension
|
|
105
|
+
points.
|
|
106
|
+
|
|
107
|
+
## Architecture boundary
|
|
108
|
+
|
|
109
|
+
The Python SDK is a thin language wrapper around OpenTelemetry and the Cluebase
|
|
110
|
+
transport. Framework integrations create spans or annotate the current span,
|
|
111
|
+
the exporter serializes those spans as ``sdk_source_signal_observed`` events,
|
|
112
|
+
and the Cluebase backend ingest core performs classification, normalization,
|
|
113
|
+
privacy projection, and raw-ingest shaping.
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
Python framework / library hook
|
|
117
|
+
-> OTel span or Cluebase source-signal attributes
|
|
118
|
+
-> ObservationSourceEventExporter
|
|
119
|
+
-> /api/v1/ingest/backend
|
|
120
|
+
-> apps/api/src/modules/ingest core
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Rules:
|
|
124
|
+
|
|
125
|
+
- Keep framework code limited to raw facts, correlation context, and standard
|
|
126
|
+
OTel attributes.
|
|
127
|
+
- Do not add Python-only classification, direct event builders, or custom
|
|
128
|
+
fallback behavior.
|
|
129
|
+
- Add classifier behavior in the backend ingest core so Node.js, Python, and
|
|
130
|
+
future SDK wrappers receive the same behavior.
|
|
131
|
+
- Preserve ``interaction_id``, ``request_span_id``, ``request_id``, and
|
|
132
|
+
``trace_id`` whenever they are available. User-action linkage is an ingest
|
|
133
|
+
contract, not a display-only field.
|
|
134
|
+
|
|
135
|
+
## Privacy and PII handling
|
|
136
|
+
|
|
137
|
+
### 1. Hard-deny: PII / secrets are stripped before transport
|
|
138
|
+
|
|
139
|
+
The SDK strips a built-in set of property keys **before** the event
|
|
140
|
+
leaves the customer process. Caller-supplied `denied_keys` are added
|
|
141
|
+
on top of `DEFAULT_DENIED_KEYS` — they cannot remove a default-denied
|
|
142
|
+
key.
|
|
143
|
+
|
|
144
|
+
Hard-deny categories (case- and separator-insensitive — `userEmail`,
|
|
145
|
+
`user-email`, `USER_EMAIL`, `email_address` all match):
|
|
146
|
+
|
|
147
|
+
- Auth credentials: `authorization`, `cookie`, `set-cookie`,
|
|
148
|
+
`password`, `passwd`, `secret`, `token`, `access_token`,
|
|
149
|
+
`refresh_token`, `session`, `session_token`, `api_key`, `apikey`,
|
|
150
|
+
`private_key`
|
|
151
|
+
- PII categories: `email`, `phone`, `credit_card`, `ssn`
|
|
152
|
+
|
|
153
|
+
This list is a strict superset of the server-side ingest hard-deny
|
|
154
|
+
(`@cluebase/shared` `INGEST_HARD_DENY_KEYS`), enforced by the
|
|
155
|
+
`tests/test_privacy_parity.py` cross-SDK parity test.
|
|
156
|
+
|
|
157
|
+
### 2. HTTP request values
|
|
158
|
+
|
|
159
|
+
This SDK does not read HTTP request or response bodies. The value surface it can
|
|
160
|
+
observe is the request query, and its values are masked by default. A value is
|
|
161
|
+
sent in plaintext only when the customer names that field on the Cluebase screen; the
|
|
162
|
+
decision is delivered to the SDK and re-checked by ingest. Hard-denied keys such
|
|
163
|
+
as `email` can never be released, whatever the screen says. See
|
|
164
|
+
`docs/arch/sdk/backend-sdk.md` for the boundary and for what would be required to
|
|
165
|
+
bring bodies into scope.
|
|
166
|
+
|
|
167
|
+
### 3. Analysis projection and later registration
|
|
168
|
+
|
|
169
|
+
Properties that pass the hard-deny gate are projected by default unless a
|
|
170
|
+
cardinality, free-text, or complex-value guard keeps them out of the analysis
|
|
171
|
+
index. Guarded non-private values remain available in
|
|
172
|
+
`analysisProperties.raw_only` so they can be registered and promoted later.
|
|
173
|
+
The map stores each original value as JSON so its type can be restored.
|
|
174
|
+
Hard-denied values are dropped and never stored in `raw_only`. After a privacy
|
|
175
|
+
review, use the project service allowlist to promote a guarded non-private
|
|
176
|
+
property into the analysis projection.
|
|
177
|
+
|
|
178
|
+
### 4. Identity traits stay privacy-safe
|
|
179
|
+
|
|
180
|
+
`cluebase.identify(user_id, traits={...})` accepts profile traits such as
|
|
181
|
+
`name`, `email`, and `avatarUrl`. Other customer-defined values remain generic
|
|
182
|
+
subject traits instead of becoming fixed Cluebase profile fields. Use a stable opaque
|
|
183
|
+
`user_id`; the SDK converts `email` into privacy-safe contact-derived
|
|
184
|
+
fields before canonical identity events leave the process.
|
|
185
|
+
|
|
186
|
+
## Build / test
|
|
187
|
+
|
|
188
|
+
```sh
|
|
189
|
+
python3 -m venv .venv
|
|
190
|
+
source .venv/bin/activate
|
|
191
|
+
pip install -e '.[test]'
|
|
192
|
+
python -m pytest
|
|
193
|
+
```
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# cluebase-backend-sdk
|
|
2
|
+
|
|
3
|
+
Shared Python backend core for Cluebase SDK wrappers (Django, FastAPI,
|
|
4
|
+
Celery). Events flow through an OTel `TracerProvider` + `ConsentSpanProcessor` +
|
|
5
|
+
`ObservationSourceEventExporter` and the shared delivery queue to
|
|
6
|
+
`/api/v1/ingest/backend`.
|
|
7
|
+
|
|
8
|
+
## Minimal integration
|
|
9
|
+
|
|
10
|
+
The SDK exposes a single ``cluebase`` namespace with eight methods (``init`` /
|
|
11
|
+
``identify`` / ``group`` / ``reset`` / ``track`` / ``set_consent`` /
|
|
12
|
+
``flush`` / ``close``). This mirrors the cross-SDK contract documented in
|
|
13
|
+
``docs/contracts/sdk/public-api.md`` §1.6.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
import asyncio
|
|
17
|
+
from cluebase_backend_sdk import cluebase
|
|
18
|
+
|
|
19
|
+
cluebase.init({
|
|
20
|
+
"endpoint": "https://api.example.com",
|
|
21
|
+
"project_key": "...",
|
|
22
|
+
"api_key": "...",
|
|
23
|
+
"service_key": "backend-api",
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
cluebase.identify("user_42", {"name": "Alice", "email": "alice@example.com"})
|
|
27
|
+
cluebase.group("organization", "org_7", {"name": "Acme Inc"})
|
|
28
|
+
cluebase.track("order_placed", {"product_name": "shirt", "amount": 100})
|
|
29
|
+
cluebase.set_consent("granted")
|
|
30
|
+
|
|
31
|
+
# Shutdown idiom — both are coroutines for cross-SDK parity.
|
|
32
|
+
asyncio.run(cluebase.flush())
|
|
33
|
+
asyncio.run(cluebase.close())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Do not pass ``environment`` or ``service_name`` in the MVP setup path.
|
|
37
|
+
Cluebase derives dev/prod from the project key prefix, and ``service_key`` is the
|
|
38
|
+
single literal customer-facing backend service label.
|
|
39
|
+
|
|
40
|
+
``cluebase.track`` custom event names must match
|
|
41
|
+
``^[A-Za-z0-9_.:-]{1,128}$``. Invalid names are ignored and do not emit
|
|
42
|
+
a source-signal custom track intent. ``cluebase.set_consent`` only records local
|
|
43
|
+
SDK state in the MVP backend SDK; it does not emit consent transition events.
|
|
44
|
+
|
|
45
|
+
Framework wrappers and runtime helpers are imported from their owning modules.
|
|
46
|
+
The package root is intentionally small: customer-facing lifecycle methods plus
|
|
47
|
+
stable setup helpers. Direct event builders are not public extension
|
|
48
|
+
points.
|
|
49
|
+
|
|
50
|
+
## Architecture boundary
|
|
51
|
+
|
|
52
|
+
The Python SDK is a thin language wrapper around OpenTelemetry and the Cluebase
|
|
53
|
+
transport. Framework integrations create spans or annotate the current span,
|
|
54
|
+
the exporter serializes those spans as ``sdk_source_signal_observed`` events,
|
|
55
|
+
and the Cluebase backend ingest core performs classification, normalization,
|
|
56
|
+
privacy projection, and raw-ingest shaping.
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
Python framework / library hook
|
|
60
|
+
-> OTel span or Cluebase source-signal attributes
|
|
61
|
+
-> ObservationSourceEventExporter
|
|
62
|
+
-> /api/v1/ingest/backend
|
|
63
|
+
-> apps/api/src/modules/ingest core
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Rules:
|
|
67
|
+
|
|
68
|
+
- Keep framework code limited to raw facts, correlation context, and standard
|
|
69
|
+
OTel attributes.
|
|
70
|
+
- Do not add Python-only classification, direct event builders, or custom
|
|
71
|
+
fallback behavior.
|
|
72
|
+
- Add classifier behavior in the backend ingest core so Node.js, Python, and
|
|
73
|
+
future SDK wrappers receive the same behavior.
|
|
74
|
+
- Preserve ``interaction_id``, ``request_span_id``, ``request_id``, and
|
|
75
|
+
``trace_id`` whenever they are available. User-action linkage is an ingest
|
|
76
|
+
contract, not a display-only field.
|
|
77
|
+
|
|
78
|
+
## Privacy and PII handling
|
|
79
|
+
|
|
80
|
+
### 1. Hard-deny: PII / secrets are stripped before transport
|
|
81
|
+
|
|
82
|
+
The SDK strips a built-in set of property keys **before** the event
|
|
83
|
+
leaves the customer process. Caller-supplied `denied_keys` are added
|
|
84
|
+
on top of `DEFAULT_DENIED_KEYS` — they cannot remove a default-denied
|
|
85
|
+
key.
|
|
86
|
+
|
|
87
|
+
Hard-deny categories (case- and separator-insensitive — `userEmail`,
|
|
88
|
+
`user-email`, `USER_EMAIL`, `email_address` all match):
|
|
89
|
+
|
|
90
|
+
- Auth credentials: `authorization`, `cookie`, `set-cookie`,
|
|
91
|
+
`password`, `passwd`, `secret`, `token`, `access_token`,
|
|
92
|
+
`refresh_token`, `session`, `session_token`, `api_key`, `apikey`,
|
|
93
|
+
`private_key`
|
|
94
|
+
- PII categories: `email`, `phone`, `credit_card`, `ssn`
|
|
95
|
+
|
|
96
|
+
This list is a strict superset of the server-side ingest hard-deny
|
|
97
|
+
(`@cluebase/shared` `INGEST_HARD_DENY_KEYS`), enforced by the
|
|
98
|
+
`tests/test_privacy_parity.py` cross-SDK parity test.
|
|
99
|
+
|
|
100
|
+
### 2. HTTP request values
|
|
101
|
+
|
|
102
|
+
This SDK does not read HTTP request or response bodies. The value surface it can
|
|
103
|
+
observe is the request query, and its values are masked by default. A value is
|
|
104
|
+
sent in plaintext only when the customer names that field on the Cluebase screen; the
|
|
105
|
+
decision is delivered to the SDK and re-checked by ingest. Hard-denied keys such
|
|
106
|
+
as `email` can never be released, whatever the screen says. See
|
|
107
|
+
`docs/arch/sdk/backend-sdk.md` for the boundary and for what would be required to
|
|
108
|
+
bring bodies into scope.
|
|
109
|
+
|
|
110
|
+
### 3. Analysis projection and later registration
|
|
111
|
+
|
|
112
|
+
Properties that pass the hard-deny gate are projected by default unless a
|
|
113
|
+
cardinality, free-text, or complex-value guard keeps them out of the analysis
|
|
114
|
+
index. Guarded non-private values remain available in
|
|
115
|
+
`analysisProperties.raw_only` so they can be registered and promoted later.
|
|
116
|
+
The map stores each original value as JSON so its type can be restored.
|
|
117
|
+
Hard-denied values are dropped and never stored in `raw_only`. After a privacy
|
|
118
|
+
review, use the project service allowlist to promote a guarded non-private
|
|
119
|
+
property into the analysis projection.
|
|
120
|
+
|
|
121
|
+
### 4. Identity traits stay privacy-safe
|
|
122
|
+
|
|
123
|
+
`cluebase.identify(user_id, traits={...})` accepts profile traits such as
|
|
124
|
+
`name`, `email`, and `avatarUrl`. Other customer-defined values remain generic
|
|
125
|
+
subject traits instead of becoming fixed Cluebase profile fields. Use a stable opaque
|
|
126
|
+
`user_id`; the SDK converts `email` into privacy-safe contact-derived
|
|
127
|
+
fields before canonical identity events leave the process.
|
|
128
|
+
|
|
129
|
+
## Build / test
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
python3 -m venv .venv
|
|
133
|
+
source .venv/bin/activate
|
|
134
|
+
pip install -e '.[test]'
|
|
135
|
+
python -m pytest
|
|
136
|
+
```
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cluebase-backend-sdk"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "Cluebase Backend SDK (Python) — OTel-based observation-source event capture for Django, FastAPI, Celery, SQLAlchemy, LangChain, LlamaIndex, CrewAI, MCP, and AI providers."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
# OTel SDK is required because the backend SDK emits through TracerProvider,
|
|
8
|
+
# BatchSpanProcessor, and ObservationSourceEventExporter.
|
|
9
|
+
dependencies = [
|
|
10
|
+
"opentelemetry-api>=1.41.1,<2.0",
|
|
11
|
+
"opentelemetry-sdk>=1.41.1,<2.0",
|
|
12
|
+
"rfc8785>=0.1.4,<0.2",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
# 断定の強さを測る道具(`[tool.mutmut]` が設定を持つ)。**`test` とは分けます。**
|
|
17
|
+
# 変異試験だけを入れて走らせられる形にしないと、設定だけが在って 1 度も動かない
|
|
18
|
+
# 状態になります(実測 2026-08-22: 設定は書かれているのに実行体が入っておらず、
|
|
19
|
+
# CI からも呼ばれていませんでした)。
|
|
20
|
+
mutation = ["mutmut>=3.2,<4.0"]
|
|
21
|
+
# Auto-instrumentation packages are loaded lazily by each
|
|
22
|
+
# ``instrument_*`` helper. Downstream consumers opt into the ones they need
|
|
23
|
+
# via these extras; the helpers degrade to a no-op when the corresponding
|
|
24
|
+
# instrumentation is not installed.
|
|
25
|
+
instrumentation-requests = ["opentelemetry-instrumentation-requests>=0.63b1,<1.0"]
|
|
26
|
+
instrumentation-httpx = ["opentelemetry-instrumentation-httpx>=0.63b1,<1.0"]
|
|
27
|
+
instrumentation-fastapi = [
|
|
28
|
+
"fastapi>=0.100,<1.0",
|
|
29
|
+
"opentelemetry-instrumentation-fastapi>=0.63b1,<1.0",
|
|
30
|
+
]
|
|
31
|
+
instrumentation-asgi = ["opentelemetry-instrumentation-asgi>=0.63b1,<1.0"]
|
|
32
|
+
instrumentation-sqlalchemy = ["opentelemetry-instrumentation-sqlalchemy>=0.63b1,<1.0"]
|
|
33
|
+
instrumentation-django = ["opentelemetry-instrumentation-django>=0.63b1,<1.0"]
|
|
34
|
+
instrumentation-celery = ["opentelemetry-instrumentation-celery>=0.63b1,<1.0"]
|
|
35
|
+
test = [
|
|
36
|
+
"pytest>=8.0,<9.0",
|
|
37
|
+
"pytest-cov>=5.0,<7.0",
|
|
38
|
+
"pyright>=1.1.411,<1.2",
|
|
39
|
+
"build>=1.2,<2.0",
|
|
40
|
+
"packaging>=25,<26",
|
|
41
|
+
"twine>=6.1,<7.0",
|
|
42
|
+
"opentelemetry-instrumentation-requests>=0.63b1,<1.0",
|
|
43
|
+
"opentelemetry-instrumentation-httpx>=0.63b1,<1.0",
|
|
44
|
+
"opentelemetry-instrumentation-fastapi>=0.63b1,<1.0",
|
|
45
|
+
"opentelemetry-instrumentation-asgi>=0.63b1,<1.0",
|
|
46
|
+
"opentelemetry-instrumentation-sqlalchemy>=0.63b1,<1.0",
|
|
47
|
+
"opentelemetry-instrumentation-django>=0.63b1,<1.0",
|
|
48
|
+
"opentelemetry-instrumentation-celery>=0.63b1,<1.0",
|
|
49
|
+
# The `instrument_*` helpers verify peer libraries before activating
|
|
50
|
+
# upstream instrumentors. These runtime peers are not transitive deps of
|
|
51
|
+
# the OTel instrumentors, so tests install them explicitly.
|
|
52
|
+
"requests>=2.31",
|
|
53
|
+
"httpx>=0.26",
|
|
54
|
+
"sqlalchemy>=2.0",
|
|
55
|
+
"django>=5.0,<6.0",
|
|
56
|
+
"fastapi>=0.100,<1.0",
|
|
57
|
+
"celery>=5.0,<6.0",
|
|
58
|
+
"langchain-core>=0.3,<1.0",
|
|
59
|
+
"llama-index-core>=0.12,<1.0",
|
|
60
|
+
"crewai>=1.15.1,<1.16",
|
|
61
|
+
"openai==2.45.0",
|
|
62
|
+
"anthropic==0.116.0",
|
|
63
|
+
"opentelemetry-instrumentation-openai==0.62.1",
|
|
64
|
+
"opentelemetry-instrumentation-anthropic==0.62.1",
|
|
65
|
+
"aiosqlite==0.21.0",
|
|
66
|
+
"setuptools>=68,<83",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
[build-system]
|
|
70
|
+
requires = ["setuptools>=68"]
|
|
71
|
+
build-backend = "setuptools.build_meta"
|
|
72
|
+
|
|
73
|
+
[tool.setuptools]
|
|
74
|
+
package-dir = {"" = "src"}
|
|
75
|
+
|
|
76
|
+
[tool.setuptools.dynamic]
|
|
77
|
+
version = {attr = "cluebase_backend_sdk._version.__version__"}
|
|
78
|
+
|
|
79
|
+
[tool.setuptools.packages.find]
|
|
80
|
+
where = ["src"]
|
|
81
|
+
|
|
82
|
+
[tool.pytest.ini_options]
|
|
83
|
+
testpaths = [
|
|
84
|
+
"../../../tests/unit/backend-sdk-python",
|
|
85
|
+
"../../../tests/integration/backend-sdk-python",
|
|
86
|
+
"../../../tests/contract/backend-sdk-python",
|
|
87
|
+
]
|
|
88
|
+
# 実環境が要る試験を既定の実行から外します (`tests/README.md` §12.2)。
|
|
89
|
+
# `testpaths` 配下の file には `--ignore-glob` が効かないため、live file は
|
|
90
|
+
# 既定実行から明示的に外します。実環境のあるジョブは path を名指しして実行します。
|
|
91
|
+
addopts = [
|
|
92
|
+
"--ignore=../../../tests/integration/backend-sdk-python/test_canonical_endpoint_live.py",
|
|
93
|
+
"--ignore=../../../tests/integration/backend-sdk-python/test_provider_content_privacy_live.py",
|
|
94
|
+
]
|
|
95
|
+
# The second entry lets cross-language conformance tests import the contract's
|
|
96
|
+
# official fixture loader (python_loader / python_equality) in dev. This is the
|
|
97
|
+
# dev-test path only; the published wheel never ships tests, so it does not
|
|
98
|
+
# duplicate the copy-on-build of _conformance/ used by installed SDKs.
|
|
99
|
+
pythonpath = [
|
|
100
|
+
".",
|
|
101
|
+
"src",
|
|
102
|
+
"../../../tests/support/backend-sdk-python",
|
|
103
|
+
"../../../tests/unit/backend-sdk-python",
|
|
104
|
+
"../../../tests/integration/backend-sdk-python",
|
|
105
|
+
"../../../tests/contract/backend-sdk-python",
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
# tests/README.md §5 の判定器。変異させる記号は網羅の情報から自動で選ばれるので、
|
|
109
|
+
# 手で宣言しません。
|
|
110
|
+
[tool.mutmut]
|
|
111
|
+
# mutmut はここに挙げた木を `mutants/` へ複製してから試験を回すので、範囲を
|
|
112
|
+
# 1 file に絞ると import 先が複製されず collect が落ちます。木は package 全体を
|
|
113
|
+
# 渡し、実際に壊す範囲は `mutmut run <glob>` の側で絞ります。
|
|
114
|
+
source_paths = ["src/cluebase_backend_sdk/"]
|
|
115
|
+
# 試験が package 直下から取り込む先。複製されないと collect が落ちます
|
|
116
|
+
# (tests/ と pyproject.toml と uv.lock は mutmut が既定で複製します)。
|
|
117
|
+
also_copy = ["scripts/", "setup.py"]
|
|
118
|
+
# 下の 2 本を **変異の走行からだけ** 外します。素の束(`pytest`)では今までどおり
|
|
119
|
+
# 走ります。理由は 2 本とも同じで、**mutmut が変異を source file 自体へ書き込む**
|
|
120
|
+
# ためです(文字列は `XX...XX` の形、関数は `x_名前__mutmut_1` を同じ module へ
|
|
121
|
+
# 足す)。原文の字面と名前の面が常に変わっているので、この 2 本は **どの変異でも
|
|
122
|
+
# 必ず落ちます**。必ず落ちる試験があると全変異が「殺した」と記録され、**生き残りが
|
|
123
|
+
# 隠れます**(tests/README.md §5.5 の裏返しで、測定が実際より良く見える向き)。
|
|
124
|
+
#
|
|
125
|
+
# 2 本が見ているのは実行時の振る舞いではなく構造なので、tests/README.md 上の担当は
|
|
126
|
+
# §5(断定の強さ)ではなく §4(構造の不変条件)です。§5 から外すのが正しい配置で、
|
|
127
|
+
# 緩めではありません。
|
|
128
|
+
#
|
|
129
|
+
# 複製先で実際に落ちることを 1 本ずつ確かめた上で外しています。**同じ探し方で
|
|
130
|
+
# 引っかかった `tests/test_prepublish_gate.py` は、複製先でも 13 passed で通る
|
|
131
|
+
# ので外していません**(読んでいるのが原文の中身ではなく file の一覧で、mutmut は
|
|
132
|
+
# file を足さないため)。
|
|
133
|
+
pytest_add_cli_args_test_selection = [
|
|
134
|
+
"tests/",
|
|
135
|
+
# 原文を読む: 全 .py を ast で読み、診断の記録が読める形で出るか確かめる
|
|
136
|
+
"--ignore=tests/test_internal_log_messages.py",
|
|
137
|
+
# 名前の面を数える: 輸出された名前が契約の宣言と一致するか確かめる
|
|
138
|
+
"--ignore=tests/test_mcp_observer_surface.py",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
[tool.pyright]
|
|
142
|
+
# Tests resolve the package via a runtime ``sys.path.insert`` to the
|
|
143
|
+
# ``src/`` layout described by ``[tool.setuptools].package-dir``. Pyright
|
|
144
|
+
# does not execute that snippet, so we mirror the search path here so
|
|
145
|
+
# imports like ``from cluebase_backend_sdk.provider.bootstrap import ...``
|
|
146
|
+
# resolve in editors without requiring an editable install.
|
|
147
|
+
include = ["src", "tests"]
|
|
148
|
+
extraPaths = [".", "src"]
|
|
149
|
+
|
|
150
|
+
# Production code in ``src/`` is held to strict type-correctness, but
|
|
151
|
+
# tests freely use ``JsonValue`` union subscripts and assertX helpers
|
|
152
|
+
# that Pyright cannot narrow without per-call casts. Tests are gated
|
|
153
|
+
# by ``pytest`` at runtime, so demote Pyright's general type issues
|
|
154
|
+
# under ``tests/`` to warnings instead of errors.
|
|
155
|
+
[[tool.pyright.executionEnvironments]]
|
|
156
|
+
root = "tests"
|
|
157
|
+
reportGeneralTypeIssues = "warning"
|
|
158
|
+
reportOptionalSubscript = "warning"
|
|
159
|
+
reportOptionalMemberAccess = "warning"
|
|
160
|
+
|
|
161
|
+
[tool.coverage.run]
|
|
162
|
+
branch = true
|
|
163
|
+
source = ["cluebase_backend_sdk"]
|
|
164
|
+
|
|
165
|
+
[tool.coverage.report]
|
|
166
|
+
# Regression floor: keep combined statement and branch coverage high enough to
|
|
167
|
+
# block large untested modules without failing on small coverage noise.
|
|
168
|
+
fail_under = 80
|
|
169
|
+
precision = 2
|
|
170
|
+
exclude_lines = [
|
|
171
|
+
"pragma: no cover",
|
|
172
|
+
"raise NotImplementedError",
|
|
173
|
+
"if TYPE_CHECKING:",
|
|
174
|
+
# Defensive ImportError handlers for optional peer dependencies
|
|
175
|
+
# (sqlalchemy, django, celery, opentelemetry-instrumentation-*). The
|
|
176
|
+
# module degrades to a no-op when the framework is absent; that branch
|
|
177
|
+
# is exercised in test environments where the framework happens to be
|
|
178
|
+
# installed but not in unit tests.
|
|
179
|
+
"except ImportError",
|
|
180
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import runpy
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from setuptools import find_packages, setup
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_VERSION_FILE = Path(__file__).parent / "src" / "cluebase_backend_sdk" / "_version.py"
|
|
10
|
+
_VERSION = runpy.run_path(str(_VERSION_FILE))["__version__"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
setup(
|
|
14
|
+
name="cluebase-backend-sdk",
|
|
15
|
+
version=_VERSION,
|
|
16
|
+
description="Cluebase Backend SDK (Python) — OTel-based observation source event capture",
|
|
17
|
+
python_requires=">=3.10",
|
|
18
|
+
package_dir={"": "src"},
|
|
19
|
+
packages=find_packages("src"),
|
|
20
|
+
)
|