atherdlp 0.0.2__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.
atherdlp/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Callable
4
+
5
+ from atherdlp.config import configure, get_config
6
+ from atherdlp.interceptor.httpx_hook import install_httpx, uninstall_httpx
7
+
8
+ _handler: Callable[[dict], None] | None = None
9
+
10
+
11
+ def set_handler(handler: Callable[[dict], None] | None) -> None:
12
+ """
13
+ Set the callback that receives intercepted payloads.
14
+
15
+ The handler must be fail-safe; exceptions are swallowed by the interceptor.
16
+ """
17
+
18
+ global _handler
19
+ _handler = handler
20
+
21
+
22
+ def _get_handler() -> Callable[[dict], None]:
23
+ h = _handler
24
+ if h is None:
25
+ return lambda _payload: None
26
+ return h
27
+
28
+
29
+ def install() -> None:
30
+ install_httpx(_get_handler())
31
+
32
+
33
+ def uninstall() -> None:
34
+ uninstall_httpx()
@@ -0,0 +1,4 @@
1
+ from atherdlp.classifier.classify import classify_destination
2
+ from atherdlp.classifier.providers import PROVIDERS, register_provider
3
+
4
+ __all__ = ["PROVIDERS", "register_provider", "classify_destination"]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from urllib.parse import urlparse
4
+
5
+ from atherdlp.classifier.providers import PROVIDERS
6
+
7
+
8
+ def classify_destination(url: str) -> dict:
9
+ host = (urlparse(url).hostname or "").lower()
10
+ meta = PROVIDERS.get(host)
11
+ if meta is None:
12
+ return {"kind": "unknown", "vendor": None}
13
+ return dict(meta)
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Mapping
4
+
5
+
6
+ PROVIDERS: dict[str, dict] = {
7
+ "api.openai.com": {"kind": "llm", "vendor": "openai"},
8
+ "api.anthropic.com": {"kind": "llm", "vendor": "anthropic"},
9
+ "generativelanguage.googleapis.com": {"kind": "llm", "vendor": "google"},
10
+ "api.cohere.ai": {"kind": "llm", "vendor": "cohere"},
11
+ "api.mistral.ai": {"kind": "llm", "vendor": "mistral"},
12
+ "api.stripe.com": {"kind": "http.out", "vendor": "stripe", "sensitivity": "pci"},
13
+ "api.github.com": {"kind": "http.out", "vendor": "github"},
14
+ }
15
+
16
+
17
+ def register_provider(host: str, meta: Mapping) -> None:
18
+ PROVIDERS[host.lower()] = dict(meta)
atherdlp/config.py ADDED
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class SDKConfig:
8
+ endpoint: str | None = None
9
+ api_key: str | None = None
10
+ timeout_s: float = 0.2
11
+ enabled: bool = True
12
+ # Max backend emit tasks accepted at once (queued in the executor + running).
13
+ # When saturated, new emits are dropped (fail-open). Use 0 for no limit (not recommended under load).
14
+ max_pending_backend_emits: int = 128
15
+
16
+
17
+ _config = SDKConfig()
18
+
19
+
20
+ def configure(
21
+ *,
22
+ endpoint: str | None = None,
23
+ api_key: str | None = None,
24
+ timeout_s: float = 0.2,
25
+ enabled: bool = True,
26
+ max_pending_backend_emits: int = 128,
27
+ ) -> None:
28
+ global _config
29
+ _config = SDKConfig(
30
+ endpoint=endpoint,
31
+ api_key=api_key,
32
+ timeout_s=timeout_s,
33
+ enabled=enabled,
34
+ max_pending_backend_emits=max_pending_backend_emits,
35
+ )
36
+
37
+
38
+ def get_config() -> SDKConfig:
39
+ return _config
40
+
@@ -0,0 +1,3 @@
1
+ from atherdlp.context.trace import TraceContext, new_span_id, new_trace, new_trace_id
2
+
3
+ __all__ = ["TraceContext", "new_trace_id", "new_span_id", "new_trace"]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ import secrets
4
+ from dataclasses import asdict, dataclass
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class TraceContext:
9
+ trace_id: str
10
+ span_id: str
11
+ parent_span_id: str | None = None
12
+
13
+ def to_dict(self) -> dict:
14
+ return asdict(self)
15
+
16
+
17
+ def new_trace_id() -> str:
18
+ # 32 hex chars (W3C trace-id compatible shape)
19
+ return secrets.token_hex(16)
20
+
21
+
22
+ def new_span_id() -> str:
23
+ # 16 hex chars (W3C span-id compatible shape)
24
+ return secrets.token_hex(8)
25
+
26
+
27
+ def new_trace(*, parent_span_id: str | None = None) -> TraceContext:
28
+ return TraceContext(trace_id=new_trace_id(), span_id=new_span_id(), parent_span_id=parent_span_id)
@@ -0,0 +1,4 @@
1
+ from atherdlp.interceptor.base import InterceptedPayload, PayloadHandler
2
+ from atherdlp.interceptor.httpx_hook import install_httpx, uninstall_httpx
3
+
4
+ __all__ = ["InterceptedPayload", "PayloadHandler", "install_httpx", "uninstall_httpx"]
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Protocol
5
+
6
+
7
+ class PayloadHandler(Protocol):
8
+ def __call__(self, payload: dict) -> None: ...
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class InterceptedPayload:
13
+ request: dict
14
+ classification: dict
15
+ trace: dict
16
+
17
+ def to_dict(self) -> dict:
18
+ return asdict(self)
@@ -0,0 +1,234 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import json
5
+ from dataclasses import dataclass
6
+ from typing import Any
7
+
8
+ import httpx
9
+
10
+ from atherdlp.classifier.classify import classify_destination
11
+ from atherdlp.context.trace import new_trace
12
+ from atherdlp.config import get_config
13
+ from atherdlp.interceptor.base import PayloadHandler
14
+ from atherdlp.limits import get_sdk_interceptor_bytes_limits
15
+ from atherdlp.transport.sender import send_to_backend
16
+
17
+ from email.parser import BytesParser
18
+ from email.policy import default
19
+
20
+ @dataclass
21
+ class _State:
22
+ installed: bool = False
23
+ handler: PayloadHandler | None = None
24
+ orig_client_init: Any | None = None
25
+ orig_async_client_init: Any | None = None
26
+
27
+
28
+ _state = _State()
29
+
30
+
31
+ def install_httpx(handler: PayloadHandler) -> None:
32
+ if _state.installed:
33
+ _state.handler = handler
34
+ return
35
+
36
+ _state.handler = handler
37
+ _state.orig_client_init = httpx.Client.__init__
38
+ _state.orig_async_client_init = httpx.AsyncClient.__init__
39
+
40
+ def _client_init(self: httpx.Client, *args: Any, **kwargs: Any) -> None:
41
+ assert _state.orig_client_init is not None
42
+ _state.orig_client_init(self, *args, **kwargs)
43
+ if getattr(self, "_transport", None) is not None:
44
+ self._transport = _AtherTransport(self._transport)
45
+ if getattr(self, "_mounts", None):
46
+ self._mounts = {k: _AtherTransport(v) for k, v in self._mounts.items()}
47
+
48
+ def _async_client_init(self: httpx.AsyncClient, *args: Any, **kwargs: Any) -> None:
49
+ assert _state.orig_async_client_init is not None
50
+ _state.orig_async_client_init(self, *args, **kwargs)
51
+ if getattr(self, "_transport", None) is not None:
52
+ self._transport = _AtherAsyncTransport(self._transport)
53
+ if getattr(self, "_mounts", None):
54
+ self._mounts = {k: _AtherAsyncTransport(v) for k, v in self._mounts.items()}
55
+
56
+ httpx.Client.__init__ = _client_init # type: ignore[assignment]
57
+ httpx.AsyncClient.__init__ = _async_client_init # type: ignore[assignment]
58
+ _state.installed = True
59
+
60
+
61
+ def uninstall_httpx() -> None:
62
+ global _state
63
+ if not _state.installed:
64
+ return
65
+ if _state.orig_client_init is not None:
66
+ httpx.Client.__init__ = _state.orig_client_init # type: ignore[assignment]
67
+ if _state.orig_async_client_init is not None:
68
+ httpx.AsyncClient.__init__ = _state.orig_async_client_init # type: ignore[assignment]
69
+ _state = _State() # reset
70
+
71
+
72
+ class _AtherTransport(httpx.BaseTransport):
73
+ def __init__(self, inner: httpx.BaseTransport):
74
+ self._inner = inner
75
+
76
+ def handle_request(self, request: httpx.Request) -> httpx.Response:
77
+ _emit(request)
78
+ return self._inner.handle_request(request)
79
+
80
+
81
+ class _AtherAsyncTransport(httpx.AsyncBaseTransport):
82
+ def __init__(self, inner: httpx.AsyncBaseTransport):
83
+ self._inner = inner
84
+
85
+ async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
86
+ _emit(request)
87
+ return await self._inner.handle_async_request(request)
88
+
89
+
90
+ def _emit(request: httpx.Request) -> None:
91
+ # Never intercept SDK-internal backend calls (prevents recursion).
92
+ if request.headers.get("x-ather-internal") == "1":
93
+ return
94
+
95
+ # Also skip interception when the destination is the configured backend endpoint.
96
+ cfg = get_config()
97
+ if cfg.endpoint:
98
+ try:
99
+ if str(request.url) == str(cfg.endpoint):
100
+ return
101
+ except Exception:
102
+ pass
103
+
104
+ trace = new_trace().to_dict()
105
+ # Propagate trace to the upstream destination (best-effort).
106
+ try:
107
+ request.headers["x-ather-trace-id"] = trace["trace_id"]
108
+ request.headers["x-ather-span-id"] = trace["span_id"]
109
+ except Exception:
110
+ pass
111
+
112
+ classification = classify_destination(str(request.url))
113
+
114
+ payload = {
115
+ "request": {
116
+ "method": request.method,
117
+ "url": str(request.url),
118
+ "headers": dict(request.headers),
119
+ "body": _safe_body(request),
120
+ },
121
+ "classification": classification,
122
+ "trace": trace,
123
+ }
124
+
125
+ # Best-effort delivery to backend (non-blocking, fail-open).
126
+ send_to_backend(payload)
127
+
128
+ handler = _state.handler
129
+ if handler is None:
130
+ return
131
+ try:
132
+ handler(payload)
133
+ except Exception:
134
+ # fail-open: never break the host app
135
+ return
136
+
137
+
138
+ def _safe_body(request: httpx.Request) -> Any:
139
+ try:
140
+ raw = request.content or b""
141
+ except httpx.RequestNotRead:
142
+ # Multipart/form uploads are streamed by default. Reading here should
143
+ # materialize the bytes and keep request semantics intact.
144
+ raw = request.read() or b""
145
+ if not raw:
146
+ return None
147
+
148
+ ctype = request.headers.get("content-type", "")
149
+
150
+ # MULTIPART (files/form fields)
151
+ if "multipart/form-data" in ctype:
152
+ return _extract_multipart(raw, ctype)
153
+
154
+ max_non, _, sample_b = get_sdk_interceptor_bytes_limits()
155
+ # LARGE BODY → SAMPLE
156
+ if len(raw) > max_non:
157
+ return _sample_bytes(raw, size=len(raw), sample_slice_bytes=sample_b)
158
+
159
+ # JSON
160
+ if "application/json" in ctype:
161
+ try:
162
+ return json.loads(raw)
163
+ except Exception:
164
+ pass
165
+
166
+ # TEXT
167
+ try:
168
+ return raw.decode("utf-8")
169
+ except Exception:
170
+ return {"_binary": True, "size": len(raw)}
171
+
172
+
173
+
174
+
175
+ def _sample_bytes(raw: bytes, *, size: int, sample_slice_bytes: int) -> dict:
176
+ sb = max(1, sample_slice_bytes)
177
+ start = raw[:sb]
178
+ mid_start = max(0, size // 2 - sb // 2)
179
+ middle = raw[mid_start : mid_start + sb]
180
+ end = raw[-sb:] if size >= sb else raw
181
+
182
+ def _to_text(chunk: bytes) -> str:
183
+ try:
184
+ return chunk.decode("utf-8", errors="replace")
185
+ except Exception:
186
+ return base64.b64encode(chunk).decode("ascii")
187
+
188
+ return {"_sampled": True, "size": size, "samples": [_to_text(start), _to_text(middle), _to_text(end)]}
189
+
190
+
191
+ def _extract_multipart(raw: bytes, content_type: str):
192
+ _, max_inline, sample_b = get_sdk_interceptor_bytes_limits()
193
+ msg = BytesParser(policy=default).parsebytes(
194
+ b"Content-Type: " + content_type.encode() + b"\n\n" + raw
195
+ )
196
+
197
+ files = []
198
+ fields = []
199
+
200
+ for part in msg.iter_parts():
201
+ content = part.get_payload(decode=True) or b""
202
+ # get the filename from the part If this exists → it's a file
203
+ filename = part.get_filename()
204
+
205
+ if filename:
206
+ size = len(content)
207
+ if size > max_inline:
208
+ sampled: Any = _sample_bytes(
209
+ content, size=size, sample_slice_bytes=sample_b
210
+ )
211
+ else:
212
+ try:
213
+ sampled = content.decode("utf-8", errors="replace")
214
+ except Exception:
215
+ sampled = {"_binary": True, "size": size}
216
+
217
+ files.append({
218
+ "filename": filename,
219
+ "content": sampled,
220
+ "size": size,
221
+ "content_type": part.get_content_type(),
222
+ })
223
+
224
+ else:
225
+ fields.append({
226
+ "name": part.get_param("name", header="content-disposition"),
227
+ "value": content.decode("utf-8", errors="replace") if content else None
228
+ })
229
+
230
+ return {
231
+ "files": files,
232
+ "fields": fields
233
+ }
234
+
atherdlp/limits.py ADDED
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from functools import lru_cache
5
+
6
+ # Default values must match AtherDLP-infra/.env.example (canonical source in repo).
7
+
8
+
9
+ def _int_env(name: str, default: int) -> int:
10
+ raw = os.environ.get(name)
11
+ if raw is None or str(raw).strip() == "":
12
+ return default
13
+ try:
14
+ return int(str(raw).strip(), 10)
15
+ except (TypeError, ValueError):
16
+ return default
17
+
18
+
19
+ @lru_cache(maxsize=1)
20
+ def get_sdk_interceptor_bytes_limits() -> tuple[int, int, int]:
21
+ """
22
+ (max_non_multipart, max_file_inline, sample_slice).
23
+
24
+ Set in process env, typically from AtherDLP-infra/.env:
25
+ ATHERDLP_SDK_MAX_NON_MULTIPART_BODY_BYTES
26
+ ATHERDLP_SDK_MAX_FILE_INLINE_BYTES
27
+ ATHERDLP_SDK_SAMPLE_SLICE_BYTES
28
+ """
29
+ return (
30
+ _int_env("ATHERDLP_SDK_MAX_NON_MULTIPART_BODY_BYTES", 2 * 1024 * 1024),
31
+ _int_env("ATHERDLP_SDK_MAX_FILE_INLINE_BYTES", 256 * 1024),
32
+ _int_env("ATHERDLP_SDK_SAMPLE_SLICE_BYTES", 16 * 1024),
33
+ )
atherdlp/py.typed ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from atherdlp.transport.sender import send_to_backend
2
+
3
+ __all__ = ["send_to_backend"]
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import re
6
+ import threading
7
+ from concurrent.futures import ThreadPoolExecutor
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from atherdlp.config import get_config
13
+
14
+ _log = logging.getLogger("atherdlp")
15
+
16
+ # Match backend correlation rules (shared.correlation.SAFE_CID): alnum + hyphen, max 64.
17
+ _SAFE_CORRELATION_ID = re.compile(r"^[a-zA-Z0-9\-]{1,64}$")
18
+
19
+ _executor = ThreadPoolExecutor(max_workers=4, thread_name_prefix="atherdlp")
20
+ _emit_lock = threading.Lock()
21
+ _pending_backend_emits = 0
22
+
23
+
24
+ def _first_request_header(hdrs: Any, canon_name_lc: str) -> str | None:
25
+ """Case-insensitive first value from payload snapshot headers."""
26
+ if not isinstance(hdrs, dict):
27
+ return None
28
+ for k, val in hdrs.items():
29
+ if str(k).lower() != canon_name_lc:
30
+ continue
31
+ if val is None:
32
+ continue
33
+ s = str(val).strip()
34
+ if s:
35
+ return s
36
+ return None
37
+
38
+
39
+ def _correlation_id_for_backend_header(payload: dict[str, Any], trace_id: Any) -> str | None:
40
+ incoming = _first_request_header(
41
+ (payload.get("request") or {}).get("headers"),
42
+ "x-correlation-id",
43
+ )
44
+ if incoming and _SAFE_CORRELATION_ID.match(incoming):
45
+ return incoming
46
+ tid = str(trace_id).strip() if trace_id is not None and str(trace_id).strip() else None
47
+ if tid and _SAFE_CORRELATION_ID.match(tid):
48
+ return tid
49
+ return None
50
+
51
+
52
+ def send_to_backend(payload: dict[str, Any]) -> None:
53
+ """
54
+ Best-effort, fail-open delivery of intercepted payloads.
55
+ Non-blocking by design: dispatches in a thread and returns immediately.
56
+ """
57
+
58
+ cfg = get_config()
59
+ if not cfg.enabled or not cfg.endpoint:
60
+ return
61
+
62
+ trace = payload.get("trace") or {}
63
+ trace_id = trace.get("trace_id")
64
+ span_id = trace.get("span_id")
65
+
66
+ headers: dict[str, str] = {"content-type": "application/json"}
67
+ # Prevent the SDK from intercepting its own backend calls.
68
+ headers["x-ather-internal"] = "1"
69
+ if cfg.api_key:
70
+ # Authentication to the backend
71
+ headers["x-api-key"] = cfg.api_key
72
+ if trace_id:
73
+ headers["x-ather-trace-id"] = str(trace_id)
74
+ cid = _correlation_id_for_backend_header(payload, trace_id)
75
+ if cid:
76
+ headers["X-Correlation-Id"] = cid
77
+ if span_id:
78
+ headers["x-ather-span-id"] = str(span_id)
79
+
80
+ limit = cfg.max_pending_backend_emits
81
+
82
+ global _pending_backend_emits
83
+
84
+ def _release_pending() -> None:
85
+ global _pending_backend_emits
86
+ if limit <= 0:
87
+ return
88
+ with _emit_lock:
89
+ _pending_backend_emits -= 1
90
+
91
+ if limit > 0:
92
+ with _emit_lock:
93
+ if _pending_backend_emits >= limit:
94
+ _log.debug(
95
+ "backend emit backlog saturated; dropping (%d/%d pending)",
96
+ _pending_backend_emits,
97
+ limit,
98
+ )
99
+ return
100
+ _pending_backend_emits += 1
101
+
102
+ def _do_send() -> None:
103
+ try:
104
+ body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
105
+ # Create a short-lived client per send to avoid shared state issues.
106
+ with httpx.Client(timeout=cfg.timeout_s) as client:
107
+ client.post(cfg.endpoint, content=body, headers=headers)
108
+ except Exception as e: # fail-open
109
+ _log.debug("backend emit failed: %s", e)
110
+ finally:
111
+ _release_pending()
112
+
113
+ try:
114
+ _executor.submit(_do_send)
115
+ except Exception as e: # fail-open
116
+ if limit > 0:
117
+ with _emit_lock:
118
+ _pending_backend_emits -= 1
119
+ _log.debug("backend emit submit failed: %s", e)
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.5
2
+ Name: atherdlp
3
+ Version: 0.0.2
4
+ Summary: Developer-first DLP HTTP interceptor (Stage 1)
5
+ Project-URL: Homepage, https://github.com/AetherDLP/AtherDLP-sdk
6
+ Project-URL: Repository, https://github.com/AetherDLP/AtherDLP-sdk
7
+ Project-URL: Issues, https://github.com/AetherDLP/AtherDLP-sdk/issues
8
+ Author-email: Subham Singh Chauhan <subhamchauhan1100@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: dlp,httpx,interceptor,observability,security,tracing
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Internet :: WWW/HTTP
21
+ Classifier: Topic :: Security
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: httpx>=0.27
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'test'
27
+ Requires-Dist: pytest>=8; extra == 'test'
28
+ Requires-Dist: respx>=0.21; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # AtherDLP (Python) — Stage 1 HTTP Interceptor
32
+
33
+ Stage 1 intercepts outbound HTTP requests, classifies the destination (O(1) registry lookup), attaches a trace context, and emits a structured payload to a handler.
34
+
35
+ ## Install (dev)
36
+
37
+ ```bash
38
+ cd AtherDLP-sdk/python
39
+ python -m pip install -e ".[test]"
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ import httpx
46
+ import atherdlp
47
+
48
+ def handler(payload: dict) -> None:
49
+ print(payload)
50
+
51
+ atherdlp.set_handler(handler)
52
+ atherdlp.install()
53
+
54
+ httpx.get("https://api.openai.com/v1/models", headers={"authorization": "Bearer x"})
55
+
56
+ atherdlp.uninstall()
57
+ ```
58
+
59
+ ### Tracing
60
+
61
+ - If the request includes a **correlation header** (e.g. **`X-Correlation-Id`**), it is **propagated automatically** (safe values only: **ASCII alphanumeric + `-`, 1–64 chars**).
62
+ - If **not** present or invalid, AtherDLP **generates its own** trace id (`trace.trace_id` in the payload).
63
+ - **Ather-specific** tracing is **always** added on the outbound call via **`x-ather-trace-id`** (and **`x-ather-span-id`**).
64
+
65
+ More detail: **`docs/SDK.md`** → *Trace And Recursion Guards*.
66
+
67
+ ```python
68
+ httpx.post(url, headers={"X-Correlation-Id": "my-route-8842"})
69
+ ```
70
+
71
+ ## Output payload
72
+
73
+ ```json
74
+ {
75
+ "request": { "method": "...", "url": "...", "headers": { }, "body": null },
76
+ "classification": { "kind": "llm|http.out|unknown", "vendor": "...", "sensitivity": "..." },
77
+ "trace": { "trace_id": "...", "span_id": "...", "parent_span_id": null }
78
+ }
79
+ ```
@@ -0,0 +1,18 @@
1
+ atherdlp/__init__.py,sha256=vZ8wBjMIwrCgfQfDncEWPrYn0o3yEmHzWkSUoSakhRU,743
2
+ atherdlp/config.py,sha256=Mm5WaqQxzhokRAvyZzwVuxkosCfDD4XKqiw8mmC91gU,947
3
+ atherdlp/limits.py,sha256=Gw8NGPkXceqvBmfP6F-AR0hOK-AWyNzGfoMP0dFlM84,993
4
+ atherdlp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ atherdlp/classifier/__init__.py,sha256=MmvbR1N0_tJ0-6ITzMm45oCkz2-_vPVJzHLqcppn-N8,202
6
+ atherdlp/classifier/classify.py,sha256=W1KmMZW7eQp1PyZG1JqUo2v78AIPgMMBhRK97UsxWWY,343
7
+ atherdlp/classifier/providers.py,sha256=FNM8M8cgS2PMo6NPkLuZaMAxH5Hznw2C82nBVDGLJyk,667
8
+ atherdlp/context/__init__.py,sha256=st34zr4liU0aWaSEwbjRQ8h-EfU26m_JC_BYv2vTWLY,157
9
+ atherdlp/context/trace.py,sha256=YnUVCt5ndwMoS9n1iKYrzxkBaMqngGDieXtnKdaqg5Y,668
10
+ atherdlp/interceptor/__init__.py,sha256=AUxn7kp21861b4cfC1Ay8indw1W-fHQ_smSP05XNI3k,235
11
+ atherdlp/interceptor/base.py,sha256=vylocgDo5UcPaUJUcAVqORHdh0v96J_M8lS0A26fspA,361
12
+ atherdlp/interceptor/httpx_hook.py,sha256=2U7DxVxpKfSSuZ-hMdaPRxghB704S2m7cbcJGWqAzoQ,7370
13
+ atherdlp/transport/__init__.py,sha256=4zN3dU68u8Hcp4Hs__uFhsW-lL4hjXjKl0oN-l3l0co,85
14
+ atherdlp/transport/sender.py,sha256=KEu60U34osOBEJZELsmE5CoOv2adWWdgaNiOmxrbuaE,3698
15
+ atherdlp-0.0.2.dist-info/METADATA,sha256=9k9VbyEQocdmTVipYF9uQQHbQmqf0oxtjBPFDTdn3yo,2651
16
+ atherdlp-0.0.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
17
+ atherdlp-0.0.2.dist-info/licenses/LICENSE,sha256=0GA5dQkfotO0XJyS5wCeOkZmFEzBQIesrR4yUDRm658,1077
18
+ atherdlp-0.0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Subham Singh Chauhan
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.