auditail 0.2.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.
auditail/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ """
2
+ Auditail Python SDK
3
+ The security layer for autonomous AI agents.
4
+ Guardrails that block risky actions, plus a tamper-evident audit trail.
5
+
6
+ https://auditail.io/docs
7
+ """
8
+
9
+ from .client import Auditail
10
+ from .types import GuardOptions, GuardResult, GuardrailRule, AuditEvent
11
+ from . import rules
12
+
13
+ __version__ = "0.2.0"
14
+ __all__ = ["Auditail", "GuardOptions", "GuardResult", "GuardrailRule", "AuditEvent", "rules"]
auditail/client.py ADDED
@@ -0,0 +1,339 @@
1
+ """
2
+ Auditail Python SDK โ€” Client
3
+ """
4
+
5
+ import asyncio
6
+ import atexit
7
+ import concurrent.futures
8
+ import inspect
9
+ import json
10
+ import random
11
+ import threading
12
+ import time
13
+ import urllib.error
14
+ import urllib.request
15
+ import uuid
16
+ from collections import deque
17
+ from dataclasses import asdict
18
+ from datetime import datetime, timezone
19
+ from typing import Any, Callable, Deque, Iterable, List, Optional
20
+
21
+ from .types import AuditEvent, GuardOptions, GuardResult, GuardrailRule
22
+
23
+ SDK_VERSION = "0.2.0"
24
+ MAX_QUEUE = 10_000
25
+ MAX_BATCH = 50
26
+ MAX_DELIVERY_ATTEMPTS = 5
27
+ DEFAULT_REDACT = {
28
+ "password", "passwd", "secret", "token", "apikey", "api_key", "authorization", "auth",
29
+ "ssn", "creditcard", "credit_card", "cardnumber", "card_number", "cvv", "privatekey", "private_key",
30
+ }
31
+
32
+
33
+ def _redact(value: Any, keys: set, depth: int = 0) -> Any:
34
+ if depth > 10:
35
+ return "[TRUNCATED]"
36
+ if isinstance(value, dict):
37
+ return {k: ("[REDACTED]" if str(k).lower() in keys else _redact(v, keys, depth + 1)) for k, v in value.items()}
38
+ if isinstance(value, (list, tuple)):
39
+ return [_redact(v, keys, depth + 1) for v in value]
40
+ return value
41
+
42
+
43
+ def _to_wire(e: AuditEvent) -> dict:
44
+ """AuditEvent -> the camelCase JSON the API expects."""
45
+ return {
46
+ "auditId": e.audit_id, "agentId": e.agent_id, "userId": e.user_id, "action": e.action,
47
+ "payload": e.payload, "approved": e.approved, "reason": e.reason, "matchedRule": e.matched_rule,
48
+ "tags": e.tags, "timestamp": e.timestamp, "environment": e.environment, "sdkVersion": e.sdk_version,
49
+ }
50
+ WAITLIST_URL = "https://auditail.io/waitlist"
51
+
52
+
53
+ class Auditail:
54
+ """
55
+ Auditail client. Instantiate once at app startup and reuse.
56
+
57
+ Example::
58
+
59
+ from auditail import Auditail, rules
60
+
61
+ aegis = Auditail(api_key="your_key")
62
+ aegis.add_rule(rules.no_external_email())
63
+ aegis.add_rule(rules.no_data_deletion())
64
+
65
+ result = aegis.guard(
66
+ action="send_email",
67
+ payload={"to": "client@example.com", "body": "Report attached."},
68
+ agent_id="financial-bot-01",
69
+ )
70
+
71
+ if result.approved:
72
+ send_email(...)
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ api_key: str,
78
+ environment: str = "production",
79
+ log_level: str = "info",
80
+ base_url: str = "https://api.auditail.io",
81
+ redact_keys: Optional[Iterable[str]] = None,
82
+ ship: bool = True,
83
+ flush_interval: float = 1.0,
84
+ on_error: Optional[Callable[[Exception], None]] = None,
85
+ ):
86
+ if not api_key:
87
+ raise ValueError(
88
+ f"[Auditail] No API key provided.\nGet your free API key at: {WAITLIST_URL}"
89
+ )
90
+
91
+ self.api_key = api_key
92
+ self.environment = environment
93
+ self.log_level = log_level
94
+ self.base_url = base_url.rstrip("/")
95
+ self.ship = ship
96
+ self.flush_interval = flush_interval
97
+ self._redact = DEFAULT_REDACT | {k.lower() for k in (redact_keys or [])}
98
+ self._on_error = on_error or (lambda e: self._log("error", str(e)))
99
+
100
+ self._rules: List[GuardrailRule] = []
101
+ self._audit_log: List[AuditEvent] = []
102
+
103
+ self._queue: Deque[AuditEvent] = deque()
104
+ self._qlock = threading.Lock()
105
+ self._flush_lock = threading.Lock()
106
+ self._wake = threading.Event()
107
+ self._stop = threading.Event()
108
+ self._worker: Optional[threading.Thread] = None
109
+ if ship:
110
+ atexit.register(self.flush)
111
+
112
+ def add_rule(self, rule: GuardrailRule) -> "Auditail":
113
+ """Register a guardrail rule. Rules are evaluated in registration order."""
114
+ self._rules.append(rule)
115
+ self._log("debug", f'Guardrail rule added: "{rule.name}"')
116
+ return self
117
+
118
+ def add_rules(self, rules: List[GuardrailRule]) -> "Auditail":
119
+ """Register multiple guardrail rules at once."""
120
+ for rule in rules:
121
+ self.add_rule(rule)
122
+ return self
123
+
124
+ def guard(
125
+ self,
126
+ action: str,
127
+ payload: dict,
128
+ agent_id: str,
129
+ user_id: Optional[str] = None,
130
+ tags: Optional[dict] = None,
131
+ ) -> GuardResult:
132
+ """
133
+ Synchronous guard evaluation.
134
+ Evaluates the action against all registered rules and logs an audit event.
135
+ """
136
+ opts = GuardOptions(
137
+ action=action,
138
+ payload=payload,
139
+ agent_id=agent_id,
140
+ user_id=user_id,
141
+ tags=tags,
142
+ )
143
+ try:
144
+ asyncio.get_running_loop()
145
+ except RuntimeError:
146
+ return asyncio.run(self._evaluate(opts))
147
+ # Already inside a running event loop (FastAPI, async LangChain):
148
+ # run the evaluation on a worker thread so we don't crash or block the loop's state.
149
+ with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
150
+ return pool.submit(asyncio.run, self._evaluate(opts)).result()
151
+
152
+ async def guard_async(
153
+ self,
154
+ action: str,
155
+ payload: dict,
156
+ agent_id: str,
157
+ user_id: Optional[str] = None,
158
+ tags: Optional[dict] = None,
159
+ ) -> GuardResult:
160
+ """
161
+ Async guard evaluation for use in async codebases (FastAPI, etc.).
162
+ """
163
+ opts = GuardOptions(
164
+ action=action,
165
+ payload=payload,
166
+ agent_id=agent_id,
167
+ user_id=user_id,
168
+ tags=tags,
169
+ )
170
+ return await self._evaluate(opts)
171
+
172
+ def get_audit_log(self) -> List[AuditEvent]:
173
+ """
174
+ Returns all audit events from this session.
175
+ In the full SDK, use the dashboard at https://app.auditail.io for persistent logs.
176
+ """
177
+ return list(self._audit_log)
178
+
179
+ # ---------------------------------------------------------------------------
180
+ # Private
181
+ # ---------------------------------------------------------------------------
182
+
183
+ async def _evaluate(self, opts: GuardOptions) -> GuardResult:
184
+ start = time.monotonic()
185
+ audit_id = self._generate_id()
186
+
187
+ self._log("debug", f'Evaluating action "{opts.action}" for agent "{opts.agent_id}"')
188
+
189
+ approved = True
190
+ reason: Optional[str] = None
191
+ matched_rule: Optional[str] = None
192
+
193
+ for rule in self._rules:
194
+ try:
195
+ result = rule.condition(opts)
196
+ triggered = await result if inspect.isawaitable(result) else result
197
+ except Exception as exc: # fail closed: a broken rule must not approve the action
198
+ self._log("error", f'Rule "{rule.name}" raised {exc!r}. Failing closed (blocking action).')
199
+ triggered = True
200
+
201
+ if triggered:
202
+ matched_rule = rule.name
203
+ if rule.action in ("block", "require_approval"):
204
+ approved = False
205
+ reason = rule.description
206
+ self._log("warn", f'Action BLOCKED by rule "{rule.name}": {rule.description}')
207
+ break
208
+ elif rule.action == "alert":
209
+ self._log("warn", f'ALERT fired by rule "{rule.name}": {rule.description}')
210
+
211
+ latency_ms = int((time.monotonic() - start) * 1000)
212
+ timestamp = datetime.now(timezone.utc).isoformat()
213
+
214
+ event = AuditEvent(
215
+ audit_id=audit_id,
216
+ agent_id=opts.agent_id,
217
+ user_id=opts.user_id,
218
+ action=opts.action,
219
+ payload=_redact(opts.payload, self._redact),
220
+ approved=approved,
221
+ reason=reason,
222
+ matched_rule=matched_rule,
223
+ tags=opts.tags,
224
+ timestamp=timestamp,
225
+ environment=self.environment,
226
+ sdk_version=SDK_VERSION,
227
+ )
228
+
229
+ self._audit_log.append(event)
230
+ status = "APPROVED" if approved else "BLOCKED"
231
+ self._log("info", f"[{status}] {opts.action} | auditId: {audit_id} | {latency_ms}ms")
232
+
233
+ self._ship_to_api(event)
234
+
235
+ return GuardResult(
236
+ approved=approved,
237
+ audit_id=audit_id,
238
+ reason=reason,
239
+ matched_rule=matched_rule,
240
+ timestamp=timestamp,
241
+ latency_ms=latency_ms,
242
+ )
243
+
244
+ def _generate_id(self) -> str:
245
+ return f"aeg_{uuid.uuid4().hex[:16]}"
246
+
247
+ def _log(self, level: str, message: str) -> None:
248
+ levels = ["debug", "info", "warn", "error", "silent"]
249
+ if level not in levels:
250
+ return
251
+ if levels.index(level) < levels.index(self.log_level):
252
+ return
253
+ prefix = "[Auditail]"
254
+ if level == "warn":
255
+ print(f"{prefix} โš ๏ธ {message}")
256
+ elif level == "error":
257
+ print(f"{prefix} โŒ {message}")
258
+ else:
259
+ print(f"{prefix} {message}")
260
+
261
+ # ---------------------------------------------------------------------------
262
+ # Delivery: non-blocking queue + background worker. Never raises into the caller.
263
+ # ---------------------------------------------------------------------------
264
+
265
+ def _ship_to_api(self, event: AuditEvent) -> None:
266
+ if not self.ship:
267
+ return
268
+ with self._qlock:
269
+ if len(self._queue) >= MAX_QUEUE:
270
+ self._queue.popleft()
271
+ self._on_error(RuntimeError(f"Auditail queue full ({MAX_QUEUE}); dropped oldest event"))
272
+ self._queue.append(event)
273
+ self._ensure_worker()
274
+ self._wake.set()
275
+
276
+ def _ensure_worker(self) -> None:
277
+ if self._worker and self._worker.is_alive():
278
+ return
279
+ self._stop.clear()
280
+ self._worker = threading.Thread(target=self._run, name="auditail-shipper", daemon=True)
281
+ self._worker.start()
282
+
283
+ def _run(self) -> None:
284
+ while not self._stop.is_set():
285
+ self._wake.wait(timeout=self.flush_interval)
286
+ self._wake.clear()
287
+ self._drain()
288
+
289
+ def flush(self) -> None:
290
+ """Deliver all queued events now. Never raises. Call before a serverless handler returns."""
291
+ self._drain()
292
+
293
+ def shutdown(self) -> None:
294
+ """Flush and stop the background worker."""
295
+ self._stop.set()
296
+ self._wake.set()
297
+ self.flush()
298
+
299
+ def _drain(self) -> None:
300
+ with self._flush_lock:
301
+ while True:
302
+ with self._qlock:
303
+ batch = list(self._queue)[:MAX_BATCH]
304
+ if not batch:
305
+ return
306
+ if not self._deliver(batch):
307
+ return # keep events queued; the next flush retries
308
+ done = {id(e) for e in batch}
309
+ with self._qlock:
310
+ self._queue = deque(e for e in self._queue if id(e) not in done)
311
+
312
+ def _deliver(self, batch: List[AuditEvent]) -> bool:
313
+ body = json.dumps({"events": [_to_wire(e) for e in batch]}).encode()
314
+ req = urllib.request.Request(
315
+ f"{self.base_url}/v1/events",
316
+ data=body,
317
+ method="POST",
318
+ headers={
319
+ "content-type": "application/json",
320
+ "authorization": f"Bearer {self.api_key}",
321
+ # Explicit UA: Cloudflare blocks the default "Python-urllib" signature (error 1010).
322
+ "user-agent": f"auditail-python/{SDK_VERSION}",
323
+ },
324
+ )
325
+ for attempt in range(1, MAX_DELIVERY_ATTEMPTS + 1):
326
+ try:
327
+ with urllib.request.urlopen(req, timeout=10):
328
+ return True
329
+ except urllib.error.HTTPError as exc:
330
+ if 400 <= exc.code < 500 and exc.code != 429:
331
+ detail = exc.read(200).decode(errors="replace")
332
+ self._on_error(RuntimeError(f"Auditail rejected {len(batch)} event(s): HTTP {exc.code} {detail}"))
333
+ return True # permanent: drop, don't retry forever
334
+ except Exception:
335
+ pass # network error: retry
336
+ if attempt < MAX_DELIVERY_ATTEMPTS:
337
+ time.sleep(random.random() * min(0.25 * 2**attempt, 4.0))
338
+ self._on_error(RuntimeError(f"Auditail could not deliver {len(batch)} event(s); will retry on next flush"))
339
+ return False
auditail/rules.py ADDED
@@ -0,0 +1,79 @@
1
+ """
2
+ Pre-built guardrail rules for common AI agent security scenarios.
3
+ """
4
+
5
+ from .types import GuardrailRule, GuardOptions
6
+
7
+
8
+ def no_external_email(internal_domains: list[str] | None = None) -> GuardrailRule:
9
+ """
10
+ Block any email action targeting an external domain.
11
+
12
+ Args:
13
+ internal_domains: Trusted domains (e.g. ['acme.com']). Emails to any other
14
+ domain are blocked. If empty, the rule is inactive.
15
+ """
16
+ domains = internal_domains or []
17
+
18
+ def condition(opts: GuardOptions) -> bool:
19
+ if not domains:
20
+ return False # not configured, skip rule
21
+ to = opts.payload.get("to", "")
22
+ if not to:
23
+ return False
24
+ return not any(to.endswith(f"@{d}") for d in domains)
25
+
26
+ return GuardrailRule(
27
+ name="no-external-email",
28
+ description="Blocked: sending email to an external domain requires human approval.",
29
+ condition=condition,
30
+ action="block",
31
+ )
32
+
33
+
34
+ def no_data_deletion() -> GuardrailRule:
35
+ """Block destructive SQL operations (DELETE, DROP, TRUNCATE)."""
36
+
37
+ def condition(opts: GuardOptions) -> bool:
38
+ query = str(opts.payload.get("query", "")).upper()
39
+ return any(kw in query for kw in ["DELETE", "DROP TABLE", "TRUNCATE"])
40
+
41
+ return GuardrailRule(
42
+ name="no-data-deletion",
43
+ description="Blocked: destructive data operations are not permitted.",
44
+ condition=condition,
45
+ action="block",
46
+ )
47
+
48
+
49
+ def no_external_api_calls() -> GuardrailRule:
50
+ """Alert when an agent makes an outbound HTTP request."""
51
+
52
+ def condition(opts: GuardOptions) -> bool:
53
+ return opts.action == "http_request"
54
+
55
+ return GuardrailRule(
56
+ name="no-external-api-calls",
57
+ description="Alert: agent is making a call to an external API.",
58
+ condition=condition,
59
+ action="alert",
60
+ )
61
+
62
+
63
+ def no_pii_in_logs() -> GuardrailRule:
64
+ """Block actions that appear to include PII (email addresses, phone numbers) in payloads."""
65
+ import re
66
+
67
+ EMAIL_RE = re.compile(r"[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}")
68
+ PHONE_RE = re.compile(r"\b(\+?\d[\d\s\-().]{7,}\d)\b")
69
+
70
+ def condition(opts: GuardOptions) -> bool:
71
+ payload_str = str(opts.payload)
72
+ return bool(EMAIL_RE.search(payload_str) or PHONE_RE.search(payload_str))
73
+
74
+ return GuardrailRule(
75
+ name="no-pii-in-logs",
76
+ description="Alert: payload appears to contain PII (email or phone number).",
77
+ condition=condition,
78
+ action="alert",
79
+ )
auditail/types.py ADDED
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Any, Callable, Awaitable, Dict, Optional, Union
3
+
4
+
5
+ @dataclass
6
+ class GuardOptions:
7
+ """Options for a single guard evaluation."""
8
+ action: str
9
+ payload: Dict[str, Any]
10
+ agent_id: str
11
+ user_id: Optional[str] = None
12
+ tags: Optional[Dict[str, str]] = None
13
+
14
+
15
+ @dataclass
16
+ class GuardResult:
17
+ """Result of a guard evaluation."""
18
+ approved: bool
19
+ audit_id: str
20
+ timestamp: str
21
+ latency_ms: int
22
+ reason: Optional[str] = None
23
+ matched_rule: Optional[str] = None
24
+
25
+
26
+ @dataclass
27
+ class GuardrailRule:
28
+ """
29
+ A single guardrail rule.
30
+
31
+ condition: callable that receives GuardOptions and returns True to BLOCK.
32
+ action: 'block' | 'require_approval' | 'alert'
33
+ """
34
+ name: str
35
+ description: str
36
+ condition: Callable[[GuardOptions], Union[bool, Awaitable[bool]]]
37
+ action: str = "block" # 'block' | 'require_approval' | 'alert'
38
+
39
+
40
+ @dataclass
41
+ class AuditEvent:
42
+ """A record of a single agent action evaluation."""
43
+ audit_id: str
44
+ agent_id: str
45
+ action: str
46
+ payload: Dict[str, Any]
47
+ approved: bool
48
+ timestamp: str
49
+ environment: str
50
+ sdk_version: str
51
+ user_id: Optional[str] = None
52
+ reason: Optional[str] = None
53
+ matched_rule: Optional[str] = None
54
+ tags: Optional[Dict[str, str]] = None
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: auditail
3
+ Version: 0.2.0
4
+ Summary: Guardrails that block risky actions, plus a tamper-evident audit trail for autonomous AI agents.
5
+ Author: Auditail
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://auditail.io
8
+ Project-URL: Documentation, https://auditail.io/docs
9
+ Project-URL: Repository, https://github.com/auditailhq/auditail
10
+ Project-URL: Request an API key, https://auditail.io/waitlist
11
+ Keywords: ai,agents,security,guardrails,audit,compliance,soc2,langchain
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Topic :: Security
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Dynamic: license-file
24
+
25
+ # auditail (Python SDK)
26
+
27
+ > The security layer for autonomous AI agents. Guardrails that block risky actions, and a tamper-evident audit trail of everything your agents do.
28
+
29
+ **[๐Ÿ“– Full Documentation](https://auditail.io/docs)** ยท **[๐Ÿš€ Join the Private Beta](https://auditail.io/waitlist)**
30
+
31
+ ---
32
+
33
+ ## Install
34
+
35
+ ```bash
36
+ pip install auditail
37
+ ```
38
+
39
+ ## Quickstart
40
+
41
+ ```python
42
+ from auditail import Auditail, rules
43
+
44
+ aegis = Auditail(api_key="your_key")
45
+
46
+ aegis.add_rules([
47
+ rules.no_external_email(internal_domains=["acme.com"]),
48
+ rules.no_data_deletion(),
49
+ ])
50
+
51
+ result = aegis.guard(
52
+ action="send_email",
53
+ payload={"to": "client@example.com", "body": "Report attached."},
54
+ agent_id="financial-bot-01",
55
+ )
56
+
57
+ if result.approved:
58
+ send_email(...)
59
+ else:
60
+ print(f"Blocked: {result.reason}")
61
+ ```
62
+
63
+ ## LangChain Integration
64
+
65
+ ```python
66
+ from langchain.agents import tool
67
+ from auditail import Auditail, rules
68
+
69
+ aegis = Auditail(api_key="your_key")
70
+ aegis.add_rule(rules.no_external_email(internal_domains=["acme.com"]))
71
+
72
+ @tool
73
+ def send_email(to: str, body: str) -> str:
74
+ """Send an email to a recipient."""
75
+ result = aegis.guard(
76
+ action="send_email",
77
+ payload={"to": to, "body": body},
78
+ agent_id="langchain-agent",
79
+ )
80
+ if not result.approved:
81
+ return f"Action blocked by Auditail: {result.reason}"
82
+ # ... actual send logic
83
+ return "Email sent."
84
+ ```
85
+
86
+ ## Private beta
87
+
88
+ The SDK, API and dashboard are live. Guardrail rules run locally in your process; every decision is delivered in the background to a **hash-chained audit log** you can browse and verify at [app.auditail.io](https://app.auditail.io).
89
+
90
+ - Batched, retried, idempotent delivery. `guard()` never waits on the network, and a delivery failure never reaches your agent.
91
+ - Secrets are redacted before an event leaves your process (`password`, `token`, `apiKey`, ... plus your own `redact_keys`).
92
+ - Call `aegis.flush()` before a serverless function returns or the process exits.
93
+
94
+ **[Request an API key](https://auditail.io/waitlist)**: keys are issued to design partners personally.
95
+
96
+ **Roadmap** (not available yet): Slack/PagerDuty alerts, human-in-the-loop approval (`require_approval` currently behaves like `block`), compliance export, private-cloud deployment.
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,9 @@
1
+ auditail/__init__.py,sha256=VXlpxXRpTZzUWEoaWTmGgdfF0ISl7X-ll3JBWlr56tk,410
2
+ auditail/client.py,sha256=W6HAB0p6IuDvobu28KR7A1Bp_07U_YkQayMcXcexrUQ,12133
3
+ auditail/rules.py,sha256=el41GydJj9oT19hYWF-vRj5XE_Gf93GxJa762OxnS5k,2534
4
+ auditail/types.py,sha256=ufDyNJ8kXcyzPPItydVwjFV_sMUd3Hej58Hi7KRTzEY,1325
5
+ auditail-0.2.0.dist-info/licenses/LICENSE,sha256=9x00mTCv_ClV0JKuyfzOwgPP3xtuN9njGmyPxjnJhOI,1065
6
+ auditail-0.2.0.dist-info/METADATA,sha256=5dKcVWQgEr4dzFRIhm50C9Zc7KhV_7qCpY9zAa-DQ8I,3280
7
+ auditail-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
8
+ auditail-0.2.0.dist-info/top_level.txt,sha256=s0wgs0OnwRM6cOHBLqEsz-f9n4-h4LSM1zwiR74jN_Q,9
9
+ auditail-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Auditail
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.
@@ -0,0 +1 @@
1
+ auditail