outlaya 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.
outlaya/__init__.py ADDED
@@ -0,0 +1,90 @@
1
+ """Outlaya Python SDK: AI cost attribution across providers.
2
+
3
+ Wrap a supported provider client, read usage from its response, and send one
4
+ normalized event to Outlaya without putting the ingest call on your request
5
+ path.
6
+
7
+ Supported providers: OpenAI, Anthropic, Gemini, OpenRouter (wrapped clients)
8
+ and Cursor (Admin API pull sync).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from ._client import (
14
+ OutlayaClient,
15
+ OutlayaIngestError,
16
+ Transport,
17
+ TransportResponse,
18
+ flush,
19
+ get_client,
20
+ init,
21
+ )
22
+ from ._context import current_metadata, metadata, with_metadata
23
+ from ._providers.anthropic import wrap_anthropic
24
+ from ._providers.cursor import (
25
+ CursorSyncError,
26
+ CursorSyncResult,
27
+ CursorTransport,
28
+ CursorTransportInput,
29
+ CursorUsagePage,
30
+ CursorUsageRecord,
31
+ sync_cursor_usage,
32
+ )
33
+ from ._providers.gemini import wrap_gemini, wrap_google_genai
34
+ from ._providers.openai import wrap_openai, wrap_openrouter
35
+ from ._types import (
36
+ ClientStats,
37
+ JsonScalar,
38
+ OutlayaClientOptions,
39
+ OutlayaEvent,
40
+ OutlayaEventClient,
41
+ OutlayaMetadata,
42
+ UsageLike,
43
+ UsageReport,
44
+ WrapOptions,
45
+ )
46
+ from ._usage import track_usage
47
+ from ._version import __version__
48
+ from ._wrap import wrap
49
+
50
+ SDK_VERSION = __version__
51
+ OutlayaClientStats = ClientStats
52
+
53
+ __all__ = [
54
+ "ClientStats",
55
+ "CursorSyncError",
56
+ "CursorSyncResult",
57
+ "CursorTransport",
58
+ "CursorTransportInput",
59
+ "CursorUsagePage",
60
+ "CursorUsageRecord",
61
+ "JsonScalar",
62
+ "OutlayaClient",
63
+ "OutlayaClientOptions",
64
+ "OutlayaClientStats",
65
+ "OutlayaEvent",
66
+ "OutlayaEventClient",
67
+ "OutlayaIngestError",
68
+ "OutlayaMetadata",
69
+ "SDK_VERSION",
70
+ "Transport",
71
+ "TransportResponse",
72
+ "UsageLike",
73
+ "UsageReport",
74
+ "WrapOptions",
75
+ "__version__",
76
+ "current_metadata",
77
+ "flush",
78
+ "get_client",
79
+ "init",
80
+ "metadata",
81
+ "sync_cursor_usage",
82
+ "track_usage",
83
+ "with_metadata",
84
+ "wrap",
85
+ "wrap_anthropic",
86
+ "wrap_gemini",
87
+ "wrap_google_genai",
88
+ "wrap_openai",
89
+ "wrap_openrouter",
90
+ ]
outlaya/_batching.py ADDED
@@ -0,0 +1,96 @@
1
+ """Packing helpers for the Outlaya ingest request limits."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from dataclasses import dataclass, field
7
+ from typing import Callable, Generic, List, Sequence, TypeVar
8
+
9
+ T = TypeVar("T")
10
+
11
+ #: The ingest route rejects event batches above 500 entries.
12
+ SDK_INGEST_MAX_BATCH_EVENTS = 500
13
+
14
+ #: 4 MiB stays below the API's 5 MiB request ceiling.
15
+ SDK_INGEST_MAX_CLIENT_BODY_BYTES = 4 * 1024 * 1024 - 1024
16
+
17
+
18
+ def utf8_byte_length(text: str) -> int:
19
+ return len(text.encode("utf-8"))
20
+
21
+
22
+ def json_byte_length(value: object) -> int:
23
+ return utf8_byte_length(json.dumps(value, separators=(",", ":")))
24
+
25
+
26
+ @dataclass
27
+ class RequestBatchPlan(Generic[T]):
28
+ batches: List[List[T]] = field(default_factory=list)
29
+ oversized_items: List[T] = field(default_factory=list)
30
+
31
+
32
+ def plan_request_batches(
33
+ items: Sequence[T],
34
+ max_items: int,
35
+ max_bytes: int,
36
+ serialize: Callable[[List[T]], str],
37
+ ) -> RequestBatchPlan[T]:
38
+ """Pack items into JSON requests within the item and byte limits.
39
+
40
+ Items that cannot fit alone are returned separately.
41
+ """
42
+ plan: RequestBatchPlan[T] = RequestBatchPlan()
43
+ current: List[T] = []
44
+
45
+ def flush() -> None:
46
+ if current:
47
+ plan.batches.append(current.copy())
48
+ current.clear()
49
+
50
+ for item in items:
51
+ if len(current) >= max_items:
52
+ flush()
53
+
54
+ if not current:
55
+ if utf8_byte_length(serialize([item])) > max_bytes:
56
+ plan.oversized_items.append(item)
57
+ else:
58
+ current.append(item)
59
+ continue
60
+
61
+ candidate = [*current, item]
62
+ if utf8_byte_length(serialize(candidate)) <= max_bytes:
63
+ current.append(item)
64
+ continue
65
+
66
+ flush()
67
+ if utf8_byte_length(serialize([item])) > max_bytes:
68
+ plan.oversized_items.append(item)
69
+ else:
70
+ current.append(item)
71
+
72
+ flush()
73
+ return plan
74
+
75
+
76
+ def prepend_and_bound(
77
+ existing: List[T],
78
+ earlier: List[T],
79
+ max_items: float,
80
+ ) -> List[T]:
81
+ """Prepend failed older items while dropping only newer tail items."""
82
+ combined = [*earlier, *existing]
83
+ if max_items == float("inf") or len(combined) <= max_items:
84
+ return combined
85
+ return combined[: max(0, int(max_items))]
86
+
87
+
88
+ __all__ = [
89
+ "SDK_INGEST_MAX_BATCH_EVENTS",
90
+ "SDK_INGEST_MAX_CLIENT_BODY_BYTES",
91
+ "RequestBatchPlan",
92
+ "json_byte_length",
93
+ "plan_request_batches",
94
+ "prepend_and_bound",
95
+ "utf8_byte_length",
96
+ ]