cognocient 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.
cognocient/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from ._reporter import CallReport, Reporter, DEFAULT_INGEST_URL
2
+ from .openai_wrapper import CognocientOpenAI
3
+ from .anthropic_wrapper import CognocientAnthropic
4
+
5
+ __all__ = ["CognocientOpenAI", "CognocientAnthropic", "CallReport", "Reporter", "DEFAULT_INGEST_URL"]
6
+ __version__ = "0.1.0"
@@ -0,0 +1,142 @@
1
+ """
2
+ Async batching reporter — fires call metadata to Cognocient's ingestion
3
+ API without ever blocking or delaying the customer's real provider call,
4
+ and without ever raising back into their code.
5
+
6
+ Runs its own background daemon thread with a plain stdlib queue.Queue,
7
+ independent of whether the customer's application uses asyncio at all.
8
+ Most of the value of this wrapper is for the plain synchronous
9
+ openai.OpenAI() / anthropic.Anthropic() clients shown in the README,
10
+ which have no event loop to hang an asyncio task off of — a thread-based
11
+ worker is the one design that reports reliably regardless of whether the
12
+ calling application is sync or async.
13
+ """
14
+
15
+ import atexit
16
+ import logging
17
+ import queue
18
+ import threading
19
+ from dataclasses import asdict, dataclass
20
+ from typing import Optional
21
+
22
+ import httpx
23
+
24
+ logger = logging.getLogger("cognocient")
25
+
26
+ DEFAULT_INGEST_URL = "https://api.cognocient.com/api/ingest/wrapper"
27
+ FLUSH_INTERVAL_SECONDS = 5.0
28
+ FLUSH_MAX_BATCH = 50
29
+ QUEUE_MAX_SIZE = 500
30
+
31
+
32
+ @dataclass
33
+ class CallReport:
34
+ model: str
35
+ provider: str
36
+ prompt_tokens: int
37
+ completion_tokens: int
38
+ latency_ms: int
39
+ status_code: int = 200
40
+ cost_usd: Optional[float] = None
41
+ tag_feature: Optional[str] = None
42
+ tag_department: Optional[str] = None
43
+ tag_user: Optional[str] = None
44
+ tag_session: Optional[str] = None
45
+ tag_tier: Optional[str] = None
46
+ tag_project: Optional[str] = None
47
+ tag_gl_account: Optional[str] = None
48
+ tag_workload: Optional[str] = None
49
+ tag_outcome: Optional[str] = None
50
+ run_id: Optional[str] = None
51
+
52
+
53
+ class Reporter:
54
+ """
55
+ One instance per client. Every failure mode (network error, ingestion
56
+ API unreachable, local queue full) is caught and logged locally at
57
+ DEBUG level — never raised back into the caller. This is the actual
58
+ reliability guarantee of the wrapper; it has to hold here, not just be
59
+ claimed in the docs. See tests/test_reporter_failure_isolation.py.
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ cognocient_key: str,
65
+ ingest_url: str = DEFAULT_INGEST_URL,
66
+ flush_interval: float = FLUSH_INTERVAL_SECONDS,
67
+ flush_max_batch: int = FLUSH_MAX_BATCH,
68
+ queue_max_size: int = QUEUE_MAX_SIZE,
69
+ http_timeout: float = 5.0,
70
+ ):
71
+ self._cognocient_key = cognocient_key
72
+ self._ingest_url = ingest_url
73
+ self._flush_interval = flush_interval
74
+ self._flush_max_batch = flush_max_batch
75
+ self._queue: "queue.Queue[CallReport]" = queue.Queue(maxsize=queue_max_size)
76
+ self._client = httpx.Client(timeout=http_timeout)
77
+ self._dropped_count = 0
78
+ self._wake = threading.Event()
79
+ self._stop = False
80
+ self._thread = threading.Thread(target=self._run, name="cognocient-reporter", daemon=True)
81
+ self._thread.start()
82
+ atexit.register(self._shutdown)
83
+
84
+ def report(self, call: CallReport) -> None:
85
+ """Non-blocking, never raises. Call this right after the real provider call returns."""
86
+ try:
87
+ self._queue.put_nowait(call)
88
+ if self._queue.qsize() >= self._flush_max_batch:
89
+ self._wake.set()
90
+ except queue.Full:
91
+ self._dropped_count += 1
92
+ logger.debug(
93
+ "cognocient: reporting queue full, dropped a report (%d dropped total)",
94
+ self._dropped_count,
95
+ )
96
+ except Exception:
97
+ logger.debug("cognocient: failed to queue a report", exc_info=True)
98
+
99
+ def flush(self) -> None:
100
+ """Force an immediate flush — e.g. before a short-lived script exits."""
101
+ try:
102
+ self._flush()
103
+ except Exception:
104
+ logger.debug("cognocient: manual flush failed", exc_info=True)
105
+
106
+ def _run(self) -> None:
107
+ while not self._stop:
108
+ self._wake.wait(timeout=self._flush_interval)
109
+ self._wake.clear()
110
+ self._flush()
111
+ self._flush() # final drain on shutdown
112
+
113
+ def _flush(self) -> None:
114
+ batch = []
115
+ while len(batch) < self._flush_max_batch:
116
+ try:
117
+ batch.append(self._queue.get_nowait())
118
+ except queue.Empty:
119
+ break
120
+ if not batch:
121
+ return
122
+ try:
123
+ self._client.post(
124
+ self._ingest_url,
125
+ json={"reports": [asdict(c) for c in batch]},
126
+ headers={"Authorization": f"Bearer {self._cognocient_key}"},
127
+ )
128
+ except Exception:
129
+ # Network error, timeout, Cognocient down — never propagate.
130
+ # The batch is simply dropped; retrying would risk piling up
131
+ # unbounded background work in the customer's process for
132
+ # something that is not their application's job to guarantee.
133
+ logger.debug("cognocient: failed to report %d call(s)", len(batch), exc_info=True)
134
+
135
+ def _shutdown(self) -> None:
136
+ try:
137
+ self._stop = True
138
+ self._wake.set()
139
+ self._thread.join(timeout=2.0)
140
+ self._client.close()
141
+ except Exception:
142
+ logger.debug("cognocient: error during reporter shutdown", exc_info=True)
cognocient/_tags.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ Attribution kwargs, matching the exact tag names the Cognocient proxy
3
+ already accepts as X-Cost-* headers (backend/app/proxy.py tag_header_map)
4
+ so a customer moving between the wrapper and the proxy uses the same
5
+ tagging model, not two different ones to learn.
6
+ """
7
+
8
+ TAG_KWARGS = (
9
+ "cognocient_feature",
10
+ "cognocient_department",
11
+ "cognocient_user",
12
+ "cognocient_session",
13
+ "cognocient_tier",
14
+ "cognocient_project",
15
+ "cognocient_gl_account",
16
+ "cognocient_workload",
17
+ "cognocient_outcome",
18
+ "cognocient_run_id",
19
+ )
20
+
21
+
22
+ def pop_tags(kwargs: dict) -> dict:
23
+ """Strips cognocient_* kwargs out of a call's kwargs and returns them
24
+ mapped to CallReport field names, so the real SDK call underneath never
25
+ sees (and never rejects) an argument it doesn't recognize."""
26
+ tags = {}
27
+ for key in TAG_KWARGS:
28
+ if key in kwargs:
29
+ value = kwargs.pop(key)
30
+ name = key[len("cognocient_"):]
31
+ tags["run_id" if name == "run_id" else f"tag_{name}"] = value
32
+ return tags
@@ -0,0 +1,89 @@
1
+ """
2
+ Drop-in wrapper around anthropic.Anthropic that reports every completed
3
+ messages.create() call to Cognocient asynchronously, without ever
4
+ blocking or delaying the real call to Anthropic, and without ever raising
5
+ a reporting failure back into the caller.
6
+
7
+ See openai_wrapper.py's module docstring for the security framing (same
8
+ tradeoffs apply here — this is not "more secure" than the proxy) and the
9
+ streaming limitation (also identical: non-streaming only in this version).
10
+ """
11
+
12
+ import time
13
+ from typing import Any
14
+
15
+ from ._reporter import DEFAULT_INGEST_URL, CallReport, Reporter
16
+ from ._tags import pop_tags
17
+
18
+
19
+ class _ReportingMessages:
20
+ def __init__(self, real_messages: Any, reporter: Reporter, provider: str):
21
+ self._real = real_messages
22
+ self._reporter = reporter
23
+ self._provider = provider
24
+
25
+ def create(self, *args, **kwargs):
26
+ tags = pop_tags(kwargs)
27
+ is_streaming = bool(kwargs.get("stream"))
28
+
29
+ start = time.monotonic()
30
+ response = self._real.create(*args, **kwargs)
31
+ latency_ms = int((time.monotonic() - start) * 1000)
32
+
33
+ if is_streaming:
34
+ return response # not reported in this version — see module docstring
35
+
36
+ try:
37
+ usage = getattr(response, "usage", None)
38
+ model = getattr(response, "model", None) or kwargs.get("model", "unknown")
39
+ # Anthropic's usage field names differ from OpenAI's — input/output, not prompt/completion.
40
+ prompt_tokens = getattr(usage, "input_tokens", 0) if usage else 0
41
+ completion_tokens = getattr(usage, "output_tokens", 0) if usage else 0
42
+ self._reporter.report(CallReport(
43
+ model=model,
44
+ provider=self._provider,
45
+ prompt_tokens=prompt_tokens,
46
+ completion_tokens=completion_tokens,
47
+ latency_ms=latency_ms,
48
+ status_code=200,
49
+ **tags,
50
+ ))
51
+ except Exception:
52
+ pass
53
+
54
+ return response
55
+
56
+ def __getattr__(self, name):
57
+ return getattr(self._real, name)
58
+
59
+
60
+ class CognocientAnthropic:
61
+ """
62
+ Drop-in replacement import for anthropic.Anthropic:
63
+
64
+ from cognocient import CognocientAnthropic as Anthropic
65
+ client = Anthropic(
66
+ api_key="sk-ant-...", # your own real Anthropic key
67
+ cognocient_key="sk-cog-...", # your Cognocient proxy key, reused here
68
+ )
69
+ client.messages.create(
70
+ model="claude-sonnet-4-6", max_tokens=1024, messages=[...],
71
+ cognocient_feature="support-bot",
72
+ )
73
+ """
74
+
75
+ def __init__(self, *args, cognocient_key: str, cognocient_ingest_url: str = DEFAULT_INGEST_URL, **kwargs):
76
+ try:
77
+ import anthropic
78
+ except ImportError as e:
79
+ raise ImportError(
80
+ "cognocient.CognocientAnthropic requires the 'anthropic' package. "
81
+ "Install it with: pip install cognocient[anthropic]"
82
+ ) from e
83
+
84
+ self._real = anthropic.Anthropic(*args, **kwargs)
85
+ self._reporter = Reporter(cognocient_key, ingest_url=cognocient_ingest_url)
86
+ self.messages = _ReportingMessages(self._real.messages, self._reporter, "anthropic")
87
+
88
+ def __getattr__(self, name):
89
+ return getattr(self._real, name)
@@ -0,0 +1,124 @@
1
+ """
2
+ Drop-in wrapper around openai.OpenAI that reports every completed
3
+ chat.completions.create() call to Cognocient asynchronously, without ever
4
+ blocking or delaying the real call to OpenAI, and without ever raising a
5
+ reporting failure back into the caller.
6
+
7
+ SECURITY NOTE (read before writing any customer-facing copy referencing
8
+ this file): this wrapper runs INSIDE your application process and holds
9
+ your real OpenAI API key to make the call directly. That is a DIFFERENT
10
+ security exposure than the Cognocient proxy (where the key lives
11
+ server-side, under Cognocient's control, in one place) — not a strictly
12
+ lesser one. This wrapper is NOT "more secure" than the proxy. Its honest,
13
+ defensible benefits are zero added request latency and zero risk of a
14
+ Cognocient outage affecting your production call. See ../README.md.
15
+
16
+ LIMITATION: usage reporting only covers non-streaming calls in this
17
+ version. Streaming calls (stream=True) are passed through to the real SDK
18
+ completely unmodified — your application behaves identically — but are
19
+ not currently reported to Cognocient, since usage totals aren't available
20
+ until a stream completes, and reliably capturing them requires wrapping
21
+ the stream iterator itself, which this version doesn't do. If most of
22
+ your traffic streams, this wrapper will under-report your usage today.
23
+ """
24
+
25
+ import time
26
+ from typing import Any
27
+
28
+ from ._reporter import DEFAULT_INGEST_URL, CallReport, Reporter
29
+ from ._tags import pop_tags
30
+
31
+
32
+ class _ReportingCompletions:
33
+ """Wraps client.chat.completions — every attribute except create()
34
+ is forwarded untouched to the real SDK object."""
35
+
36
+ def __init__(self, real_completions: Any, reporter: Reporter, provider: str):
37
+ self._real = real_completions
38
+ self._reporter = reporter
39
+ self._provider = provider
40
+
41
+ def create(self, *args, **kwargs):
42
+ tags = pop_tags(kwargs)
43
+ is_streaming = bool(kwargs.get("stream"))
44
+
45
+ start = time.monotonic()
46
+ # The real call always happens first, and its result/exception is
47
+ # returned to the caller exactly as the real SDK would — nothing
48
+ # about reporting can change this line's outcome.
49
+ response = self._real.create(*args, **kwargs)
50
+ latency_ms = int((time.monotonic() - start) * 1000)
51
+
52
+ if is_streaming:
53
+ return response # not reported in this version — see module docstring
54
+
55
+ try:
56
+ usage = getattr(response, "usage", None)
57
+ model = getattr(response, "model", None) or kwargs.get("model", "unknown")
58
+ prompt_tokens = getattr(usage, "prompt_tokens", 0) if usage else 0
59
+ completion_tokens = getattr(usage, "completion_tokens", 0) if usage else 0
60
+ self._reporter.report(CallReport(
61
+ model=model,
62
+ provider=self._provider,
63
+ prompt_tokens=prompt_tokens,
64
+ completion_tokens=completion_tokens,
65
+ latency_ms=latency_ms,
66
+ status_code=200,
67
+ **tags,
68
+ ))
69
+ except Exception:
70
+ # A bug in this wrapper's own bookkeeping must never surface
71
+ # to the caller — the real response above has already
72
+ # returned successfully by this point.
73
+ pass
74
+
75
+ return response
76
+
77
+ def __getattr__(self, name):
78
+ return getattr(self._real, name)
79
+
80
+
81
+ class _ReportingChat:
82
+ def __init__(self, real_chat: Any, reporter: Reporter, provider: str):
83
+ self._real = real_chat
84
+ self.completions = _ReportingCompletions(real_chat.completions, reporter, provider)
85
+
86
+ def __getattr__(self, name):
87
+ return getattr(self._real, name)
88
+
89
+
90
+ class CognocientOpenAI:
91
+ """
92
+ Drop-in replacement import for openai.OpenAI:
93
+
94
+ from cognocient import CognocientOpenAI as OpenAI
95
+ client = OpenAI(
96
+ api_key="sk-...", # your own real OpenAI key
97
+ cognocient_key="sk-cog-...", # your Cognocient proxy key, reused here
98
+ )
99
+ client.chat.completions.create(
100
+ model="gpt-4o", messages=[...],
101
+ cognocient_feature="support-bot", # optional attribution, same field names as the proxy's X-Cost-* headers
102
+ )
103
+
104
+ Every method the real OpenAI client exposes still works unchanged —
105
+ this class only intercepts chat.completions.create() to report usage
106
+ after the fact; everything else is forwarded to the real client
107
+ untouched via attribute delegation.
108
+ """
109
+
110
+ def __init__(self, *args, cognocient_key: str, cognocient_ingest_url: str = DEFAULT_INGEST_URL, **kwargs):
111
+ try:
112
+ import openai
113
+ except ImportError as e:
114
+ raise ImportError(
115
+ "cognocient.CognocientOpenAI requires the 'openai' package. "
116
+ "Install it with: pip install cognocient[openai]"
117
+ ) from e
118
+
119
+ self._real = openai.OpenAI(*args, **kwargs)
120
+ self._reporter = Reporter(cognocient_key, ingest_url=cognocient_ingest_url)
121
+ self.chat = _ReportingChat(self._real.chat, self._reporter, "openai")
122
+
123
+ def __getattr__(self, name):
124
+ return getattr(self._real, name)
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: cognocient
3
+ Version: 0.1.0
4
+ Summary: Drop-in async-reporting wrapper for the OpenAI and Anthropic Python SDKs — live cost attribution without changing your base_url.
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.9
8
+ Requires-Dist: httpx>=0.24
9
+ Provides-Extra: anthropic
10
+ Requires-Dist: anthropic>=0.25; extra == 'anthropic'
11
+ Provides-Extra: dev
12
+ Requires-Dist: anthropic>=0.25; extra == 'dev'
13
+ Requires-Dist: openai>=1.0; extra == 'dev'
14
+ Requires-Dist: pytest>=7.0; extra == 'dev'
15
+ Provides-Extra: openai
16
+ Requires-Dist: openai>=1.0; extra == 'openai'
17
+ Description-Content-Type: text/markdown
18
+
19
+ # cognocient
20
+
21
+ A drop-in wrapper around the OpenAI and Anthropic Python SDKs that reports
22
+ usage to Cognocient asynchronously, so you get live cost attribution
23
+ without changing your `base_url` or routing traffic through a proxy.
24
+
25
+ ```bash
26
+ pip install cognocient[openai] # or cognocient[anthropic], or both
27
+ ```
28
+
29
+ ```python
30
+ # Before
31
+ from openai import OpenAI
32
+ client = OpenAI(api_key="sk-...")
33
+
34
+ # After
35
+ from cognocient import CognocientOpenAI as OpenAI
36
+ client = OpenAI(
37
+ api_key="sk-...", # your own real OpenAI key, used exactly as before
38
+ cognocient_key="sk-cog-...", # the same proxy key you'd use with the Cognocient proxy
39
+ )
40
+
41
+ client.chat.completions.create(
42
+ model="gpt-4o",
43
+ messages=[{"role": "user", "content": "hello"}],
44
+ cognocient_feature="support-bot", # optional attribution — same field names the proxy accepts as X-Cost-* headers
45
+ )
46
+ ```
47
+
48
+ Every method the real SDK exposes still works unchanged. This wrapper only
49
+ intercepts `chat.completions.create()` (`messages.create()` for Anthropic)
50
+ to time the call and report its usage after the fact; everything else is
51
+ forwarded to the real client untouched.
52
+
53
+ ## This is one of three ways to see your Cognocient dashboard
54
+
55
+ | | Live attribution | Pre-call enforcement (block/degrade) | Code change |
56
+ |---|---|---|---|
57
+ | **Proxy** (`base_url` swap) | Yes | Yes | One line |
58
+ | **This wrapper** | Yes | No — see below | Swap the import, add a key |
59
+ | **CSV/OTel import** | No (historical only) | No | None |
60
+
61
+ ## Security — read this before you decide
62
+
63
+ **This wrapper is not more secure than the proxy. It is a different
64
+ tradeoff, not a strictly better one.**
65
+
66
+ With the proxy, your real provider API key lives server-side, under
67
+ Cognocient's control, in one place. With this wrapper, your real provider
68
+ key stays in your own application process, exactly as it does today
69
+ without Cognocient at all — the wrapper calls the provider directly,
70
+ using your key, inside your runtime. Some security teams prefer that
71
+ (no third-party network hop in the request path); others are less
72
+ comfortable with third-party code executing inside their process with
73
+ key access. Both are reasonable positions. We're not going to tell you
74
+ this "removes a security roadblock" — it trades one shape of exposure
75
+ for a different one.
76
+
77
+ What this wrapper honestly gives you over the proxy:
78
+ - **Zero added request latency.** Reporting happens after your real
79
+ call already returned, on a background thread, off the critical path.
80
+ - **Zero risk of a Cognocient outage affecting your production call.**
81
+ If Cognocient's ingestion API is down or unreachable, your call to
82
+ OpenAI/Anthropic still completes normally — see "Reliability" below.
83
+
84
+ What you give up versus the proxy: pre-call enforcement. Because
85
+ Cognocient only hears about a call after it already happened, budgets
86
+ configured in Cognocient cannot block or degrade a call made through
87
+ this wrapper before it fires. The dashboard will say so explicitly for
88
+ any account using this path.
89
+
90
+ ## Reliability
91
+
92
+ Reporting is fire-and-forget on a background thread with a bounded local
93
+ queue, flushed every few seconds or every 50 calls, whichever comes
94
+ first. If the ingestion API is slow, down, or unreachable:
95
+
96
+ - Your real provider call is completely unaffected — it already happened
97
+ before reporting was attempted.
98
+ - No exception is ever raised into your code from a reporting failure.
99
+ - No retry loop that could pile up work in your process — a failed batch
100
+ is dropped and logged locally at `DEBUG` level via the `cognocient`
101
+ logger, not retried.
102
+
103
+ See `tests/test_reporter_failure_isolation.py` for a test that simulates
104
+ an unreachable ingestion endpoint and asserts the real call still
105
+ completes normally.
106
+
107
+ ## Known limitation: streaming isn't reported yet
108
+
109
+ `stream=True` calls are passed through to the real SDK completely
110
+ unmodified — your application behaves identically — but are **not**
111
+ currently reported to Cognocient. Usage totals aren't available until a
112
+ stream completes, and reliably capturing them requires wrapping the
113
+ stream iterator itself, which this version doesn't do. If most of your
114
+ traffic streams, this wrapper will under-report your usage today. Use
115
+ the proxy or the CSV/OTel importer if that matters for your evaluation.
116
+
117
+ ## Attribution fields
118
+
119
+ Same field names the proxy accepts as `X-Cost-*` headers, passed as
120
+ keyword arguments instead:
121
+
122
+ | Wrapper kwarg | Proxy header |
123
+ |---|---|
124
+ | `cognocient_feature` | `X-Cost-Feature` |
125
+ | `cognocient_department` | `X-Cost-Department` |
126
+ | `cognocient_user` | `X-Cost-User` |
127
+ | `cognocient_session` | `X-Cost-Session` |
128
+ | `cognocient_tier` | `X-Cost-Tier` |
129
+ | `cognocient_project` | `X-Cost-Project` |
130
+ | `cognocient_gl_account` | `X-Cost-GL-Account` |
131
+ | `cognocient_workload` | `X-Cost-Workload` |
132
+ | `cognocient_outcome` | `X-Cost-Outcome` |
133
+ | `cognocient_run_id` | `X-Cost-Run-ID` |
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ pip install -e ".[dev]"
139
+ pytest
140
+ python benchmark/benchmark_wrapper_overhead.py
141
+ ```
@@ -0,0 +1,9 @@
1
+ cognocient/__init__.py,sha256=wiWFp6g7jGFrR6AdS0R2LtpAAvSBs9CQmktaXyJ3RFE,285
2
+ cognocient/_reporter.py,sha256=mdcNNj7AOKKVb7aq-Uk20P8DO5D_Ra31Y-pUu_pone4,5160
3
+ cognocient/_tags.py,sha256=TJla6jqcBZZi2q_CuNtb6F4Goy6NPdtH1LvF5u_7qqU,1038
4
+ cognocient/anthropic_wrapper.py,sha256=cETjM20wJf-Q6Q0FMopCl-ksA_ILFWFSq6zTj1N7nLo,3316
5
+ cognocient/openai_wrapper.py,sha256=rOZmN3eDIGcGp80MDQfkUEWrbgry5jopNvo6tB3Y2WY,5150
6
+ cognocient-0.1.0.dist-info/METADATA,sha256=oRLapIa4hC8FpsWlhe6639gXscR-zuXAO9tOXTOQ7jo,5609
7
+ cognocient-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
8
+ cognocient-0.1.0.dist-info/licenses/LICENSE,sha256=qlYye1b2g2NDMrkTcarAdqVSNzUB31iShDBWUH24RWw,1067
9
+ cognocient-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cognocient
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.