aipr-py 0.2.4__tar.gz

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.
aipr_py-0.2.4/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yunare Maia
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.
aipr_py-0.2.4/PKG-INFO ADDED
@@ -0,0 +1,239 @@
1
+ Metadata-Version: 2.4
2
+ Name: aipr-py
3
+ Version: 0.2.4
4
+ Summary: Read a repository's AI contribution policy before you (or your agent) contribute.
5
+ Author-email: Yunare Maia <yunare@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/yunaremaia/aipr
8
+ Keywords: ai-policy,open-source,contributing,cli,agents
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Software Development :: Version Control :: Git
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ Requires-Dist: regex>=2024.1.0
19
+ Dynamic: license-file
20
+
21
+ # aipr
22
+
23
+ [![CI](https://github.com/yunaremaia/aipr/actions/workflows/ci.yml/badge.svg)](https://github.com/yunaremaia/aipr/actions/workflows/ci.yml)
24
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
25
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yunaremaia/aipr/blob/main/LICENSE)
26
+
27
+ **AI Policy Read** - read an open-source repository's AI contribution policy
28
+ before you (or your agent) contribute.
29
+
30
+ `aipr` fetches the governance files that usually carry AI rules
31
+ (`CONTRIBUTING.md`, `AI_POLICY.md`, `AGENTS.md`, `CLAUDE.md`, ...), classifies
32
+ the repository's stance with weighted phrase matching, and answers one
33
+ question: **can an AI-assisted or autonomous contribution land here?**
34
+
35
+ ```
36
+ $ aipr asciimoo/hister
37
+ aipr: asciimoo/hister
38
+ [BLOCKED] human-only policy
39
+ sources: CONTRIBUTING.md, README.md
40
+ confidence: 1.0 score: 19.0
41
+ autonomous contribution: NOT SAFE - require human co-authorship
42
+ [+5.0] ...Issues and PR descriptions must be fully human-written...
43
+ [+5.0] ...AI should never be the main author of the PR...
44
+
45
+ $ aipr apache/maka
46
+ aipr: apache/maka
47
+ [UNKNOWN] no explicit AI policy found
48
+ exit=2
49
+ ```
50
+
51
+ ## Why
52
+
53
+ More repositories are publishing explicit AI policies - from "we welcome
54
+ AI-assisted work" to "agents are strictly forbidden". Violating one burns the
55
+ contributor (and, for autonomous agents, the operator): rejected PRs at best,
56
+ blocks at worst. `aipr` makes the check mechanical and cheap, for humans
57
+ deciding where to spend review effort and for agents deciding where to spend
58
+ their quota.
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ # 1. From PyPI
64
+ pip install aipr-py
65
+
66
+ # 2. Standalone from GitHub
67
+ pip install git+https://github.com/yunaremaia/aipr.git
68
+ # requires Python 3.10+; GH_TOKEN recommended (anonymous API calls rate-limit fast)
69
+ export GH_TOKEN=ghp_xxx # classic token with public repo read access
70
+
71
+ # 3. As a GitHub CLI extension (recommended)
72
+ gh extension install yunaremaia/aipr
73
+ ```
74
+
75
+ The `gh extension install` method is the easiest — after install, `gh aipr OWNER/REPO` works immediately.
76
+
77
+ No dependencies beyond the standard library. `pytest` only to develop.
78
+
79
+ ## Usage
80
+
81
+ ### Standalone
82
+
83
+ ```bash
84
+ aipr OWNER/REPO # classify a GitHub repository
85
+ aipr --text FILE # classify a local governance file
86
+ aipr --json OWNER/REPO # machine-readable output
87
+ aipr --sarif OWNER/REPO # SARIF 2.1.0 output for GitHub Code Scanning
88
+ ```
89
+
90
+ ### As a GitHub CLI extension
91
+
92
+ After `gh extension install yunaremaia/aipr`, use `gh aipr` identically:
93
+
94
+ ```bash
95
+ gh aipr OWNER/REPO
96
+ gh aipr --json OWNER/REPO
97
+ gh aipr --sarif OWNER/REPO
98
+ gh aipr --text FILE
99
+ ```
100
+
101
+ Exit codes are preserved (0/1/2) for CI conditionals.
102
+
103
+ ### `init` — scaffold AI policy files
104
+
105
+ Generate `AI_POLICY.md` and `AI_TOOL_POLICY.md` in your repo:
106
+
107
+ ```bash
108
+ aipr init [--dir .] [--type disclose|permissive|human_only] [--org ORG]
109
+ ```
110
+
111
+ Presets:
112
+ - `permissive` — explicitly welcomes AI-assisted contributions (aipr-safe)
113
+ - `disclose_ok` (default) — allowed with `Assisted-by: AI` disclosure trailer
114
+ - `human_only` — AI must not be the main author (NOT autonomous-safe)
115
+
116
+ ### Verdicts
117
+
118
+ | Verdict | Meaning | Autonomous-safe? |
119
+ |---|---|---|
120
+ | `human_only` | AI must not be the main author / human-written only / bans agents | no |
121
+ | `restrictive` | heavy process: mandatory disclosure + human-in-the-loop requirements | no |
122
+ | `disclose_ok` | allowed with a disclosure trailer (`Assisted-by: AI`) | yes* |
123
+ | `permissive` | explicitly welcomes AI-assisted contributions | yes |
124
+ | `unknown` | no explicit policy found | ask first |
125
+
126
+ \* still follow the disclosure rules - "safe" means *no human co-authorship
127
+ required by policy*, not *no obligations*.
128
+
129
+ ### Exit codes (for CI and agents)
130
+
131
+ | Code | Meaning |
132
+ |---|---|
133
+ | 0 | all inspected repos are autonomous-safe |
134
+ | 1 | at least one repo is restricted or human-only |
135
+ | 2 | at least one repo is unknown / no policy found (ranks worse than 1) |
136
+ | 64 | usage error |
137
+
138
+ Batch mode: `aipr owner/repo1 owner/repo2 ...` prints one block per repo
139
+ (JSON array with `--json`) and the exit code reflects the worst result —
140
+ so an unverified repo can never pass a gate silently.
141
+
142
+ Policy fetches are cached on disk for 24h (`~/.cache/aipr`, configurable via
143
+ `AIPR_CACHE_DIR` / `AIPR_CACHE_TTL`), so repeated scans cost zero API calls.
144
+ Use `--no-cache` to force a fresh fetch.
145
+
146
+ ## How classification works
147
+
148
+ Weighted regex matching over concatenated governance text. Restrictive phrases
149
+ score positive ("must be fully human-written" +5), permissive ones negative
150
+ ("we warmly welcome AI-assisted" -3.5). The strongest signals force the
151
+ verdict; weak mixed signals lean restrictive on purpose - when in doubt, do
152
+ not send a bot.
153
+
154
+ Known limits: English-only patterns; phrase matching cannot understand nuance;
155
+ a repo can carry policy in unusual files we don't probe. Treat UNKNOWN as
156
+ "read it yourself".
157
+
158
+ ## CI/CD Integration
159
+
160
+ ### Block PRs Violating AI Policy
161
+
162
+ Drop `.github/workflows/aipr.yml` into your repository to automatically block pull requests targeting repos with human-only or restrictive AI policies:
163
+
164
+ ```yaml
165
+ # .github/workflows/aipr.yml – block PRs against repos with human-only AI policies
166
+ name: AI Policy Check
167
+ on:
168
+ pull_request:
169
+ branches: [main, master]
170
+
171
+ jobs:
172
+ aipr:
173
+ runs-on: ubuntu-latest
174
+ steps:
175
+ - uses: actions/checkout@v5
176
+ - uses: actions/setup-python@v5
177
+ with:
178
+ python-version: "3.10"
179
+ - run: pip install git+https://github.com/yunaremaia/aipr.git
180
+ - name: Check AI policy
181
+ run: |
182
+ aipr "$GITHUB_REPOSITORY" --json || true
183
+ # Exit 1 = human_only/restrictive (block)
184
+ # Exit 2 = unknown (warn, not block)
185
+ aipr "$GITHUB_REPOSITORY" --json | jq -e '.verdict == "human_only" or .verdict == "restrictive"' && exit 1 || exit 0
186
+ ```
187
+
188
+ ### GitHub Code Scanning (SARIF)
189
+
190
+ Use `--sarif` to output SARIF 2.1.0 (Static Analysis Results Interchange Format)
191
+ and upload to GitHub Code Scanning. This surfaces AI policy compliance as
192
+ alerts in the GitHub Security tab.
193
+
194
+ ```bash
195
+ # Generate SARIF output
196
+ aipr --sarif OWNER/REPO > aipr-results.sarif
197
+ # Upload to GitHub Code Scanning via GitHub Actions:
198
+ # github/codeql-action/upload-sarif with sarif_file: aipr-results.sarif
199
+ ```
200
+
201
+ Verdict mapping to SARIF levels:
202
+ - `human_only` / `restrictive` → `error` (blocks contribution)
203
+ - `unknown` → `warning` (needs manual review)
204
+ - `permissive` / `disclose_ok` → `note` (safe to proceed)
205
+
206
+ Example GitHub Actions workflow snippet:
207
+
208
+ ```yaml
209
+ name: AI Policy Check (SARIF)
210
+ on:
211
+ pull_request:
212
+ branches: [main]
213
+
214
+ jobs:
215
+ aipr-check:
216
+ runs-on: ubuntu-latest
217
+ permissions:
218
+ security-events: write
219
+ steps:
220
+ - uses: actions/checkout@v4
221
+ - uses: actions/setup-python@v5
222
+ with:
223
+ python-version: '3.11'
224
+ - run: pip install git+https://github.com/yunaremaia/aipr.git
225
+ - run: aipr --sarif ${{ github.event.pull_request.head.repo.full_name }} > aipr-results.sarif
226
+ - uses: github/codeql-action/upload-sarif@v3
227
+ with:
228
+ sarif_file: aipr-results.sarif
229
+ ```
230
+
231
+ ## Status
232
+
233
+ Early beta - battle-tested against a handful of real policies (hister,
234
+ modular, polars, MDAnalysis, maka). Rule additions welcome: open an issue with
235
+ the policy text and the verdict you expected.
236
+
237
+ ## License
238
+
239
+ MIT
@@ -0,0 +1,219 @@
1
+ # aipr
2
+
3
+ [![CI](https://github.com/yunaremaia/aipr/actions/workflows/ci.yml/badge.svg)](https://github.com/yunaremaia/aipr/actions/workflows/ci.yml)
4
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://www.python.org/downloads/)
5
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/yunaremaia/aipr/blob/main/LICENSE)
6
+
7
+ **AI Policy Read** - read an open-source repository's AI contribution policy
8
+ before you (or your agent) contribute.
9
+
10
+ `aipr` fetches the governance files that usually carry AI rules
11
+ (`CONTRIBUTING.md`, `AI_POLICY.md`, `AGENTS.md`, `CLAUDE.md`, ...), classifies
12
+ the repository's stance with weighted phrase matching, and answers one
13
+ question: **can an AI-assisted or autonomous contribution land here?**
14
+
15
+ ```
16
+ $ aipr asciimoo/hister
17
+ aipr: asciimoo/hister
18
+ [BLOCKED] human-only policy
19
+ sources: CONTRIBUTING.md, README.md
20
+ confidence: 1.0 score: 19.0
21
+ autonomous contribution: NOT SAFE - require human co-authorship
22
+ [+5.0] ...Issues and PR descriptions must be fully human-written...
23
+ [+5.0] ...AI should never be the main author of the PR...
24
+
25
+ $ aipr apache/maka
26
+ aipr: apache/maka
27
+ [UNKNOWN] no explicit AI policy found
28
+ exit=2
29
+ ```
30
+
31
+ ## Why
32
+
33
+ More repositories are publishing explicit AI policies - from "we welcome
34
+ AI-assisted work" to "agents are strictly forbidden". Violating one burns the
35
+ contributor (and, for autonomous agents, the operator): rejected PRs at best,
36
+ blocks at worst. `aipr` makes the check mechanical and cheap, for humans
37
+ deciding where to spend review effort and for agents deciding where to spend
38
+ their quota.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ # 1. From PyPI
44
+ pip install aipr-py
45
+
46
+ # 2. Standalone from GitHub
47
+ pip install git+https://github.com/yunaremaia/aipr.git
48
+ # requires Python 3.10+; GH_TOKEN recommended (anonymous API calls rate-limit fast)
49
+ export GH_TOKEN=ghp_xxx # classic token with public repo read access
50
+
51
+ # 3. As a GitHub CLI extension (recommended)
52
+ gh extension install yunaremaia/aipr
53
+ ```
54
+
55
+ The `gh extension install` method is the easiest — after install, `gh aipr OWNER/REPO` works immediately.
56
+
57
+ No dependencies beyond the standard library. `pytest` only to develop.
58
+
59
+ ## Usage
60
+
61
+ ### Standalone
62
+
63
+ ```bash
64
+ aipr OWNER/REPO # classify a GitHub repository
65
+ aipr --text FILE # classify a local governance file
66
+ aipr --json OWNER/REPO # machine-readable output
67
+ aipr --sarif OWNER/REPO # SARIF 2.1.0 output for GitHub Code Scanning
68
+ ```
69
+
70
+ ### As a GitHub CLI extension
71
+
72
+ After `gh extension install yunaremaia/aipr`, use `gh aipr` identically:
73
+
74
+ ```bash
75
+ gh aipr OWNER/REPO
76
+ gh aipr --json OWNER/REPO
77
+ gh aipr --sarif OWNER/REPO
78
+ gh aipr --text FILE
79
+ ```
80
+
81
+ Exit codes are preserved (0/1/2) for CI conditionals.
82
+
83
+ ### `init` — scaffold AI policy files
84
+
85
+ Generate `AI_POLICY.md` and `AI_TOOL_POLICY.md` in your repo:
86
+
87
+ ```bash
88
+ aipr init [--dir .] [--type disclose|permissive|human_only] [--org ORG]
89
+ ```
90
+
91
+ Presets:
92
+ - `permissive` — explicitly welcomes AI-assisted contributions (aipr-safe)
93
+ - `disclose_ok` (default) — allowed with `Assisted-by: AI` disclosure trailer
94
+ - `human_only` — AI must not be the main author (NOT autonomous-safe)
95
+
96
+ ### Verdicts
97
+
98
+ | Verdict | Meaning | Autonomous-safe? |
99
+ |---|---|---|
100
+ | `human_only` | AI must not be the main author / human-written only / bans agents | no |
101
+ | `restrictive` | heavy process: mandatory disclosure + human-in-the-loop requirements | no |
102
+ | `disclose_ok` | allowed with a disclosure trailer (`Assisted-by: AI`) | yes* |
103
+ | `permissive` | explicitly welcomes AI-assisted contributions | yes |
104
+ | `unknown` | no explicit policy found | ask first |
105
+
106
+ \* still follow the disclosure rules - "safe" means *no human co-authorship
107
+ required by policy*, not *no obligations*.
108
+
109
+ ### Exit codes (for CI and agents)
110
+
111
+ | Code | Meaning |
112
+ |---|---|
113
+ | 0 | all inspected repos are autonomous-safe |
114
+ | 1 | at least one repo is restricted or human-only |
115
+ | 2 | at least one repo is unknown / no policy found (ranks worse than 1) |
116
+ | 64 | usage error |
117
+
118
+ Batch mode: `aipr owner/repo1 owner/repo2 ...` prints one block per repo
119
+ (JSON array with `--json`) and the exit code reflects the worst result —
120
+ so an unverified repo can never pass a gate silently.
121
+
122
+ Policy fetches are cached on disk for 24h (`~/.cache/aipr`, configurable via
123
+ `AIPR_CACHE_DIR` / `AIPR_CACHE_TTL`), so repeated scans cost zero API calls.
124
+ Use `--no-cache` to force a fresh fetch.
125
+
126
+ ## How classification works
127
+
128
+ Weighted regex matching over concatenated governance text. Restrictive phrases
129
+ score positive ("must be fully human-written" +5), permissive ones negative
130
+ ("we warmly welcome AI-assisted" -3.5). The strongest signals force the
131
+ verdict; weak mixed signals lean restrictive on purpose - when in doubt, do
132
+ not send a bot.
133
+
134
+ Known limits: English-only patterns; phrase matching cannot understand nuance;
135
+ a repo can carry policy in unusual files we don't probe. Treat UNKNOWN as
136
+ "read it yourself".
137
+
138
+ ## CI/CD Integration
139
+
140
+ ### Block PRs Violating AI Policy
141
+
142
+ Drop `.github/workflows/aipr.yml` into your repository to automatically block pull requests targeting repos with human-only or restrictive AI policies:
143
+
144
+ ```yaml
145
+ # .github/workflows/aipr.yml – block PRs against repos with human-only AI policies
146
+ name: AI Policy Check
147
+ on:
148
+ pull_request:
149
+ branches: [main, master]
150
+
151
+ jobs:
152
+ aipr:
153
+ runs-on: ubuntu-latest
154
+ steps:
155
+ - uses: actions/checkout@v5
156
+ - uses: actions/setup-python@v5
157
+ with:
158
+ python-version: "3.10"
159
+ - run: pip install git+https://github.com/yunaremaia/aipr.git
160
+ - name: Check AI policy
161
+ run: |
162
+ aipr "$GITHUB_REPOSITORY" --json || true
163
+ # Exit 1 = human_only/restrictive (block)
164
+ # Exit 2 = unknown (warn, not block)
165
+ aipr "$GITHUB_REPOSITORY" --json | jq -e '.verdict == "human_only" or .verdict == "restrictive"' && exit 1 || exit 0
166
+ ```
167
+
168
+ ### GitHub Code Scanning (SARIF)
169
+
170
+ Use `--sarif` to output SARIF 2.1.0 (Static Analysis Results Interchange Format)
171
+ and upload to GitHub Code Scanning. This surfaces AI policy compliance as
172
+ alerts in the GitHub Security tab.
173
+
174
+ ```bash
175
+ # Generate SARIF output
176
+ aipr --sarif OWNER/REPO > aipr-results.sarif
177
+ # Upload to GitHub Code Scanning via GitHub Actions:
178
+ # github/codeql-action/upload-sarif with sarif_file: aipr-results.sarif
179
+ ```
180
+
181
+ Verdict mapping to SARIF levels:
182
+ - `human_only` / `restrictive` → `error` (blocks contribution)
183
+ - `unknown` → `warning` (needs manual review)
184
+ - `permissive` / `disclose_ok` → `note` (safe to proceed)
185
+
186
+ Example GitHub Actions workflow snippet:
187
+
188
+ ```yaml
189
+ name: AI Policy Check (SARIF)
190
+ on:
191
+ pull_request:
192
+ branches: [main]
193
+
194
+ jobs:
195
+ aipr-check:
196
+ runs-on: ubuntu-latest
197
+ permissions:
198
+ security-events: write
199
+ steps:
200
+ - uses: actions/checkout@v4
201
+ - uses: actions/setup-python@v5
202
+ with:
203
+ python-version: '3.11'
204
+ - run: pip install git+https://github.com/yunaremaia/aipr.git
205
+ - run: aipr --sarif ${{ github.event.pull_request.head.repo.full_name }} > aipr-results.sarif
206
+ - uses: github/codeql-action/upload-sarif@v3
207
+ with:
208
+ sarif_file: aipr-results.sarif
209
+ ```
210
+
211
+ ## Status
212
+
213
+ Early beta - battle-tested against a handful of real policies (hister,
214
+ modular, polars, MDAnalysis, maka). Rule additions welcome: open an issue with
215
+ the policy text and the verdict you expected.
216
+
217
+ ## License
218
+
219
+ MIT
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "aipr-py"
7
+ version = "0.2.4"
8
+ description = "Read a repository's AI contribution policy before you (or your agent) contribute."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Yunare Maia", email = "yunare@gmail.com" }]
13
+ keywords = ["ai-policy", "open-source", "contributing", "cli", "agents"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Software Development :: Version Control :: Git",
21
+ ]
22
+ dependencies = [
23
+ "regex>=2024.1.0",
24
+ ]
25
+
26
+ [project.scripts]
27
+ aipr = "aipr.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/yunaremaia/aipr"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """aipr: read an open-source repository's AI contribution policy."""
2
+
3
+ __version__ = "0.2.3"
@@ -0,0 +1,84 @@
1
+ """Thread-safe TTL cache for policy detection results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import os
7
+ import threading
8
+ import time
9
+ from dataclasses import dataclass
10
+ from typing import TYPE_CHECKING, Optional
11
+
12
+ if TYPE_CHECKING:
13
+ from aipr.detector import Policy
14
+
15
+
16
+ # Default TTL: 24 hours (in seconds)
17
+ DEFAULT_CACHE_TTL = int(os.environ.get("AIPR_CACHE_TTL", "86400"))
18
+ MAX_CACHE_SIZE = int(os.environ.get("AIPR_CACHE_SIZE", "1024"))
19
+
20
+
21
+ @dataclass
22
+ class CacheEntry:
23
+ ts: float
24
+ policy: object # Policy dataclass
25
+
26
+
27
+ class TTLCache:
28
+ """Thread-safe TTL cache with LRU eviction and deep copy on read.
29
+
30
+ Replaces lru_cache for Policy objects to ensure:
31
+ 1. Thread safety via threading.Lock
32
+ 2. TTL-based expiration (configurable via AIPR_CACHE_TTL env var)
33
+ 3. Deep copy on read to prevent mutation of cached state
34
+ """
35
+
36
+ def __init__(self, ttl: int = DEFAULT_CACHE_TTL, maxsize: int = MAX_CACHE_SIZE):
37
+ self._ttl = ttl
38
+ self._maxsize = maxsize
39
+ self._cache: dict[str, CacheEntry] = {}
40
+ self._lock = threading.Lock()
41
+
42
+ def get(self, key: str) -> Optional[object]:
43
+ """Get a cached Policy, returning a deep copy.
44
+
45
+ Returns None if key not found or entry expired.
46
+ """
47
+ with self._lock:
48
+ entry = self._cache.get(key)
49
+ if entry is None:
50
+ return None
51
+ if time.time() - entry.ts > self._ttl:
52
+ del self._cache[key]
53
+ return None
54
+ return copy.deepcopy(entry.policy)
55
+
56
+ def put(self, key: str, policy: object) -> None:
57
+ """Store a Policy with current timestamp.
58
+
59
+ Evicts oldest entries if cache exceeds maxsize.
60
+ """
61
+ with self._lock:
62
+ while len(self._cache) >= self._maxsize:
63
+ oldest_key = min(self._cache, key=lambda k: self._cache[k].ts)
64
+ del self._cache[oldest_key]
65
+ self._cache[key] = CacheEntry(ts=time.time(), policy=copy.deepcopy(policy))
66
+
67
+ def clear(self) -> None:
68
+ """Clear all cached entries."""
69
+ with self._lock:
70
+ self._cache.clear()
71
+
72
+ def __len__(self) -> int:
73
+ with self._lock:
74
+ return len(self._cache)
75
+
76
+ def __contains__(self, key: str) -> bool:
77
+ with self._lock:
78
+ entry = self._cache.get(key)
79
+ if entry is None:
80
+ return False
81
+ if time.time() - entry.ts > self._ttl:
82
+ del self._cache[key]
83
+ return False
84
+ return True