-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpyproject.toml
More file actions
510 lines (484 loc) · 19.9 KB
/
Copy pathpyproject.toml
File metadata and controls
510 lines (484 loc) · 19.9 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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
#####################################################################################################################################################################################################
# Project: Juniper
# Sub-Project: JuniperCanopy
# Application: juniper_canopy
# File Name: pyproject.toml
# Author: Paul Calnon
# Version: 0.5.0
#
# Date Created: 2025-11-03
# Last Modified: 2026-02-27
#
# License: MIT License
# Copyright: Copyright (c) 2024,2025,2026 Paul Calnon
#
# Description:
# Python project configuration for Juniper Canopy
# Configures Black, isort, pytest, coverage, and mypy
#
# References:
# - CANOPY-P1-001: CI/CD Pipeline Parity
#####################################################################################################################################################################################################
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "juniper-canopy"
version = "0.7.0"
description = "Real-time monitoring dashboard for Cascade Correlation Neural Network"
readme = "README.md"
requires-python = ">=3.11"
license = { text = "MIT" }
authors = [{ name = "Paul Calnon", email = "paul.calnon@gmail.com" }]
keywords = [
"neural-network",
"visualization",
"monitoring",
"dashboard",
"cascade-correlation",
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Scientific/Engineering :: Artificial Intelligence",
"Topic :: Scientific/Engineering :: Visualization",
]
dependencies = [
"dash>=3.0.0",
"dash-bootstrap-components>=2.0.0",
"fastapi>=0.100.0",
"uvicorn[standard]>=0.20.0",
"plotly>=5.0.0",
"numpy>=1.24.0",
"scipy>=1.10.0",
"PyYAML>=6.0",
"pydantic>=2.0.0",
"pydantic-settings>=2.0.0",
"websockets>=12.0",
# CFG-13: ``python-dotenv`` was previously declared explicitly here.
# Removed because:
# 1. No canopy code in ``src/`` imports ``dotenv`` directly
# (``grep -rn 'from dotenv\|import dotenv' src/`` returns zero
# hits outside test reports).
# 2. ``src/settings.py:124`` uses ``env_file=".env"`` on the
# pydantic-settings ``Settings`` class. pydantic-settings
# declares ``python-dotenv>=0.21.0`` as a **required (non-optional)**
# runtime dep (verified via ``importlib.metadata.metadata
# ('pydantic-settings').get_all('Requires-Dist')``), so it
# transitively installs whenever canopy is installed.
# 3. The lockfile correctly resolves ``python-dotenv==1.2.2`` via
# the pydantic-settings edge; this PR does not require a lock
# regen.
# Sibling repos juniper-data and juniper-cascor DO have direct
# ``from dotenv import load_dotenv`` calls and correctly keep the
# explicit dep; canopy is the only repo where it was dead.
"nest-asyncio>=1.5.0",
"requests>=2.28.0",
# METRICS-MON R4.2 / seed-10: native async HTTP for health-probe
# paths. The previous ``urllib.request.urlopen`` + ``run_in_executor``
# pattern offloaded each probe to a thread-pool worker; under N
# concurrent probes that exhausts the default 32-worker pool. ``httpx.AsyncClient``
# gives us native async I/O so the event loop stays responsive
# without burning thread-pool budget.
"httpx>=0.27",
"a2wsgi>=1.10.0",
"colorama>=0.4.6",
"networkx>=3.0",
"psutil>=5.9.0",
"python-multipart>=0.0.6",
# METRICS-MON R2.1.5 / seed-06: shared observability primitives
# (DependencyStatus, ReadinessResponse, probe_dependency,
# JuniperJsonFormatter, RequestIdMiddleware, PrometheusMiddleware,
# configure_logging, configure_sentry, get_prometheus_app,
# set_build_info). 0.3.0 adds MetricsAuthMiddleware
# (POC §6 promotion from data #157 + cascor #313); pinned at the
# first version that exports the middleware so the SEC-16 surface
# is consistent across services. 0.4.0 adds optional ``git_sha`` /
# ``build_date`` passthrough on ``set_build_info`` + the shared
# ``ReadinessResponse`` model for build provenance (juniper-ml
# notes/BUILD_PROVENANCE_DESIGN_2026-06-14.md).
"juniper-observability>=0.4.0,<0.5.0",
# E-8 boot-time dependency-floor self-check. canopy calls
# ``enforce_dependency_floors(distribution="juniper-canopy")`` in its FastAPI
# lifespan (``src/main.py``) to fail loud before binding if any installed
# ``juniper-*`` wheel is below canopy's declared floor -- the automatic
# *prevention* companion to the E-2 env-floor-drift *detector* (``make
# check-env`` / ``juniper-env-drift-check``), catching the "green tests /
# dead app" class at first boot. Unconditional (the check must always run).
# 0.5.0 floor: adds ``enforce_auth_posture`` (SEC-F01 / HO-2), the boot-time
# auth-posture self-check the lifespan calls right after the floor check.
"juniper-service-core>=0.5.0,<0.8.0",
# METRICS-MON R2.2.5 / seed-05: WS frame schema validation. Pinned
# at the first stable release; pulled in transitively by
# juniper-cascor-client too, but pinned explicitly here so the
# dependency edge is visible to anyone reading canopy's pyproject.
"juniper-cascor-protocol>=0.1.0,<0.3.0",
# 2026-05-10: promoted from the [observability] extra into required
# deps. ``src/observability.py:152`` unconditionally imports
# ``prometheus_client`` whenever ``CANOPY_METRICS_ENABLED=true``, and
# the deploy stack always sets that env (via .env.observability) under
# the observability profile. Keeping it as an extra meant
# ``pip install juniper-canopy`` (no extra) silently produced an
# image that crashed at import. The runtime cost is identical
# because the production lockfile already pins it via the extra.
"prometheus-client>=0.20.0",
]
[project.urls]
Homepage = "https://github.com/pcalnon/juniper-canopy"
Documentation = "https://github.com/pcalnon/juniper-canopy#readme"
Repository = "https://github.com/pcalnon/juniper-canopy"
Issues = "https://github.com/pcalnon/juniper-canopy/issues"
[project.optional-dependencies]
juniper-data = [
# METRICS-MON R4.3 / seed-13: bumped from >=0.3.0 to >=0.4.1 because
# canopy now constructs ``JuniperDataClient`` with the ``on_request``
# instrumentation kwarg (added in 0.4.1). Pin floor matches the
# release that introduced the kwarg.
"juniper-data-client>=0.4.1,<0.6.0",
"requests>=2.28.0",
]
juniper-cascor = [
# Floor 0.7.0 (CL2, juniper-ml training-runtime defects plan §7/§13): the
# CascorServiceAdapter liveness seams consume the cascor-client CL1 surface
# shipped in 0.7.0 (cascor-client#92) — the stream classes' ``is_connected``
# / ``is_alive(window_sec)`` liveness properties and WS auto-pong — and the
# metrics relay retires its manual heartbeat-pong workaround because 0.7.0's
# ``CascorTrainingStream`` auto-pongs and never yields ``ping`` frames.
# ``FakeCascorTrainingStream`` gained matching liveness parity in 0.7.0.
# Earlier releases lack the surface, so the floor is the oldest version the
# adapter can run against. (Prior floor 0.6.0: FakeCascorClient ``_request``
# in-memory parity, cascor-client#91.)
#
# Floor 0.8.0 (2026-09-05, the X7 C8 fix): 0.8.0 narrows
# ``RETRY_ALLOWED_METHODS`` to ``["HEAD", "GET"]``, so a failed
# ``POST /v1/training/start`` is no longer retried into up to four training
# runs (cascor-client ``APD-CCLIENT-001``). Every earlier release -- 0.7.0
# included -- retries ``POST``/``PUT``/``PATCH``/``DELETE``, and canopy is
# the consumer that issues the training-start. The floor is what stops a
# resolver handing canopy a client that does that; the cap alone would not.
# 0.8.0 rather than 0.7.1 because it also adds ``backoff_factor`` as a
# public constructor parameter (``APD-CCLIENT-013``) -- additive public API
# is a feature, so MINOR. The ``<0.8.0`` cap this replaced was widened to
# ``<0.9.0`` first (canopy#584), because a cap excluding the artefact would
# have blocked the release outright; the floor could only follow, since a
# floor pinned at an unpublished version resolves nothing.
# Verified against the published wheel, not the checkout:
# ``pip install juniper-cascor-client==0.8.0`` then
# ``constants.RETRY_ALLOWED_METHODS`` -> ``['HEAD', 'GET']``.
"juniper-cascor-client>=0.8.0,<0.9.0",
]
observability = [
# prometheus-client moved to [project] dependencies on 2026-05-10
# (see comment near juniper-cascor-protocol). Kept here is sentry,
# which is genuinely opt-in.
"sentry-sdk>=2.0.0",
]
ui-test = [
# Issue #4 / FRONTEND_ISSUES_PLAN_2026-05-09.md §5: Playwright-based browser
# automation for the UI sub-suite. Skeleton landed in PR-3; full inventory
# lands in PR-10. Browser binaries installed via ``playwright install
# --with-deps chromium`` (see .github/workflows/ci.yml ui-tests lane).
"pytest-playwright>=0.5",
"playwright>=1.45",
]
demo = [
# CFG-01 (v7 roadmap §13439): ``torch`` is imported unconditionally at
# module level by ``src/demo_mode.py:63`` and ``src/backend/demo_backend.py:45``.
# ``pip install juniper-canopy`` without this extra cannot import the
# demo modules. Kept out of ``[project] dependencies`` to avoid the
# ~2GB install footprint on production deployments that drive a remote
# cascor service via ``[juniper-cascor]`` and never load demo mode
# (matches the lazy-import convention in ``src/backend/data_adapter.py:363,406``
# whose existing comments call out the size cost explicitly).
# The standalone demo runner ``util/juniper_canopy-demo.bash`` continues
# to install torch via ``conf/requirements.txt`` + the PyTorch CPU index
# URL for size-optimised bash-script installs; this extra is the
# canonical path for ``pip install juniper-canopy[demo]``.
# SEC-F17 (CVE-2025-3001, lstm_cell memory corruption, fixed 2.10.0):
# floor raised 2.0.0 -> 2.10.0 to exclude the vulnerable 2.0-2.9 range.
"torch>=2.10.0",
]
dev = [
"juniper-canopy[juniper-data,juniper-cascor,observability,demo]",
"pytest>=8.0",
"pytest-cov>=5.0",
"pytest-asyncio>=0.23",
"bandit[sarif]>=1.7",
"pip-audit>=2.7",
]
# ==========================================
# Setuptools Package Discovery
# ==========================================
[tool.setuptools.packages.find]
where = [".", "src"]
include = ["juniper_canopy*", "backend*", "communication*", "frontend*", "logger*"]
# ==========================================
# Black Configuration
# ==========================================
[tool.black]
line-length = 512
target-version = ['py312', 'py313', 'py314']
include = '\.pyi?$'
extend-exclude = '''
/(
\.git
| \.pytest_cache
| \.trunk
| __pycache__
| data
| logs
| reports
| htmlcov
| images
)/
'''
# ==========================================
# isort Configuration
# ==========================================
[tool.isort]
profile = "black"
line_length = 512
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
split_on_trailing_comma = true
skip_gitignore = true
known_first_party = ["src"]
sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
skip = [".git", "__pycache__", ".pytest_cache", ".trunk", "data", "logs", "reports", "htmlcov"]
skip_glob = ["*/data/*", "*/logs/*", "*/reports/*"]
# ==========================================
# Bandit Security Scanner Configuration
# ==========================================
# Bandit config is consolidated in .bandit.yml (single source of truth).
# Do NOT add [tool.bandit] here — it would create conflicting skip lists.
# ==========================================
# MyPy Type Checker Configuration
# ==========================================
[tool.mypy]
python_version = "3.14"
mypy_path = "src"
explicit_package_bases = true
warn_return_any = true
warn_unused_configs = true
strict_optional = true
exclude = [
"^data/",
"^logs/",
"^reports/",
"^htmlcov/",
"^tests/",
]
[[tool.mypy.overrides]]
module = [
"dash.*",
"dash_bootstrap_components.*",
"plotly.*",
"fastapi.*",
"uvicorn.*",
"starlette.*",
"requests.*",
"yaml.*",
"scipy.*",
"colorama.*",
"psutil.*",
"networkx.*",
"redis.*",
"cassandra.*",
"h5py.*",
"a2wsgi.*",
"nest_asyncio.*",
"websockets.*",
"werkzeug.*",
"flask.*",
"torch.*",
"juniper_data_client.*",
"juniper_cascor_client.*",
"pydantic",
"pydantic.*",
"pydantic_settings",
"pydantic_settings.*",
]
ignore_missing_imports = true
# Modules pending strict_optional migration (CAN-MED-014)
[[tool.mypy.overrides]]
module = [
"main",
"config_manager",
"demo_mode",
"backend.data_adapter",
"backend.cassandra_client",
"frontend.dashboard_manager",
"frontend.components.metrics_panel",
"frontend.components.network_visualizer",
"frontend.components.hdf5_snapshots_panel",
]
strict_optional = false
# Basedpyright / Pyright: suppress unresolved third-party imports when the IDE interpreter
# has no site-packages for this project (prefer JuniperPython per AGENTS.md).
[tool.basedpyright]
include = ["src"]
reportMissingImports = "none"
# ==========================================
# Pytest Configuration (additional)
# ==========================================
[tool.pytest.ini_options]
minversion = "7.0"
testpaths = ["src/tests"]
pythonpath = ["src"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
timeout = 60
timeout_method = "signal"
addopts = [
"-ra",
"-q",
"--strict-markers",
"--strict-config",
"--continue-on-collection-errors",
"--tb=short",
# Exclude the Playwright UI subsuite from the default invocation.
# pytest-playwright's session-level browser fixture leaks an event loop
# that breaks every async test that runs after a UI test in the same
# pytest session (180 async failures with UI included → 0 without).
# Run UI tests via a separate invocation: `make test-ui` (or
# `pytest src/tests/ui --override-ini=addopts=`).
"--ignore=src/tests/ui",
]
markers = [
"unit: Unit tests (fast, no external dependencies)",
"integration: Integration tests (may use DB, files, etc.)",
"performance: Performance tests",
"regression: Regression tests",
"e2e: End-to-end tests (require full system)",
"slow: Slow-running tests (>1 second)",
"requires_redis: Tests requiring Redis connection (set REDIS_INTEGRATION_TEST=1)",
"requires_cassandra: Tests requiring Cassandra connection (set CASSANDRA_INTEGRATION_TEST=1)",
"requires_cascor: Tests requiring CasCor backend",
"requires_server: Tests requiring live server running",
"requires_display: Tests requiring display for visualization",
"api: Tests for API endpoints",
"generators: Tests for data generators",
"ui: Browser-automation tests (Playwright; requires `playwright install chromium` and the `ui-test` extra)",
]
asyncio_mode = "auto"
consider_namespace_packages = false
filterwarnings = [
"error::RuntimeWarning",
"ignore::DeprecationWarning:dash.*",
"ignore::DeprecationWarning:plotly.*",
"ignore::pytest.PytestUnraisableExceptionWarning",
"once:CASCOR_.*is deprecated:DeprecationWarning:settings",
# _brotli is an indirect dep via httpx (used by starlette.testclient in
# tests/conftest.py). Under a free-threading interpreter (cpython-314t)
# its import re-enables the GIL with a RuntimeWarning that the
# 'error::RuntimeWarning' rule above would otherwise promote into a
# hard failure for any test whose fixture chain pulls in the test client.
"ignore:The global interpreter lock \\(GIL\\) has been enabled to load module:RuntimeWarning",
# ``_generate_spiral_dataset_local`` is the deprecated local fallback used
# when the JuniperData service is unreachable. Several tests intentionally
# exercise that fallback path; one test (``test_juniper_data_integration``)
# asserts the warning is emitted via ``pytest.warns`` and is unaffected.
"ignore:DemoMode\\._generate_spiral_dataset_local\\(\\) is deprecated:DeprecationWarning",
]
# ==========================================
# Coverage Configuration (additional)
# ==========================================
[tool.coverage.run]
source = ["src"]
omit = [
"tests/*",
"tests/**/*",
"*/tests/*",
"**/tests/*",
"**/test_*.py",
"**/__pycache__/*",
"*/__pycache__/*",
"**/site-packages/*",
"*/site-packages/*",
"**/conftest.py",
"*/conftest.py",
"conftest.py",
"**/venv/*",
"*/venv/*",
"**/.venv/*",
"*/.venv/*",
]
branch = true
parallel = true
[tool.coverage.report]
show_missing = true
skip_covered = false
skip_empty = true
fail_under = 80
precision = 2
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"@abstractmethod",
"@abc.abstractmethod",
]
[tool.coverage.html]
directory = "reports/coverage"
title = "Juniper Canopy Coverage Report"
[tool.coverage.xml]
output = "coverage.xml"
[tool.coverage.json]
output = "coverage.json"
pretty_print = true
# ═══════════════════════════════════════════════════════════════════════════
# Ruff — async-route audit only (Phase 1 wiring, disabled state).
# Canopy's primary lint stack stays flake8 + black + isort + bandit + mypy.
# Ruff is added solely for the ASYNC* ruleset (BUG-JD-10 class prevention),
# enforced via a manual-stage pre-commit hook. See juniper-ml notes/
# ASYNC_ROUTE_AUDIT_HOOK_MIGRATION_PLAN.md.
# ═══════════════════════════════════════════════════════════════════════════
[tool.ruff]
# Note: ruff >=0.15 caps line-length at 320. Canopy's ecosystem
# convention is 512, but since this ruff config is only used for the
# ASYNC* ruleset (not formatting), the value doesn't affect output;
# pinned at the 320 cap so the config parses without error.
line-length = 320
target-version = "py312"
[tool.ruff.lint]
# Empty `select` so regular `ruff check` is a no-op. The ASYNC* ruleset is
# explicitly opted into via the pre-commit hook's `--select ASYNC` arg.
# Phase 4 may flip this to `select = ["ASYNC"]` if we want the rules
# applied during regular runs too.
select = []
[tool.ruff.lint.per-file-ignores]
# Phase 0 enumeration (juniper-ml notes/ASYNC_ROUTE_VIOLATIONS_2026-05-06.md
# §2.4) flagged 20 violations across 5 files. The per-file-ignores below
# silence the **false positives** so Phase 2's CI-annotation lane focuses
# on real production fixes (the 4 ASYNC230/240 sites in src/main.py for
# snapshot-history file I/O and pathlib ops).
#
# ASYNC109 = "async function definition with a `timeout` parameter".
# All ignored sites pass `timeout` through to a primitive that handles it
# correctly (httpx.AsyncClient, asyncio.wait_for, or pytest-async fixtures).
# All ASYNC109 in test files are pytest-async timeout fixtures.
"src/tests/**/*.py" = ["ASYNC109"]
# Discovery polls remote services with bounded `timeout`; correct pattern.
"src/discovery.py" = ["ASYNC109"]
# Health probe with timeout; correct pattern.
"src/health.py" = ["ASYNC109"]
# Adapter method passes `timeout` through to httpx.AsyncClient.
# Note: the file also has an ASYNC110 violation (asyncio.sleep in while
# loop) at line 134 — left visible so Phase 3 can review it as a possible
# `Event.wait()` candidate.
"src/backend/cascor_service_adapter.py" = ["ASYNC109"]