-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
323 lines (289 loc) · 12 KB
/
Copy pathpyproject.toml
File metadata and controls
323 lines (289 loc) · 12 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
[build-system]
requires = ["setuptools>=77.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "pf-core"
version = "0.26.0"
description = "Python foundation for LLM apps whose prompts and spend you can actually see — versioned prompts, every call recorded and replayable, budgets, evals, and resumable jobs on one schema; opt-in LLM, database, and FastAPI layers"
readme = "README.md"
requires-python = ">=3.12"
license = "Apache-2.0"
license-files = ["LICENSE", "NOTICE"]
authors = [{ name = "Mike Farr" }]
keywords = [
"llm",
"openrouter",
"anthropic",
"fastapi",
"sqlalchemy",
"framework",
"cost-tracking",
"eval",
"structured-logging",
"prompt-versioning",
"llm-observability",
"background-jobs",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Typing :: Typed",
]
dependencies = [
# Foundation kernel: the dependency-light architectural base — structured
# logging, exception hierarchy, config + env resolvers, utils, the
# ``Service`` base class, and console output. Everything else (LLM
# clients + anti-slop guards, HTTP utils, CLI scaffolding, database, web,
# jobs, eval) lives behind opt-in extras so a project can adopt pf-core's
# discipline without installing the LLM/HTTP stack. See
# docs/INSTALLATION.md for the extras matrix.
#
# These five are the only hard deps: dotenv (config), pyyaml (yaml config,
# imported lazily), structlog (logging), nanoid (id generation), rich
# (console output in pf_core.output). No httpx, pydantic, json-repair,
# tenacity, or typer here — those moved to [http]/[validate]/[llm]/[cli].
"python-dotenv>=1.2",
"pyyaml>=6.0.2",
"structlog>=26.1",
"nanoid>=2.0",
"rich>=15.0",
]
[project.optional-dependencies]
# ---------------------------------------------------------------------------
# Capability extras — each unlocks a tier of pf-core's framework surface.
# Each composes orthogonally on the foundation base: [db] without LLM, [web]
# without [db], [llm] standalone.
# ---------------------------------------------------------------------------
# HTTP utils: pf_core.utils.url_liveness / pf_core.utils.urls (URL liveness +
# Wayback round-timestamp checks). The lightest network tier — just httpx.
# Pulled in transitively by [llm].
http = ["httpx>=0.28"]
# CLI scaffolding: pf_core.cli.create_cli. Install this to reuse pf-core's
# Typer-based command scaffolding in your own CLI; foundation-only projects
# can equally just depend on typer directly. Pulled in by [jobs].
cli = [
"typer>=0.27",
"click>=8.4",
]
# Anti-slop output guards: pf_core.llm.parse (+ json-repair recovery) and
# pf_core.llm.validate (pydantic schema/semantic validation). Pure-Python —
# NO httpx / clients / tenacity / db — so a project can validate LLM output
# (or wire its own transport) without the client stack. The stdlib/pyyaml
# members of pf_core.llm (url_check, prompts, router, safe_apply, recording)
# need no extra at all once pf_core.llm imports lazily.
validate = [
"json-repair>=0.63",
"pydantic>=2.13",
]
# LLM tier: pf_core.clients.* (OpenRouter / Brave / Claude Code) on top of
# the anti-slop guards. = [validate] + [http] (clients need httpx) + tenacity
# (retry). [llm] ⊇ [validate], so every existing [llm]/[full]/[tracking]
# consumer keeps the same dependency closure (mirrors [tracking] = [db,llm]).
# Note: tracked_call records to the DB, so it additionally needs [tracking]
# (= [db,llm]); importing it without [db] raises a friendly 'tracking' error.
llm = [
"pf-core[validate,http]",
"tenacity>=9.1",
]
# Database layer: pf_core.db.*, pf_core.alembic, fully-functional cost guards
# (BudgetRepo / CostRateRepo). Without [db], pf_core.budget.check_budget still
# imports and short-circuits gracefully; pf_core.db.* and pf_core.alembic
# raise ImportError. SQLite needs no driver extra (stdlib).
db = [
"sqlalchemy>=2.0.52",
"alembic>=1.19",
]
# FastAPI web layer: pf_core.web.app_factory, error pages, markdown,
# pagination, templates.
web = [
"fastapi>=0.141",
"jinja2>=3.1.6",
"uvicorn[standard]>=0.52",
]
# Job tracker: pf_core.jobs.* (state machine, step history, worker leases)
# and the pf-jobs CLI. Requires [db] for persistence and [cli] for the CLI;
# pydantic backs job-argument schema validation in pf_core.jobs.registry.
# [tracking] is required, not optional: jobs._schema imports
# llm.tracking.schema at module scope so job-attribution FKs land on the shared
# metadata, so pf_core.jobs cannot import without it.
jobs = ["pf-core[db,cli,tracking]", "pydantic>=2.13"]
# DB-backed LLM run tracking: pf_core.llm.tracking.* and pf_core.llm.cache.*
# (one row per call with prompts, tokens, cost, validations, job
# attribution). Requires [db] for persistence and [llm] for the parse/retry
# machinery it records around (tracking code itself uses tenacity).
tracking = ["pf-core[db,llm]"]
# Admin dashboard sub-app: pf_core.web.llm_admin (runs, costs, jobs, cache
# stats, budgets). Requires [web,tracking].
admin = ["pf-core[web,tracking]"]
# Eval harness: pf_core.eval.* (golden-set replay, structured-diff and
# LLM-judge comparators) and the pf-eval CLI. Requires [tracking,jobs];
# jinja2 renders the HTML report (EvalReport.write_html, pf-eval run --output).
eval = ["pf-core[tracking,jobs]", "jinja2>=3.1.6"]
# OpenTelemetry export: pf_core.llm.tracking.otel and the pf-export CLI send
# llm_runs to any OTLP/HTTP backend as GenAI spans. Requires [tracking] (the
# runs) and [cli] (pf-export). Opt-in tooling, so [full] does not include it.
otel = [
"pf-core[tracking,cli]",
"opentelemetry-api>=1.45",
"opentelemetry-sdk>=1.45",
"opentelemetry-exporter-otlp-proto-http>=1.45",
]
# ---------------------------------------------------------------------------
# Database driver extras — pick one per consumer (mutually exclusive).
# ---------------------------------------------------------------------------
mysql = ["pymysql>=1.2"]
postgres = ["psycopg[binary]>=3.3"]
# ---------------------------------------------------------------------------
# Other capability extras
# ---------------------------------------------------------------------------
# Redis-backed cache region: pf_core.cache (with graceful no-op fallback
# when Redis is unreachable).
redis = ["dogpile.cache>=1.5", "redis>=8.1"]
# Per-IP rate limiting on FastAPI routes. Requires [web].
ratelimit = [
"pf-core[web]",
"slowapi>=0.1.10",
]
# Strict JSON Schema validation in pf_core.llm.validate (stdlib jsonschema
# rather than the lighter Pydantic path).
jsonschema = ["jsonschema>=4.26"]
# Article fetch + extract: pf_core.utils.article_fetch (title, body,
# publish-date with Wayback Machine fallback for paywalled URLs). tenacity
# drives the Wayback retry loop.
articles = [
"trafilatura>=2.2",
"htmldate>=1.10",
"tenacity>=9.1",
]
# Intent-named meta-extra: "I want to crawl/fetch web pages." Composes the
# fetch+extract stack ([articles]) with the URL-utils tier ([http]) —
# article_fetch imports pf_core.utils.urls, which needs httpx. Exists so a
# consumer asking "how do I crawl?" gets one named line instead of having to
# know the composition. Front-end serving is a different intent: [web].
crawl = ["pf-core[http,articles]"]
# Direct Anthropic API client (pf_core.clients.anthropic). Provides a
# multimodal-capable alternative to the OpenRouter and Claude Code
# transports for consumers that want the official SDK's vision support
# and direct usage / cache-token reporting.
# <1.0: SDK 1.0 removed temperature/top_p/top_k from messages.create().
anthropic = ["anthropic>=0.105,<1.0"]
# Perceptual-hash image dedup: pf_core.utils.phash (DCT-based phash via
# ImageHash + Pillow). Used by document-processing pipelines to detect
# repeated page decorations (header logos, footer marks) and re-encoded
# duplicates that sha256 would miss.
image-phash = [
"ImageHash>=4.3.2",
"Pillow>=12.3", # Security floor: 12.3.0 fixed the 2026 CVE batch.
]
# ---------------------------------------------------------------------------
# Meta-extra for full-stack apps that want the whole framework.
# Add the dialect driver ([mysql]/[postgres]) and any optional capability
# extras ([articles]) separately.
# ---------------------------------------------------------------------------
full = [
"pf-core[db]",
"pf-core[web]",
"pf-core[llm]",
"pf-core[cli]",
"pf-core[jobs]",
"pf-core[tracking]",
"pf-core[admin]",
"pf-core[eval]",
"pf-core[redis]",
"pf-core[ratelimit]",
"pf-core[jsonschema]",
]
# ---------------------------------------------------------------------------
# Dev / test extras
# ---------------------------------------------------------------------------
dev = [
"pytest>=9.1",
"httpx>=0.28",
"factory-boy>=3.3.3",
"ruff~=0.16.0",
"mypy~=2.3.0",
"types-PyYAML",
"types-PyMySQL",
"types-jsonschema",
"types-nanoid",
"pre-commit>=4.6",
]
test-containers = [
"testcontainers[mysql]>=4.15",
]
[project.urls]
Documentation = "https://github.com/phierceweb/pf-core/blob/main/src/pf_core/docs/modules.md"
Repository = "https://github.com/phierceweb/pf-core"
Changelog = "https://github.com/phierceweb/pf-core/blob/main/CHANGELOG.md"
Issues = "https://github.com/phierceweb/pf-core/issues"
[project.scripts]
pf-guards = "pf_core.guards.structure:run_cli"
pf-doctor = "pf_core.doctor:run_cli"
pf-jobs = "pf_core.cli.jobs:app"
pf-export = "pf_core.cli.export:main"
pf-eval = "pf_core.eval.cli:main"
pf-setup = "pf_core.wiring:run_cli"
[project.entry-points.pytest11]
pf_core = "pf_core.testing.fixtures"
[tool.setuptools.packages.find]
where = ["src"]
[tool.setuptools.package-data]
"pf_core" = ["docs/*.md", "docs/recipes/*.md", "py.typed"]
"pf_core.web.llm_admin" = ["templates/*.html"]
"pf_core.web.jobs_admin" = ["templates/*.html"]
[tool.pytest.ini_options]
testpaths = ["tests"] # don't collect templates/consumer-*/tests/ (they hold __PKG__ tokens)
pythonpath = ["."]
# ---------------------------------------------------------------------------
# Type-check config — mypy
# ---------------------------------------------------------------------------
# Non-strict: annotated code is checked, untyped defs are not forced. `files`
# scopes a bare `mypy` to the same surface CI checks.
[tool.mypy]
python_version = "3.12"
# A directory expands to *.py only, so bin/'s extensionless entry points must be
# named one by one; scripts_are_modules stops them all resolving to __main__.
files = [
"src",
"bin",
"bin/new-consumer",
"bin/pf-eval",
"bin/pf-jobs",
"bin/verify-bare-install",
]
scripts_are_modules = true
# ---------------------------------------------------------------------------
# Lint config — ruff
# ---------------------------------------------------------------------------
# Conservative set: what ruff catches by default (E, W, F) plus bugbear (B).
# Layout is enforced separately by `ruff format` (pre-commit + CI). isort (I)
# and pyupgrade (UP) stay off. E501 stays off because the formatter cannot
# break long string literals — Jinja blocks, markdown tables in docstrings, SQL.
[tool.ruff]
line-length = 100
target-version = "py312"
# Same reason as [tool.mypy] files: directory traversal skips extensionless scripts.
extend-include = [
"bin/new-consumer",
"bin/pf-eval",
"bin/pf-jobs",
"bin/verify-bare-install",
]
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes (unused imports, undefined names, etc.)
"B", # flake8-bugbear (common bugs / antipatterns)
]
ignore = [
"E501", # line too long — defer to a separate formatting pass
"B008", # function call in default arg — common in FastAPI / Typer
"B904", # raise-from — `raise X from y` discipline pre-dates this lint
]
[tool.ruff.lint.per-file-ignores]
# Tests may use `assert False` (B011) in must-not-reach branches.
"tests/**/*.py" = ["B011"]