genieos 0.1.0__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.
@@ -0,0 +1,11 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ .env
10
+ .env.*
11
+ .DS_Store
@@ -0,0 +1,12 @@
1
+ # genieos (Python SDK)
2
+
3
+ ## 0.1.0
4
+
5
+ Initial release.
6
+
7
+ - `GenieOS` (sync) and `AsyncGenieOS` (async) clients on top of `httpx`.
8
+ - Pydantic v2 models for typed responses.
9
+ - Auto idempotency keys; retry-on-429/5xx with exponential back-off.
10
+ - Resources: `workspace`, `templates`, `events`, `webhooks`, `audit`, `keys`.
11
+ - Webhook signature helpers: `verify_webhook`, `sign_webhook`, `WebhookSignatureError`.
12
+ - Typed errors inheriting from `GenieOSError`: auth / not-found / validation / conflict / rate-limit / server / network.
genieos-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MailGenius
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
genieos-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,146 @@
1
+ Metadata-Version: 2.4
2
+ Name: genieos
3
+ Version: 0.1.0
4
+ Summary: Official Python SDK for GenieOS — sync + async clients, typed responses, idempotency-aware retries, webhook verification.
5
+ Project-URL: Homepage, https://docs.genieos.pro/sdks/python
6
+ Project-URL: Documentation, https://docs.genieos.pro/sdks/python
7
+ Project-URL: Repository, https://github.com/GenieOS-0/sdk-python
8
+ Project-URL: Issues, https://github.com/GenieOS-0/sdk-python/issues
9
+ Project-URL: Changelog, https://github.com/GenieOS-0/sdk-python/blob/main/CHANGELOG.md
10
+ Author-email: GenieOS <developers@genieos.pro>
11
+ License: MIT
12
+ License-File: LICENSE
13
+ Keywords: drip,email,genieos,mcp,sequences,transactional
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Topic :: Communications :: Email
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.9
26
+ Requires-Dist: httpx>=0.27
27
+ Requires-Dist: pydantic>=2.6
28
+ Provides-Extra: dev
29
+ Requires-Dist: mypy>=1.10; extra == 'dev'
30
+ Requires-Dist: posthog>=3.0.0; extra == 'dev'
31
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
32
+ Requires-Dist: pytest>=8.0; extra == 'dev'
33
+ Requires-Dist: python-dotenv>=1.0.0; extra == 'dev'
34
+ Requires-Dist: respx>=0.21; extra == 'dev'
35
+ Requires-Dist: ruff>=0.6; extra == 'dev'
36
+ Provides-Extra: telemetry
37
+ Requires-Dist: posthog>=3.0.0; extra == 'telemetry'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # genieos — Python SDK
41
+
42
+ [![PyPI version](https://img.shields.io/pypi/v/genieos.svg)](https://pypi.org/project/genieos)
43
+
44
+ The official Python SDK for [GenieOS](https://genieos.pro). Sync and
45
+ async clients, typed responses (Pydantic), automatic idempotency,
46
+ retry-on-429/5xx with exponential back-off, and webhook signature
47
+ verification.
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ pip install genieos
53
+ ```
54
+
55
+ Requires Python 3.9+ and `httpx>=0.27`, `pydantic>=2.6`.
56
+
57
+ ## Quickstart (sync)
58
+
59
+ ```python
60
+ from genieos import GenieOS
61
+
62
+ with GenieOS(api_key="gos_live_...") as mg:
63
+ ws = mg.workspace.get()
64
+ print(ws.name, "on", ws.plan)
65
+
66
+ send = mg.templates.send(
67
+ "welcome",
68
+ to="aki@example.com",
69
+ variables={"firstName": "Aki"},
70
+ )
71
+ print("queued:", send.id)
72
+ ```
73
+
74
+ The API key is also picked up from the `GENIEOS_API_KEY` env var,
75
+ matching the Node SDK and CLI.
76
+
77
+ ## Quickstart (async)
78
+
79
+ ```python
80
+ import asyncio
81
+ from genieos import AsyncGenieOS
82
+
83
+ async def main():
84
+ async with AsyncGenieOS() as mg: # MAILGENIUS_API_KEY env var
85
+ await mg.events.emit(
86
+ "subscription.cancelled",
87
+ email="aki@example.com",
88
+ traits={"tier": "pro", "reason": "moving to weekly"},
89
+ )
90
+
91
+ asyncio.run(main())
92
+ ```
93
+
94
+ ## Webhook verification (Flask)
95
+
96
+ ```python
97
+ from flask import Flask, request, abort
98
+ from genieos import verify_webhook, WebhookSignatureError
99
+
100
+ app = Flask(__name__)
101
+
102
+ @app.post("/genieos/webhook")
103
+ def webhook():
104
+ try:
105
+ delivery = verify_webhook(
106
+ request.get_data(as_text=True),
107
+ request.headers,
108
+ secret=os.environ["MAILGENIUS_WEBHOOK_SECRET"],
109
+ )
110
+ except WebhookSignatureError as e:
111
+ abort(400, str(e))
112
+
113
+ if delivery.type == "send.delivered":
114
+ ... # handle it
115
+ return "", 204
116
+ ```
117
+
118
+ `verify_webhook` rejects out-of-window timestamps (default ±5 min) and
119
+ performs constant-time signature comparison. The same module exposes
120
+ `sign_webhook` for local testing.
121
+
122
+ ## Error handling
123
+
124
+ ```python
125
+ from genieos import (
126
+ GenieOSRateLimitError,
127
+ GenieOSValidationError,
128
+ GenieOSAuthError,
129
+ )
130
+
131
+ try:
132
+ mg.events.emit("checkout.completed", email="aki@example.com")
133
+ except GenieOSRateLimitError as e:
134
+ time.sleep(e.retry_after_seconds)
135
+ except GenieOSValidationError as e:
136
+ log.warning("422: %s", e.body)
137
+ except GenieOSAuthError:
138
+ rotate_my_key()
139
+ ```
140
+
141
+ All SDK errors inherit from `GenieOSError` and expose `.code`,
142
+ `.status`, `.request_id`, `.body`.
143
+
144
+ ## License
145
+
146
+ MIT
@@ -0,0 +1,107 @@
1
+ # genieos — Python SDK
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/genieos.svg)](https://pypi.org/project/genieos)
4
+
5
+ The official Python SDK for [GenieOS](https://genieos.pro). Sync and
6
+ async clients, typed responses (Pydantic), automatic idempotency,
7
+ retry-on-429/5xx with exponential back-off, and webhook signature
8
+ verification.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ pip install genieos
14
+ ```
15
+
16
+ Requires Python 3.9+ and `httpx>=0.27`, `pydantic>=2.6`.
17
+
18
+ ## Quickstart (sync)
19
+
20
+ ```python
21
+ from genieos import GenieOS
22
+
23
+ with GenieOS(api_key="gos_live_...") as mg:
24
+ ws = mg.workspace.get()
25
+ print(ws.name, "on", ws.plan)
26
+
27
+ send = mg.templates.send(
28
+ "welcome",
29
+ to="aki@example.com",
30
+ variables={"firstName": "Aki"},
31
+ )
32
+ print("queued:", send.id)
33
+ ```
34
+
35
+ The API key is also picked up from the `GENIEOS_API_KEY` env var,
36
+ matching the Node SDK and CLI.
37
+
38
+ ## Quickstart (async)
39
+
40
+ ```python
41
+ import asyncio
42
+ from genieos import AsyncGenieOS
43
+
44
+ async def main():
45
+ async with AsyncGenieOS() as mg: # MAILGENIUS_API_KEY env var
46
+ await mg.events.emit(
47
+ "subscription.cancelled",
48
+ email="aki@example.com",
49
+ traits={"tier": "pro", "reason": "moving to weekly"},
50
+ )
51
+
52
+ asyncio.run(main())
53
+ ```
54
+
55
+ ## Webhook verification (Flask)
56
+
57
+ ```python
58
+ from flask import Flask, request, abort
59
+ from genieos import verify_webhook, WebhookSignatureError
60
+
61
+ app = Flask(__name__)
62
+
63
+ @app.post("/genieos/webhook")
64
+ def webhook():
65
+ try:
66
+ delivery = verify_webhook(
67
+ request.get_data(as_text=True),
68
+ request.headers,
69
+ secret=os.environ["MAILGENIUS_WEBHOOK_SECRET"],
70
+ )
71
+ except WebhookSignatureError as e:
72
+ abort(400, str(e))
73
+
74
+ if delivery.type == "send.delivered":
75
+ ... # handle it
76
+ return "", 204
77
+ ```
78
+
79
+ `verify_webhook` rejects out-of-window timestamps (default ±5 min) and
80
+ performs constant-time signature comparison. The same module exposes
81
+ `sign_webhook` for local testing.
82
+
83
+ ## Error handling
84
+
85
+ ```python
86
+ from genieos import (
87
+ GenieOSRateLimitError,
88
+ GenieOSValidationError,
89
+ GenieOSAuthError,
90
+ )
91
+
92
+ try:
93
+ mg.events.emit("checkout.completed", email="aki@example.com")
94
+ except GenieOSRateLimitError as e:
95
+ time.sleep(e.retry_after_seconds)
96
+ except GenieOSValidationError as e:
97
+ log.warning("422: %s", e.body)
98
+ except GenieOSAuthError:
99
+ rotate_my_key()
100
+ ```
101
+
102
+ All SDK errors inherit from `GenieOSError` and expose `.code`,
103
+ `.status`, `.request_id`, `.body`.
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,72 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "genieos"
7
+ version = "0.1.0"
8
+ description = "Official Python SDK for GenieOS — sync + async clients, typed responses, idempotency-aware retries, webhook verification."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "GenieOS", email = "developers@genieos.pro" }]
12
+ requires-python = ">=3.9"
13
+ keywords = ["genieos", "email", "transactional", "drip", "sequences", "mcp"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.9",
20
+ "Programming Language :: Python :: 3.10",
21
+ "Programming Language :: Python :: 3.11",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Topic :: Communications :: Email",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "httpx>=0.27",
29
+ "pydantic>=2.6",
30
+ ]
31
+
32
+ [project.urls]
33
+ Homepage = "https://docs.genieos.pro/sdks/python"
34
+ Documentation = "https://docs.genieos.pro/sdks/python"
35
+ Repository = "https://github.com/GenieOS-0/sdk-python"
36
+ Issues = "https://github.com/GenieOS-0/sdk-python/issues"
37
+ Changelog = "https://github.com/GenieOS-0/sdk-python/blob/main/CHANGELOG.md"
38
+
39
+ [project.optional-dependencies]
40
+ telemetry = [
41
+ "posthog>=3.0.0",
42
+ ]
43
+ dev = [
44
+ "pytest>=8.0",
45
+ "pytest-asyncio>=0.24",
46
+ "respx>=0.21",
47
+ "mypy>=1.10",
48
+ "ruff>=0.6",
49
+ "posthog>=3.0.0",
50
+ "python-dotenv>=1.0.0",
51
+ ]
52
+
53
+ [tool.hatch.build.targets.wheel]
54
+ packages = ["src/genieos"]
55
+
56
+ [tool.hatch.build.targets.sdist]
57
+ include = ["src/genieos", "README.md", "LICENSE", "CHANGELOG.md", "pyproject.toml"]
58
+
59
+ [tool.pytest.ini_options]
60
+ asyncio_mode = "auto"
61
+ testpaths = ["tests"]
62
+
63
+ [tool.ruff]
64
+ target-version = "py39"
65
+ line-length = 100
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "I", "N", "B", "UP", "ASYNC"]
69
+
70
+ [tool.mypy]
71
+ strict = true
72
+ python_version = "3.9"
@@ -0,0 +1,56 @@
1
+ """
2
+ GenieOS — official Python SDK.
3
+
4
+ Quickstart::
5
+
6
+ from genieos import GenieOS
7
+
8
+ with GenieOS(api_key="gos_live_...") as mg:
9
+ ws = mg.workspace.get()
10
+ print(ws.name, ws.plan)
11
+
12
+ send = mg.templates.send(
13
+ "welcome",
14
+ to="aki@example.com",
15
+ variables={"firstName": "Aki"},
16
+ )
17
+ print("send id:", send.id)
18
+
19
+ For asynchronous code, use ``AsyncGenieOS`` which exposes the same
20
+ resource surface with awaitable methods.
21
+ """
22
+ from ._errors import (
23
+ GenieOSAuthError,
24
+ GenieOSConflictError,
25
+ GenieOSError,
26
+ GenieOSNetworkError,
27
+ GenieOSNotFoundError,
28
+ GenieOSRateLimitError,
29
+ GenieOSServerError,
30
+ GenieOSValidationError,
31
+ )
32
+ from ._transport import DEFAULT_BASE_URL
33
+ from .async_client import AsyncGenieOS
34
+ from .client import GenieOS
35
+ from .webhooks import VerifiedDelivery, WebhookSignatureError, sign_webhook, verify_webhook
36
+
37
+ __version__ = "0.1.0"
38
+
39
+ __all__ = [
40
+ "__version__",
41
+ "DEFAULT_BASE_URL",
42
+ "GenieOS",
43
+ "AsyncGenieOS",
44
+ "GenieOSError",
45
+ "GenieOSAuthError",
46
+ "GenieOSNotFoundError",
47
+ "GenieOSValidationError",
48
+ "GenieOSConflictError",
49
+ "GenieOSRateLimitError",
50
+ "GenieOSServerError",
51
+ "GenieOSNetworkError",
52
+ "VerifiedDelivery",
53
+ "WebhookSignatureError",
54
+ "verify_webhook",
55
+ "sign_webhook",
56
+ ]
@@ -0,0 +1,124 @@
1
+ """
2
+ Typed error hierarchy for the GenieOS SDK — mirrors the catalogue
3
+ in ``packages/sdk-node/src/errors.ts`` so the same docs cover both
4
+ SDKs.
5
+
6
+ The base class is :class:`GenieOSError`. Every subclass corresponds
7
+ to a documented API error code so callers can
8
+ ``except GenieOSRateLimitError`` instead of matching on string codes.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ from typing import Any, Optional
13
+
14
+
15
+ class GenieOSError(Exception):
16
+ """Base class for every error raised by the GenieOS SDK."""
17
+
18
+ def __init__(
19
+ self,
20
+ message: str,
21
+ *,
22
+ code: str = "genieos_error",
23
+ status: Optional[int] = None,
24
+ request_id: Optional[str] = None,
25
+ body: Optional[Any] = None,
26
+ ) -> None:
27
+ super().__init__(message)
28
+ self.code = code
29
+ self.status = status
30
+ self.request_id = request_id
31
+ self.body = body
32
+
33
+ def __repr__(self) -> str: # pragma: no cover - cosmetic
34
+ parts = [f"{self.__class__.__name__}({self.args[0]!r}", f"code={self.code!r}"]
35
+ if self.status is not None:
36
+ parts.append(f"status={self.status}")
37
+ if self.request_id is not None:
38
+ parts.append(f"request_id={self.request_id!r}")
39
+ return ", ".join(parts) + ")"
40
+
41
+
42
+ class GenieOSAuthError(GenieOSError):
43
+ """401/403 — bearer token missing, invalid, revoked, or out-of-scope."""
44
+
45
+
46
+ class GenieOSNotFoundError(GenieOSError):
47
+ """404 — addressed resource does not exist (or is in another workspace)."""
48
+
49
+
50
+ class GenieOSValidationError(GenieOSError):
51
+ """422 — request body / query failed schema validation."""
52
+
53
+
54
+ class GenieOSConflictError(GenieOSError):
55
+ """409 — idempotency conflict, optimistic-concurrency mismatch, etc."""
56
+
57
+
58
+ class GenieOSRateLimitError(GenieOSError):
59
+ """429 — per-key or per-workspace rate limit exceeded.
60
+
61
+ ``retry_after_seconds`` mirrors the ``Retry-After`` header on the
62
+ response and is the recommended back-off duration before the next
63
+ attempt.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ message: str,
69
+ *,
70
+ retry_after_seconds: float,
71
+ **kwargs: Any,
72
+ ) -> None:
73
+ super().__init__(message, **kwargs)
74
+ self.retry_after_seconds = retry_after_seconds
75
+
76
+
77
+ class GenieOSServerError(GenieOSError):
78
+ """5xx — transient server-side fault. Retried automatically by the SDK."""
79
+
80
+
81
+ class GenieOSNetworkError(GenieOSError):
82
+ """Local network / DNS / TLS failure."""
83
+
84
+
85
+ def from_response(
86
+ *,
87
+ status: int,
88
+ body: Any,
89
+ request_id: Optional[str],
90
+ retry_after_seconds: Optional[float] = None,
91
+ ) -> GenieOSError:
92
+ """Convert an HTTP error response into the appropriate typed error."""
93
+ err_obj = (body or {}).get("error", {}) if isinstance(body, dict) else {}
94
+ code = err_obj.get("code") or f"http_{status}"
95
+ message = err_obj.get("message") or f"HTTP {status}"
96
+ common = {"code": code, "status": status, "request_id": request_id, "body": body}
97
+ if status in (401, 403):
98
+ return GenieOSAuthError(message, **common)
99
+ if status == 404:
100
+ return GenieOSNotFoundError(message, **common)
101
+ if status == 409:
102
+ return GenieOSConflictError(message, **common)
103
+ if status == 422:
104
+ return GenieOSValidationError(message, **common)
105
+ if status == 429:
106
+ # `or` would coerce a legitimate 0 ("retry now") into 1s, so check None.
107
+ ra = retry_after_seconds if retry_after_seconds is not None else 1.0
108
+ return GenieOSRateLimitError(message, retry_after_seconds=ra, **common)
109
+ if status >= 500:
110
+ return GenieOSServerError(message, **common)
111
+ return GenieOSError(message, **common)
112
+
113
+
114
+ __all__ = [
115
+ "GenieOSError",
116
+ "GenieOSAuthError",
117
+ "GenieOSNotFoundError",
118
+ "GenieOSValidationError",
119
+ "GenieOSConflictError",
120
+ "GenieOSRateLimitError",
121
+ "GenieOSServerError",
122
+ "GenieOSNetworkError",
123
+ "from_response",
124
+ ]
@@ -0,0 +1,82 @@
1
+ """
2
+ Optional PostHog telemetry for the GenieOS Python SDK.
3
+
4
+ Telemetry is enabled only when POSTHOG_PROJECT_TOKEN is set. It tracks
5
+ SDK-usage signals (which operations are called, error rates by code,
6
+ webhook verification outcomes) to help the GenieOS team understand
7
+ how the SDK is used and where developers hit problems.
8
+
9
+ No PII is ever sent — the distinct_id is a SHA-256 hash of the API key
10
+ prefix, so it is workspace-scoped but cannot be reverse-engineered.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ import os
16
+ from typing import Any
17
+
18
+ _client: Any = None
19
+ _initialized = False
20
+
21
+ # Used for calls (e.g. webhook verification) where no API key is available.
22
+ ANONYMOUS_DISTINCT_ID = "sdk-py-webhook-verifier"
23
+
24
+
25
+ def _distinct_id(api_key: str) -> str:
26
+ """Return a non-reversible, workspace-scoped identifier."""
27
+ return "sdk-py-" + hashlib.sha256(api_key.encode()).hexdigest()[:16]
28
+
29
+
30
+ def get_client() -> Any | None:
31
+ """Return the (lazily-initialized) PostHog client, or None if disabled."""
32
+ global _client, _initialized
33
+ if _initialized:
34
+ return _client
35
+
36
+ _initialized = True
37
+ token = os.environ.get("POSTHOG_PROJECT_TOKEN", "").strip()
38
+ if not token:
39
+ return None
40
+
41
+ try:
42
+ from posthog import Posthog # type: ignore[import]
43
+
44
+ kwargs: dict[str, Any] = {"enable_exception_autocapture": True}
45
+ host = os.environ.get("POSTHOG_HOST", "").strip()
46
+ if host:
47
+ kwargs["host"] = host
48
+ _client = Posthog(token, **kwargs)
49
+ except Exception:
50
+ # Never let PostHog errors surface to SDK consumers.
51
+ _client = None
52
+
53
+ return _client
54
+
55
+
56
+ def capture(
57
+ api_key: str,
58
+ event: str,
59
+ properties: dict[str, Any] | None = None,
60
+ *,
61
+ distinct_id: str | None = None,
62
+ ) -> None:
63
+ """Fire a PostHog event; silently no-ops if telemetry is disabled.
64
+
65
+ If ``distinct_id`` is provided it is used directly; otherwise it is
66
+ derived from ``api_key`` via SHA-256 so no raw key is transmitted.
67
+ """
68
+ client = get_client()
69
+ if client is None:
70
+ return
71
+ try:
72
+ did = distinct_id if distinct_id is not None else _distinct_id(api_key)
73
+ client.capture(
74
+ distinct_id=did,
75
+ event=event,
76
+ properties=properties or {},
77
+ )
78
+ except Exception:
79
+ pass
80
+
81
+
82
+ __all__ = ["capture", "get_client", "ANONYMOUS_DISTINCT_ID"]