apidepth 0.1.0__py3-none-any.whl

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.
apidepth/__init__.py ADDED
@@ -0,0 +1,220 @@
1
+ """Apidepth Python SDK.
2
+
3
+ Tracks outbound API latency, error rates, and rate-limit quota across
4
+ third-party vendors — Stripe, OpenAI, Anthropic, Twilio, GitHub, and more.
5
+ Requires zero changes to existing HTTP call sites.
6
+
7
+ Quick start (non-framework)::
8
+
9
+ import apidepth
10
+
11
+ apidepth.configure(
12
+ api_key="apd_live_...",
13
+ environment="production",
14
+ )
15
+ apidepth.instrument() # patches requests and httpx if installed
16
+
17
+ For **Django**, add ``"apidepth.integrations.django"`` to ``INSTALLED_APPS``
18
+ and set ``APIDEPTH = {"api_key": ...}`` in ``settings.py``.
19
+
20
+ For **Flask**, use ``apidepth.integrations.flask.Apidepth(app)``.
21
+
22
+ Module-level globals
23
+ --------------------
24
+ ``_configuration``
25
+ Lazily created :class:`~apidepth.configuration.Configuration` singleton.
26
+ Always access via :func:`get_configuration`.
27
+
28
+ ``_logger``
29
+ The ``logging.Logger`` used by all SDK components. Defaults to the
30
+ standard ``"apidepth"`` logger. Override with :func:`set_logger` to
31
+ route SDK log output through a framework logger (Django, Flask).
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ import platform
37
+ import sys
38
+ from typing import Any, Dict, Optional
39
+
40
+ from apidepth.version import VERSION
41
+ from apidepth.configuration import Configuration
42
+
43
+ __version__ = VERSION
44
+
45
+ _configuration: Optional[Configuration] = None
46
+ _configuration_lock = __import__("threading").Lock()
47
+ _logger: Optional[logging.Logger] = None
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Public API
52
+ # ---------------------------------------------------------------------------
53
+
54
+ def configure(**kwargs: Any) -> Configuration:
55
+ """Set one or more configuration options and return the singleton.
56
+
57
+ Must be called before :func:`instrument` and before any instrumented HTTP
58
+ requests are made. Framework integrations call this automatically inside
59
+ their boot hooks, so manual calls are only needed for non-framework use.
60
+
61
+ Args:
62
+ **kwargs: Any attribute defined on
63
+ :class:`~apidepth.configuration.Configuration`. Common options::
64
+
65
+ apidepth.configure(
66
+ api_key=os.environ["APIDEPTH_API_KEY"],
67
+ environment="production",
68
+ sample_rate=0.5,
69
+ ignored_hosts=["internal.example.com"],
70
+ )
71
+
72
+ Returns:
73
+ The updated :class:`~apidepth.configuration.Configuration` singleton.
74
+
75
+ Raises:
76
+ TypeError: If an unknown configuration key is passed.
77
+ """
78
+ from apidepth.configuration import Configuration as _Cfg
79
+ _valid = {k for k in vars(_Cfg()) if not k.startswith("_")}
80
+ unknown = set(kwargs) - _valid
81
+ if unknown:
82
+ raise TypeError(
83
+ f"apidepth.configure() got unexpected keyword argument(s): "
84
+ f"{', '.join(sorted(unknown))}. "
85
+ f"Valid options: {', '.join(sorted(_valid))}."
86
+ )
87
+ config = get_configuration()
88
+ for key, value in kwargs.items():
89
+ setattr(config, key, value)
90
+ return config
91
+
92
+
93
+ def get_configuration() -> Configuration:
94
+ """Return the process-wide :class:`~apidepth.configuration.Configuration` singleton.
95
+
96
+ Creates the singleton with default values on the first call. Thread-safe.
97
+ """
98
+ global _configuration
99
+ if _configuration is None:
100
+ with _configuration_lock:
101
+ if _configuration is None:
102
+ _configuration = Configuration()
103
+ return _configuration
104
+
105
+
106
+ def get_logger() -> logging.Logger:
107
+ """Return the SDK logger, creating the default one if none has been set.
108
+
109
+ The default logger is ``logging.getLogger("apidepth")``. Framework
110
+ integrations replace it with the framework's own logger via
111
+ :func:`set_logger` so all SDK output flows through the framework's
112
+ logging pipeline.
113
+ """
114
+ global _logger
115
+ if _logger is None:
116
+ _logger = logging.getLogger("apidepth")
117
+ return _logger
118
+
119
+
120
+ def set_logger(logger: logging.Logger) -> None:
121
+ """Replace the SDK logger.
122
+
123
+ Call this inside a framework boot hook before any SDK log output is
124
+ produced. After this call all SDK modules that use
125
+ ``logging.getLogger("apidepth")`` will pick up the new logger because
126
+ the Python logging system resolves loggers by name at emit time.
127
+
128
+ Args:
129
+ logger: The replacement :class:`logging.Logger`.
130
+ """
131
+ global _logger
132
+ _logger = logger
133
+
134
+
135
+ def instrument() -> None:
136
+ """Patch installed HTTP client libraries (*requests*, *httpx*).
137
+
138
+ Safe to call multiple times — subsequent calls after the first are
139
+ no-ops. Must be called after :func:`configure`. Framework integrations
140
+ (Django, Flask) call this automatically.
141
+
142
+ After this call, all outbound HTTP requests made through *requests* or
143
+ *httpx* to recognised vendor hostnames are automatically captured and
144
+ enqueued for the next batch flush.
145
+ """
146
+ from apidepth.instrumentation import instrument as _instrument
147
+ _instrument()
148
+
149
+
150
+ def sdk_metadata() -> Dict[str, Any]:
151
+ """Return a metadata dict included in every batch payload.
152
+
153
+ The metadata lets the collector correlate data-quality issues with
154
+ specific SDK versions, Python runtimes, and app servers without
155
+ requiring a support ticket. Mirrors the Ruby gem's ``sdk_metadata``
156
+ which includes ``rails_version`` and ``app_server``.
157
+
158
+ Returns:
159
+ A dict with keys ``name``, ``version``, ``python_version``,
160
+ ``python_platform``, and ``app_server``.
161
+ """
162
+ return {
163
+ "name": "apidepth-python",
164
+ "version": VERSION,
165
+ "python_version": sys.version.split()[0],
166
+ "python_platform": platform.platform(),
167
+ "app_server": _detect_app_server(),
168
+ }
169
+
170
+
171
+ def _detect_app_server() -> str:
172
+ """Detect the WSGI/ASGI server by inspecting already-loaded modules.
173
+
174
+ Uses ``sys.modules`` rather than attempting new imports so that the
175
+ detection is zero-cost and does not force-load server libraries as a
176
+ side-effect. This mirrors the Ruby gem's ``detect_app_server`` which
177
+ uses ``defined?(Puma)`` / ``defined?(Unicorn)`` / ``defined?(PhusionPassenger)``.
178
+
179
+ Servers checked (in priority order):
180
+
181
+ * ``gunicorn`` — most common WSGI server
182
+ * ``uwsgi`` — uWSGI (the ``uwsgi`` C extension module)
183
+ * ``waitress`` — pure-Python WSGI server
184
+ * ``uvicorn`` — ASGI server (FastAPI, Starlette)
185
+ * ``hypercorn`` — ASGI server
186
+ * ``daphne`` — ASGI server (Django Channels)
187
+
188
+ Returns:
189
+ The lowercase server name, or ``"unknown"`` if none is detected.
190
+ """
191
+ mods = sys.modules
192
+ if "gunicorn" in mods:
193
+ return "gunicorn"
194
+ if "uwsgi" in mods:
195
+ return "uwsgi"
196
+ if "waitress" in mods:
197
+ return "waitress"
198
+ if "uvicorn" in mods:
199
+ return "uvicorn"
200
+ if "hypercorn" in mods:
201
+ return "hypercorn"
202
+ if "daphne" in mods:
203
+ return "daphne"
204
+ return "unknown"
205
+
206
+
207
+ def sanitize_log(s: str) -> str:
208
+ """Strip CR, LF, and TAB from *s* and truncate to 200 characters.
209
+
210
+ Used throughout the SDK before interpolating untrusted strings (vendor
211
+ names, error messages, hostnames from registry data) into log output.
212
+ Prevents log-injection attacks (CVE-2025-27111 class).
213
+
214
+ Args:
215
+ s: Any string value. Non-string inputs are coerced via ``str()``.
216
+
217
+ Returns:
218
+ The sanitised, truncated string.
219
+ """
220
+ return str(s).translate(str.maketrans("\r\n\t", " "))[:200]