sdk-garden-python 0.1.0__tar.gz → 0.2.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,137 @@
1
+ Metadata-Version: 2.5
2
+ Name: sdk-garden-python
3
+ Version: 0.2.0
4
+ Summary: Lightweight SDK observability client for Python — stdlib only, non-blocking
5
+ Project-URL: Homepage, https://sdkgarden.com
6
+ Project-URL: Repository, https://github.com/tjb/SDKGarden
7
+ License: MIT
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+
11
+ # sdk-garden-python
12
+ *Lightweight SDK observability client for Python — stdlib only, non-blocking*
13
+
14
+ Add SDK Garden telemetry to your Python SDK in minutes. Events are sent on a background daemon thread — your users never see a delay or an exception from the instrumentation layer.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ pip install sdk-garden-python
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```python
25
+ from sdk_garden import SdkGarden
26
+
27
+ sg = SdkGarden(api_key="YOUR_API_KEY", sdk_name="my-sdk", sdk_version="1.0.0").start()
28
+
29
+ @sg.track_method("process_payment")
30
+ def process_payment(amount):
31
+ ...
32
+ ```
33
+
34
+ Track arbitrary events:
35
+
36
+ ```python
37
+ sg.track("feature_used", properties={"feature": "streaming"})
38
+ ```
39
+
40
+ Track errors with automatic PII scrubbing:
41
+
42
+ ```python
43
+ try:
44
+ process_payment(100)
45
+ except Exception as e:
46
+ sg.track_error(e)
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ | Parameter | Type | Default | Description |
52
+ |-----------|------|---------|-------------|
53
+ | `api_key` | `str` | required | Your SDK Garden API key (`sgk_...`) |
54
+ | `sdk_name` | `str` | required | Name of your SDK (e.g. `"my-payments-sdk"`) |
55
+ | `sdk_version` | `str` | required | Version of your SDK (e.g. `"1.2.3"`) |
56
+ | `endpoint` | `str` | `https://ingest.sdkgarden.com/track` | Ingest endpoint URL (for self-hosted SDK Garden, `https://<your-host>/track`) |
57
+ | `debug` | `bool` | `False` | Print `[SDK Garden]` debug lines to stdout |
58
+ | `disabled` | `bool` | `False` | Disable all tracking (no thread spawned, no network calls) |
59
+ | `batch_size` | `int` | `10` | Maximum events per HTTP POST, from 1 to 100 |
60
+ | `flush_interval` | `float` | `5.0` | Seconds between flush cycles, from 0.1 to 86400 (a full batch is sent sooner) |
61
+ | `started_at` | `float` | import time | `time.time()` when your SDK began initializing; reported as `time_to_init_ms` for the dashboard's init timing |
62
+ | `methods` | iterable of `str` | `None` | Public methods of your SDK that are not wrapped with `track_method()`, so the dashboard's API Surface panel can show them when nobody calls them |
63
+ | `customer_id` / `customer_name` | `str` | `None` | The app or customer account this client runs for (see [Customers](#customers)) |
64
+
65
+ ### API surface
66
+
67
+ Every name passed to `track_method()` is declared as part of your SDK's API surface, once per client (in the `sdk_initialized` event, or in one `api_surface` event for methods decorated after `start()`). The dashboard lists those methods next to the ones people call, so the methods nobody uses show up with zero calls. Add methods you don't decorate with the `methods` parameter. Up to 500 names are declared.
68
+
69
+ ### Customers
70
+
71
+ Tell SDK Garden which of your customers a client belongs to, and the dashboard's Customers page shows who is on an old version, who still calls a deprecated method and who hits errors, so you know who to contact.
72
+
73
+ ```python
74
+ # When your SDK knows which account it runs for, for example after resolving its API key:
75
+ sg.set_customer(account.id, name=account.display_name)
76
+
77
+ # Or when creating the client, if you already know it:
78
+ sg = SdkGarden(api_key="sgk_...", sdk_name="my-sdk", sdk_version="1.0.0", customer_id="acct_123").start()
79
+ ```
80
+
81
+ Use an ID from your own system, such as an account, project or app ID, and optionally a display name. Nothing is attributed unless you set one. Don't pass email addresses or other personal data: an ID that contains an email address is ignored, and so is a name that contains one. IDs are cut to 128 characters and names to 200. `set_customer(None)` stops attributing.
82
+
83
+ ## What gets sent
84
+
85
+ Each event carries your SDK's name and version, an event type, a random session ID created per client instance, and a timestamp. Depending on the event:
86
+
87
+ - **`sdk_initialized`** (sent by `start()`): Python implementation and version, `time_to_init_ms`, and the names of your SDK's methods (see [API surface](#api-surface)).
88
+ - **`method_called` / `method_success`**: method name, duration, and the *number* of positional arguments.
89
+ - **`sdk_error`**: error type and message (email addresses are redacted) plus the method and duration.
90
+ - **If you set a customer**, every event carries its ID and name (see [Customers](#customers)).
91
+
92
+ Never sent: argument or return values, request or response bodies, stack traces or environment variables. Error messages go out as they are apart from email addresses, so anything your SDK puts in an exception message, such as a hostname, a URL or an ID, goes with it. Nothing is sent when `disabled=True`.
93
+
94
+ ### Telling your users
95
+
96
+ Copy this into your SDK's README or privacy docs so your users know what's collected (edit the names to match):
97
+
98
+ > **Telemetry.** This SDK reports anonymous usage data to its maintainers through [SDK Garden](https://github.com/tjb/SDKGarden): the SDK version, runtime version, which SDK methods are called, how long they take, and the type and message of the errors they raise (with email addresses removed). It never sends arguments, return values or request contents. To opt out, set `YOUR_SDK_TELEMETRY_DISABLED=1` *(wire this environment variable to `disabled=True` in your SDK)*.
99
+
100
+ ## Behaviour Guarantees
101
+
102
+ **All network errors are caught and discarded. Your program will never see an exception from sdk-garden-python.**
103
+
104
+ **Runs on a daemon thread — your program exits normally even if events are queued.** Only that thread makes requests. When the interpreter exits, the events still queued are sent first, for at most 2 seconds; anything left after that is dropped. sdk-garden-python prioritises zero impact on your SDK over delivery guarantees. The automatic send does not run when the process is killed by a signal or ends through `os._exit()`, which is how `multiprocessing` workers started with `fork` or `forkserver` (the default on Linux) end: call `flush()` at the end of their work.
105
+
106
+ **Pass `disabled=True` in test environments to suppress all network calls.** When disabled, `start()` returns immediately without spawning a thread, and `track()` / `track_method()` / `track_error()` are all no-ops.
107
+
108
+ **Each connect and read waits at most 2 seconds.** A slow endpoint, such as one that trickles its response, can hold the background thread longer, but your code never waits for it: `flush()`, `shutdown()` and the send at exit keep their own time limits.
109
+
110
+ **Queue overflow is silent.** The internal queue holds a maximum of 100 events. If your SDK generates events faster than the flush thread can drain them, new events are discarded without raising an exception. With `debug=True`, each one is logged.
111
+
112
+ ## Sending and shutting down
113
+
114
+ The background thread sends queued events every `flush_interval` seconds, sooner once `batch_size` events are waiting, and once more when the interpreter exits. Two methods let you control this yourself. Both ask the background thread to send and wait for it for a bounded time, even if a request hangs, and never raise:
115
+
116
+ ```python
117
+ # Send everything queued now, waiting at most 2 seconds by default
118
+ # (e.g. at the end of a serverless handler or a short script).
119
+ sg.flush()
120
+
121
+ # Send everything queued (at most 2 seconds by default), then stop the client for good:
122
+ # the thread exits, tracking calls do nothing, and start() cannot restart it.
123
+ sg.shutdown()
124
+ ```
125
+
126
+ `sg.shutdown(timeout=0)` stops the client and drops queued events without sending them. `flush()` and `shutdown()` are safe to call from a signal handler, such as a SIGTERM handler that closes your SDK. (On older Pythons such as 3.8, don't track the first event of a forked child from a signal handler: that starts the background thread, which can deadlock there.) A child process created with `os.fork()` (for example a prefork server worker) starts with an empty queue and its own background thread, which starts on its first event; the parent keeps sending what it had queued.
127
+
128
+ ## Notes
129
+
130
+ - `start()` must be called before events are flushed. Calling `track()` before `start()` is safe — events are queued and will be flushed once `start()` is called.
131
+ - Events are serialized when they are tracked, so later changes to `properties` are not sent. Values JSON cannot encode (`datetime`, `Decimal`, ...) are sent as strings. An event that cannot be serialized at all (a circular reference), holds `nan` or `inf`, or is over 64 KiB serialized is dropped on its own, so it never costs the rest of its batch. For the same reason NUL characters and unpaired surrogates (say, from a `surrogateescape`-decoded path), which the server cannot store, are sent as U+FFFD, and an event whose name is not a string of at most 100 characters is dropped.
132
+ - `track_method()` returns a decorator. The decorated function behaves identically to the original; timing and error telemetry are captured transparently. An `async def` function stays a coroutine function and is timed until its coroutine finishes; a generator function's call is recorded when the generator is created.
133
+ - Error messages passed to `track_error()` are PII-scrubbed: email addresses matching the standard `user@example.com` pattern are replaced with `[REDACTED]` before the event is enqueued, and only the start of the message is sent: its first 1000 characters, cut to 1000 UTF-8 bytes, as much as the server keeps.
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,127 @@
1
+ # sdk-garden-python
2
+ *Lightweight SDK observability client for Python — stdlib only, non-blocking*
3
+
4
+ Add SDK Garden telemetry to your Python SDK in minutes. Events are sent on a background daemon thread — your users never see a delay or an exception from the instrumentation layer.
5
+
6
+ ## Install
7
+
8
+ ```bash
9
+ pip install sdk-garden-python
10
+ ```
11
+
12
+ ## Quick Start
13
+
14
+ ```python
15
+ from sdk_garden import SdkGarden
16
+
17
+ sg = SdkGarden(api_key="YOUR_API_KEY", sdk_name="my-sdk", sdk_version="1.0.0").start()
18
+
19
+ @sg.track_method("process_payment")
20
+ def process_payment(amount):
21
+ ...
22
+ ```
23
+
24
+ Track arbitrary events:
25
+
26
+ ```python
27
+ sg.track("feature_used", properties={"feature": "streaming"})
28
+ ```
29
+
30
+ Track errors with automatic PII scrubbing:
31
+
32
+ ```python
33
+ try:
34
+ process_payment(100)
35
+ except Exception as e:
36
+ sg.track_error(e)
37
+ ```
38
+
39
+ ## Configuration
40
+
41
+ | Parameter | Type | Default | Description |
42
+ |-----------|------|---------|-------------|
43
+ | `api_key` | `str` | required | Your SDK Garden API key (`sgk_...`) |
44
+ | `sdk_name` | `str` | required | Name of your SDK (e.g. `"my-payments-sdk"`) |
45
+ | `sdk_version` | `str` | required | Version of your SDK (e.g. `"1.2.3"`) |
46
+ | `endpoint` | `str` | `https://ingest.sdkgarden.com/track` | Ingest endpoint URL (for self-hosted SDK Garden, `https://<your-host>/track`) |
47
+ | `debug` | `bool` | `False` | Print `[SDK Garden]` debug lines to stdout |
48
+ | `disabled` | `bool` | `False` | Disable all tracking (no thread spawned, no network calls) |
49
+ | `batch_size` | `int` | `10` | Maximum events per HTTP POST, from 1 to 100 |
50
+ | `flush_interval` | `float` | `5.0` | Seconds between flush cycles, from 0.1 to 86400 (a full batch is sent sooner) |
51
+ | `started_at` | `float` | import time | `time.time()` when your SDK began initializing; reported as `time_to_init_ms` for the dashboard's init timing |
52
+ | `methods` | iterable of `str` | `None` | Public methods of your SDK that are not wrapped with `track_method()`, so the dashboard's API Surface panel can show them when nobody calls them |
53
+ | `customer_id` / `customer_name` | `str` | `None` | The app or customer account this client runs for (see [Customers](#customers)) |
54
+
55
+ ### API surface
56
+
57
+ Every name passed to `track_method()` is declared as part of your SDK's API surface, once per client (in the `sdk_initialized` event, or in one `api_surface` event for methods decorated after `start()`). The dashboard lists those methods next to the ones people call, so the methods nobody uses show up with zero calls. Add methods you don't decorate with the `methods` parameter. Up to 500 names are declared.
58
+
59
+ ### Customers
60
+
61
+ Tell SDK Garden which of your customers a client belongs to, and the dashboard's Customers page shows who is on an old version, who still calls a deprecated method and who hits errors, so you know who to contact.
62
+
63
+ ```python
64
+ # When your SDK knows which account it runs for, for example after resolving its API key:
65
+ sg.set_customer(account.id, name=account.display_name)
66
+
67
+ # Or when creating the client, if you already know it:
68
+ sg = SdkGarden(api_key="sgk_...", sdk_name="my-sdk", sdk_version="1.0.0", customer_id="acct_123").start()
69
+ ```
70
+
71
+ Use an ID from your own system, such as an account, project or app ID, and optionally a display name. Nothing is attributed unless you set one. Don't pass email addresses or other personal data: an ID that contains an email address is ignored, and so is a name that contains one. IDs are cut to 128 characters and names to 200. `set_customer(None)` stops attributing.
72
+
73
+ ## What gets sent
74
+
75
+ Each event carries your SDK's name and version, an event type, a random session ID created per client instance, and a timestamp. Depending on the event:
76
+
77
+ - **`sdk_initialized`** (sent by `start()`): Python implementation and version, `time_to_init_ms`, and the names of your SDK's methods (see [API surface](#api-surface)).
78
+ - **`method_called` / `method_success`**: method name, duration, and the *number* of positional arguments.
79
+ - **`sdk_error`**: error type and message (email addresses are redacted) plus the method and duration.
80
+ - **If you set a customer**, every event carries its ID and name (see [Customers](#customers)).
81
+
82
+ Never sent: argument or return values, request or response bodies, stack traces or environment variables. Error messages go out as they are apart from email addresses, so anything your SDK puts in an exception message, such as a hostname, a URL or an ID, goes with it. Nothing is sent when `disabled=True`.
83
+
84
+ ### Telling your users
85
+
86
+ Copy this into your SDK's README or privacy docs so your users know what's collected (edit the names to match):
87
+
88
+ > **Telemetry.** This SDK reports anonymous usage data to its maintainers through [SDK Garden](https://github.com/tjb/SDKGarden): the SDK version, runtime version, which SDK methods are called, how long they take, and the type and message of the errors they raise (with email addresses removed). It never sends arguments, return values or request contents. To opt out, set `YOUR_SDK_TELEMETRY_DISABLED=1` *(wire this environment variable to `disabled=True` in your SDK)*.
89
+
90
+ ## Behaviour Guarantees
91
+
92
+ **All network errors are caught and discarded. Your program will never see an exception from sdk-garden-python.**
93
+
94
+ **Runs on a daemon thread — your program exits normally even if events are queued.** Only that thread makes requests. When the interpreter exits, the events still queued are sent first, for at most 2 seconds; anything left after that is dropped. sdk-garden-python prioritises zero impact on your SDK over delivery guarantees. The automatic send does not run when the process is killed by a signal or ends through `os._exit()`, which is how `multiprocessing` workers started with `fork` or `forkserver` (the default on Linux) end: call `flush()` at the end of their work.
95
+
96
+ **Pass `disabled=True` in test environments to suppress all network calls.** When disabled, `start()` returns immediately without spawning a thread, and `track()` / `track_method()` / `track_error()` are all no-ops.
97
+
98
+ **Each connect and read waits at most 2 seconds.** A slow endpoint, such as one that trickles its response, can hold the background thread longer, but your code never waits for it: `flush()`, `shutdown()` and the send at exit keep their own time limits.
99
+
100
+ **Queue overflow is silent.** The internal queue holds a maximum of 100 events. If your SDK generates events faster than the flush thread can drain them, new events are discarded without raising an exception. With `debug=True`, each one is logged.
101
+
102
+ ## Sending and shutting down
103
+
104
+ The background thread sends queued events every `flush_interval` seconds, sooner once `batch_size` events are waiting, and once more when the interpreter exits. Two methods let you control this yourself. Both ask the background thread to send and wait for it for a bounded time, even if a request hangs, and never raise:
105
+
106
+ ```python
107
+ # Send everything queued now, waiting at most 2 seconds by default
108
+ # (e.g. at the end of a serverless handler or a short script).
109
+ sg.flush()
110
+
111
+ # Send everything queued (at most 2 seconds by default), then stop the client for good:
112
+ # the thread exits, tracking calls do nothing, and start() cannot restart it.
113
+ sg.shutdown()
114
+ ```
115
+
116
+ `sg.shutdown(timeout=0)` stops the client and drops queued events without sending them. `flush()` and `shutdown()` are safe to call from a signal handler, such as a SIGTERM handler that closes your SDK. (On older Pythons such as 3.8, don't track the first event of a forked child from a signal handler: that starts the background thread, which can deadlock there.) A child process created with `os.fork()` (for example a prefork server worker) starts with an empty queue and its own background thread, which starts on its first event; the parent keeps sending what it had queued.
117
+
118
+ ## Notes
119
+
120
+ - `start()` must be called before events are flushed. Calling `track()` before `start()` is safe — events are queued and will be flushed once `start()` is called.
121
+ - Events are serialized when they are tracked, so later changes to `properties` are not sent. Values JSON cannot encode (`datetime`, `Decimal`, ...) are sent as strings. An event that cannot be serialized at all (a circular reference), holds `nan` or `inf`, or is over 64 KiB serialized is dropped on its own, so it never costs the rest of its batch. For the same reason NUL characters and unpaired surrogates (say, from a `surrogateescape`-decoded path), which the server cannot store, are sent as U+FFFD, and an event whose name is not a string of at most 100 characters is dropped.
122
+ - `track_method()` returns a decorator. The decorated function behaves identically to the original; timing and error telemetry are captured transparently. An `async def` function stays a coroutine function and is timed until its coroutine finishes; a generator function's call is recorded when the generator is created.
123
+ - Error messages passed to `track_error()` are PII-scrubbed: email addresses matching the standard `user@example.com` pattern are replaced with `[REDACTED]` before the event is enqueued, and only the start of the message is sent: its first 1000 characters, cut to 1000 UTF-8 bytes, as much as the server keeps.
124
+
125
+ ## License
126
+
127
+ MIT
@@ -4,14 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sdk-garden-python"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "Lightweight SDK observability client for Python — stdlib only, non-blocking"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
11
  license = { text = "MIT" }
12
12
 
13
13
  [project.urls]
14
- Homepage = "https://sdkgarden.dev"
14
+ Homepage = "https://sdkgarden.com"
15
15
  Repository = "https://github.com/tjb/SDKGarden"
16
16
 
17
17
  [tool.hatch.build.targets.wheel]