aicert 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.
aicert/validation.py ADDED
@@ -0,0 +1,322 @@
1
+ """Validation utilities for aicert."""
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass, field
6
+ from typing import Any, Dict, List, Optional, Union
7
+
8
+ from jsonschema import ValidationError, validate
9
+
10
+
11
+ @dataclass
12
+ class ValidationResult:
13
+ """Result of validating LLM output."""
14
+ ok_json: bool = False
15
+ ok_schema: bool = False
16
+ extra_keys: List[str] = field(default_factory=list)
17
+ error: Optional[str] = None
18
+ parsed: Optional[Any] = None
19
+
20
+
21
+ def extract_json_from_block(text: str) -> Optional[str]:
22
+ """Extract JSON from a ```json ... ``` block."""
23
+ # Match ```json at start of line, followed by any content, ending with ```
24
+ pattern = r'```json\s*\n(.*?)\n```'
25
+ match = re.search(pattern, text, re.DOTALL)
26
+ if match:
27
+ return match.group(1).strip()
28
+ return None
29
+
30
+
31
+ def extract_json_basic(text: str) -> Optional[str]:
32
+ """Basic extraction of first JSON object/array from text."""
33
+ # Find the first { or [ that starts a JSON object/array
34
+ text = text.strip()
35
+
36
+ # Find first { or [ at the start or after whitespace
37
+ start_idx = -1
38
+ for i, char in enumerate(text):
39
+ if char == '{':
40
+ start_idx = i
41
+ break
42
+ elif char == '[':
43
+ start_idx = i
44
+ break
45
+
46
+ if start_idx == -1:
47
+ return None
48
+
49
+ # Try to parse from this position
50
+ # We'll try to find the matching closing bracket
51
+ end_idx = -1
52
+ stack = []
53
+ for i in range(start_idx, len(text)):
54
+ char = text[i]
55
+ if char == '{' or char == '[':
56
+ stack.append(char)
57
+ elif char == '}' or char == ']':
58
+ if not stack:
59
+ continue
60
+ opening = stack.pop()
61
+ if (opening == '{' and char == '}') or (opening == '[' and char == ']'):
62
+ if not stack:
63
+ end_idx = i + 1
64
+ break
65
+
66
+ if end_idx > 0:
67
+ return text[start_idx:end_idx]
68
+
69
+ # Fallback: try to find any closing bracket at the end
70
+ # This is a simple heuristic
71
+ if start_idx == 0:
72
+ # Try to find the last } or ]
73
+ for i in range(len(text) - 1, start_idx - 1, -1):
74
+ if text[i] in '}]':
75
+ return text[start_idx:i + 1]
76
+
77
+ return None
78
+
79
+
80
+ def parse_output(text: str, extract_json: bool) -> tuple[bool, Any | None, str | None]:
81
+ """
82
+ Parse output text to extract JSON.
83
+
84
+ Args:
85
+ text: The raw output text from the LLM
86
+ extract_json: Whether to try extracting JSON from blocks or text
87
+
88
+ Returns:
89
+ Tuple of (ok_json: bool, parsed: Any|None, error: str|None)
90
+ """
91
+ if extract_json:
92
+ # Try to extract from ```json block first
93
+ json_text = extract_json_from_block(text)
94
+ if json_text is None:
95
+ # Fall back to basic extraction
96
+ json_text = extract_json_basic(text)
97
+
98
+ if json_text is None:
99
+ return False, None, "No JSON found in output"
100
+ else:
101
+ # Use entire text as JSON
102
+ json_text = text
103
+
104
+ if json_text is None:
105
+ return False, None, "No JSON found in output"
106
+
107
+ try:
108
+ parsed = json.loads(json_text)
109
+ return True, parsed, None
110
+ except json.JSONDecodeError as e:
111
+ return False, None, f"Invalid JSON: {str(e)}"
112
+
113
+
114
+ def find_extra_keys(obj: Any, schema: Dict[str, Any], parent_path: str = "") -> List[str]:
115
+ """
116
+ Find extra keys in an object that are not in the schema.
117
+
118
+ Args:
119
+ obj: The parsed JSON object
120
+ schema: The JSON schema
121
+ parent_path: Path for error reporting
122
+
123
+ Returns:
124
+ List of extra key names
125
+ """
126
+ extra_keys: List[str] = []
127
+
128
+ if not isinstance(obj, dict) or not isinstance(schema, dict):
129
+ return extra_keys
130
+
131
+ # Check if this object has additionalProperties: false
132
+ additional_properties = schema.get("additionalProperties", None)
133
+ is_strict = additional_properties is False
134
+
135
+ # Get defined properties
136
+ properties = schema.get("properties", {})
137
+ required_fields = schema.get("required", [])
138
+
139
+ for key, value in obj.items():
140
+ key_path = f"{parent_path}.{key}" if parent_path else key
141
+
142
+ if key in properties:
143
+ # Key is defined, check nested schema
144
+ prop_schema = properties[key]
145
+ if isinstance(prop_schema, dict):
146
+ # Recursively check nested objects
147
+ nested_extra = find_extra_keys(value, prop_schema, key_path)
148
+ extra_keys.extend(nested_extra)
149
+ elif is_strict:
150
+ # Key not defined and additionalProperties is false
151
+ extra_keys.append(key_path)
152
+
153
+ return extra_keys
154
+
155
+
156
+ def remove_additional_properties(schema: Dict[str, Any]) -> Dict[str, Any]:
157
+ """Remove additionalProperties from schema and nested schemas."""
158
+ if not isinstance(schema, dict):
159
+ return schema
160
+
161
+ result = {}
162
+ for key, value in schema.items():
163
+ if key == "additionalProperties":
164
+ continue
165
+ elif key == "properties" and isinstance(value, dict):
166
+ result[key] = {k: remove_additional_properties(v) for k, v in value.items()}
167
+ elif isinstance(value, dict):
168
+ result[key] = remove_additional_properties(value)
169
+ else:
170
+ result[key] = value
171
+ return result
172
+
173
+
174
+ def validate_schema(parsed: Any, schema: Dict[str, Any], allow_extra_keys: bool = True) -> tuple[bool, str | None, List[str]]:
175
+ """
176
+ Validate parsed JSON against a schema.
177
+
178
+ Args:
179
+ parsed: The parsed JSON object
180
+ schema: The JSON schema
181
+ allow_extra_keys: If False, enforce that no extra keys exist
182
+
183
+ Returns:
184
+ Tuple of (ok_schema: bool, error: str|None, extra_keys: List[str])
185
+ """
186
+ extra_keys: List[str] = []
187
+
188
+ if not isinstance(schema, dict):
189
+ return True, None, []
190
+
191
+ # Check for strict mode - extra key enforcement
192
+ if not allow_extra_keys:
193
+ extra_keys = find_extra_keys(parsed, schema)
194
+ if extra_keys:
195
+ return False, f"Extra keys not allowed: {', '.join(extra_keys)}", extra_keys
196
+
197
+ # If allow_extra_keys=True, remove additionalProperties restriction
198
+ # before validation to allow extra keys
199
+ validation_schema = schema if allow_extra_keys else schema
200
+ if allow_extra_keys and schema.get("additionalProperties") is False:
201
+ validation_schema = remove_additional_properties(schema)
202
+
203
+ try:
204
+ validate(instance=parsed, schema=validation_schema)
205
+ return True, None, extra_keys
206
+ except ValidationError as e:
207
+ return False, f"Schema validation error: {e.message}", extra_keys
208
+
209
+
210
+ def validate_output(
211
+ text: str,
212
+ schema: Dict[str, Any],
213
+ extract_json: bool,
214
+ allow_extra_keys: bool = True
215
+ ) -> ValidationResult:
216
+ """
217
+ Validate LLM output against a schema.
218
+
219
+ Args:
220
+ text: The raw output text from the LLM
221
+ schema: The JSON schema to validate against
222
+ extract_json: Whether to try extracting JSON from blocks
223
+ allow_extra_keys: If False, enforce that no extra keys exist
224
+
225
+ Returns:
226
+ ValidationResult with ok_json, ok_schema, extra_keys, and error
227
+ """
228
+ # Step 1: Parse JSON from output
229
+ ok_json, parsed, error = parse_output(text, extract_json)
230
+
231
+ if not ok_json:
232
+ return ValidationResult(
233
+ ok_json=False,
234
+ ok_schema=False,
235
+ error=error
236
+ )
237
+
238
+ # Step 2: Validate against schema
239
+ ok_schema, schema_error, extra_keys = validate_schema(parsed, schema, allow_extra_keys)
240
+
241
+ return ValidationResult(
242
+ ok_json=True,
243
+ ok_schema=ok_schema,
244
+ extra_keys=extra_keys,
245
+ error=schema_error,
246
+ parsed=parsed
247
+ )
248
+
249
+
250
+ # Keep old functions for backward compatibility
251
+
252
+ class SchemaLoadError(Exception):
253
+ """Error raised when schema loading fails."""
254
+
255
+ def __init__(self, message: str, schema_path: str = None, hint: str = None):
256
+ self.message = message
257
+ self.schema_path = schema_path
258
+ self.hint = hint
259
+ super().__init__(self._format_message())
260
+
261
+ def _format_message(self) -> str:
262
+ """Format the error message with context."""
263
+ parts = []
264
+ if self.schema_path:
265
+ parts.append(f"[bold red]Schema file: {self.schema_path}[/bold red]")
266
+ parts.append(f"[bold red]Error:[/bold red] {self.message}")
267
+ if self.hint:
268
+ parts.append(f"[bold yellow]Hint:[/bold yellow] {self.hint}")
269
+ return "\n".join(parts)
270
+
271
+
272
+ def load_json_schema(schema_path: str) -> Dict[str, Any]:
273
+ """Load a JSON schema from a file with better error messages."""
274
+ import yaml
275
+ from pathlib import Path
276
+
277
+ try:
278
+ with open(schema_path, "r") as f:
279
+ schema = yaml.safe_load(f)
280
+ except FileNotFoundError:
281
+ raise SchemaLoadError(
282
+ message=f"Schema file not found: {schema_path}",
283
+ schema_path=schema_path,
284
+ hint="Make sure the path is correct and the file exists."
285
+ )
286
+ except yaml.YAMLError as e:
287
+ raise SchemaLoadError(
288
+ message=f"Invalid YAML in schema file: {e}",
289
+ schema_path=schema_path,
290
+ hint="Check for syntax errors like incorrect indentation."
291
+ )
292
+ except Exception as e:
293
+ raise SchemaLoadError(
294
+ message=f"Failed to load schema: {e}",
295
+ schema_path=schema_path,
296
+ hint="Make sure the file is valid JSON or YAML."
297
+ )
298
+
299
+ # Validate that it's a dict (could be JSON or YAML parsed)
300
+ if not isinstance(schema, dict):
301
+ raise SchemaLoadError(
302
+ message=f"Schema must be a dictionary/object, got {type(schema).__name__}",
303
+ schema_path=schema_path,
304
+ hint="The schema file should contain a JSON object with '$schema', 'type', 'properties', etc."
305
+ )
306
+
307
+ return schema
308
+
309
+
310
+ def validate_json(data: Any, schema: Dict[str, Any]) -> bool:
311
+ """Validate JSON data against a schema."""
312
+ validate(instance=data, schema=schema)
313
+ return True
314
+
315
+
316
+ def validate_json_file(data_path: str, schema_path: str) -> bool:
317
+ """Validate a JSON file against a schema."""
318
+ import yaml
319
+ with open(data_path, "r") as f:
320
+ data = yaml.safe_load(f)
321
+ schema = load_json_schema(schema_path)
322
+ return validate_json(data, schema)
@@ -0,0 +1,306 @@
1
+ Metadata-Version: 2.4
2
+ Name: aicert
3
+ Version: 0.1.0
4
+ Summary: CI for LLM JSON outputs - Validate, test, and measure LLM outputs
5
+ Author: aicert contributors
6
+ License: MIT
7
+ Keywords: llm,cli,validation,testing,json
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: typer>=0.21.0
19
+ Requires-Dist: pydantic>=2.5.0
20
+ Requires-Dist: pyyaml>=6.0.1
21
+ Requires-Dist: jsonschema>=4.20.0
22
+ Requires-Dist: httpx>=0.25.0
23
+ Requires-Dist: rich>=14.0.0
24
+ Requires-Dist: cryptography>=41.0.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.4.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+ Requires-Dist: black>=23.0.0; extra == "dev"
29
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+ # aicert
33
+
34
+ **Reliability tooling for structured LLM outputs.**
35
+
36
+ Measure, validate, and enforce JSON stability in CI.
37
+
38
+ ---
39
+
40
+ ## Why aicert?
41
+
42
+ LLMs are probabilistic systems.
43
+
44
+ If your application depends on structured JSON generated by an LLM, that output becomes an implicit contract within your system. Model updates, prompt changes, or configuration tweaks can cause that contract to drift — often without immediate visibility.
45
+
46
+ Traditional tests typically validate a single run. They do not measure variability across repeated executions.
47
+
48
+ Aicert treats LLM output like any other production dependency:
49
+
50
+ * Validate against a JSON Schema
51
+ * Measure stability across repeated runs
52
+ * Track latency and variability
53
+ * Enforce reliability thresholds in CI
54
+
55
+ Instead of assuming structured outputs remain stable, you can verify that they do.
56
+
57
+ ---
58
+
59
+ ## What It Enables
60
+
61
+ Aicert helps you:
62
+
63
+ * Detect schema breakage before deployment
64
+ * Quantify output variability across runs
65
+ * Catch regressions caused by model or prompt changes
66
+ * Enforce reliability standards automatically in CI
67
+ * Treat prompts and structured outputs as testable infrastructure
68
+
69
+ It converts non-deterministic behavior into measurable signals.
70
+
71
+ ---
72
+
73
+ ## Who It’s For
74
+
75
+ Aicert is designed for teams shipping structured LLM outputs into production systems:
76
+
77
+ * Backend APIs powered by LLMs
78
+ * Extraction and classification pipelines
79
+ * Decision-support systems
80
+ * Any workflow where JSON output drives downstream logic
81
+
82
+ If structured LLM output is part of your system contract, Aicert provides guardrails.
83
+
84
+ ---
85
+
86
+ ## Installation
87
+
88
+ ```bash
89
+ pip install aicert
90
+ ```
91
+
92
+ For development:
93
+
94
+ ```bash
95
+ pip install -e .
96
+ ```
97
+
98
+ ---
99
+
100
+ ## What It Measures
101
+
102
+ * **Compliance** — % of outputs matching JSON Schema
103
+ * **Stability** — % of identical outputs across repeated runs
104
+ * **Latency** — P50 / P95 response times
105
+ * **Similarity** — Structural or semantic similarity
106
+ * **CI Gating** — Threshold-based pass/fail enforcement
107
+
108
+ ---
109
+
110
+ ## Basic Workflow
111
+
112
+ ### 1. Create `aicert.yaml`
113
+
114
+ ```yaml
115
+ project: my-project
116
+
117
+ providers:
118
+ - id: openai-gpt4
119
+ provider: openai
120
+ model: gpt-4
121
+ temperature: 0.1
122
+
123
+ prompt_file: prompt.txt
124
+ cases_file: cases.jsonl
125
+ schema_file: schema.json
126
+
127
+ runs: 50
128
+ concurrency: 10
129
+ timeout_s: 30
130
+
131
+ validation:
132
+ extract_json: true
133
+ allow_extra_keys: false
134
+
135
+ thresholds:
136
+ min_stability: 85
137
+ min_compliance: 95
138
+ p95_latency_ms: 5000
139
+
140
+ ci:
141
+ runs: 10
142
+ save_on_fail: true
143
+ ```
144
+
145
+ ---
146
+
147
+ ### 2. Run Stability Tests
148
+
149
+ ```bash
150
+ aicert stability aicert.yaml
151
+ ```
152
+
153
+ ---
154
+
155
+ ### 3. Enforce in CI
156
+
157
+ ```bash
158
+ aicert ci aicert.yaml
159
+ ```
160
+
161
+ Non-zero exit codes indicate threshold failures.
162
+
163
+ ---
164
+
165
+ ## Commands
166
+
167
+ ```bash
168
+ aicert init
169
+ aicert doctor aicert.yaml
170
+ aicert run aicert.yaml
171
+ aicert stability aicert.yaml
172
+ aicert compare aicert.yaml
173
+ aicert ci aicert.yaml
174
+ aicert diff <run_a> <run_b>
175
+ aicert report <run_dir>
176
+ ```
177
+
178
+ ---
179
+
180
+ # Aicert Pro
181
+
182
+ Aicert Pro turns measurement into enforcement.
183
+
184
+ The core package tells you how your LLM behaves.
185
+ Pro defines what behavior is acceptable — and blocks regressions automatically.
186
+
187
+ When structured outputs drive production systems, “informational metrics” are not enough.
188
+ You need a reliability boundary.
189
+
190
+ Aicert Pro adds:
191
+
192
+ * Baseline locking — capture a known-good state
193
+ * Regression enforcement — fail CI when stability or compliance degrades
194
+ * Schema and prompt drift detection — detect structural changes immediately
195
+ * Cost regression limits — prevent silent cost creep
196
+ * Policy-backed CI gating — enforce standards across commits
197
+
198
+ Pro is designed for teams that treat LLM output as production infrastructure.
199
+
200
+ ---
201
+
202
+ ## What Changes With Pro?
203
+
204
+ Without Pro:
205
+
206
+ You measure stability.
207
+ You review results manually.
208
+ Regressions can slip through if thresholds are ignored.
209
+
210
+ With Pro:
211
+
212
+ Known-good behavior is locked.
213
+ Deviations are compared automatically.
214
+ CI fails when output contracts drift.
215
+ Reliability becomes enforceable policy.
216
+ Pro moves you from observation to control.
217
+
218
+ ## Installing Aicert Pro
219
+
220
+ After purchasing a subscription:
221
+
222
+ ```bash
223
+ pip install aicert-pro
224
+ ```
225
+
226
+ Set your license key:
227
+
228
+ ```bash
229
+ export AICERT_LICENSE="your_signed_license_key"
230
+ ```
231
+
232
+ Verify:
233
+
234
+ ```bash
235
+ aicert-pro license verify
236
+ ```
237
+
238
+ Use baseline enforcement:
239
+
240
+ ```bash
241
+ aicert-pro baseline save aicert.yaml
242
+ aicert-pro baseline check aicert.yaml
243
+ ```
244
+
245
+ ---
246
+
247
+ ## Licensing
248
+
249
+ Aicert Pro uses signed offline license keys.
250
+
251
+ * No SaaS dependency
252
+ * No account login
253
+ * CI-friendly
254
+ * Time-limited per subscription
255
+
256
+ ---
257
+
258
+ ## Pricing
259
+
260
+ * **$29 / month**
261
+ * **$290 / year (2 months free)**
262
+
263
+ Purchase:
264
+ [https://mfifth.github.io/aicert/](https://mfifth.github.io/aicert/)
265
+
266
+ Questions:
267
+ [mfifth@gmail.com](mailto:mfifth@gmail.com)
268
+
269
+ ---
270
+
271
+ ## GitHub Actions Example
272
+
273
+ ```yaml
274
+ - run: pip install aicert
275
+ - run: aicert ci aicert.yaml
276
+ env:
277
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
278
+ ```
279
+
280
+ Pro enforcement in CI:
281
+
282
+ ```yaml
283
+ - run: pip install aicert-pro
284
+ - run: aicert-pro baseline check aicert.yaml
285
+ env:
286
+ AICERT_LICENSE: ${{ secrets.AICERT_LICENSE }}
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Exit Codes
292
+
293
+ | Code | Meaning |
294
+ | ---- | ------------------------ |
295
+ | 0 | Success |
296
+ | 2 | Threshold failure |
297
+ | 3 | Config/schema error |
298
+ | 4 | Provider/auth error |
299
+ | 5 | License error (Pro only) |
300
+
301
+ ---
302
+
303
+ ## License
304
+
305
+ Core package (`aicert`) is MIT licensed.
306
+ Aicert Pro is commercial software.
@@ -0,0 +1,22 @@
1
+ aicert/__init__.py,sha256=Wr15C3CQxuxepOgzREU4WQl9TkSRSNTMHTslmrV73_Y,63
2
+ aicert/__main__.py,sha256=4P50AkHMqohCf03NwoGtALlQFF0w0KZAu-braeWXxLY,116
3
+ aicert/artifacts.py,sha256=lbSkeIYR3XUPLTQBI8VqY6O5iMBSJGpdpaYmA8NC88Q,3031
4
+ aicert/cli.py,sha256=7nL9CCyb_PoAvdAS7kF5Mi0GbwK8_EtTqll9Wvq2iPY,56603
5
+ aicert/config.py,sha256=EeBHZcH_aMKvAgm7aojJ-1DccfkHte5bQpGYLbMZihk,7188
6
+ aicert/doctor.py,sha256=Ig1SwOrXbDV_pEFE2UpjzNnbyVfSLeWuGhjCv1QxoZU,12569
7
+ aicert/hashing.py,sha256=uqQmg2qp7Lfb3N3EYqpfw6bBDvletks8ZoHHKmJHcUU,705
8
+ aicert/metrics.py,sha256=sh-yMk7qrH_3QC_IZEr-PASnaYi43sHql3ueCNym8JA,11152
9
+ aicert/runner.py,sha256=l3BHDpypQKTiHm2c3B_ClUO_P2kNEOCz0ncU3eVIDFI,20722
10
+ aicert/templating.py,sha256=EXqjw69VjHKTNATp-_sXycs6HABraMZnq3bRrKiA0lY,2472
11
+ aicert/validation.py,sha256=m2Qx7vyNxUiajzFXW2UDjAWl7qqihbvaPLifviV1fiY,10371
12
+ aicert/providers/__init__.py,sha256=z4JPvhJkVsz7gW8gf4b4lUgvMg9cK6rj78p7WhA1v68,374
13
+ aicert/providers/anthropic.py,sha256=FUlZZ6LP_o9GP5Qbk0sVXEiBtf3Iz8Fj0gvqnfXfelc,6381
14
+ aicert/providers/base.py,sha256=pp3mGD6EY8AMRgOEOfsB1ZXXZHAGxLEufUWtfMX3-tE,966
15
+ aicert/providers/openai.py,sha256=aQCqGUxEWjOUyUpu9ceCaqNUYig1dRWqWiF0Qu3z_iI,5272
16
+ aicert/providers/openai_compatible.py,sha256=o9BWUcNQUmWJljWIqXx5LH0Q1ysl7tW52I9Wok66FQA,5373
17
+ aicert-0.1.0.dist-info/licenses/LICENSE,sha256=m8Wu9ollES96-xdQn31baYdbK4xIDfHR8S0lCQt0Aow,1068
18
+ aicert-0.1.0.dist-info/METADATA,sha256=uIgMchOoGJKD8shY0W1I-yAsafy9WtRd3GlGygvw6dc,6413
19
+ aicert-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
20
+ aicert-0.1.0.dist-info/entry_points.txt,sha256=iCeFwGlD159K_db1wkq5ExrF6RTklQYVioQC7ZtnRoo,42
21
+ aicert-0.1.0.dist-info/top_level.txt,sha256=RnxinJ8aabE4rxqgfQ68ZutBZKUC2NRDgMBVfciv4dQ,7
22
+ aicert-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ aicert = aicert.cli:app
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matt Quinto
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
+ aicert