mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-09-01 04:00:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2da730bdb9 | ||
|
|
1edc7840b2 | ||
|
|
cbe267590c | ||
|
|
8e7ac74acf | ||
|
|
566dab08e3 |
@@ -18,9 +18,6 @@
|
||||
.git/**
|
||||
.github
|
||||
.github/**
|
||||
!.github/requirements/
|
||||
!.github/requirements/explorer-extra-py313.txt
|
||||
!.github/requirements/pep517-build.txt
|
||||
.claude
|
||||
.claude/**
|
||||
.codex
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
name: 'Setup Semantica'
|
||||
description: 'Install Python, cache pip, and install the semantica package into a workflow'
|
||||
author: 'Semantica'
|
||||
|
||||
inputs:
|
||||
python-version:
|
||||
description: 'Python version to set up'
|
||||
required: false
|
||||
default: '3.11'
|
||||
version:
|
||||
description: 'Version constraint to append to the pip spec, e.g. "==0.6.7" or ">=0.6,<0.7". Leave empty for the latest release.'
|
||||
required: false
|
||||
default: ''
|
||||
extras:
|
||||
description: 'Comma-separated extras to install, e.g. "explorer,all"'
|
||||
required: false
|
||||
default: ''
|
||||
cache:
|
||||
description: 'Pip cache mode passed straight to actions/setup-python ("pip" to enable). Left empty (disabled) by default because this action is meant to run standalone in any caller repo, and actions/setup-python errors out if it cannot find a requirements.txt/pyproject.toml/setup.py/poetry.lock to key the cache on. Opt in only when the caller repo has one of those files.'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
outputs:
|
||||
version:
|
||||
description: 'The installed semantica version'
|
||||
value: ${{ steps.verify.outputs.version }}
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
steps:
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
cache: ${{ inputs.cache }}
|
||||
|
||||
- name: Install semantica
|
||||
shell: bash
|
||||
env:
|
||||
SEMANTICA_EXTRAS: ${{ inputs.extras }}
|
||||
SEMANTICA_VERSION: ${{ inputs.version }}
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
if [ -n "$SEMANTICA_EXTRAS" ]; then
|
||||
spec="semantica[$SEMANTICA_EXTRAS]$SEMANTICA_VERSION"
|
||||
else
|
||||
spec="semantica$SEMANTICA_VERSION"
|
||||
fi
|
||||
python -m pip install -- "$spec"
|
||||
|
||||
- name: Verify install
|
||||
id: verify
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION=$(python -c "import semantica; print(semantica.__version__)")
|
||||
echo "Installed semantica $VERSION"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
@@ -101,29 +101,6 @@ updates:
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
|
||||
# Explorer frontend (npm)
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/explorer"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
time: "03:30" # 3:30 AM UTC (9:00 AM IST)
|
||||
open-pull-requests-limit: 10
|
||||
reviewers:
|
||||
- "KaifAhmad1"
|
||||
assignees:
|
||||
- "KaifAhmad1"
|
||||
commit-message:
|
||||
prefix: "security"
|
||||
include: "scope"
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "javascript"
|
||||
- "security"
|
||||
allow:
|
||||
- dependency-type: "production"
|
||||
- dependency-type: "development"
|
||||
|
||||
# Docker dependencies (if you use Docker)
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# CI tool requirements
|
||||
|
||||
Hash-pinned `pip install` targets for CI/release/Dockerfile steps that install
|
||||
something other than the project's own audited `requirements-ci.txt` set.
|
||||
These exist because OpenSSF Scorecard's Pinned-Dependencies check flags any
|
||||
`pip install` in a workflow or Dockerfile that isn't hash-verified, and
|
||||
`requirements-ci.txt` alone doesn't cover build/release/security tooling or
|
||||
the project's own local-source install.
|
||||
|
||||
Each `.txt` was generated from the adjacent `.in` (or, for `explorer-extra-py311.txt`,
|
||||
`explorer-extra-py313.txt`, and `base-deps.txt`, from `pyproject.toml` directly) with:
|
||||
|
||||
```
|
||||
uv pip compile <input> --python-version 3.11 --python-platform linux \
|
||||
--constraint requirements-ci.txt --generate-hashes -o <output>.txt
|
||||
```
|
||||
|
||||
(`--constraint requirements-ci.txt` is omitted for `bootstrap.txt`,
|
||||
`build-tools.txt`, `uv-tool.txt`, `twine.txt`, `pip-audit.txt`, and
|
||||
`security-scan-tools.txt`, since those install standalone tooling with no
|
||||
version relationship to the project's own dependency tree.)
|
||||
|
||||
Regenerate a file the same way after bumping a pinned version, and re-run it
|
||||
whenever `requirements-ci.txt` changes if the file used `--constraint` (see
|
||||
each file's own autogenerated header comment for its exact command).
|
||||
|
||||
| File | Used by | Installs |
|
||||
| --- | --- | --- |
|
||||
| `bootstrap.txt` | security.yml, security-scan.yml, benchmark.yml | pip, setuptools (upgrade before anything else) |
|
||||
| `pep517-build.txt` | ci.yml, benchmark.yml, Dockerfile | exact `[build-system] requires` from `pyproject.toml` (setuptools, wheel) - installed with `--no-build-isolation` before any `pip install -e .` / `pip install .`, since `--no-deps` alone doesn't stop pip's PEP 517 build isolation from fetching those two *unhashed* |
|
||||
| `explorer-extra-py311.txt` | ci.yml | semantica's base deps + the `explorer` extra, resolved for python 3.11 |
|
||||
| `explorer-extra-py313.txt` | Dockerfile | the same, resolved for python 3.13 (the image's actual interpreter) |
|
||||
| `pytest-tool.txt` | ci.yml | pytest, for the pre-all-extras deterministic test |
|
||||
| `uv-tool.txt` | ci.yml | uv, to verify requirements-ci.txt is current |
|
||||
| `build-tools.txt` | ci.yml, release.yml | build, wheel |
|
||||
| `twine.txt` | release.yml | twine |
|
||||
| `pip-audit.txt` | security.yml | pip-audit |
|
||||
| `security-scan-tools.txt` | security-scan.yml | safety, bandit, semgrep, jq |
|
||||
| `base-deps.txt` | benchmark.yml | semantica's base deps (no extras) |
|
||||
| `benchmark-extra.txt` | benchmark.yml | the benchmark-only libs (neo4j, pdfplumber, etc.) |
|
||||
|
||||
`explorer-extra-py31{1,3}.txt` and `base-deps.txt` are large (they mirror
|
||||
most of `requirements-ci.txt`) because semantica's `dependencies` list in
|
||||
`pyproject.toml` isn't extras-gated - installing the package at all pulls
|
||||
the full base set. That's expected, not a mistake.
|
||||
|
||||
`explorer-extra-py311.txt` and `explorer-extra-py313.txt` are **not**
|
||||
interchangeable, and can't be collapsed into one file compiled for either
|
||||
version: `librosa`'s `audioread` dependency needs `standard-aifc` /
|
||||
`standard-sunau` only under `python_version >= "3.13"` (Python 3.13 dropped
|
||||
`aifc`/`sunau` from stdlib). A file resolved for 3.11 simply omits those
|
||||
packages' hashes, so installing it with `--require-hashes` on a real 3.13
|
||||
interpreter (the Dockerfile's base image) fails outright rather than
|
||||
silently under-pinning. Any other file shared across a 3.11 and 3.13
|
||||
consumer would need the same split if it hits a similar stdlib-removal
|
||||
edge case - check for `ERROR: In --require-hashes mode, all requirements
|
||||
must have their versions pinned` on the *other* Python version before
|
||||
assuming one `--python-version` covers every consumer.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +0,0 @@
|
||||
rdflib
|
||||
neo4j
|
||||
faiss-cpu
|
||||
torch
|
||||
pyarrow
|
||||
pdfplumber
|
||||
python-pptx
|
||||
openpyxl
|
||||
lxml
|
||||
python-docx
|
||||
beautifulsoup4
|
||||
chardet
|
||||
langdetect
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
pip
|
||||
setuptools
|
||||
@@ -1,10 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/bootstrap.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/bootstrap.txt
|
||||
pip==26.2.1 \
|
||||
--hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \
|
||||
--hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f
|
||||
# via -r .github/requirements/bootstrap.in
|
||||
setuptools==84.0.0 \
|
||||
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
|
||||
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
|
||||
# via -r .github/requirements/bootstrap.in
|
||||
@@ -1,2 +0,0 @@
|
||||
build==1.6.0
|
||||
wheel==0.48.0
|
||||
@@ -1,20 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/build-tools.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/build-tools.txt
|
||||
build==1.6.0 \
|
||||
--hash=sha256:bd2c8afc603e7a2e0ce70e2ea85f0a6d02043bafbd307f5bada0f98669eca5af \
|
||||
--hash=sha256:f7aaf1ebbb79178a02ba248bb524f2176b256017e17e8e4bd4289c7b38cc2bad
|
||||
# via -r .github/requirements/build-tools.in
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via
|
||||
# build
|
||||
# wheel
|
||||
pyproject-hooks==1.2.0 \
|
||||
--hash=sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8 \
|
||||
--hash=sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913
|
||||
# via build
|
||||
wheel==0.48.0 \
|
||||
--hash=sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab \
|
||||
--hash=sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322
|
||||
# via -r .github/requirements/build-tools.in
|
||||
@@ -1 +0,0 @@
|
||||
checkov==3.3.1
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,2 +0,0 @@
|
||||
setuptools==84.0.0
|
||||
wheel==0.48.0
|
||||
@@ -1,14 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/pep517-build.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/pep517-build.txt
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via wheel
|
||||
setuptools==84.0.0 \
|
||||
--hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \
|
||||
--hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73
|
||||
# via -r .github/requirements/pep517-build.in
|
||||
wheel==0.48.0 \
|
||||
--hash=sha256:3217dcc807155e45db462d7ef2431f5ddda0d7273b700d05a67b271ceb1287ab \
|
||||
--hash=sha256:94800765601e9171bf5d58d066e640662842bcedcbab982b2c90787a2c987322
|
||||
# via -r .github/requirements/pep517-build.in
|
||||
@@ -1 +0,0 @@
|
||||
pip-audit==2.10.1
|
||||
@@ -1,423 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/pip-audit.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/pip-audit.txt
|
||||
boolean-py==5.0 \
|
||||
--hash=sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95 \
|
||||
--hash=sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9
|
||||
# via license-expression
|
||||
cachecontrol==0.14.4 \
|
||||
--hash=sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b \
|
||||
--hash=sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1
|
||||
# via pip-audit
|
||||
certifi==2026.7.22 \
|
||||
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
|
||||
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
|
||||
# via requests
|
||||
charset-normalizer==3.5.1 \
|
||||
--hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \
|
||||
--hash=sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d \
|
||||
--hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \
|
||||
--hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \
|
||||
--hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \
|
||||
--hash=sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f \
|
||||
--hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \
|
||||
--hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \
|
||||
--hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \
|
||||
--hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \
|
||||
--hash=sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838 \
|
||||
--hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \
|
||||
--hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \
|
||||
--hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \
|
||||
--hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \
|
||||
--hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \
|
||||
--hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \
|
||||
--hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \
|
||||
--hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \
|
||||
--hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \
|
||||
--hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \
|
||||
--hash=sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8 \
|
||||
--hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \
|
||||
--hash=sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf \
|
||||
--hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \
|
||||
--hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \
|
||||
--hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \
|
||||
--hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \
|
||||
--hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \
|
||||
--hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \
|
||||
--hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \
|
||||
--hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \
|
||||
--hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \
|
||||
--hash=sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb \
|
||||
--hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \
|
||||
--hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \
|
||||
--hash=sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b \
|
||||
--hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \
|
||||
--hash=sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d \
|
||||
--hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \
|
||||
--hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \
|
||||
--hash=sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818 \
|
||||
--hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \
|
||||
--hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \
|
||||
--hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \
|
||||
--hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \
|
||||
--hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \
|
||||
--hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \
|
||||
--hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \
|
||||
--hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \
|
||||
--hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \
|
||||
--hash=sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a \
|
||||
--hash=sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420 \
|
||||
--hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \
|
||||
--hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \
|
||||
--hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \
|
||||
--hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \
|
||||
--hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \
|
||||
--hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \
|
||||
--hash=sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18 \
|
||||
--hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \
|
||||
--hash=sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f \
|
||||
--hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \
|
||||
--hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \
|
||||
--hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \
|
||||
--hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \
|
||||
--hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \
|
||||
--hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \
|
||||
--hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \
|
||||
--hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \
|
||||
--hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \
|
||||
--hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \
|
||||
--hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \
|
||||
--hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \
|
||||
--hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \
|
||||
--hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \
|
||||
--hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \
|
||||
--hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \
|
||||
--hash=sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f \
|
||||
--hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \
|
||||
--hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \
|
||||
--hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \
|
||||
--hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \
|
||||
--hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \
|
||||
--hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \
|
||||
--hash=sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f \
|
||||
--hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \
|
||||
--hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \
|
||||
--hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \
|
||||
--hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \
|
||||
--hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \
|
||||
--hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \
|
||||
--hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \
|
||||
--hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \
|
||||
--hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \
|
||||
--hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \
|
||||
--hash=sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20 \
|
||||
--hash=sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f \
|
||||
--hash=sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3 \
|
||||
--hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \
|
||||
--hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \
|
||||
--hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \
|
||||
--hash=sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2 \
|
||||
--hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \
|
||||
--hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \
|
||||
--hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \
|
||||
--hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \
|
||||
--hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \
|
||||
--hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \
|
||||
--hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \
|
||||
--hash=sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa \
|
||||
--hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \
|
||||
--hash=sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5 \
|
||||
--hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \
|
||||
--hash=sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d \
|
||||
--hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \
|
||||
--hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \
|
||||
--hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \
|
||||
--hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \
|
||||
--hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \
|
||||
--hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \
|
||||
--hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \
|
||||
--hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \
|
||||
--hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \
|
||||
--hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \
|
||||
--hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \
|
||||
--hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \
|
||||
--hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \
|
||||
--hash=sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab \
|
||||
--hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \
|
||||
--hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \
|
||||
--hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \
|
||||
--hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \
|
||||
--hash=sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d \
|
||||
--hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \
|
||||
--hash=sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea \
|
||||
--hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \
|
||||
--hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \
|
||||
--hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \
|
||||
--hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \
|
||||
--hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \
|
||||
--hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \
|
||||
--hash=sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a \
|
||||
--hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \
|
||||
--hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \
|
||||
--hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \
|
||||
--hash=sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa \
|
||||
--hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \
|
||||
--hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \
|
||||
--hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \
|
||||
--hash=sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795 \
|
||||
--hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \
|
||||
--hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \
|
||||
--hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \
|
||||
--hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \
|
||||
--hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \
|
||||
--hash=sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda \
|
||||
--hash=sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17 \
|
||||
--hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \
|
||||
--hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \
|
||||
--hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \
|
||||
--hash=sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182 \
|
||||
--hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \
|
||||
--hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \
|
||||
--hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \
|
||||
--hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \
|
||||
--hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \
|
||||
--hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \
|
||||
--hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \
|
||||
--hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \
|
||||
--hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \
|
||||
--hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f
|
||||
# via requests
|
||||
cyclonedx-python-lib==11.12.0 \
|
||||
--hash=sha256:0e807521a921a5c3cb8ce1153f8a61d29eedfe76a46aac2796b7c6b573391a54 \
|
||||
--hash=sha256:16767c4039de90c04e9f03348f8f0ed4b8ff842eaa7eefcad3a95685f970dacf
|
||||
# via pip-audit
|
||||
defusedxml==0.7.1 \
|
||||
--hash=sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69 \
|
||||
--hash=sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61
|
||||
# via py-serializable
|
||||
filelock==3.32.4 \
|
||||
--hash=sha256:22e58ca3b1ae3b98993b762d7338367ae64fe50252bf78d59da3bfebcdf1cedd \
|
||||
--hash=sha256:2bde2e4cf732e0153406d8a7bc80620ecf5e621fe0d25e41143c4e3b4733ff30
|
||||
# via cachecontrol
|
||||
idna==3.19 \
|
||||
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
|
||||
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
|
||||
# via requests
|
||||
license-expression==30.4.4 \
|
||||
--hash=sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4 \
|
||||
--hash=sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd
|
||||
# via cyclonedx-python-lib
|
||||
markdown-it-py==4.2.0 \
|
||||
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
|
||||
# via rich
|
||||
mdurl==0.1.2 \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
msgpack==1.2.2 \
|
||||
--hash=sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516 \
|
||||
--hash=sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9 \
|
||||
--hash=sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c \
|
||||
--hash=sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3 \
|
||||
--hash=sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77 \
|
||||
--hash=sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4 \
|
||||
--hash=sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f \
|
||||
--hash=sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986 \
|
||||
--hash=sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f \
|
||||
--hash=sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc \
|
||||
--hash=sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc \
|
||||
--hash=sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34 \
|
||||
--hash=sha256:1f3af0baafd184436501004828bb3df64eeb2fc49dfe9d89abcf604956094563 \
|
||||
--hash=sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c \
|
||||
--hash=sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a \
|
||||
--hash=sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808 \
|
||||
--hash=sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb \
|
||||
--hash=sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b \
|
||||
--hash=sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782 \
|
||||
--hash=sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367 \
|
||||
--hash=sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e \
|
||||
--hash=sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761 \
|
||||
--hash=sha256:46ec851571d8f1b6e29794ebb9dd36f785008da6d14f57c702e60781d6caf648 \
|
||||
--hash=sha256:4710d881d8fb047deed2485707409116722af2b992d3fefd73c7667c4e350839 \
|
||||
--hash=sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35 \
|
||||
--hash=sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc \
|
||||
--hash=sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3 \
|
||||
--hash=sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab \
|
||||
--hash=sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23 \
|
||||
--hash=sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84 \
|
||||
--hash=sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7 \
|
||||
--hash=sha256:58ce37a4a54577115922385d37201d9a44d66d0167dfbbf4770a2e9bf8ea7ba3 \
|
||||
--hash=sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640 \
|
||||
--hash=sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5 \
|
||||
--hash=sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839 \
|
||||
--hash=sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b \
|
||||
--hash=sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695 \
|
||||
--hash=sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d \
|
||||
--hash=sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f \
|
||||
--hash=sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923 \
|
||||
--hash=sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8 \
|
||||
--hash=sha256:7afa5431f6f3487c584187ca6c8e2a34e9b106529893b3e720eabb068f6ac970 \
|
||||
--hash=sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3 \
|
||||
--hash=sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683 \
|
||||
--hash=sha256:82b1bdf293267afaadcc608b125e7fc6576bb0785a60c4fa7d07c7ab76ed76ec \
|
||||
--hash=sha256:86f173a584f72f6164801f31866d22a581f60c991572cf922aed9ab8eb422b77 \
|
||||
--hash=sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949 \
|
||||
--hash=sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533 \
|
||||
--hash=sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc \
|
||||
--hash=sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe \
|
||||
--hash=sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba \
|
||||
--hash=sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff \
|
||||
--hash=sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36 \
|
||||
--hash=sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5 \
|
||||
--hash=sha256:9bd3d1557c3fe1a095068210708a03e3e4795973392af6f4047060e70abd9a6c \
|
||||
--hash=sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929 \
|
||||
--hash=sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e \
|
||||
--hash=sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b \
|
||||
--hash=sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a \
|
||||
--hash=sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5 \
|
||||
--hash=sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754 \
|
||||
--hash=sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd \
|
||||
--hash=sha256:a9b4cf3685a135666d27d0d7a73fece74e2fad01d9b508fded89e843512f0e90 \
|
||||
--hash=sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea \
|
||||
--hash=sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f \
|
||||
--hash=sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212 \
|
||||
--hash=sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3 \
|
||||
--hash=sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21 \
|
||||
--hash=sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3 \
|
||||
--hash=sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54 \
|
||||
--hash=sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7 \
|
||||
--hash=sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc \
|
||||
--hash=sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b \
|
||||
--hash=sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f \
|
||||
--hash=sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b \
|
||||
--hash=sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896 \
|
||||
--hash=sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248 \
|
||||
--hash=sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9 \
|
||||
--hash=sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b \
|
||||
--hash=sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8 \
|
||||
--hash=sha256:e05a94a0442de86818a30281c6cc2cb9cc7aa148386fd3541c4d4774b73cb3a9 \
|
||||
--hash=sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5 \
|
||||
--hash=sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf \
|
||||
--hash=sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04 \
|
||||
--hash=sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba \
|
||||
--hash=sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650 \
|
||||
--hash=sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f \
|
||||
--hash=sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22
|
||||
# via cachecontrol
|
||||
packageurl-python==0.17.6 \
|
||||
--hash=sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25 \
|
||||
--hash=sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9
|
||||
# via cyclonedx-python-lib
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via
|
||||
# pip-audit
|
||||
# pip-requirements-parser
|
||||
pip==26.2.1 \
|
||||
--hash=sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e \
|
||||
--hash=sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f
|
||||
# via pip-api
|
||||
pip-api==0.0.34 \
|
||||
--hash=sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb \
|
||||
--hash=sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625
|
||||
# via pip-audit
|
||||
pip-audit==2.10.1 \
|
||||
--hash=sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc \
|
||||
--hash=sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a
|
||||
# via -r .github/requirements/pip-audit.in
|
||||
pip-requirements-parser==32.0.1 \
|
||||
--hash=sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526 \
|
||||
--hash=sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3
|
||||
# via pip-audit
|
||||
platformdirs==4.11.5 \
|
||||
--hash=sha256:89f8d42695853b89c7170bd49bc3dc593f98a71e695ede88e06a3b247bc4563b \
|
||||
--hash=sha256:e8b31f4f8bcbbedef91a6b57a706255e4f148d2a4e01648382a0a47342539173
|
||||
# via pip-audit
|
||||
py-serializable==2.1.0 \
|
||||
--hash=sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103 \
|
||||
--hash=sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304
|
||||
# via cyclonedx-python-lib
|
||||
pygments==2.21.0 \
|
||||
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
|
||||
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
|
||||
# via rich
|
||||
pyparsing==3.3.2 \
|
||||
--hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
|
||||
--hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
|
||||
# via pip-requirements-parser
|
||||
requests==2.34.2 \
|
||||
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||
# via
|
||||
# cachecontrol
|
||||
# pip-audit
|
||||
rich==15.0.0 \
|
||||
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
|
||||
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
|
||||
# via pip-audit
|
||||
sortedcontainers==2.4.0 \
|
||||
--hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
|
||||
--hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
|
||||
# via cyclonedx-python-lib
|
||||
tomli==2.4.1 \
|
||||
--hash=sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853 \
|
||||
--hash=sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe \
|
||||
--hash=sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5 \
|
||||
--hash=sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d \
|
||||
--hash=sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd \
|
||||
--hash=sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26 \
|
||||
--hash=sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54 \
|
||||
--hash=sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6 \
|
||||
--hash=sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c \
|
||||
--hash=sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a \
|
||||
--hash=sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd \
|
||||
--hash=sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f \
|
||||
--hash=sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5 \
|
||||
--hash=sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9 \
|
||||
--hash=sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662 \
|
||||
--hash=sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9 \
|
||||
--hash=sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1 \
|
||||
--hash=sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585 \
|
||||
--hash=sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e \
|
||||
--hash=sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c \
|
||||
--hash=sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41 \
|
||||
--hash=sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f \
|
||||
--hash=sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085 \
|
||||
--hash=sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15 \
|
||||
--hash=sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7 \
|
||||
--hash=sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c \
|
||||
--hash=sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36 \
|
||||
--hash=sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076 \
|
||||
--hash=sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac \
|
||||
--hash=sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8 \
|
||||
--hash=sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232 \
|
||||
--hash=sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece \
|
||||
--hash=sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a \
|
||||
--hash=sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897 \
|
||||
--hash=sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d \
|
||||
--hash=sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4 \
|
||||
--hash=sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917 \
|
||||
--hash=sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396 \
|
||||
--hash=sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a \
|
||||
--hash=sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc \
|
||||
--hash=sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba \
|
||||
--hash=sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f \
|
||||
--hash=sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257 \
|
||||
--hash=sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30 \
|
||||
--hash=sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf \
|
||||
--hash=sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9 \
|
||||
--hash=sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049
|
||||
# via pip-audit
|
||||
tomli-w==1.2.0 \
|
||||
--hash=sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90 \
|
||||
--hash=sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021
|
||||
# via pip-audit
|
||||
typing-extensions==4.16.0 \
|
||||
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
|
||||
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
|
||||
# via cyclonedx-python-lib
|
||||
urllib3==2.7.0 \
|
||||
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via requests
|
||||
@@ -1 +0,0 @@
|
||||
pytest==9.1.1
|
||||
@@ -1,32 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/pytest-tool.in --generate-hashes --python-version 3.11 --python-platform linux --constraint requirements-ci.txt -o .github/requirements/pytest-tool.txt
|
||||
iniconfig==2.3.0 \
|
||||
--hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \
|
||||
--hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
pluggy==1.6.0 \
|
||||
--hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \
|
||||
--hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
pygments==2.20.0 \
|
||||
--hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
|
||||
--hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# pytest
|
||||
pytest==9.1.1 \
|
||||
--hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \
|
||||
--hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c
|
||||
# via
|
||||
# -c requirements-ci.txt
|
||||
# -r .github/requirements/pytest-tool.in
|
||||
@@ -1,4 +0,0 @@
|
||||
safety==3.8.1
|
||||
bandit==1.9.4
|
||||
semgrep==1.175.0
|
||||
jq==1.12.0
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
||||
twine==7.0.0
|
||||
@@ -1,470 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/twine.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/twine.txt
|
||||
backports-tarfile==1.2.0 \
|
||||
--hash=sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34 \
|
||||
--hash=sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991
|
||||
# via jaraco-context
|
||||
certifi==2026.7.22 \
|
||||
--hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
|
||||
--hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
|
||||
# via requests
|
||||
cffi==2.1.1 \
|
||||
--hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \
|
||||
--hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \
|
||||
--hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \
|
||||
--hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \
|
||||
--hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \
|
||||
--hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \
|
||||
--hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \
|
||||
--hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \
|
||||
--hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \
|
||||
--hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \
|
||||
--hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \
|
||||
--hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \
|
||||
--hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \
|
||||
--hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \
|
||||
--hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \
|
||||
--hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \
|
||||
--hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \
|
||||
--hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \
|
||||
--hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \
|
||||
--hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \
|
||||
--hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \
|
||||
--hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \
|
||||
--hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \
|
||||
--hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \
|
||||
--hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \
|
||||
--hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \
|
||||
--hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \
|
||||
--hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \
|
||||
--hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \
|
||||
--hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \
|
||||
--hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \
|
||||
--hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \
|
||||
--hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \
|
||||
--hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \
|
||||
--hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \
|
||||
--hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \
|
||||
--hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \
|
||||
--hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \
|
||||
--hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \
|
||||
--hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \
|
||||
--hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \
|
||||
--hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \
|
||||
--hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \
|
||||
--hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \
|
||||
--hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \
|
||||
--hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \
|
||||
--hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \
|
||||
--hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \
|
||||
--hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \
|
||||
--hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \
|
||||
--hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \
|
||||
--hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \
|
||||
--hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \
|
||||
--hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \
|
||||
--hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \
|
||||
--hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \
|
||||
--hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \
|
||||
--hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \
|
||||
--hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \
|
||||
--hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \
|
||||
--hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \
|
||||
--hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \
|
||||
--hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \
|
||||
--hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \
|
||||
--hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \
|
||||
--hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \
|
||||
--hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \
|
||||
--hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \
|
||||
--hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \
|
||||
--hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \
|
||||
--hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \
|
||||
--hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \
|
||||
--hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \
|
||||
--hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \
|
||||
--hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \
|
||||
--hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \
|
||||
--hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \
|
||||
--hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \
|
||||
--hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \
|
||||
--hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \
|
||||
--hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \
|
||||
--hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \
|
||||
--hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \
|
||||
--hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \
|
||||
--hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \
|
||||
--hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \
|
||||
--hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \
|
||||
--hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \
|
||||
--hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \
|
||||
--hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \
|
||||
--hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \
|
||||
--hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \
|
||||
--hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \
|
||||
--hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \
|
||||
--hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \
|
||||
--hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \
|
||||
--hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \
|
||||
--hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \
|
||||
--hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \
|
||||
--hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264
|
||||
# via cryptography
|
||||
charset-normalizer==3.5.1 \
|
||||
--hash=sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45 \
|
||||
--hash=sha256:012a22b88a77ca2e59b98ac5889b0deb604147666032f45e6d6e217634d2550d \
|
||||
--hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \
|
||||
--hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \
|
||||
--hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \
|
||||
--hash=sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f \
|
||||
--hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \
|
||||
--hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \
|
||||
--hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \
|
||||
--hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \
|
||||
--hash=sha256:13e3afe97712e8887cd516e960c63f0b93122971e5b5e4b2622fe7701771e838 \
|
||||
--hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \
|
||||
--hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \
|
||||
--hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \
|
||||
--hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \
|
||||
--hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \
|
||||
--hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \
|
||||
--hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \
|
||||
--hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \
|
||||
--hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \
|
||||
--hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \
|
||||
--hash=sha256:29880d17a8eb0b5cfdfd8944b468322928059aa35f1f5fa8ff22b149ec0b42f8 \
|
||||
--hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \
|
||||
--hash=sha256:2e9cf9253119d8e5d111f05d71626786fd3d6193817316eab1ca088cdb8593cf \
|
||||
--hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \
|
||||
--hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \
|
||||
--hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \
|
||||
--hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \
|
||||
--hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \
|
||||
--hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \
|
||||
--hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \
|
||||
--hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \
|
||||
--hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \
|
||||
--hash=sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb \
|
||||
--hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \
|
||||
--hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \
|
||||
--hash=sha256:3e5e1224c0a6a90e05843e07adfec669edebec17801c67072f51e59561d63c0b \
|
||||
--hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \
|
||||
--hash=sha256:433c5a81eade63b47e522303bad236f59dba55ea6951746f5558355eeed8c75d \
|
||||
--hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \
|
||||
--hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \
|
||||
--hash=sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818 \
|
||||
--hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \
|
||||
--hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \
|
||||
--hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \
|
||||
--hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \
|
||||
--hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \
|
||||
--hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \
|
||||
--hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \
|
||||
--hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \
|
||||
--hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \
|
||||
--hash=sha256:55261ac0d2941c42f196dd576f543d87a8ee03cd6f5e30dfb4d807b2e3b9121a \
|
||||
--hash=sha256:56490c595a28b1bb27dfc583e816152a9767721ef58b2c03b13f954d2f707420 \
|
||||
--hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \
|
||||
--hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \
|
||||
--hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \
|
||||
--hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \
|
||||
--hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \
|
||||
--hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \
|
||||
--hash=sha256:5ca0555312ae2fe82715cada7fac375530c2f3349e1eaa1bcb33d0283ac79a18 \
|
||||
--hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \
|
||||
--hash=sha256:5e2d0e146dcb57034f8b97dc58d2d512cb90aba253960ce449f695fec6a82c6f \
|
||||
--hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \
|
||||
--hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \
|
||||
--hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \
|
||||
--hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \
|
||||
--hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \
|
||||
--hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \
|
||||
--hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \
|
||||
--hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \
|
||||
--hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \
|
||||
--hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \
|
||||
--hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \
|
||||
--hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \
|
||||
--hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \
|
||||
--hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \
|
||||
--hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \
|
||||
--hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \
|
||||
--hash=sha256:789b8982559ae28dad2356519f841655756cdcd96616410590ae0b17454ee64f \
|
||||
--hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \
|
||||
--hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \
|
||||
--hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \
|
||||
--hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \
|
||||
--hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \
|
||||
--hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \
|
||||
--hash=sha256:85de3134b5379856e323ba37c19c9256d39425f7b76a63af52b09fb4664c2e8f \
|
||||
--hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \
|
||||
--hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \
|
||||
--hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \
|
||||
--hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \
|
||||
--hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \
|
||||
--hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \
|
||||
--hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \
|
||||
--hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \
|
||||
--hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \
|
||||
--hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \
|
||||
--hash=sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20 \
|
||||
--hash=sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f \
|
||||
--hash=sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3 \
|
||||
--hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \
|
||||
--hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \
|
||||
--hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \
|
||||
--hash=sha256:994e883d17c559cdfd38c84003c8b27d25424a1077272a17e7cd27bfe0bf57b2 \
|
||||
--hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \
|
||||
--hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \
|
||||
--hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \
|
||||
--hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \
|
||||
--hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \
|
||||
--hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \
|
||||
--hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \
|
||||
--hash=sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa \
|
||||
--hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \
|
||||
--hash=sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5 \
|
||||
--hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \
|
||||
--hash=sha256:a951ad59cad9145664a730d3036b40b844e74d2d3683da40111463cd3a83845d \
|
||||
--hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \
|
||||
--hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \
|
||||
--hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \
|
||||
--hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \
|
||||
--hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \
|
||||
--hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \
|
||||
--hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \
|
||||
--hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \
|
||||
--hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \
|
||||
--hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \
|
||||
--hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \
|
||||
--hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \
|
||||
--hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \
|
||||
--hash=sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab \
|
||||
--hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \
|
||||
--hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \
|
||||
--hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \
|
||||
--hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \
|
||||
--hash=sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d \
|
||||
--hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \
|
||||
--hash=sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea \
|
||||
--hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \
|
||||
--hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \
|
||||
--hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \
|
||||
--hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \
|
||||
--hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \
|
||||
--hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \
|
||||
--hash=sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a \
|
||||
--hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \
|
||||
--hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \
|
||||
--hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \
|
||||
--hash=sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa \
|
||||
--hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \
|
||||
--hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \
|
||||
--hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \
|
||||
--hash=sha256:e06efa066f7dbadbc84ebc126a97c452a6451dfcf589d89d788484949e1cf795 \
|
||||
--hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \
|
||||
--hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \
|
||||
--hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \
|
||||
--hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \
|
||||
--hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \
|
||||
--hash=sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda \
|
||||
--hash=sha256:eb12fb2ba69ffa05f8695f61c69e591dc4b4a12ac3757ac8af8adb259bf56d17 \
|
||||
--hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \
|
||||
--hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \
|
||||
--hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \
|
||||
--hash=sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182 \
|
||||
--hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \
|
||||
--hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \
|
||||
--hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \
|
||||
--hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \
|
||||
--hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \
|
||||
--hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \
|
||||
--hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \
|
||||
--hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \
|
||||
--hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \
|
||||
--hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f
|
||||
# via requests
|
||||
cryptography==50.0.1 \
|
||||
--hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \
|
||||
--hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \
|
||||
--hash=sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6 \
|
||||
--hash=sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e \
|
||||
--hash=sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361 \
|
||||
--hash=sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054 \
|
||||
--hash=sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f \
|
||||
--hash=sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6 \
|
||||
--hash=sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49 \
|
||||
--hash=sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5 \
|
||||
--hash=sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149 \
|
||||
--hash=sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88 \
|
||||
--hash=sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad \
|
||||
--hash=sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a \
|
||||
--hash=sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f \
|
||||
--hash=sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2 \
|
||||
--hash=sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20 \
|
||||
--hash=sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45 \
|
||||
--hash=sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f \
|
||||
--hash=sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b \
|
||||
--hash=sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527 \
|
||||
--hash=sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3 \
|
||||
--hash=sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6 \
|
||||
--hash=sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367 \
|
||||
--hash=sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0 \
|
||||
--hash=sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94 \
|
||||
--hash=sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239 \
|
||||
--hash=sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b \
|
||||
--hash=sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a \
|
||||
--hash=sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9 \
|
||||
--hash=sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5 \
|
||||
--hash=sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc \
|
||||
--hash=sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648 \
|
||||
--hash=sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986 \
|
||||
--hash=sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959 \
|
||||
--hash=sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0 \
|
||||
--hash=sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17 \
|
||||
--hash=sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e \
|
||||
--hash=sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733 \
|
||||
--hash=sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f \
|
||||
--hash=sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8 \
|
||||
--hash=sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf \
|
||||
--hash=sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671 \
|
||||
--hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \
|
||||
--hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \
|
||||
--hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef
|
||||
# via secretstorage
|
||||
docutils==0.23 \
|
||||
--hash=sha256:25d013af9bf23bc1c7b2b093dff4208166c53a94786c9e447808335ef1185fea \
|
||||
--hash=sha256:746f5060322511280a1e50eb76846ed6bf2342984b2ac04dc42caa1a8d78799e
|
||||
# via readme-renderer
|
||||
id==1.6.1 \
|
||||
--hash=sha256:d0732d624fb46fd4e7bc4e5152f00214450953b9e772c182c1c22964def1a069 \
|
||||
--hash=sha256:f5ec41ed2629a508f5d0988eda142e190c9c6da971100612c4de9ad9f9b237ca
|
||||
# via twine
|
||||
idna==3.19 \
|
||||
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
|
||||
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
|
||||
# via requests
|
||||
importlib-metadata==9.0.1 \
|
||||
--hash=sha256:ab830580bc0ef3db61ce8fae716389e5462b67e033018bab6d8f80ef17172f99 \
|
||||
--hash=sha256:bba5600596a7e21f3eef53281cf28d6a5195634d2f2b78ff9501a3272c6eaab0
|
||||
# via keyring
|
||||
jaraco-classes==3.4.0 \
|
||||
--hash=sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd \
|
||||
--hash=sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790
|
||||
# via keyring
|
||||
jaraco-context==6.1.2 \
|
||||
--hash=sha256:bf8150b79a2d5d91ae48629d8b427a8f7ba0e1097dd6202a9059f29a36379535 \
|
||||
--hash=sha256:f1a6c9d391e661cc5b8d39861ff077a7dc24dc23833ccee564b234b81c82dfe3
|
||||
# via keyring
|
||||
jaraco-functools==4.6.0 \
|
||||
--hash=sha256:880c577ec9720b3a052d5bc611fb9f2269b3d87902ef42440df443b88e443280 \
|
||||
--hash=sha256:99e3dc0060c5cbe8fcd1cdb36258e2a65ca40f1566b2033b12abb1bb44dd3c30
|
||||
# via keyring
|
||||
jeepney==0.9.0 \
|
||||
--hash=sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683 \
|
||||
--hash=sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732
|
||||
# via
|
||||
# keyring
|
||||
# secretstorage
|
||||
keyring==25.7.0 \
|
||||
--hash=sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f \
|
||||
--hash=sha256:fe01bd85eb3f8fb3dd0405defdeac9a5b4f6f0439edbb3149577f244a2e8245b
|
||||
# via twine
|
||||
markdown-it-py==4.2.0 \
|
||||
--hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \
|
||||
--hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a
|
||||
# via rich
|
||||
mdurl==0.1.2 \
|
||||
--hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \
|
||||
--hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba
|
||||
# via markdown-it-py
|
||||
more-itertools==11.1.0 \
|
||||
--hash=sha256:48e8f4d9e7e5878571ecf6f2b4e57634f93cd474cc8cfbd2376f2d11b396e30d \
|
||||
--hash=sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192
|
||||
# via
|
||||
# jaraco-classes
|
||||
# jaraco-functools
|
||||
nh3==0.3.7 \
|
||||
--hash=sha256:157ec1eb7a62f3d9a7badb8d82d89aa810e3e24e097eedfa481a25d0c8a99877 \
|
||||
--hash=sha256:15f5fbf090f5c88d61c820e1fc1fceecb6520cca9fe85649c06b57ef9dc9ff62 \
|
||||
--hash=sha256:18f4278ecd157d43cb35acd5aae9f35cfa79f546b4922bd86536adc0f6312102 \
|
||||
--hash=sha256:19f288c938ec6eef1f5d2c6cab47838e71fef8097e1c1233802be5a6230ba086 \
|
||||
--hash=sha256:4968fe8d2db97c6f047659bf46a449fd8ec377f44ebf3e0a1b96c0d3a333ae32 \
|
||||
--hash=sha256:5ffdfcb9a686ffb12765376bcfb6b5b55728516d3c0ee317d29982381ded3df8 \
|
||||
--hash=sha256:614dac4a4c36ad084e78447d16fe898dedd762e354a7ab9cda2984e82f67883d \
|
||||
--hash=sha256:618e3059caf41ccdf5dcccb3fa9df4cf6e4efe23d1382a8bbfca272a8a4f8bfc \
|
||||
--hash=sha256:6698a822132beedab80f131c08d8d0ac5a178ddeb488d02ca4b67716ecfac7af \
|
||||
--hash=sha256:6c3aa50eb26e9228238271db9f983cbc3b006dfbfeca2d4dc34c33ddc6ac5ea5 \
|
||||
--hash=sha256:6e4280115d44c3b278eef712a86748c1a723105cd79feec46952383117ab4e59 \
|
||||
--hash=sha256:70f5ac8626e899a4bab0ef74ca2f5bd602f49c7b739e6e5026b4afc6d63dac42 \
|
||||
--hash=sha256:71860d01c16f4d8c72e334e0674beb2b0899dbd0bf760de18932ef4390303848 \
|
||||
--hash=sha256:808def0c8c07843e6e50dc84f532457bfa2cfd17417b219a5d9e7c773709331a \
|
||||
--hash=sha256:874b7d67a067bd29a59223f6270fc30da4edd8e6d87fd219fc93bcbaa662c946 \
|
||||
--hash=sha256:91a4dab4e94d9fc54b9f67b1adfb23e81fab7ab43f33c3b8c97be9aa38f789ba \
|
||||
--hash=sha256:94fd6e59553fbb9ffd8ba71bbd5a54e3126ba01799a097ae30d5341d750bc6ac \
|
||||
--hash=sha256:9b7279d43323a25225df23576af6594a16693f61431170848b8b2ac21ad4f174 \
|
||||
--hash=sha256:bc42bb1193c1e28a1e74c2cabaca178e118a7103e8832699fef8a2b3e2496493 \
|
||||
--hash=sha256:be53a4825585f701955cb9baf49f478f56eb81e20294329fe4bc689dd5dd81fa \
|
||||
--hash=sha256:d56e76bd3cadb09b6b0cef364850811663734b348a25f5f587a2819c495367bd \
|
||||
--hash=sha256:de2b2aab32ea303405debefdcfc58043d3e635fa3f67b9eb140d2b0e0c0d2563 \
|
||||
--hash=sha256:e8fd1ab205258b29254f72db377d99e2c96aa7653ef3b015ccab0420b094b506 \
|
||||
--hash=sha256:eae64328e46a25785535afcb6885b6f182ecaf5ee8c88f8c075422db8aacc65b \
|
||||
--hash=sha256:f04b7d333b27f13ca439da3cf1c75c2fba34f104969f6ce4ac8e7079699c2f4a \
|
||||
--hash=sha256:f266d3f1b3647449923a8e406524632220dd5d8b647078dfe45b885d33d10479 \
|
||||
--hash=sha256:fd4a70efb45d5372174f718878eb7a35c12677626a63b2f103b23b833457dcac
|
||||
# via readme-renderer
|
||||
packaging==26.3 \
|
||||
--hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \
|
||||
--hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
|
||||
# via twine
|
||||
pycparser==3.0 \
|
||||
--hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \
|
||||
--hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992
|
||||
# via cffi
|
||||
pygments==2.21.0 \
|
||||
--hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \
|
||||
--hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c
|
||||
# via
|
||||
# readme-renderer
|
||||
# rich
|
||||
readme-renderer==46.0 \
|
||||
--hash=sha256:af3e964914f6310a33ff67b72a4bdd940bed8d7c3bdecd2d14f40edf284bfe90 \
|
||||
--hash=sha256:d0dae1f74bb273b534770cb4cccb6bb78735540afdb03c2146f4e19dcd412560
|
||||
# via twine
|
||||
requests==2.34.2 \
|
||||
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||
# via
|
||||
# requests-toolbelt
|
||||
# twine
|
||||
requests-toolbelt==1.0.0 \
|
||||
--hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \
|
||||
--hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06
|
||||
# via twine
|
||||
rfc3986==2.0.0 \
|
||||
--hash=sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd \
|
||||
--hash=sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c
|
||||
# via twine
|
||||
rich==15.0.0 \
|
||||
--hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \
|
||||
--hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36
|
||||
# via twine
|
||||
secretstorage==3.5.0 \
|
||||
--hash=sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137 \
|
||||
--hash=sha256:f04b8e4689cbce351744d5537bf6b1329c6fc68f91fa666f60a380edddcd11be
|
||||
# via keyring
|
||||
twine==7.0.0 \
|
||||
--hash=sha256:85cdb29c518efef867360ae4acd4b0dfd61c8654a22fca08e6f8539f05022177 \
|
||||
--hash=sha256:b854164df26db268af05f49aa5c0344b10e27a494343ff05b1e0bad3b135f5a7
|
||||
# via -r .github/requirements/twine.in
|
||||
urllib3==2.7.0 \
|
||||
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via
|
||||
# id
|
||||
# requests
|
||||
# twine
|
||||
zipp==4.1.0 \
|
||||
--hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \
|
||||
--hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602
|
||||
# via importlib-metadata
|
||||
@@ -1 +0,0 @@
|
||||
uv==0.12.1
|
||||
@@ -1,23 +0,0 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile .github/requirements/uv-tool.in --generate-hashes --python-version 3.11 --python-platform linux -o .github/requirements/uv-tool.txt
|
||||
uv==0.12.1 \
|
||||
--hash=sha256:04290ea4001dca31ac8a8324113a4930dccad69ce35dbf6eaae307d54880890d \
|
||||
--hash=sha256:153ec0959a15397514438aefc1d7cd04235f335dd6bb53ea0f9e6e82c5a49f03 \
|
||||
--hash=sha256:173ee216f17d89fc39f65339d311a53584fc7de4918d27c0f3c7edafabc6b54d \
|
||||
--hash=sha256:1de49d9b04438f1ad2f41a1441dbbe19e230b94fca56d632818cfaed69e03bfc \
|
||||
--hash=sha256:1e8fd95fe98768e29436ad57f9ef7b68dc294b7b9862ef63396af8b15ab85e6c \
|
||||
--hash=sha256:27211df9b277f440dea438a4e525ba40250fb721ad39b8927eefc2d91f9aea15 \
|
||||
--hash=sha256:29399e1e73b67ed24abe82bc971aa4eb8419c4de804784290f39cf681f0b51ce \
|
||||
--hash=sha256:2e9b0b86e180abc5968b979c6e25203b32e85969abb5083ee1e8b88a5aa98a76 \
|
||||
--hash=sha256:3bd5db002adc763aa8d277f5b44f8d6e3fd82d20f2e51225b0bbdae1badc7259 \
|
||||
--hash=sha256:41b8fc2335f682312a1ca39a7b4abfd6af800992065c663582ca3e4d51cf9258 \
|
||||
--hash=sha256:5bd04849dd5346517cc4e57b4b3aa0b01c67c423878260c04f5893a038fe25b6 \
|
||||
--hash=sha256:6f7e72543264d2420ebb2ddc84696a751af2d6c5910046b7666589118f47292b \
|
||||
--hash=sha256:71f86410264c69a3e8acd18171897dd8ab1a13350cf40f718e4def5db2b724be \
|
||||
--hash=sha256:76d87de420213ca92fa403e87023c4c7c6956c6726c6b96d91c42cfe620173a3 \
|
||||
--hash=sha256:9331dda0dc4990512c232f86e1d3a7b83c13f459777fcc2bd46030911b40eaaa \
|
||||
--hash=sha256:b255ac23958e45f39f9c7a4cd65890df5ef46f539a3b14de03bd296bbba9cb60 \
|
||||
--hash=sha256:bd02f2da212e6a983115dc64a6fc94e9256c2d60e056d6b669de0a6025aaec05 \
|
||||
--hash=sha256:e35e0030480a8c3bf8ecd87ae4a6f6a224009e15e96a6fbb3634ac11ab75d582 \
|
||||
--hash=sha256:ead7ad064f291a5df358c3ffa8ffab347a32bd5a75a6a068ca22254c2539a829
|
||||
# via -r .github/requirements/uv-tool.in
|
||||
@@ -28,29 +28,11 @@ jobs:
|
||||
|
||||
BENCHMARK_REAL_LIBS: "1"
|
||||
run: |
|
||||
pip install -r .github/requirements/bootstrap.txt --require-hashes
|
||||
# --no-deps + a hash-pinned install of the same base dependency set
|
||||
# (rather than a bare `pip install -e .`) so every fetched package
|
||||
# is hash-verified (Scorecard Pinned-Dependencies); the local
|
||||
# editable install itself has nothing to hash.
|
||||
#
|
||||
# --no-deps only skips *runtime* dependency resolution - `-e .`
|
||||
# still does a PEP 517 build, which by default creates an isolated
|
||||
# build env and fetches [build-system] requires (setuptools,
|
||||
# wheel) completely outside any hash checking. Install
|
||||
# pep517-build.txt (pins that exact build-system.requires) first
|
||||
# and pass --no-build-isolation so pip reuses those hash-verified
|
||||
# copies instead of fetching its own.
|
||||
pip install -r .github/requirements/pep517-build.txt --require-hashes
|
||||
pip install --no-deps --no-build-isolation -e .
|
||||
pip install -r .github/requirements/base-deps.txt --require-hashes
|
||||
# NOTE: benchmarks/ does not currently exist in this repo, so this
|
||||
# step and the run below it fail on any real invocation - pre-existing,
|
||||
# unrelated to this pinning change. Left as-is since there's nothing
|
||||
# to hash without knowing what belongs there.
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .
|
||||
pip install -r benchmarks/requirements.txt
|
||||
python -m spacy download en_core_web_sm
|
||||
pip install -r .github/requirements/benchmark-extra.txt --require-hashes
|
||||
pip install rdflib neo4j faiss-cpu torch pyarrow pdfplumber python-pptx openpyxl lxml python-docx beautifulsoup4 chardet langdetect
|
||||
|
||||
- name: Execute Benchmarks (Real Mode)
|
||||
env:
|
||||
|
||||
@@ -52,39 +52,16 @@ jobs:
|
||||
# environment is installed. The Explorer extra supplies the
|
||||
# production API dependencies without importing optional vector
|
||||
# providers such as Pinecone during test collection.
|
||||
#
|
||||
# --no-deps + a separate hash-pinned install (rather than the old
|
||||
# `pip install -e ".[explorer]" pytest==9.1.1`) so every fetched
|
||||
# package is hash-verified (Scorecard Pinned-Dependencies); the
|
||||
# local editable install itself has nothing to hash.
|
||||
# .github/requirements/explorer-extra-py311.txt is
|
||||
# `uv pip compile pyproject.toml --extra explorer --python-version 3.11 --constraint requirements-ci.txt --generate-hashes`
|
||||
# - regenerate it the same way if pyproject.toml's base/explorer
|
||||
# deps change. Resolved specifically for this job's python 3.11
|
||||
# (see the Dockerfile's explorer-extra-py313.txt for why this
|
||||
# can't be shared with python 3.13: audioread needs extra
|
||||
# standard-aifc/standard-sunau hashes only on 3.13+).
|
||||
#
|
||||
# --no-deps only skips *runtime* dependency resolution - `-e .`
|
||||
# still does a PEP 517 build, which by default creates an isolated
|
||||
# build env and fetches [build-system] requires (setuptools,
|
||||
# wheel) completely outside any hash checking. Install
|
||||
# pep517-build.txt (pins that exact build-system.requires) first
|
||||
# and pass --no-build-isolation so pip reuses those hash-verified
|
||||
# copies instead of fetching its own.
|
||||
pip install -r .github/requirements/pep517-build.txt --require-hashes
|
||||
pip install --no-deps --no-build-isolation -e .
|
||||
pip install -r .github/requirements/explorer-extra-py311.txt --require-hashes
|
||||
pip install -r .github/requirements/pytest-tool.txt --require-hashes
|
||||
pip install -e ".[explorer]" pytest==9.1.1
|
||||
- name: Test deterministic Explorer backend path
|
||||
run: |
|
||||
pytest -q tests/explorer/test_explorer_deterministic_rendering_e2e.py
|
||||
- name: Install pinned Python dependencies
|
||||
run: |
|
||||
pip install -r requirements-ci.txt --require-hashes
|
||||
pip install -r requirements-ci.txt
|
||||
- name: Verify requirements-ci.txt is up to date
|
||||
run: |
|
||||
pip install -r .github/requirements/uv-tool.txt --require-hashes
|
||||
pip install uv==0.12.1
|
||||
# Re-resolve with the committed file as a constraint: upstream package
|
||||
# releases must NOT fail CI (deps only change when pyproject.toml
|
||||
# changes intentionally). Compare only version lines (pkg==ver),
|
||||
@@ -95,10 +72,10 @@ jobs:
|
||||
diff \
|
||||
<(grep -E '^[a-zA-Z0-9._-]+==' requirements-ci.txt | sed 's/ \\$//') \
|
||||
<(grep -E '^[a-zA-Z0-9._-]+==' /tmp/requirements-ci-check.txt)
|
||||
# build is a dev-time dependency; wheel is build-time only (neither is
|
||||
# in requirements-ci.txt) — install the same pinned versions
|
||||
# [build-system] declares so --no-isolation works below.
|
||||
- run: pip install -r .github/requirements/build-tools.txt --require-hashes
|
||||
- run: pip install build
|
||||
# wheel is build-time only (not in requirements-ci.txt) — install the
|
||||
# same pinned version [build-system] declares so --no-isolation works.
|
||||
- run: pip install wheel==0.48.0
|
||||
- name: Build package (no isolation — pinned deps)
|
||||
run: python -m build --no-isolation
|
||||
- name: Verify Explorer frontend is packaged
|
||||
|
||||
@@ -10,15 +10,13 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze Python
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # for github/codeql-action/upload-sarif below
|
||||
actions: read # for github/codeql-action/init's CodeQL bundle cache lookup
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -34,7 +32,7 @@ jobs:
|
||||
# meaningful state carried over from a failed attempt.
|
||||
- name: Initialize CodeQL (attempt 1)
|
||||
id: codeql-init-1
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
languages: python
|
||||
@@ -44,7 +42,7 @@ jobs:
|
||||
- name: Initialize CodeQL (attempt 2)
|
||||
id: codeql-init-2
|
||||
if: steps.codeql-init-1.outcome == 'failure'
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
continue-on-error: true
|
||||
with:
|
||||
languages: python
|
||||
@@ -54,17 +52,17 @@ jobs:
|
||||
- name: Initialize CodeQL (attempt 3)
|
||||
id: codeql-init-3
|
||||
if: steps.codeql-init-2.outcome == 'failure'
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
with:
|
||||
languages: python
|
||||
queries: security-and-quality
|
||||
config-file: .github/codeql/codeql-config.yml
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/autobuild@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
with:
|
||||
category: "/language:python"
|
||||
upload: false
|
||||
@@ -74,7 +72,7 @@ jobs:
|
||||
# Uploads results only when Default Setup is not active.
|
||||
# If Default Setup is still enabled, this step skips gracefully
|
||||
# instead of failing the workflow with HTTP 409.
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
with:
|
||||
sarif_file: ${{ steps.codeql.outputs.sarif-output }}
|
||||
category: "/language:python"
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
name: Container Security Scan
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
# Mirrors .dockerignore's opt-in list exactly - anything not listed there
|
||||
# can't reach the build context, so it can't change the built image.
|
||||
paths:
|
||||
- 'Dockerfile'
|
||||
- '.dockerignore'
|
||||
- 'pyproject.toml'
|
||||
- 'README.md'
|
||||
- 'LICENSE'
|
||||
- 'MANIFEST.in'
|
||||
- '.github/requirements/explorer-extra-py313.txt'
|
||||
- '.github/requirements/pep517-build.txt'
|
||||
- 'semantica/**'
|
||||
- 'integrations/**'
|
||||
- 'explorer/**'
|
||||
- '.github/workflows/container-scan.yml'
|
||||
schedule:
|
||||
- cron: '30 2 * * 1' # weekly, catches new CVEs published against the base image between pushes
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # for github/codeql-action/upload-sarif below
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t semantica:scan .
|
||||
|
||||
# Run Trivy as a digest-pinned image rather than the aquasecurity/trivy-action
|
||||
# marketplace wrapper: the aquasecurity GitHub org has an IP allow list on its
|
||||
# API that 403s verify-action-pins.sh's live tag->SHA check from Actions-runner
|
||||
# IPs, and this repo already treats Trivy's action pin as a known past target
|
||||
# for tag-repointing (see the LiteLLM/Trivy 2026 incident note above). Pulling
|
||||
# by sha256 digest from Docker Hub is immutable and verifiable independently of
|
||||
# GitHub's API, so it sidesteps both problems at once instead of carving a skip
|
||||
# exception into the pin verifier for an org already flagged as higher-risk.
|
||||
#
|
||||
# Report-only for now: this is Trivy's first run against this image, so we
|
||||
# don't yet know the CRITICAL/HIGH baseline. Findings still land in the
|
||||
# Security tab either way. Once triaged, add `--exit-code 1` (like
|
||||
# Safety/Bandit-HIGH in security-scan.yml) to make it a hard gate.
|
||||
- name: Scan image for vulnerabilities (Trivy)
|
||||
run: |
|
||||
docker run --rm \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v "$PWD:/output" \
|
||||
aquasec/trivy@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969 \
|
||||
image --format sarif --output /output/trivy-results.sarif \
|
||||
--severity CRITICAL,HIGH --ignore-unfixed semantica:scan
|
||||
|
||||
- name: Upload Trivy SARIF
|
||||
if: always()
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-container
|
||||
|
||||
- name: Generate SBOM (Syft)
|
||||
if: always()
|
||||
uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 # v0.24.2
|
||||
with:
|
||||
image: semantica:scan
|
||||
format: spdx-json
|
||||
output-file: semantica-sbom.spdx.json
|
||||
@@ -28,14 +28,12 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
MSDO:
|
||||
# currently only windows-latest is supported
|
||||
runs-on: windows-latest
|
||||
permissions:
|
||||
contents: read
|
||||
security-events: write # for github/codeql-action/upload-sarif below
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
@@ -59,7 +57,7 @@ jobs:
|
||||
# avoiding the guardian.cmd/checkov exit-code bug in the MSDO wrapper.
|
||||
tools: eslint,templateanalyzer,terrascan
|
||||
- name: Upload results to Security tab
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
with:
|
||||
sarif_file: ${{ steps.msdo.outputs.sarifFile }}
|
||||
|
||||
@@ -68,7 +66,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install Checkov
|
||||
run: pip install -r .github/requirements/checkov.txt --require-hashes
|
||||
run: python -m pip install checkov==3.3.1
|
||||
|
||||
- name: Run Checkov
|
||||
shell: pwsh
|
||||
@@ -84,7 +82,7 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Upload Checkov results to Security tab
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
uses: github/codeql-action/upload-sarif@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4
|
||||
if: always()
|
||||
with:
|
||||
sarif_file: reports/checkov.sarif
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
name: Install Matrix
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1' # weekly, catches upstream dependency breakage between releases
|
||||
workflow_run:
|
||||
# The Release workflow publishes the GitHub release *before* it uploads to
|
||||
# PyPI (see release.yml), so triggering on `release: published` would race
|
||||
# the PyPI upload and could pass by silently installing the prior version.
|
||||
# workflow_run fires only after the whole Release workflow - including the
|
||||
# PyPI publish step - has finished.
|
||||
workflows: ['Release']
|
||||
types: [completed]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
verify-install:
|
||||
if: github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success'
|
||||
name: pip install semantica (${{ matrix.os }}, py${{ matrix.python-version }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
python-version: ['3.9', '3.10', '3.11', '3.12']
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- name: Pin expected version for release-triggered runs
|
||||
id: expected-version
|
||||
if: github.event_name == 'workflow_run'
|
||||
shell: bash
|
||||
env:
|
||||
EXPECTED_TAG: ${{ github.event.workflow_run.head_branch }}
|
||||
run: |
|
||||
expected="${EXPECTED_TAG#v}"
|
||||
if [ -z "$expected" ]; then
|
||||
echo "::error::Could not determine a release tag from the triggering workflow run (head_branch was empty)."
|
||||
exit 1
|
||||
fi
|
||||
echo "constraint===$expected" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- id: setup-semantica
|
||||
uses: ./.github/actions/setup-semantica
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip'
|
||||
version: ${{ steps.expected-version.outputs.constraint }}
|
||||
|
||||
- name: Smoke test import
|
||||
shell: bash
|
||||
run: |
|
||||
python -c "
|
||||
import semantica
|
||||
print('semantica', semantica.__version__, 'installed and importable')
|
||||
"
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write # for the GitHub Release
|
||||
id-token: write # for PyPI Trusted Publishing (OIDC), attestation signing, and Sigstore
|
||||
id-token: write # for PyPI Trusted Publishing (OIDC) and attestation signing
|
||||
attestations: write # for SLSA build provenance
|
||||
# If you add another job to this workflow, give it its own explicit
|
||||
# `permissions:` block rather than relying on the workflow-level default
|
||||
@@ -39,11 +39,11 @@ jobs:
|
||||
# Install the pinned dependency set (with hashes) so the sdist/wheel
|
||||
# build runs against the same versions CI tests against.
|
||||
- name: Install pinned build dependencies
|
||||
run: pip install -r requirements-ci.txt --require-hashes
|
||||
# build is a dev-time dependency; wheel is build-time only (neither is
|
||||
# in requirements-ci.txt) — install the same pinned versions
|
||||
# [build-system] declares so --no-isolation works below.
|
||||
- run: pip install -r .github/requirements/build-tools.txt --require-hashes
|
||||
run: pip install -r requirements-ci.txt
|
||||
- run: pip install build
|
||||
# wheel is build-time only (not in requirements-ci.txt) — install the
|
||||
# same pinned version [build-system] declares so --no-isolation works.
|
||||
- run: pip install wheel==0.48.0
|
||||
- name: Build package (no isolation — pinned deps)
|
||||
run: python -m build --no-isolation
|
||||
- name: Verify Explorer frontend is packaged
|
||||
@@ -63,29 +63,11 @@ jobs:
|
||||
|
||||
print("Explorer frontend is packaged")
|
||||
PY
|
||||
- name: Verify PyPI long-description will render
|
||||
run: |
|
||||
pip install -r .github/requirements/twine.txt --require-hashes
|
||||
twine check dist/*
|
||||
- name: Attest build provenance
|
||||
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4
|
||||
with:
|
||||
subject-path: 'dist/*'
|
||||
# attest-build-provenance publishes to the GH attestations API only, which
|
||||
# OpenSSF Scorecard's Signed-Releases check does not inspect - it looks for
|
||||
# signature files attached as release assets. Sign here too so
|
||||
# `dist/*.sigstore.json` bundles ship alongside the wheel/sdist on the
|
||||
# GitHub Release itself.
|
||||
- name: Sign artifacts with Sigstore
|
||||
uses: sigstore/gh-action-sigstore-python@790bc6befb9d733738f18d8f895854b453640ec9 # v3.5.0
|
||||
- uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
|
||||
with:
|
||||
inputs: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
- uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
||||
with:
|
||||
files: |
|
||||
dist/*.whl
|
||||
dist/*.tar.gz
|
||||
dist/*.sigstore.json
|
||||
files: dist/*
|
||||
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
name: Scorecard supply-chain security
|
||||
|
||||
permissions: read-all
|
||||
|
||||
on:
|
||||
branch_protection_rule:
|
||||
schedule:
|
||||
- cron: '30 1 * * 6' # weekly
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
security-events: write # to upload SARIF results
|
||||
id-token: write # to publish results and get a badge
|
||||
contents: read
|
||||
actions: read # to detect GitHub Actions workflows
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run analysis
|
||||
uses: ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc # v2.4.4
|
||||
with:
|
||||
results_file: results.sarif
|
||||
results_format: sarif
|
||||
publish_results: true
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: SARIF file
|
||||
path: results.sarif
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload to code-scanning
|
||||
uses: github/codeql-action/upload-sarif@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4
|
||||
with:
|
||||
sarif_file: results.sarif
|
||||
@@ -44,15 +44,15 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pip install -r .github/requirements/bootstrap.txt --require-hashes
|
||||
python -m pip install --upgrade pip
|
||||
# Install the pinned dependency set FIRST so Safety scans Semantica's
|
||||
# exact CI/release dependency tree (requirements-ci.txt is generated
|
||||
# from pyproject.toml extras, so this covers the project's real deps).
|
||||
pip install -r requirements-ci.txt --require-hashes
|
||||
pip install -r requirements-ci.txt
|
||||
# Tooling AFTER the pinned set: installing safety/bandit/semgrep/jq
|
||||
# first lets the pinned requirements overwrite their transitive deps
|
||||
# (e.g. rich), which breaks the safety CLI at runtime.
|
||||
pip install -r .github/requirements/security-scan-tools.txt --require-hashes
|
||||
pip install safety bandit semgrep jq
|
||||
|
||||
- name: Run Safety Check (Package Vulnerabilities)
|
||||
run: |
|
||||
|
||||
@@ -25,18 +25,18 @@ jobs:
|
||||
# Upgrade first: actions/setup-python's baked-in setuptools has been
|
||||
# behind known-vulnerable floors before (e.g. PYSEC-2026-3447 /
|
||||
# setuptools 75.1.0), so don't trust the preinstalled one.
|
||||
- run: pip install -r .github/requirements/bootstrap.txt --require-hashes
|
||||
- run: python -m pip install --upgrade pip setuptools
|
||||
# Audit the pinned dependency set (requirements-ci.txt is compiled from
|
||||
# pyproject.toml with --extra all — the same coverage as the [all]
|
||||
# extra, minus the Linux-only gpu set — so this keeps scan parity with
|
||||
# CI/release builds without a time-dependent resolution). This is the
|
||||
# fix for PYSEC-2024-38 (#869): the bare-env job never had fastapi or
|
||||
# python-multipart installed to look at.
|
||||
- run: pip install -r requirements-ci.txt --require-hashes
|
||||
- run: pip install -r requirements-ci.txt
|
||||
# PR runs gate on findings, since they're scoped to actual
|
||||
# pyproject.toml changes under review. The schedule/workflow_dispatch
|
||||
# runs stay non-blocking until a full pass over pre-existing findings
|
||||
# across the whole [all] tree has been done.
|
||||
- run: pip install -r .github/requirements/pip-audit.txt --require-hashes
|
||||
- run: pip install pip-audit
|
||||
- run: pip-audit -r requirements-ci.txt
|
||||
continue-on-error: ${{ github.event_name != 'pull_request' }}
|
||||
|
||||
@@ -213,15 +213,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- **RETE engine matched every fact against every rule — `AlphaNode._matches()` and `BetaNode._can_join()` were placeholder stubs that always returned `True`** (closes #300)
|
||||
- `semantica/reasoning/rete_engine.py` shipped a Rete network whose per-condition alpha test and cross-condition beta join were both `return True` stubs, so `match_patterns()` fired every rule for every fact regardless of predicate, arity, or shared-variable consistency
|
||||
- New module-level `unify_condition()` reuses the regex-based approach from `Reasoner._match_pattern()`: a condition pattern like `Person(?x)` / `Parent(?x, ?y)` is compiled against a fact's `predicate(arg, ...)` string, `?var` becomes a named capture group, and a variable seen twice within one condition (e.g. `Loves(?x, ?x)`) becomes a backreference, so it only unifies when both positions hold the same value. Returns the bindings dict or `None`
|
||||
- Reworked propagation to carry partial-match **tokens** instead of bare facts: a new `Token` dataclass bundles the accumulated `facts` with the consistent `bindings`. `AlphaNode` emits a single-fact token per match; `BetaNode.join()` merges a left token with a right token, concatenating their facts in condition order and returning the merged token only when shared variables agree (conflicting values → `None`, no join). Terminal activations carry the full fact list and accumulated bindings through to the emitted match
|
||||
- This fixes a P1 chained-join defect: rules with three or more conditions (e.g. `Person(?x)`, `Parent(?x, ?y)`, `Located(?y, ?z)`) previously lost bindings and accumulated wrong facts at the third join, and a conflicting third condition could spuriously fire. Beta nodes now keep both `left_tokens` and `right_tokens` memories and join each new token against every token on the opposite side, so deep chains stay binding-consistent and third-level conflicts are correctly suppressed
|
||||
- Fixed an adjacent network-topology bug surfaced by the above: newly created beta nodes were never appended to their input nodes' `children`, so tokens could not propagate; propagation was reworked to support chained joins and to thread bindings end-to-end
|
||||
- Reconciled with the rule-actions/provenance layer (#1096) merged after this fix was opened: `execute_matches()` still dedupes and fires `Rule.actions`/legacy `handler` through a bound `Reasoner` via `_make_activation_key`, now sourced from the Token model's own `bindings` instead of the interim `_bindings_for_rule()` regex re-extraction, which is removed as redundant
|
||||
- New `tests/reasoning/test_rete_engine.py`: `unify_condition` unit cases (single/multi variable, literal args, predicate mismatch, repeated-variable equality), alpha match/reject, beta consistent-join vs conflict-reject, end-to-end rules (single-condition fires only the matching fact; multi-condition join fires only on consistent bindings), and a `TestThreeConditionChain` suite (valid three-condition match, third-level conflict suppression, insertion-order independence, `Match.facts` complete and in condition order, multiple left tokens joining one right fact, parity against `Reasoner._match_rule()`, and `reset()` clearing all token memory)
|
||||
|
||||
- **KG provenance tests asserted on generated ID strings instead of stored records, and `kg_provenance.py` was missed by the `utcnow` sweep** (closes #946) by @pravit-amp
|
||||
- The KG workflow and integration suites checked that a tracker call returned an ID matching a prefix (`assert cent_id.startswith("centrality_")`) without ever reading the record back, so an ID generator that returned a well-formed string and wrote nothing would have passed. Worse, some of those calls named tracker methods that do not exist anywhere in `semantica/` (`track_layer_analysis`, `track_centrality_score`), so the assertions were satisfied with no real interaction behind them
|
||||
- Those tests now read provenance back through `get_provenance()` and assert on algorithm metadata, and call the methods that actually persist records. Verified by mutation rather than by a green run alone: neutering the manager's storage write (`self.storage.store(...)` → no-op) fails 10 tests
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
cff-version: 1.2.0
|
||||
message: "If you use this software, please cite it as below."
|
||||
title: "Semantica: Graph-Native Infrastructure for Context and Accountable AI Systems"
|
||||
type: software
|
||||
authors:
|
||||
- name: "Semantica"
|
||||
repository-code: "https://github.com/semantica-agi/semantica"
|
||||
url: "https://getsemantica.ai"
|
||||
license: MIT
|
||||
version: 0.6.7
|
||||
date-released: 2026-08-28
|
||||
keywords:
|
||||
- knowledge-graph
|
||||
- context-graph
|
||||
- ai-agents
|
||||
- llm
|
||||
- decision-intelligence
|
||||
- provenance
|
||||
- explainability
|
||||
- graph-rag
|
||||
+4
-38
@@ -1,5 +1,5 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:26-alpine@sha256:2d984a15c9b54fd0aeb608b8e0d0d83529eb34d2966db27a1fb4f1edc3d298a3 AS frontend-builder
|
||||
FROM node:26-alpine AS frontend-builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY explorer/package*.json ./explorer/
|
||||
@@ -9,18 +9,7 @@ RUN npm ci
|
||||
COPY explorer/ ./
|
||||
RUN mkdir -p /app/semantica && npm run build
|
||||
|
||||
# CVE-2026-14456 (OpenSSL QUIC-server DoS, flagged against this base image's
|
||||
# openssl/libssl3t64/openssl-provider-legacy): the Debian fix
|
||||
# (3.5.7-1~deb13u2) is only in trixie-proposed-updates as of this writing,
|
||||
# not yet promoted to trixie-security, so there's no package to pin here
|
||||
# today. Deliberately NOT running `apt-get upgrade` to chase it - that
|
||||
# breaks build reproducibility (terrascan AC_DOCKER_0052) and still
|
||||
# wouldn't reach a proposed-updates-only package. Once Debian ships the fix
|
||||
# and rebuilds this tag, the docker Dependabot ecosystem in
|
||||
# .github/dependabot.yml opens a PR bumping the digest pin above. Also: this
|
||||
# image only serves plain HTTP via uvicorn and never opens a QUIC listener,
|
||||
# so the bug isn't reachable here regardless.
|
||||
FROM python:3.14-slim@sha256:cae66f2ef0ec51a9891263eeee7f987dacf0a9879e8aa9353d5606e0530619a5 AS runtime
|
||||
FROM python:3.13-slim AS runtime
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
@@ -33,35 +22,12 @@ WORKDIR /app
|
||||
RUN groupadd --system semantica \
|
||||
&& useradd --system --gid semantica --home-dir /app --shell /usr/sbin/nologin semantica
|
||||
|
||||
COPY pyproject.toml README.md LICENSE MANIFEST.in \
|
||||
.github/requirements/explorer-extra-py313.txt .github/requirements/pep517-build.txt ./
|
||||
COPY pyproject.toml README.md LICENSE MANIFEST.in ./
|
||||
COPY semantica/ ./semantica/
|
||||
COPY integrations/ ./integrations/
|
||||
COPY --from=frontend-builder /app/semantica/static ./semantica/static
|
||||
|
||||
# explorer-extra-py313.txt is `uv pip compile pyproject.toml --extra explorer
|
||||
# --python-version 3.13 --constraint requirements-ci.txt --generate-hashes`
|
||||
# (see ci.yml's explorer-extra-py311.txt for the CI counterpart, resolved
|
||||
# for CI's python 3.11 instead - the two aren't interchangeable: audioread
|
||||
# (via librosa) needs standard-aifc/standard-sunau only on python>=3.13,
|
||||
# since aifc/sunau left stdlib there, so a 3.11-resolved lockfile is
|
||||
# missing hashes pip needs on this image's actual 3.13 interpreter and
|
||||
# --require-hashes fails outright rather than silently under-pinning).
|
||||
# Every fetched package is hash-verified (Scorecard Pinned-Dependencies)
|
||||
# and pinned to the same versions CI audited, e.g. msgpack==1.2.1 and
|
||||
# setuptools==84.0.0 (which also replaces the base image's vulnerable
|
||||
# 70.3.0, CVE-2025-47273 - nothing else in the tree pulls a newer copy).
|
||||
# --no-deps on the local package itself: it's our own source tree, not a
|
||||
# fetch, so there's nothing to hash-pin there - but `pip install .` still
|
||||
# does a PEP 517 build, which by default creates an *isolated* build env
|
||||
# and fetches [build-system] requires (setuptools, wheel) completely
|
||||
# outside any hash checking. pep517-build.txt pins that exact
|
||||
# build-system.requires; installing it first and passing
|
||||
# --no-build-isolation makes pip reuse those hash-verified copies instead
|
||||
# of fetching its own.
|
||||
RUN pip install --no-cache-dir -r explorer-extra-py313.txt -r pep517-build.txt --require-hashes \
|
||||
&& pip install --no-cache-dir --no-deps --no-build-isolation . \
|
||||
&& rm -f explorer-extra-py313.txt pep517-build.txt \
|
||||
RUN pip install --no-cache-dir ".[explorer]" \
|
||||
&& chown -R semantica:semantica /app
|
||||
|
||||
USER semantica
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
# Growth & Distribution Playbook
|
||||
|
||||
North star: **10,000 developers who actually use Semantica in real projects**, not a raw PyPI download number. Downloads are a lagging indicator of distribution, not a target to optimize directly.
|
||||
|
||||
```
|
||||
GitHub stars → Website visitors → PyPI installs → Weekly active users → Production deployments → Enterprise customers
|
||||
```
|
||||
The last two matter far more than the download count.
|
||||
|
||||
## Guardrails — do not do this
|
||||
|
||||
- No fake/looping CI jobs that repeatedly `pip install semantica` purely to inflate the graph. It's detectable, it produces zero real users, and it damages credibility with anyone doing diligence (investors, enterprise buyers, security reviewers).
|
||||
- No package-splitting purely to multiply install counts — only split into `semantica-*` packages when there's a real architectural reason.
|
||||
- No meaningless Docker pulls or notebook launches with no real content behind them.
|
||||
- Every item below should get someone from "installed it" to "used it for something real." If a channel can't do that, it's not worth building.
|
||||
|
||||
## 30-day priority sprint
|
||||
|
||||
Ordered by leverage-to-effort ratio; do these first.
|
||||
|
||||
| # | Initiative | Target |
|
||||
| - | ---------- | ------ |
|
||||
| 1 | ✅ GitHub Actions example + reusable `setup-semantica` composite action + install-matrix badge | done |
|
||||
| 2 | Google Colab notebooks | 10 |
|
||||
| 3 | Docker images (RAG, Graph, Agent, API) | 4-5 |
|
||||
| 4 | Hugging Face Spaces demos | 3-4 |
|
||||
| 5 | LangChain integration + example | 1 |
|
||||
| 6 | LlamaIndex integration + example | 1 |
|
||||
| 7 | Vector/graph DB integrations (Qdrant, Weaviate, Neo4j) | 3 |
|
||||
| 8 | MCP server + example | 1 (already have `mcp/` — package as a distributable example) |
|
||||
| 9 | Production-quality starter repos (FastAPI, Streamlit, Gradio) | 3 |
|
||||
| 10 | `awesome-rag` / `awesome-llm` / `awesome-knowledge-graph` list submissions | 3+ PRs |
|
||||
|
||||
Push everything through: GitHub → Discord (`sV34vps5hH`) → X (`@BuildSemantica`) → GitHub Discussions → Reddit → Hacker News → relevant newsletters.
|
||||
|
||||
## Full channel checklist
|
||||
|
||||
### CI/CD (highest-intent distribution — installs tied to real pipelines)
|
||||
|
||||
- [x] GitHub Actions example in `examples/ci/github-actions.yml`
|
||||
- [x] Reusable composite GitHub Action — [`.github/actions/setup-semantica`](.github/actions/setup-semantica/action.yml), modeled on `actions/setup-python`; usable by any repo as `uses: semantica-agi/semantica/.github/actions/setup-semantica@main`
|
||||
- [x] "pip install" status badge in the README, backed by [`.github/workflows/install-matrix.yml`](.github/workflows/install-matrix.yml) — verifies the *published* package installs cleanly on Ubuntu/macOS/Windows across Python 3.9-3.12, weekly + on every release
|
||||
- [x] GitLab CI template — `examples/ci/gitlab-ci.yml`
|
||||
- [x] CircleCI template — `examples/ci/circleci-config.yml`
|
||||
- [ ] Jenkins, Azure DevOps, Bitbucket Pipelines, Buildkite, Travis CI equivalents
|
||||
|
||||
### Release pipeline hardening (already had Trusted Publishing/OIDC + SLSA attestation — this rounds it out to match top-tier OSS release practice)
|
||||
|
||||
- [x] `twine check` gate in `.github/workflows/release.yml` before publish — catches a broken PyPI long-description render before it goes live instead of after (a malformed README on the live PyPI page is a silent conversion killer)
|
||||
- [x] `CITATION.cff` (see Academic & research below)
|
||||
- [x] OpenSSF Scorecard (see Discoverability below)
|
||||
- [ ] Considered and deliberately skipped: Release Drafter / auto-generated changelogs — this repo hand-curates `CHANGELOG.md` with far more detail (PR numbers, contributors, phase-1 limitations) than a bot would produce. Don't introduce this without checking with maintainers first.
|
||||
- [ ] Renovate / Dependabot config templates that auto-bump the `semantica` version in downstream repos — real recurring CI runs on real adopters
|
||||
- [ ] Nightly scheduled workflow template that tests a downstream project against `semantica@latest`
|
||||
|
||||
### Containers & dev environments
|
||||
|
||||
- [ ] Official Docker images: RAG, Graph, Agent, API, `+Postgres`, `+Neo4j`, `+Qdrant`
|
||||
- [ ] `docker-compose` examples (repo already has `docker-compose.dev.yml` / `docker-compose.yml` as a base)
|
||||
- [ ] `.devcontainer/devcontainer.json` for one-click "Reopen in Container"
|
||||
- [ ] GitHub Codespaces-ready config
|
||||
- [ ] Gitpod config
|
||||
- [ ] "Use this template" GitHub repo button so new projects start with `semantica` in `requirements.txt`
|
||||
|
||||
### Notebooks & hosted demos
|
||||
|
||||
- [ ] 10-20 Google Colab notebooks (Graph RAG, agent memory, entity resolution, semantic search, document intelligence)
|
||||
- [ ] Kaggle Notebooks/Kernels
|
||||
- [ ] Binder / mybinder.org config for instant repo launch
|
||||
- [ ] SageMaker Studio Lab / Databricks Community Edition / Paperspace Gradient examples
|
||||
- [ ] Hugging Face Spaces (Streamlit/Gradio) demos with `semantica` in `requirements.txt`
|
||||
- [ ] Public hosted playground (source on GitHub, install visible)
|
||||
|
||||
### Framework & data-store integrations
|
||||
|
||||
- [x] LangChain integration — `integrations/langchain/` (`SemanticaRetriever`, `SemanticaVectorStore`, `SemanticaKGTool`/`SemanticaDecisionTool`), `pip install semantica[langchain]`, shipped in 0.6.7
|
||||
- [ ] LlamaIndex integration + example
|
||||
- [ ] LangGraph example
|
||||
- [ ] Neo4j integration/example (docs already list it as a supported graph store — turn into a runnable example repo)
|
||||
- [ ] Vector DB examples: Qdrant, Weaviate, Milvus, Pinecone, Chroma, FAISS, pgvector, OpenSearch/Elasticsearch (FAISS/Pinecone/Weaviate/Qdrant/Milvus/PgVector already supported per `docs/community-projects.md` — package each as a standalone example)
|
||||
- [ ] LLM provider quickstarts: OpenAI, Anthropic, Gemini, Groq, Ollama, HuggingFace, DeepSeek, LiteLLM (already-supported providers per docs — each gets its own copy-paste quickstart)
|
||||
- [ ] CrewAI / Agno integration examples (already documented under `docs/integrations/`) — promote as standalone repos, not just docs pages
|
||||
|
||||
### Package managers & installers
|
||||
|
||||
- [ ] conda-forge feedstock
|
||||
- [ ] Homebrew formula for the CLI
|
||||
- [ ] Nix/nixpkgs packaging
|
||||
- [ ] Chocolatey / Scoop (Windows)
|
||||
- [ ] Document `uv add semantica` and `poetry add semantica` explicitly alongside `pip install`
|
||||
|
||||
### Downstream packages & CLI
|
||||
|
||||
- [ ] Genuinely useful `semantica-*` packages only where warranted (e.g. `semantica-rag`, `semantica-connectors`) — each pulls `semantica` as a real dependency
|
||||
- [ ] Make sure `semantica init / ingest / index / query / serve` CLI flows are the default onboarding path in every tutorial
|
||||
- [ ] VS Code extension wrapping the CLI (scaffold + run commands from the command palette)
|
||||
- [ ] JetBrains plugin equivalent
|
||||
|
||||
### Templates & starters
|
||||
|
||||
- [ ] Cookiecutter templates: `cookiecutter-semantic-rag`, `cookiecutter-ai-agent`, `cookiecutter-enterprise-rag`
|
||||
- [ ] Starter repos: FastAPI, Streamlit, Gradio, Next.js frontend + Semantica backend
|
||||
- [ ] Cloud deploy templates: AWS, GCP, Azure, Modal, Railway, Render, Fly.io (repo already has `deploy/azure`, `deploy/gcp`, `deploy/fly`, `deploy/railway`, `deploy/render`, `deploy/kubernetes`, `deploy/helm` — link these prominently from the README/quickstart, they're already-built distribution surface)
|
||||
- [ ] Terraform / Pulumi / Helm modules published to their respective registries
|
||||
|
||||
### Discoverability & curation
|
||||
|
||||
- [ ] Submit to `awesome-rag`, `awesome-llm`, `awesome-knowledge-graph`, `awesome-python`
|
||||
- [ ] Pitch newsletters with engaged Python/AI audiences (Python Weekly, Import AI, TLDR AI, etc.)
|
||||
- [x] PyPI trove classifiers/keywords and `project.urls` (Homepage/Docs/Repository/Changelog/Bug Tracker) — already complete in `pyproject.toml`
|
||||
- [ ] Get listed on Papers With Code for any retrieval/graph-RAG benchmark work
|
||||
- [x] [OpenSSF Scorecard](https://scorecard.dev/viewer/?uri=github.com/semantica-agi/semantica) badge + weekly workflow (`.github/workflows/scorecard.yml`) — a concrete trust signal security/procurement teams check before greenlighting adoption, which gates real (non-CI-bot) install growth at enterprises
|
||||
|
||||
### Academic & research
|
||||
|
||||
- [x] `CITATION.cff` at repo root — enables GitHub's native "Cite this repository" button, feeds Google Scholar/academic tooling; complements `docs/citation.md` (still needs a real Zenodo DOI to replace the `XXXXXXX` placeholder in both places once one is minted)
|
||||
- [ ] arXiv paper if there's real architectural novelty to describe
|
||||
- [ ] Zenodo DOI for citability (`docs/citation.md` already exists — make sure it points to a real DOI)
|
||||
- [ ] Workshop/tutorial sessions at PyData/ODSC-style events with hands-on install steps
|
||||
- [ ] University course material / bootcamp adoption outreach
|
||||
|
||||
### Content
|
||||
|
||||
- [ ] Reproducible benchmark repos (Graph RAG vs vector RAG, retrieval@k, enterprise-scale retrieval) with `pip install semantica && python benchmark.py`
|
||||
- [ ] 20-30 real-world example applications (RAG, enterprise document intelligence, financial entity graphs, code knowledge graphs, research discovery, agent memory)
|
||||
- [ ] Blog/tutorial posts on Dev.to, Medium, personal blogs — always with runnable code, not just prose
|
||||
- [ ] Contribute integrations/PRs to other projects building RAG/agents/knowledge graphs — "I implemented Semantica support" beats "please use Semantica"
|
||||
|
||||
## Tracking
|
||||
|
||||
Don't just watch the raw PyPI number — use download analytics (e.g. PePy) to separate CI/bot traffic from real installs, and track the funnel above end-to-end where possible (stars → site visits → installs → weekly actives).
|
||||
@@ -26,7 +26,7 @@
|
||||
|
||||
#### Built for High-Stakes, Regulated Domains
|
||||
|
||||
[](https://github.com/semantica-agi/semantica) [](https://github.com/semantica-agi/semantica/network/members) [](https://github.com/semantica-agi/semantica/graphs/contributors) [](https://pypi.org/project/semantica/) [](https://pepy.tech/project/semantica) [](https://www.python.org/) [](https://opensource.org/licenses/MIT) [](https://github.com/semantica-agi/semantica/actions) [](https://github.com/semantica-agi/semantica/actions/workflows/install-matrix.yml) [](https://scorecard.dev/viewer/?uri=github.com/semantica-agi/semantica) [](https://deepwiki.com/semantica-agi/semantica)
|
||||
[](https://github.com/semantica-agi/semantica) [](https://github.com/semantica-agi/semantica/network/members) [](https://github.com/semantica-agi/semantica/graphs/contributors) [](https://pypi.org/project/semantica/) [](https://pepy.tech/project/semantica) [](https://www.python.org/) [](https://opensource.org/licenses/MIT) [](https://github.com/semantica-agi/semantica/actions) [](https://deepwiki.com/semantica-agi/semantica)
|
||||
|
||||
[](https://getsemantica.ai/) [](https://docs.getsemantica.ai/) [](https://discord.gg/sV34vps5hH) [](https://x.com/BuildSemantica) [](https://www.youtube.com/watch?v=QfnNZg4-dZA) [](CHANGELOG.md)
|
||||
|
||||
@@ -1534,20 +1534,6 @@ git clone https://github.com/semantica-agi/semantica.git
|
||||
cd semantica && pip install -e ".[dev]" && pytest tests/
|
||||
```
|
||||
|
||||
### CI & Deployment
|
||||
|
||||
Wiring `semantica` into your own CI is a two-minute job. On GitHub Actions, use the reusable composite action:
|
||||
|
||||
```yaml
|
||||
- uses: semantica-agi/semantica/.github/actions/setup-semantica@main
|
||||
with:
|
||||
python-version: '3.11'
|
||||
```
|
||||
|
||||
Copy-paste starting templates for GitHub Actions, GitLab CI, and CircleCI live in [examples/ci/](examples/ci/). The published package itself is verified installable across Ubuntu/macOS/Windows and Python 3.9-3.12 every week by the [Install Matrix workflow](.github/workflows/install-matrix.yml).
|
||||
|
||||
Ready-made deployment configs for AWS, GCP, Azure, Fly.io, Railway, Render, Kubernetes, and Helm are in [deploy/](deploy/).
|
||||
|
||||
---
|
||||
|
||||
## Enterprise
|
||||
|
||||
+24
-152
@@ -28,7 +28,6 @@ The `semantica.llms` module provides a unified interface for connecting to Large
|
||||
## When To Use / When Not To Use
|
||||
|
||||
**Use LLM integrations for:**
|
||||
|
||||
- Text generation, summarization, and question-answering tasks
|
||||
- Complex reasoning that requires natural language understanding
|
||||
- Structured data extraction from unstructured text
|
||||
@@ -36,7 +35,6 @@ The `semantica.llms` module provides a unified interface for connecting to Large
|
||||
- Tasks where context, ambiguity, or domain knowledge matter
|
||||
|
||||
**Deterministic tools may be better for:**
|
||||
|
||||
- Pattern matching that regular expressions can handle
|
||||
- Simple rule-based classification with clear criteria
|
||||
- Mathematical calculations or statistical analysis
|
||||
@@ -44,7 +42,6 @@ The `semantica.llms` module provides a unified interface for connecting to Large
|
||||
- Data transformations with known logic
|
||||
|
||||
**A full LLM may be unnecessary for:**
|
||||
|
||||
- Simple keyword search or exact string matching
|
||||
- Deterministic workflows with predefined decision trees
|
||||
- High-frequency, low-latency operations where inference overhead matters
|
||||
@@ -62,7 +59,7 @@ Four factors drive provider selection, each optimized for different use cases:
|
||||
|
||||
**Accuracy** matters most in high-stakes decisions: clinical contraindication checks, credit committee reasoning, and legal document analysis. Frontier models like Claude or GPT-4 available through `LiteLLM` provide the strongest reasoning capabilities.
|
||||
|
||||
**Data residency** constraints eliminate cloud providers for classified or HIPAA-regulated workloads. `HuggingFaceLLM` with local model paths, or `Ollama` pointed at a local server, both enable fully air-gapped deployments without network calls.
|
||||
**Data residency** constraints eliminate cloud providers for classified or HIPAA-regulated workloads. `HuggingFaceLLM` with local model paths enables fully air-gapped deployments without network calls.
|
||||
|
||||
**Cost at scale** favors high-throughput providers like Novita AI for bulk extraction pipelines processing thousands of documents per hour where per-token costs accumulate quickly.
|
||||
|
||||
@@ -146,131 +143,6 @@ risk_data = oai.generate_structured(
|
||||
|
||||
The default model `gpt-3.5-turbo` is fine for classification and light extraction. Switch to `gpt-4o` for complex multi-step regulatory reasoning or document understanding.
|
||||
|
||||
## Anthropic — Complex Reasoning and Structured Extraction
|
||||
|
||||
**Anthropic** provides the Claude model family, built with an emphasis on careful, instruction-following behavior and strong performance on multi-step reasoning, long-document analysis, and code-related tasks. Claude models tend to be more cautious about ambiguous instructions than other providers. That matters when the cost of a confidently wrong answer is high.
|
||||
|
||||
The `Anthropic` provider wraps the Claude API. Reach for it when the task involves reasoning through several dependent steps (not just single-turn extraction), when you're processing long source documents that need to stay in context, or when you need schema-validated structured output rather than best-effort JSON.
|
||||
|
||||
Install with `pip install "semantica[llm-anthropic]"` (or just `pip install anthropic`) before using this provider.
|
||||
|
||||
```python
|
||||
from semantica.llms import Anthropic
|
||||
|
||||
claude = Anthropic(model="claude-sonnet-4-6", api_key="YOUR_ANTHROPIC_KEY")
|
||||
# api_key falls back to the ANTHROPIC_API_KEY environment variable
|
||||
|
||||
# is_available() only confirms a client was constructed from some key.
|
||||
# It does not validate the key or check network reachability - an
|
||||
# invalid or expired key still passes this check and fails at generate().
|
||||
if not claude.is_available():
|
||||
raise RuntimeError("Anthropic provider not configured - set ANTHROPIC_API_KEY")
|
||||
|
||||
# Plain generation - multi-step reasoning over a contract clause
|
||||
verdict = claude.generate(
|
||||
"A vendor contract has a 30-day termination-for-convenience clause "
|
||||
"but a 90-day data-return obligation that survives termination. "
|
||||
"If the customer terminates on day 1, when must vendor-held data "
|
||||
"be returned? Answer with the date basis only.",
|
||||
temperature=0.1,
|
||||
)
|
||||
print(verdict)
|
||||
# "Day 120 from termination notice. The 90-day return period runs from
|
||||
# the termination date (day 30), not from the notice date."
|
||||
|
||||
# Structured, schema-validated output
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ContractRisk(BaseModel):
|
||||
clause: str
|
||||
risk_level: str
|
||||
days_to_deadline: int
|
||||
|
||||
risk = claude.generate_typed(
|
||||
"Extract the termination clause risk from: vendor contract, "
|
||||
"30-day termination for convenience, 90-day post-termination "
|
||||
"data return obligation.",
|
||||
schema=ContractRisk,
|
||||
)
|
||||
print(risk.risk_level, risk.days_to_deadline)
|
||||
# "medium" 90
|
||||
```
|
||||
|
||||
Model selection follows the same tier structure as the other providers: a Haiku model for high-volume classification where cost matters more than depth, a Sonnet model as the default for most extraction and reasoning tasks, an Opus model when a task genuinely needs the deepest reasoning available and latency/cost are secondary. Check Anthropic's docs for the current model identifiers, since they're versioned and change over time.
|
||||
|
||||
## Gemini — Long Context and Multimodal Input
|
||||
|
||||
**Gemini** is Google's model family, with a context window large enough to hold entire codebases or long regulatory filings in a single call, and native support for image and document input alongside text. Reach for it when a task needs to reference a large amount of source material at once, or when the input isn't plain text.
|
||||
|
||||
The `Gemini` provider tries the newer `google-genai` SDK first and falls back to the older `google-generativeai` package if that's what's installed. Install with `pip install "semantica[llm-gemini]"` (or `pip install google-genai`) before using this provider.
|
||||
|
||||
```python
|
||||
from semantica.llms import Gemini
|
||||
|
||||
gemini = Gemini(model="gemini-pro", api_key="YOUR_GEMINI_KEY")
|
||||
# api_key falls back to the GEMINI_API_KEY environment variable
|
||||
|
||||
if not gemini.is_available():
|
||||
raise RuntimeError("Gemini provider not configured - set GEMINI_API_KEY")
|
||||
|
||||
response = gemini.generate(
|
||||
"Summarize the key obligations in a standard NDA in three bullet points."
|
||||
)
|
||||
print(response)
|
||||
|
||||
data = gemini.generate_structured(
|
||||
"Extract the party names and effective date from: "
|
||||
"This Agreement is entered into between Acme Corp and Globex LLC, "
|
||||
"effective January 1, 2026."
|
||||
)
|
||||
print(data)
|
||||
```
|
||||
|
||||
## Ollama — Local, Air-Gapped Inference
|
||||
|
||||
**Ollama** runs models entirely on your own machine, with no API key and no outbound network call. It's the right choice for air-gapped environments, offline development, or any workload where the source data can't leave the local network.
|
||||
|
||||
Unlike the other providers here, `Ollama` takes a `base_url` instead of an `api_key`. It talks to a local Ollama server over HTTP. Start the server with `ollama serve` and pull a model with `ollama pull llama2` before using this provider. Install the Python client with `pip install "semantica[llm-ollama]"` (or `pip install ollama`).
|
||||
|
||||
```python
|
||||
from semantica.llms import Ollama
|
||||
|
||||
llm = Ollama(model="llama2", base_url="http://localhost:11434")
|
||||
|
||||
if not llm.is_available():
|
||||
raise RuntimeError("Ollama provider not configured - is 'ollama serve' running?")
|
||||
|
||||
response = llm.generate("Explain the difference between a hash map and a tree map.")
|
||||
print(response)
|
||||
```
|
||||
|
||||
`is_available()` for Ollama does a real connectivity check (it calls the server's `list()` endpoint), unlike the API-key-based providers above, so a `False` here usually means the server isn't running rather than a missing credential.
|
||||
|
||||
## DeepSeek — Budget Reasoning at Scale
|
||||
|
||||
**DeepSeek** exposes an OpenAI-compatible API at a fraction of the cost of the larger US providers, with reasoning quality that holds up well for extraction and classification work. It's a reasonable default when you're processing a large volume of documents and don't need the deepest reasoning tier.
|
||||
|
||||
Install with `pip install "semantica[llm-deepseek]"` (or `pip install openai`, since DeepSeek is accessed through the OpenAI client pointed at a different base URL).
|
||||
|
||||
```python
|
||||
from semantica.llms import DeepSeek
|
||||
|
||||
llm = DeepSeek(model="deepseek-chat", api_key="YOUR_DEEPSEEK_KEY")
|
||||
# api_key falls back to the DEEPSEEK_API_KEY environment variable
|
||||
|
||||
if not llm.is_available():
|
||||
raise RuntimeError("DeepSeek provider not configured - set DEEPSEEK_API_KEY")
|
||||
|
||||
response = llm.generate("List three risks of using a floating IP in a Kubernetes ingress.")
|
||||
print(response)
|
||||
|
||||
data = llm.generate_structured(
|
||||
"Extract the CVE ID and affected product from: "
|
||||
"CVE-2024-3400 affects PAN-OS GlobalProtect gateways."
|
||||
)
|
||||
print(data)
|
||||
```
|
||||
|
||||
## LiteLLM — One Interface, 100+ Providers
|
||||
|
||||
**LiteLLM** is a universal adapter that provides a single interface to over 100 different LLM providers, including Anthropic Claude, Azure OpenAI, AWS Bedrock, Google Vertex AI, and local Ollama instances. It acts as a translation layer, converting your unified API calls into provider-specific requests, enabling easy switching between providers without code changes.
|
||||
@@ -434,32 +306,30 @@ for t in triplets:
|
||||
|
||||
## Novita AI — Cost-Efficient Bulk Extraction
|
||||
|
||||
**Novita AI** exposes an OpenAI-compatible API at low per-call cost, making it a reasonable choice for high-volume NER pipelines where cost matters more than getting the single best answer.
|
||||
|
||||
Install with `pip install "semantica[llm-novita]"` (or `pip install openai`, since Novita is accessed through the OpenAI client pointed at a different base URL).
|
||||
|
||||
```python
|
||||
from semantica.llms import Novita
|
||||
|
||||
llm = Novita(model="deepseek/deepseek-v3.2", api_key="YOUR_NOVITA_KEY")
|
||||
# api_key falls back to the NOVITA_API_KEY environment variable
|
||||
|
||||
if not llm.is_available():
|
||||
raise RuntimeError("Novita provider not configured - set NOVITA_API_KEY")
|
||||
|
||||
response = llm.generate("Summarize the Basel III leverage ratio requirement.")
|
||||
|
||||
data = llm.generate_structured(
|
||||
"Extract drug names and dosages from: "
|
||||
"Patient received warfarin 5mg daily, aspirin 75mg daily, metformin 500mg twice daily."
|
||||
)
|
||||
```
|
||||
|
||||
Novita is also reachable as a provider name string for the NER interface, without going through the `Novita` class directly:
|
||||
Novita AI exposes an OpenAI-compatible API and is available as a built-in provider for the extraction layer. It is accessed differently from the `semantica.llms` classes — through `create_provider` from `semantica.semantic_extract.providers` — making it the right choice for high-volume NER pipelines where per-call cost matters.
|
||||
|
||||
```python
|
||||
from semantica.semantic_extract.providers import create_provider
|
||||
from semantica.semantic_extract import NamedEntityRecognizer
|
||||
|
||||
# create_provider pools instances — same key reuses the same object
|
||||
provider = create_provider(
|
||||
"novita",
|
||||
api_key="YOUR_NOVITA_KEY", # or set NOVITA_API_KEY env var
|
||||
model="deepseek/deepseek-v3.2", # default model
|
||||
)
|
||||
|
||||
if provider.is_available():
|
||||
# Plain generation
|
||||
response = provider.generate("Summarise the Basel III leverage ratio requirement.")
|
||||
|
||||
# Structured extraction — returns parsed dict
|
||||
data = provider.generate_structured(
|
||||
"Extract drug names and dosages from: "
|
||||
"Patient received warfarin 5mg daily, aspirin 75mg daily, metformin 500mg twice daily."
|
||||
)
|
||||
|
||||
# Use Novita through the NER interface — provider name as string
|
||||
ner = NamedEntityRecognizer(
|
||||
methods=["llm"],
|
||||
provider="novita",
|
||||
@@ -469,9 +339,11 @@ entities = ner.extract_entities(
|
||||
"CVE-2024-3400 is exploited by UNC3886 targeting PAN-OS GlobalProtect."
|
||||
)
|
||||
for e in entities:
|
||||
print("{} ({}) conf={:.2f}".format(e.text, e.label, e.confidence))
|
||||
print("{} ({}) — conf={:.2f}".format(e.text, e.label, e.confidence))
|
||||
```
|
||||
|
||||
Novita requires the `openai` Python client under the hood — install with `pip install "semantica[llm-openai]"` or `pip install openai`.
|
||||
|
||||
## Domain Examples
|
||||
|
||||
<Tabs>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
# CI templates
|
||||
|
||||
Copy-paste starting points for wiring `semantica` into your own project's CI. Each file is a
|
||||
complete, working config — rename it into your project (see the comment at the top of each file
|
||||
for the target path) and swap the smoke-test / test step for whatever your project does with
|
||||
Semantica. Each template installs `semantica` unconditionally and your own project's dependencies
|
||||
only if a `requirements.txt` is present; if your project uses `pyproject.toml`, Poetry, or Pipenv
|
||||
instead, adjust the marked install line (each file calls it out inline).
|
||||
|
||||
| File | Target path in your repo |
|
||||
| ---- | ------------------------- |
|
||||
| [`github-actions.yml`](github-actions.yml) | `.github/workflows/semantica.yml` |
|
||||
| [`gitlab-ci.yml`](gitlab-ci.yml) | `.gitlab-ci.yml` |
|
||||
| [`circleci-config.yml`](circleci-config.yml) | `.circleci/config.yml` |
|
||||
|
||||
If your own project is hosted on GitHub, you can skip the setup boilerplate entirely and use
|
||||
Semantica's reusable composite action instead:
|
||||
|
||||
```yaml
|
||||
- uses: semantica-agi/semantica/.github/actions/setup-semantica@main
|
||||
with:
|
||||
python-version: '3.11'
|
||||
# extras: 'explorer,all' # optional
|
||||
# version: '==0.6.7' # optional, pin an exact release
|
||||
# cache: 'pip' # optional, only if your repo has a requirements.txt/pyproject.toml/etc.
|
||||
```
|
||||
|
||||
`@main` always tracks this repo's default branch, which is convenient but — like any mutable
|
||||
ref — can change out from under you between runs. For production CI, pin it to a commit SHA
|
||||
instead (find one via `git rev-parse` against a tagged release, or the commit history for
|
||||
[`.github/actions/setup-semantica/`](../../.github/actions/setup-semantica/)) and update the pin
|
||||
deliberately when you want to pick up changes, the same way this repo's own workflows are pinned
|
||||
(see [`verify-action-pins.yml`](../../.github/workflows/verify-action-pins.yml)).
|
||||
|
||||
It installs Python, installs `semantica`, and verifies the import (pip caching is opt-in via `cache: 'pip'`, since not every caller repo has a requirements file to key the cache on) — see
|
||||
[`.github/actions/setup-semantica/action.yml`](../../.github/actions/setup-semantica/action.yml).
|
||||
@@ -1,40 +0,0 @@
|
||||
# Drop this in as .circleci/config.yml in your own project.
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
test:
|
||||
docker:
|
||||
- image: cimg/python:3.11
|
||||
steps:
|
||||
- checkout
|
||||
# A content-hashed cache key (e.g. `{{ checksum "requirements.txt" }}`)
|
||||
# is more precise but breaks if that exact file doesn't exist in your
|
||||
# project - swap in one matched to however you declare dependencies
|
||||
# once you've adjusted the install step below.
|
||||
- restore_cache:
|
||||
keys:
|
||||
- pip-cache-v1
|
||||
- run:
|
||||
name: Install dependencies
|
||||
command: |
|
||||
pip install --upgrade pip
|
||||
pip install semantica
|
||||
# Install your own project's dependencies however your project
|
||||
# declares them - adjust this to match, e.g. `pip install -e .`
|
||||
# for pyproject.toml / setup.cfg, or `poetry install`.
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- save_cache:
|
||||
key: pip-cache-v1
|
||||
paths:
|
||||
- ~/.cache/pip
|
||||
- run:
|
||||
name: Smoke test
|
||||
command: python -c "import semantica; print('semantica', semantica.__version__)"
|
||||
- run:
|
||||
name: Run tests
|
||||
command: pytest
|
||||
|
||||
workflows:
|
||||
test:
|
||||
jobs:
|
||||
- test
|
||||
@@ -1,44 +0,0 @@
|
||||
# Drop this in as .github/workflows/semantica.yml in your own project.
|
||||
#
|
||||
# Installs Semantica and runs a smoke import + your test suite. Swap the
|
||||
# smoke-test step for whatever your project actually does with Semantica
|
||||
# (build a context graph, run an ingest pipeline, etc.).
|
||||
#
|
||||
# Third-party actions below are pinned to a commit SHA rather than a mutable
|
||||
# tag - a moved tag can silently swap in different code. Update the pin (and
|
||||
# the trailing "# vX" comment) deliberately when you want a newer version;
|
||||
# see semantica-agi/semantica's own .github/workflows/verify-action-pins.yml
|
||||
# for one way to keep pins honest automatically.
|
||||
name: Semantica
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7
|
||||
with:
|
||||
python-version: '3.11'
|
||||
cache: 'pip'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install semantica
|
||||
# Install your own project's dependencies however your project
|
||||
# declares them - adjust this to match. Examples:
|
||||
# pip install -r requirements.txt
|
||||
# pip install -e . # pyproject.toml / setup.cfg
|
||||
# pip install -e ".[dev]"
|
||||
# poetry install
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
|
||||
- name: Run tests
|
||||
run: pytest
|
||||
@@ -1,20 +0,0 @@
|
||||
# Drop this in as .gitlab-ci.yml in your own project.
|
||||
semantica-test:
|
||||
image: python:3.11-slim
|
||||
cache:
|
||||
paths:
|
||||
- .cache/pip
|
||||
variables:
|
||||
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
|
||||
script:
|
||||
- pip install --upgrade pip
|
||||
- pip install semantica
|
||||
# Install your own project's dependencies however your project declares
|
||||
# them - adjust this to match, e.g. `pip install -e .` for pyproject.toml
|
||||
# / setup.cfg, or `poetry install`.
|
||||
- if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- python -c "import semantica; print('semantica', semantica.__version__)"
|
||||
- pytest
|
||||
rules:
|
||||
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
||||
- if: '$CI_COMMIT_BRANCH == "main"'
|
||||
Generated
+6
-6
@@ -2083,9 +2083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4250,9 +4250,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
||||
@@ -318,20 +318,6 @@ interface EdgeListResponse {
|
||||
|
||||
const PAGE_LIMIT = 1000;
|
||||
|
||||
/** Surface the server's `detail` message (e.g. auth/setup guidance) on non-OK responses. */
|
||||
async function fetchErrorDetail(response: Response): Promise<string> {
|
||||
try {
|
||||
const body: unknown = await response.json();
|
||||
const detail = (body as { detail?: unknown } | null)?.detail;
|
||||
if (typeof detail === "string" && detail.trim()) {
|
||||
return ` — ${detail.trim()}`;
|
||||
}
|
||||
} catch {
|
||||
// Non-JSON or unreadable body: fall back to the status-only message.
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
async function fetchAllNodes(
|
||||
signal: AbortSignal,
|
||||
onProgress?: (progress: GraphLoadProgress) => void,
|
||||
@@ -349,7 +335,7 @@ async function fetchAllNodes(
|
||||
|
||||
const response = await fetch(url.toString(), { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}${await fetchErrorDetail(response)}`);
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: NodeListResponse = await response.json();
|
||||
@@ -404,7 +390,7 @@ async function fetchAllEdges(
|
||||
|
||||
const response = await fetch(url.toString(), { signal });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fetch failed: ${response.status}${await fetchErrorDetail(response)}`);
|
||||
throw new Error(`Fetch failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: EdgeListResponse = await response.json();
|
||||
|
||||
+2
-10
@@ -49,14 +49,7 @@ dependencies = [
|
||||
"scipy>=1.13.1",
|
||||
"scikit-learn>=1.7.2",
|
||||
"umap-learn>=0.5.12",
|
||||
# thinc (spacy's core dep) dropped Python 3.9 wheels at 8.3.10, and later
|
||||
# spacy patch releases (3.8.8+) require thinc>=8.3.9-only-on-3.10+ ranges,
|
||||
# which forces a source build that fails outright on 3.9 (see Install
|
||||
# Matrix run history). Capping both keeps 3.9 on the last wheel-compatible
|
||||
# pair; 3.10+ is left unconstrained to always get the latest spacy/thinc.
|
||||
"spacy>=3.4.0,<3.8.8; python_version < '3.10'",
|
||||
"spacy>=3.4.0; python_version >= '3.10'",
|
||||
"thinc<8.3.5; python_version < '3.10'",
|
||||
"spacy>=3.4.0",
|
||||
"transformers>=4.20.0",
|
||||
"torch>=1.13.1",
|
||||
"sentence-transformers>=2.2.0",
|
||||
@@ -114,12 +107,11 @@ llm-gemini = ["google-genai>=0.1.0"]
|
||||
llm-anthropic = ["anthropic>=0.122.0"]
|
||||
llm-ollama = ["ollama>=0.1.0"]
|
||||
llm-deepseek = ["openai>=1.0.0"]
|
||||
llm-novita = ["openai>=1.0.0"]
|
||||
llm-litellm = ["litellm>=1.83.9"]
|
||||
llm-instructor = ["instructor>=1.15.3"]
|
||||
|
||||
llm-all = [
|
||||
"semantica[llm-openai,llm-groq,llm-gemini,llm-anthropic,llm-ollama,llm-deepseek,llm-novita,llm-litellm,llm-instructor]"
|
||||
"semantica[llm-openai,llm-groq,llm-gemini,llm-anthropic,llm-ollama,llm-deepseek,llm-litellm,llm-instructor]"
|
||||
]
|
||||
|
||||
# ---- Document Parsing ----
|
||||
|
||||
+9
-114
@@ -3714,61 +3714,19 @@ def store_stats(cli_ctx: CLIContext, backend: str, fmt: str, local_json: bool) -
|
||||
_run_with_error_handling(_action)
|
||||
|
||||
|
||||
_MIGRATE_SUPPORTED_BACKENDS = {"faiss", "sqlite", "pgvector"}
|
||||
_MIGRATE_BATCH_SIZE = 500
|
||||
|
||||
|
||||
def _migrate_backend_config(vs_cfg: Dict[str, Any], backend: str) -> Dict[str, Any]:
|
||||
"""Resolve per-backend config out of the vector_store config section.
|
||||
|
||||
Supports both a per-backend nested shape (``vector_store.faiss.dimension``)
|
||||
and the common flat single-backend shape (``vector_store.backend`` +
|
||||
sibling keys), since either can appear depending on how many backends a
|
||||
user has configured.
|
||||
"""
|
||||
nested = vs_cfg.get(backend)
|
||||
if isinstance(nested, dict):
|
||||
return dict(nested)
|
||||
if vs_cfg.get("backend") == backend:
|
||||
return {k: v for k, v in vs_cfg.items() if k != "backend"}
|
||||
return {}
|
||||
|
||||
|
||||
def _require_faiss_index_path(cfg: Dict[str, Any], role: str) -> str:
|
||||
"""FAISS has no server to hold state between commands: a fresh FAISSStore
|
||||
starts empty and nothing outside the process persists it, so migration
|
||||
needs an explicit on-disk index to read from or write to."""
|
||||
index_path = cfg.get("index_path")
|
||||
if not index_path:
|
||||
raise click.ClickException(
|
||||
f"faiss as migration {role} requires 'index_path' in the vector_store "
|
||||
f"config (vector_store.faiss.index_path or vector_store.index_path "
|
||||
f"when faiss is the configured backend)."
|
||||
)
|
||||
return index_path
|
||||
|
||||
|
||||
@store.command("migrate")
|
||||
@click.option("--from", "from_backend", required=True)
|
||||
@click.option("--to", "to_backend", required=True)
|
||||
@click.option("--namespace", default=None)
|
||||
@click.option("--dry-run", "local_dry", is_flag=True, default=False)
|
||||
@click.option("--json", "local_json", is_flag=True, default=False)
|
||||
@click.pass_obj
|
||||
def store_migrate(cli_ctx: CLIContext, from_backend: str, to_backend: str,
|
||||
namespace: Optional[str], local_dry: bool, local_json: bool) -> None:
|
||||
namespace: Optional[str], local_dry: bool) -> None:
|
||||
"""Migrate data between backends.
|
||||
|
||||
Direct migration is only wired up between faiss, sqlite, and pgvector -
|
||||
these are the backends whose storage contract supports paging through
|
||||
every stored vector. Migrating to or from qdrant, pinecone, milvus, or
|
||||
weaviate still needs the export/reindex workaround below, since each of
|
||||
those needs its own enumeration design (Qdrant scroll, Pinecone list,
|
||||
etc.) that hasn't been built yet.
|
||||
|
||||
\b
|
||||
Example:
|
||||
semantica store migrate --from faiss --to sqlite --namespace production --dry-run
|
||||
semantica store migrate --from faiss --to qdrant --namespace production --dry-run
|
||||
"""
|
||||
cli_ctx = _require_ctx(cli_ctx)
|
||||
|
||||
@@ -3776,76 +3734,13 @@ def store_migrate(cli_ctx: CLIContext, from_backend: str, to_backend: str,
|
||||
if _is_dry(cli_ctx, local_dry):
|
||||
_dry(cli_ctx, "migrate", from_backend=from_backend, to_backend=to_backend)
|
||||
return
|
||||
|
||||
if from_backend not in _MIGRATE_SUPPORTED_BACKENDS or to_backend not in _MIGRATE_SUPPORTED_BACKENDS:
|
||||
raise click.ClickException(
|
||||
f"Direct backend migration ({from_backend} → {to_backend}) is only supported "
|
||||
f"between {', '.join(sorted(_MIGRATE_SUPPORTED_BACKENDS))}. To migrate involving "
|
||||
"another backend, export your data first:\n"
|
||||
" semantica export --format parquet --output dump.parquet\n"
|
||||
f" semantica embed index dump.parquet --store {to_backend}"
|
||||
+ (f" --namespace {namespace}" if namespace else "")
|
||||
)
|
||||
|
||||
from .vector_store import VectorStore
|
||||
|
||||
vs_cfg = cli_ctx.config.to_dict().get("vector_store", {}) or {}
|
||||
source_cfg = _migrate_backend_config(vs_cfg, from_backend)
|
||||
dest_cfg = _migrate_backend_config(vs_cfg, to_backend)
|
||||
|
||||
source_index_path = None
|
||||
if from_backend == "faiss":
|
||||
source_index_path = _require_faiss_index_path(source_cfg, "source")
|
||||
dest_index_path = None
|
||||
if to_backend == "faiss":
|
||||
dest_index_path = _require_faiss_index_path(dest_cfg, "destination")
|
||||
|
||||
source = VectorStore(backend=from_backend, config=source_cfg)
|
||||
if source_index_path:
|
||||
source._backend_store.load_index(source_index_path)
|
||||
|
||||
source_dimension = getattr(source._backend_store, "dimension", None)
|
||||
if source_dimension and "dimension" not in dest_cfg:
|
||||
dest_cfg["dimension"] = source_dimension
|
||||
|
||||
dest = VectorStore(backend=to_backend, config=dest_cfg)
|
||||
if dest_index_path and Path(dest_index_path).exists():
|
||||
dest._backend_store.load_index(dest_index_path)
|
||||
|
||||
migrated = 0
|
||||
vectors_batch: List[Any] = []
|
||||
metadata_batch: List[Dict[str, Any]] = []
|
||||
ids_batch: List[str] = []
|
||||
|
||||
def _flush() -> None:
|
||||
nonlocal migrated
|
||||
if not vectors_batch:
|
||||
return
|
||||
dest.store_vectors(list(vectors_batch), list(metadata_batch), ids=list(ids_batch))
|
||||
migrated += len(vectors_batch)
|
||||
vectors_batch.clear()
|
||||
metadata_batch.clear()
|
||||
ids_batch.clear()
|
||||
|
||||
for item in source.iter_vectors(batch_size=_MIGRATE_BATCH_SIZE):
|
||||
meta = dict(item.get("metadata") or {})
|
||||
if namespace and "namespace" not in meta:
|
||||
meta["namespace"] = namespace
|
||||
vectors_batch.append(item["vector"])
|
||||
metadata_batch.append(meta)
|
||||
ids_batch.append(item["id"])
|
||||
if len(vectors_batch) >= _MIGRATE_BATCH_SIZE:
|
||||
_flush()
|
||||
_flush()
|
||||
|
||||
if dest_index_path and migrated:
|
||||
dest._backend_store.save_index(dest_index_path)
|
||||
|
||||
result = {"from": from_backend, "to": to_backend, "migrated": migrated}
|
||||
if _is_json(cli_ctx, local_json):
|
||||
_jecho(result)
|
||||
else:
|
||||
_ok(cli_ctx, f"Migrated {migrated} vectors from {from_backend} to {to_backend}")
|
||||
raise click.ClickException(
|
||||
f"Direct backend migration ({from_backend} → {to_backend}) is not yet supported "
|
||||
"by the vector store layer. To migrate, export your data first:\n"
|
||||
" semantica export --format parquet --output dump.parquet\n"
|
||||
f" semantica embed index dump.parquet --store {to_backend}"
|
||||
+ (f" --namespace {namespace}" if namespace else "")
|
||||
)
|
||||
|
||||
_run_with_error_handling(_action)
|
||||
|
||||
|
||||
@@ -1286,19 +1286,13 @@ class AgentMemory:
|
||||
"""
|
||||
return self.retrieve(content, max_results=limit, **kwargs)
|
||||
|
||||
def find_by_entity(
|
||||
self, entity_id: str, limit: Optional[int] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
def find_by_entity(self, entity_id: str, limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Find by entity.
|
||||
|
||||
Args:
|
||||
entity_id: Entity ID to search for
|
||||
limit: Maximum results. None (the default) returns ALL matches.
|
||||
The previous default of 10 silently truncated results — an
|
||||
erasure workflow computing "what references this entity"
|
||||
from a truncated page would leave the remainder live
|
||||
(#1018). Callers that want pagination pass an explicit limit.
|
||||
limit: Maximum results (default: 10)
|
||||
|
||||
Returns:
|
||||
List of memory dicts containing the entity
|
||||
@@ -1314,9 +1308,9 @@ class AgentMemory:
|
||||
if mem_dict:
|
||||
results.append(mem_dict)
|
||||
break
|
||||
if limit is not None and len(results) >= limit:
|
||||
if len(results) >= limit:
|
||||
break
|
||||
return results if limit is None else results[:limit]
|
||||
return results[:limit]
|
||||
|
||||
def find_by_relationship(
|
||||
self, relationship_type: str, limit: int = 10
|
||||
|
||||
@@ -76,11 +76,11 @@ Production Use Cases:
|
||||
- Insurance: Claim decisions, underwriting assessments
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from dataclasses import InitVar, dataclass, field
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional
|
||||
import json
|
||||
import uuid
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -100,9 +100,8 @@ class Decision:
|
||||
valid_from: Optional[str] = None
|
||||
valid_until: Optional[str] = None
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
"""Validate decision data."""
|
||||
if auto_generate_id and not self.decision_id: # Handle both None and empty string
|
||||
self.decision_id = str(uuid.uuid4())
|
||||
@@ -147,9 +146,8 @@ class DecisionContext:
|
||||
risk_factors: List[str]
|
||||
cross_system_inputs: Dict[str, Any] = field(default_factory=dict)
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
"""Validate decision context data."""
|
||||
if auto_generate_id and not self.context_id: # Handle both None and empty string
|
||||
self.context_id = str(uuid.uuid4())
|
||||
@@ -186,9 +184,8 @@ class Policy:
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
"""Validate policy data."""
|
||||
if auto_generate_id and not self.policy_id: # Handle both None and empty string
|
||||
self.policy_id = str(uuid.uuid4())
|
||||
@@ -230,9 +227,8 @@ class PolicyException:
|
||||
approval_timestamp: datetime
|
||||
justification: str
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
"""Validate policy exception data."""
|
||||
if auto_generate_id and not self.exception_id: # Handle both None and empty string
|
||||
self.exception_id = str(uuid.uuid4())
|
||||
@@ -269,9 +265,8 @@ class Precedent:
|
||||
similarity_score: float
|
||||
relationship_type: str # "similar_scenario", "same_policy", "exception_precedent"
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
"""Validate precedent data."""
|
||||
if auto_generate_id and not self.precedent_id: # Handle both None and empty string
|
||||
self.precedent_id = str(uuid.uuid4())
|
||||
@@ -310,9 +305,8 @@ class ApprovalChain:
|
||||
approval_context: str
|
||||
timestamp: datetime
|
||||
metadata: Dict[str, Any] = field(default_factory=dict)
|
||||
auto_generate_id: InitVar[bool] = True
|
||||
|
||||
def __post_init__(self, auto_generate_id: bool) -> None:
|
||||
def __post_init__(self, auto_generate_id: bool = True):
|
||||
"""Validate approval chain data."""
|
||||
if auto_generate_id and not self.approval_id: # Handle both None and empty string
|
||||
self.approval_id = str(uuid.uuid4())
|
||||
|
||||
@@ -10,53 +10,28 @@ Supported Providers:
|
||||
- OpenAI: OpenAI API (GPT-3.5, GPT-4, etc.)
|
||||
- HuggingFaceLLM: HuggingFace Transformers for local LLM inference
|
||||
- LiteLLM: Unified interface to 100+ LLM providers (OpenAI, Anthropic, Groq, Azure, Bedrock, Vertex AI, etc.)
|
||||
- Anthropic: Anthropic Claude API (Claude sonnet, Opus, Haiku, etc.)
|
||||
- Gemini: Google Gemini API
|
||||
- Ollama: Local models served through Ollama
|
||||
- DeepSeek: DeepSeek's OpenAI-compatible API
|
||||
- Novita: Novita AI's OpenAI-compatible API
|
||||
|
||||
Example Usage:
|
||||
>>> from semantica.llms import Groq, OpenAI, HuggingFaceLLM, LiteLLM, Anthropic
|
||||
>>>
|
||||
>>> from semantica.llms import Groq, OpenAI, HuggingFaceLLM, LiteLLM
|
||||
>>>
|
||||
>>> # Groq provider
|
||||
>>> groq = Groq(model="llama-3.1-8b-instant", api_key="your-key")
|
||||
>>> response = groq.generate("Hello, world!")
|
||||
>>>
|
||||
>>>
|
||||
>>> # OpenAI provider
|
||||
>>> openai = OpenAI(model="gpt-4", api_key="your-key")
|
||||
>>> response = openai.generate("Hello, world!")
|
||||
>>>
|
||||
>>>
|
||||
>>> # HuggingFace LLM provider
|
||||
>>> hf = HuggingFaceLLM(model_name="gpt2")
|
||||
>>> response = hf.generate("Hello, world!")
|
||||
>>>
|
||||
>>>
|
||||
>>> # LiteLLM provider (supports 100+ LLMs)
|
||||
>>> llm = LiteLLM(model="openai/gpt-4o", api_key="your-key")
|
||||
>>> response = llm.generate("Hello, world!")
|
||||
>>> # Or use other providers via LiteLLM
|
||||
>>> llm = LiteLLM(model="anthropic/claude-sonnet-4-20250514")
|
||||
>>> response = llm.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Anthropic provider
|
||||
>>> claude = Anthropic(model="claude-sonnet-4-6", api_key="the-key")
|
||||
>>> response = claude.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Gemini provider
|
||||
>>> gemini = Gemini(model="gemini-pro", api_key="your-key")
|
||||
>>> response = gemini.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Ollama provider (local, no api_key)
|
||||
>>> ollama = Ollama(model="llama2")
|
||||
>>> response = ollama.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # DeepSeek provider
|
||||
>>> deepseek = DeepSeek(model="deepseek-chat", api_key="your-key")
|
||||
>>> response = deepseek.generate("Hello, world!")
|
||||
>>>
|
||||
>>> # Novita provider
|
||||
>>> novita = Novita(model="deepseek/deepseek-v3.2", api_key="your-key")
|
||||
>>> response = novita.generate("Hello, world!")
|
||||
|
||||
Author: Semantica Contributors
|
||||
License: MIT
|
||||
@@ -66,20 +41,6 @@ from .groq import Groq
|
||||
from .openai import OpenAI
|
||||
from .huggingface import HuggingFaceLLM
|
||||
from .litellm import LiteLLM
|
||||
from .anthropic import Anthropic
|
||||
from .gemini import Gemini
|
||||
from .ollama import Ollama
|
||||
from .deepseek import DeepSeek
|
||||
from .novita import Novita
|
||||
|
||||
__all__ = [
|
||||
"Groq",
|
||||
"OpenAI",
|
||||
"HuggingFaceLLM",
|
||||
"LiteLLM",
|
||||
"Anthropic",
|
||||
"Gemini",
|
||||
"Ollama",
|
||||
"DeepSeek",
|
||||
"Novita",
|
||||
]
|
||||
__all__ = ["Groq", "OpenAI", "HuggingFaceLLM", "LiteLLM"]
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
"""
|
||||
Anthropic LLM Provider
|
||||
|
||||
Wrapper for Anthropic Claude API provider with clean interface
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import AnthropicProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.anthropic")
|
||||
|
||||
|
||||
class Anthropic:
|
||||
"""
|
||||
Anthropic Claude LLM provider wrapper.
|
||||
|
||||
Provides clean interface to Anthropic's Claude API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Anthropic
|
||||
>>> claude = Anthropic(model="claude-sonnet-4-6", api_key="the-key")
|
||||
>>> response = claude.generate("What is API key?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "claude-sonnet-4-6",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Anthropic provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: claude-sonnet-4-6)
|
||||
api_key: Anthropic API key (default: from ANTHROPIC_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = AnthropicProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Anthropic provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generates structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Anthropic provider not available. Set ANTHROPIC_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -1,111 +0,0 @@
|
||||
"""
|
||||
DeepSeek LLM Provider
|
||||
|
||||
Wrapper for DeepSeek API provider with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import DeepSeekProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.deepseek")
|
||||
|
||||
|
||||
class DeepSeek:
|
||||
"""
|
||||
DeepSeek LLM provider wrapper.
|
||||
|
||||
Provides clean interface to DeepSeek's OpenAI-compatible API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import DeepSeek
|
||||
>>> llm = DeepSeek(model="deepseek-chat", api_key="your-key")
|
||||
>>> response = llm.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "deepseek-chat",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize DeepSeek provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "deepseek-chat")
|
||||
api_key: DeepSeek API key (default: from DEEPSEEK_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = DeepSeekProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if DeepSeek provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"DeepSeek provider not available. Set DEEPSEEK_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"DeepSeek provider not available. Set DEEPSEEK_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"DeepSeek provider not available. Set DEEPSEEK_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -1,111 +0,0 @@
|
||||
"""
|
||||
Gemini LLM Provider
|
||||
|
||||
Wrapper for Google Gemini API provider with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import GeminiProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.gemini")
|
||||
|
||||
|
||||
class Gemini:
|
||||
"""
|
||||
Google Gemini LLM provider wrapper.
|
||||
|
||||
Provides clean interface to Google's Gemini API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Gemini
|
||||
>>> gemini = Gemini(model="gemini-pro", api_key="your-key")
|
||||
>>> response = gemini.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "gemini-pro",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Gemini provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "gemini-pro")
|
||||
api_key: Gemini API key (default: from GEMINI_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = GeminiProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Gemini provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Gemini provider not available. Set GEMINI_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or parsing fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Gemini provider not available. Set GEMINI_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Gemini provider not available. Set GEMINI_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -1,111 +0,0 @@
|
||||
"""
|
||||
Novita LLM Provider
|
||||
|
||||
Wrapper for Novita AI's OpenAI-compatible API with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from ..semantic_extract.providers import NovitaProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.novita")
|
||||
|
||||
|
||||
class Novita:
|
||||
"""
|
||||
Novita AI LLM provider wrapper.
|
||||
|
||||
Provides clean interface to Novita's OpenAI-compatible API.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Novita
|
||||
>>> llm = Novita(model="deepseek/deepseek-v3.2", api_key="your-key")
|
||||
>>> response = llm.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "deepseek/deepseek-v3.2",
|
||||
api_key: Optional[str] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Novita provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "deepseek/deepseek-v3.2")
|
||||
api_key: Novita API key (default: from NOVITA_API_KEY env var)
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = NovitaProvider(api_key=api_key, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.api_key = api_key
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Novita provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Novita provider not available. Set NOVITA_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Novita provider not available. Set NOVITA_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Novita provider not available. Set NOVITA_API_KEY or pass api_key."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -1,116 +0,0 @@
|
||||
"""
|
||||
Ollama LLM Provider
|
||||
|
||||
Wrapper for local Ollama models with clean interface.
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
from ..semantic_extract.providers import OllamaProvider
|
||||
from ..utils.exceptions import ProcessingError
|
||||
from ..utils.logging import get_logger
|
||||
|
||||
logger = get_logger("llms.ollama")
|
||||
|
||||
|
||||
class Ollama:
|
||||
"""
|
||||
Ollama LLM provider wrapper.
|
||||
|
||||
Provides clean interface to a local Ollama server. Unlike the other
|
||||
providers here, this one has no API key. It talks to an Ollama
|
||||
instance over HTTP, so make sure `ollama serve` is running first.
|
||||
|
||||
Example:
|
||||
>>> from semantica.llms import Ollama
|
||||
>>> llm = Ollama(model="llama2")
|
||||
>>> response = llm.generate("What is AI?")
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "llama2",
|
||||
base_url: str = "http://localhost:11434",
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Ollama provider.
|
||||
|
||||
Args:
|
||||
model: Model name (default: "llama2")
|
||||
base_url: Ollama server URL (default: "http://localhost:11434")
|
||||
**kwargs: Additional provider options
|
||||
"""
|
||||
self.provider = OllamaProvider(base_url=base_url, model=model, **kwargs)
|
||||
self.model = model
|
||||
self.base_url = base_url
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Ollama provider is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
def generate(self, prompt: str, **kwargs) -> str:
|
||||
"""
|
||||
Generate text from prompt.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options (temperature, max_tokens, etc.)
|
||||
|
||||
Returns:
|
||||
Generated text response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Ollama provider not available. Make sure Ollama is running "
|
||||
"and reachable at the configured base_url."
|
||||
)
|
||||
return self.provider.generate(prompt, **kwargs)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> Union[Dict[str, Any], List[Any]]:
|
||||
"""
|
||||
Generate structured JSON output.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
Parsed JSON response. A dict for a top-level JSON object, or a
|
||||
list if the model returns a top-level JSON array.
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or parsing fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Ollama provider not available. Make sure Ollama is running "
|
||||
"and reachable at the configured base_url."
|
||||
)
|
||||
return self.provider.generate_structured(prompt, **kwargs)
|
||||
|
||||
def generate_typed(self, prompt: str, schema: Any, max_retries: int = 3, **kwargs) -> Any:
|
||||
"""
|
||||
Generate output validated against a Pydantic schema.
|
||||
|
||||
Args:
|
||||
prompt: Input prompt text
|
||||
schema: Pydantic model class to validate the output against
|
||||
max_retries: Number of retries if validation fails (default: 3)
|
||||
**kwargs: Generation options
|
||||
|
||||
Returns:
|
||||
An instance of `schema`, populated from the model's response
|
||||
|
||||
Raises:
|
||||
ProcessingError: If provider is not available or generation fails
|
||||
"""
|
||||
if not self.is_available():
|
||||
raise ProcessingError(
|
||||
"Ollama provider not available. Make sure Ollama is running "
|
||||
"and reachable at the configured base_url."
|
||||
)
|
||||
return self.provider.generate_typed(prompt, schema, max_retries=max_retries, **kwargs)
|
||||
@@ -41,132 +41,67 @@ from ..utils.logging import get_logger
|
||||
from ..utils.progress_tracker import get_progress_tracker
|
||||
from .reasoner import Fact, Rule, _make_activation_key
|
||||
|
||||
logger = get_logger("rete_engine")
|
||||
|
||||
def _extract_bindings(condition: Any, fact: Fact) -> Dict[str, Any]:
|
||||
"""Extract ``?var`` bindings by matching a condition pattern against a fact.
|
||||
|
||||
def _build_condition_regex(
|
||||
pattern: str,
|
||||
initial_bindings: Optional[Dict[str, str]] = None,
|
||||
) -> str:
|
||||
"""Build an anchored regex string for a condition pattern.
|
||||
``condition`` is the pattern stored on the alpha node (typically a string
|
||||
like ``"Person(?x)"``); ``fact`` is the working-memory :class:`Fact`. The
|
||||
fact's canonical string form (``Predicate(arg1, arg2, ...)``) is matched
|
||||
against the pattern using the same ``?\\w+`` placeholder convention as the
|
||||
Reasoner, so downstream actions receive real bindings (e.g. ``{"x": "John"}``)
|
||||
instead of the empty dict that previously left ``?x`` placeholders
|
||||
unsubstituted.
|
||||
|
||||
Splits the pattern on ``?var`` placeholders, escaping the literal
|
||||
segments so surrounding parentheses/commas match literally. Variables
|
||||
become named groups (or backreferences when repeated); variables already
|
||||
present in ``initial_bindings`` are inlined as their literal value.
|
||||
|
||||
Args:
|
||||
pattern: The condition pattern string (e.g. ``"Person(?x)"``).
|
||||
initial_bindings: Bindings already established upstream. Variables
|
||||
already bound are matched as literals rather than captured.
|
||||
|
||||
Returns:
|
||||
An anchored regex string (``^...$``) suitable for ``re.compile`` /
|
||||
``re.match``.
|
||||
Returns an empty dict when the condition is not a string pattern or does
|
||||
not match -- callers treat that as "no bindings extracted".
|
||||
"""
|
||||
bindings = initial_bindings or {}
|
||||
segments = re.split(r"(\?\w+)", pattern)
|
||||
if not isinstance(condition, str):
|
||||
return {}
|
||||
|
||||
segments = re.split(r"(\?\w+)", condition)
|
||||
seen_vars: Set[str] = set()
|
||||
p_regex = ""
|
||||
for seg in segments:
|
||||
if seg.startswith("?"):
|
||||
var_name = seg[1:]
|
||||
if var_name in bindings:
|
||||
# Already bound — require the exact literal value.
|
||||
p_regex += re.escape(bindings[var_name])
|
||||
elif var_name in seen_vars:
|
||||
# Same variable used twice — enforce a backreference.
|
||||
if var_name in seen_vars:
|
||||
p_regex += f"(?P={var_name})"
|
||||
else:
|
||||
p_regex += f"(?P<{var_name}>.+?)"
|
||||
seen_vars.add(var_name)
|
||||
else:
|
||||
p_regex += re.escape(seg)
|
||||
return f"^{p_regex}$"
|
||||
|
||||
|
||||
def unify_condition(
|
||||
condition: Any,
|
||||
fact: Fact,
|
||||
initial_bindings: Optional[Dict[str, str]] = None,
|
||||
) -> Optional[Dict[str, str]]:
|
||||
"""Unify a condition pattern against a fact.
|
||||
|
||||
A condition is a pattern string such as ``"Person(?x)"`` or
|
||||
``"knows(?x, ?y)"`` where tokens beginning with ``?`` are variables.
|
||||
The fact is rendered via its ``__str__`` representation
|
||||
(``predicate(arg1, arg2)``) and matched against the pattern.
|
||||
|
||||
This mirrors ``Reasoner._match_pattern`` but is self-contained so the
|
||||
RETE engine does not need a live ``Reasoner`` instance.
|
||||
|
||||
Args:
|
||||
condition: The condition pattern (string). Non-string conditions
|
||||
are stringified before matching.
|
||||
fact: The fact to test.
|
||||
initial_bindings: Bindings already established upstream. Variables
|
||||
already bound must match the corresponding literal in the fact.
|
||||
|
||||
Returns:
|
||||
A dict of variable bindings if the fact unifies with the condition,
|
||||
otherwise ``None``.
|
||||
"""
|
||||
bindings = dict(initial_bindings or {})
|
||||
pattern = condition if isinstance(condition, str) else str(condition)
|
||||
fact_str = str(fact)
|
||||
|
||||
# Build the anchored regex once (variables already bound are inlined as
|
||||
# literals). See ``_build_condition_regex`` for the segment handling.
|
||||
p_regex = _build_condition_regex(pattern, bindings)
|
||||
p_regex = f"^{p_regex}$"
|
||||
|
||||
try:
|
||||
match = re.match(p_regex, fact_str)
|
||||
except re.error as e:
|
||||
logger.warning(
|
||||
"unify_condition failed to compile/match condition "
|
||||
"%r (regex: %r) against fact %r: %s",
|
||||
pattern,
|
||||
p_regex,
|
||||
fact_str,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
except Exception as e: # noqa: BLE001 - mirror Reasoner._match_pattern
|
||||
logger.warning(
|
||||
"unify_condition unexpected error matching condition "
|
||||
"%r (regex: %r) against fact %r: %s",
|
||||
pattern,
|
||||
p_regex,
|
||||
fact_str,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
match = re.match(p_regex, str(fact))
|
||||
except re.error:
|
||||
return {}
|
||||
if not match:
|
||||
return None
|
||||
|
||||
for var, value in match.groupdict().items():
|
||||
if var in bindings and bindings[var] != value:
|
||||
return None # Binding conflict.
|
||||
bindings[var] = value
|
||||
return bindings
|
||||
return {}
|
||||
return {k: v for k, v in match.groupdict().items() if v is not None}
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
"""A partial match flowing through the Rete network.
|
||||
def _bindings_for_rule(rule: Rule, facts: List[Fact]) -> Dict[str, Any]:
|
||||
"""Merge ``?var`` bindings from matching a rule's conditions against facts.
|
||||
|
||||
A token represents an ordered collection of concrete facts that have
|
||||
been unified so far, together with the consistent variable bindings
|
||||
accumulated across those facts.
|
||||
|
||||
Alpha nodes emit single-fact tokens. Beta nodes merge a left token and
|
||||
a right token into a new token whose ``facts`` are the concatenation of
|
||||
both sides (preserving condition order) and whose ``bindings`` are the
|
||||
consistent union of both sides.
|
||||
Each fact is matched against every condition of the rule; the first
|
||||
condition that yields bindings for a fact contributes them. Bindings from
|
||||
all facts are merged so multi-condition (joined) rules receive the full
|
||||
variable environment. Later conflicting values do not overwrite earlier
|
||||
ones, preserving the binding that a join already validated.
|
||||
"""
|
||||
|
||||
facts: List[Fact] = field(default_factory=list)
|
||||
bindings: Dict[str, str] = field(default_factory=dict)
|
||||
bindings: Dict[str, Any] = {}
|
||||
for fact in facts:
|
||||
for condition in rule.conditions:
|
||||
extracted = _extract_bindings(condition, fact)
|
||||
if not extracted:
|
||||
continue
|
||||
for key, value in extracted.items():
|
||||
bindings.setdefault(key, value)
|
||||
break
|
||||
return bindings
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -193,68 +128,19 @@ class AlphaNode(ReteNode):
|
||||
def __init__(self, node_id: str, condition: Any):
|
||||
super().__init__(node_id)
|
||||
self.condition = condition
|
||||
# Single-fact tokens produced by unifying each matched fact with
|
||||
# this node's condition.
|
||||
self.tokens: List[Token] = []
|
||||
# Pre-compile the condition regex once. Alpha nodes never have
|
||||
# initial bindings, so the pattern is stable for the node's lifetime
|
||||
# and every incoming fact reuses this compiled matcher instead of
|
||||
# rebuilding it (avoids repeated regex construction overhead).
|
||||
pattern = condition if isinstance(condition, str) else str(condition)
|
||||
self._compiled: Optional[re.Pattern] = None
|
||||
try:
|
||||
self._compiled = re.compile(_build_condition_regex(pattern))
|
||||
except re.error as e:
|
||||
logger.warning(
|
||||
"AlphaNode %r failed to compile condition %r: %s; "
|
||||
"node will never match",
|
||||
node_id,
|
||||
pattern,
|
||||
e,
|
||||
)
|
||||
self.matches: List[Fact] = []
|
||||
|
||||
def add_fact(self, fact: Fact) -> Optional[Token]:
|
||||
"""Add fact if it matches the condition, returning its token.
|
||||
def add_fact(self, fact: Fact) -> bool:
|
||||
"""Add fact if it matches condition."""
|
||||
if self._matches(fact):
|
||||
self.matches.append(fact)
|
||||
return True
|
||||
return False
|
||||
|
||||
Returns the single-fact ``Token`` produced by unification when the
|
||||
fact matches, otherwise ``None``.
|
||||
"""
|
||||
bindings = self._matches(fact)
|
||||
if bindings is not None:
|
||||
token = Token(facts=[fact], bindings=dict(bindings))
|
||||
self.tokens.append(token)
|
||||
return token
|
||||
return None
|
||||
|
||||
def _matches(self, fact: Fact) -> Optional[Dict[str, str]]:
|
||||
"""Check if fact matches the alpha node condition.
|
||||
|
||||
Uses the pre-compiled regex built in ``__init__`` for performance,
|
||||
since RETE evaluates many facts against every alpha node.
|
||||
|
||||
Returns the variable bindings produced by unification if the fact
|
||||
matches, otherwise ``None``. An empty dict signals a match with no
|
||||
variables (still distinct from ``None``).
|
||||
"""
|
||||
if self._compiled is None:
|
||||
# Compilation failed at build time; treat as non-matching.
|
||||
return None
|
||||
fact_str = str(fact)
|
||||
try:
|
||||
match = self._compiled.match(fact_str)
|
||||
except Exception as e: # noqa: BLE001 - mirror unify_condition
|
||||
logger.warning(
|
||||
"AlphaNode %r unexpected error matching condition "
|
||||
"%r against fact %r: %s",
|
||||
self.node_id,
|
||||
self.condition,
|
||||
fact_str,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
if not match:
|
||||
return None
|
||||
return match.groupdict()
|
||||
def _matches(self, fact: Fact) -> bool:
|
||||
"""Check if fact matches condition."""
|
||||
# Simple matching - can be enhanced
|
||||
return True
|
||||
|
||||
|
||||
class BetaNode(ReteNode):
|
||||
@@ -264,28 +150,19 @@ class BetaNode(ReteNode):
|
||||
super().__init__(node_id)
|
||||
self.left = left
|
||||
self.right = right
|
||||
# Token memories for each side. Incoming tokens are stored here so
|
||||
# that later-arriving tokens on the opposite side can be joined
|
||||
# against every token already seen (chained joins).
|
||||
self.left_tokens: List[Token] = []
|
||||
self.right_tokens: List[Token] = []
|
||||
self.matches: List[Tuple[Fact, Fact]] = []
|
||||
|
||||
def join(self, left_token: Token, right_token: Token) -> Optional[Token]:
|
||||
"""Join a left token with a right token.
|
||||
def join(self, left_fact: Fact, right_fact: Fact) -> bool:
|
||||
"""Join facts from left and right nodes."""
|
||||
if self._can_join(left_fact, right_fact):
|
||||
self.matches.append((left_fact, right_fact))
|
||||
return True
|
||||
return False
|
||||
|
||||
Returns a new merged ``Token`` (facts concatenated in condition
|
||||
order, bindings unified) when the two tokens are consistent,
|
||||
otherwise ``None`` on a binding conflict.
|
||||
"""
|
||||
merged = dict(left_token.bindings)
|
||||
for var, value in right_token.bindings.items():
|
||||
if var in merged and merged[var] != value:
|
||||
return None # Binding conflict — cannot join.
|
||||
merged[var] = value
|
||||
return Token(
|
||||
facts=list(left_token.facts) + list(right_token.facts),
|
||||
bindings=merged,
|
||||
)
|
||||
def _can_join(self, left_fact: Fact, right_fact: Fact) -> bool:
|
||||
"""Check if facts can be joined."""
|
||||
# Simple join logic - can be enhanced
|
||||
return True
|
||||
|
||||
|
||||
class TerminalNode(ReteNode):
|
||||
@@ -371,16 +248,12 @@ class ReteEngine:
|
||||
self._add_rule_to_network(rule)
|
||||
|
||||
self.logger.info(
|
||||
f"Built Rete network with {len(self.network)} nodes "
|
||||
f"for {len(rules)} rules"
|
||||
f"Built Rete network with {len(self.network)} nodes for {len(rules)} rules"
|
||||
)
|
||||
self.progress_tracker.stop_tracking(
|
||||
tracking_id,
|
||||
status="completed",
|
||||
message=(
|
||||
f"Built Rete network with {len(self.network)} nodes "
|
||||
f"for {len(rules)} rules"
|
||||
),
|
||||
message=f"Built Rete network with {len(self.network)} nodes for {len(rules)} rules",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
@@ -408,10 +281,6 @@ class ReteEngine:
|
||||
self.node_counter += 1
|
||||
beta_node = BetaNode(node_id, current, alpha_nodes[i])
|
||||
self.network[node_id] = beta_node
|
||||
# Wire the beta node as a child of both its inputs so facts
|
||||
# propagating from either side reach the join.
|
||||
current.children.append(beta_node)
|
||||
alpha_nodes[i].children.append(beta_node)
|
||||
current = beta_node
|
||||
final_node = current
|
||||
else:
|
||||
@@ -442,58 +311,40 @@ class ReteEngine:
|
||||
# Find matching alpha nodes
|
||||
for node_id, node in self.network.items():
|
||||
if isinstance(node, AlphaNode):
|
||||
token = node.add_fact(fact)
|
||||
if token is not None:
|
||||
# Propagate the single-fact token to children.
|
||||
self._propagate_token(node, token)
|
||||
if node.add_fact(fact):
|
||||
# Propagate to children
|
||||
self._propagate_from_alpha(node, fact)
|
||||
|
||||
def _propagate_token(self, source: ReteNode, token: Token) -> None:
|
||||
"""Propagate ``token`` (arriving from ``source``) to its children.
|
||||
|
||||
A ``Token`` carries the ordered facts and consistent bindings of a
|
||||
partial match. Beta children attempt joins and, on success, emit a
|
||||
new merged token downstream; terminal children turn the token into a
|
||||
rule activation using the token's complete facts and bindings.
|
||||
"""
|
||||
for child in source.children:
|
||||
def _propagate_from_alpha(self, alpha_node: AlphaNode, fact: Fact) -> None:
|
||||
"""Propagate from alpha node to children."""
|
||||
for child in alpha_node.children:
|
||||
if isinstance(child, BetaNode):
|
||||
self._propagate_to_beta(child, source, token)
|
||||
# Join with matches from left side
|
||||
for left_fact in alpha_node.matches:
|
||||
if child.join(left_fact, fact):
|
||||
# Propagate to children
|
||||
for grandchild in child.children:
|
||||
if isinstance(grandchild, TerminalNode):
|
||||
facts = [left_fact, fact]
|
||||
match = Match(
|
||||
rule=grandchild.rule,
|
||||
facts=facts,
|
||||
bindings=_bindings_for_rule(
|
||||
grandchild.rule, facts
|
||||
),
|
||||
confidence=1.0,
|
||||
)
|
||||
grandchild.activate(match)
|
||||
elif isinstance(child, TerminalNode):
|
||||
# Direct activation
|
||||
match = Match(
|
||||
rule=child.rule,
|
||||
facts=list(token.facts),
|
||||
bindings=dict(token.bindings),
|
||||
facts=[fact],
|
||||
bindings=_bindings_for_rule(child.rule, [fact]),
|
||||
confidence=1.0,
|
||||
)
|
||||
child.activate(match)
|
||||
|
||||
def _propagate_to_beta(
|
||||
self,
|
||||
beta: "BetaNode",
|
||||
source: ReteNode,
|
||||
token: Token,
|
||||
) -> None:
|
||||
"""Attempt joins at ``beta`` for a token arriving from one side.
|
||||
|
||||
The incoming token is stored in the corresponding side's memory,
|
||||
then joined against every token already recorded on the opposite
|
||||
side. Each successful join produces a new merged token that is
|
||||
propagated further downstream, enabling correct chained joins across
|
||||
three or more conditions.
|
||||
"""
|
||||
if source is beta.left:
|
||||
beta.left_tokens.append(token)
|
||||
for right_token in list(beta.right_tokens):
|
||||
merged = beta.join(token, right_token)
|
||||
if merged is not None:
|
||||
self._propagate_token(beta, merged)
|
||||
elif source is beta.right:
|
||||
beta.right_tokens.append(token)
|
||||
for left_token in list(beta.left_tokens):
|
||||
merged = beta.join(left_token, token)
|
||||
if merged is not None:
|
||||
self._propagate_token(beta, merged)
|
||||
|
||||
def match_patterns(self, facts: Optional[List[Fact]] = None) -> List[Match]:
|
||||
"""
|
||||
Match patterns using Rete algorithm.
|
||||
@@ -617,11 +468,8 @@ class ReteEngine:
|
||||
self.facts.clear()
|
||||
self.reset_action_history()
|
||||
for node in self.network.values():
|
||||
if isinstance(node, AlphaNode):
|
||||
node.tokens.clear()
|
||||
elif isinstance(node, BetaNode):
|
||||
node.left_tokens.clear()
|
||||
node.right_tokens.clear()
|
||||
if isinstance(node, AlphaNode) or isinstance(node, BetaNode):
|
||||
node.matches.clear()
|
||||
elif isinstance(node, TerminalNode):
|
||||
node.activations.clear()
|
||||
|
||||
|
||||
@@ -673,7 +673,6 @@ class GeminiProvider(BaseProvider):
|
||||
self.model = model
|
||||
self.client = None
|
||||
self._use_new_genai = False
|
||||
self._legacy_model_cache: Dict[str, Any] = {}
|
||||
self._init_client()
|
||||
|
||||
def _init_client(self):
|
||||
@@ -695,38 +694,6 @@ class GeminiProvider(BaseProvider):
|
||||
self.client = None
|
||||
self.logger.warning("Gemini SDK not installed. Install with: pip install semantica[llm-gemini]")
|
||||
|
||||
def _legacy_client_for(self, requested_model: str):
|
||||
"""Return a legacy-SDK GenerativeModel bound to this instance's own
|
||||
API key, for the given model name.
|
||||
|
||||
The legacy google-generativeai package keeps its API key as
|
||||
module-level state (genai.configure()), so any GenerativeModel built
|
||||
by a different GeminiProvider instance in the same process can leave
|
||||
that state pointing at a different key. Re-asserting configure()
|
||||
with this instance's key right before use, instead of only once at
|
||||
construction, keeps sequential calls across instances from reading
|
||||
each other's credentials. A cache keyed by model name avoids
|
||||
rebuilding a GenerativeModel on every call for the common case of
|
||||
one model being reused.
|
||||
"""
|
||||
try:
|
||||
import google.generativeai as old_genai
|
||||
old_genai.configure(api_key=self.api_key)
|
||||
except Exception:
|
||||
# _init_client() already required this import to reach the
|
||||
# legacy path in the first place, so this only happens when
|
||||
# self.client was injected directly (tests). Fall back to it
|
||||
# without reasserting credentials rather than failing calls
|
||||
# that never needed the real SDK.
|
||||
return self.client
|
||||
if requested_model == self.model:
|
||||
return self.client
|
||||
cached = self._legacy_model_cache.get(requested_model)
|
||||
if cached is None:
|
||||
cached = old_genai.GenerativeModel(requested_model)
|
||||
self._legacy_model_cache[requested_model] = cached
|
||||
return cached
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if provider is available."""
|
||||
return self.client is not None
|
||||
@@ -757,8 +724,7 @@ class GeminiProvider(BaseProvider):
|
||||
)
|
||||
return self._resp_text(resp)
|
||||
else:
|
||||
legacy_client = self._legacy_client_for(kwargs.get("model", self.model))
|
||||
response = legacy_client.generate_content(prompt, generation_config=config or None)
|
||||
response = self.client.generate_content(prompt, generation_config=config or None)
|
||||
return self._resp_text(response)
|
||||
|
||||
def generate_structured(self, prompt: str, **kwargs) -> dict:
|
||||
@@ -767,24 +733,15 @@ class GeminiProvider(BaseProvider):
|
||||
raise ProcessingError("Gemini client not initialized.")
|
||||
|
||||
json_prompt = f"{prompt}\n\nReturn the response as valid JSON only."
|
||||
|
||||
config = {}
|
||||
self._add_if_set(config, kwargs, "temperature", "top_p", "top_k", "stop_sequences", "candidate_count")
|
||||
if "max_tokens" in kwargs:
|
||||
config["max_output_tokens"] = kwargs["max_tokens"]
|
||||
|
||||
if self._use_new_genai:
|
||||
model = kwargs.get("model", self.model)
|
||||
resp = self.client.models.generate_content(
|
||||
model=model, contents=json_prompt, config=config or None
|
||||
)
|
||||
resp = self.client.models.generate_content(model=model, contents=json_prompt)
|
||||
try:
|
||||
return self._parse_json(self._resp_text(resp))
|
||||
except Exception as e:
|
||||
raise ProcessingError(f"Failed to parse JSON from Gemini response: {e}")
|
||||
else:
|
||||
legacy_client = self._legacy_client_for(kwargs.get("model", self.model))
|
||||
response = legacy_client.generate_content(json_prompt, generation_config=config or None)
|
||||
response = self.client.generate_content(json_prompt)
|
||||
try:
|
||||
return self._parse_json(self._resp_text(response))
|
||||
except Exception as e:
|
||||
@@ -1010,8 +967,6 @@ class OllamaProvider(BaseProvider):
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if provider is available."""
|
||||
if self.client is None:
|
||||
self._init_client()
|
||||
return self.client is not None
|
||||
|
||||
def _build_options(self, kwargs: dict) -> Optional[dict]:
|
||||
@@ -1063,6 +1018,7 @@ class DeepSeekProvider(BaseProvider):
|
||||
self.api_key = api_key or config.get_api_key("deepseek")
|
||||
self.base_url = "https://api.deepseek.com/v1"
|
||||
self.model = model
|
||||
self.base_url = "https://api.deepseek.com/v1"
|
||||
self.client = None
|
||||
self._init_client()
|
||||
|
||||
@@ -1089,7 +1045,7 @@ class DeepSeekProvider(BaseProvider):
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
return response.choices[0].message.content
|
||||
@@ -1102,9 +1058,8 @@ class DeepSeekProvider(BaseProvider):
|
||||
create_kwargs = {
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
try:
|
||||
@@ -1148,7 +1103,7 @@ class NovitaProvider(BaseProvider):
|
||||
"model": kwargs.get("model", self.model),
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
return response.choices[0].message.content
|
||||
@@ -1163,7 +1118,7 @@ class NovitaProvider(BaseProvider):
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"response_format": {"type": "json_object"},
|
||||
}
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens", "top_p", "frequency_penalty", "presence_penalty", "seed", "stop", "user")
|
||||
self._add_if_set(create_kwargs, kwargs, "temperature", "max_tokens")
|
||||
|
||||
response = self.client.chat.completions.create(**create_kwargs)
|
||||
try:
|
||||
|
||||
@@ -64,8 +64,6 @@ class SlidingWindowChunker:
|
||||
raise ValidationError("overlap must be non-negative")
|
||||
if self.overlap >= self.chunk_size:
|
||||
raise ValidationError("overlap must be less than chunk_size")
|
||||
if self.stride <= 0:
|
||||
raise ValidationError("stride must be positive")
|
||||
|
||||
def chunk(self, text: str, **options) -> List[Chunk]:
|
||||
"""
|
||||
@@ -217,20 +215,15 @@ class SlidingWindowChunker:
|
||||
Returns:
|
||||
list: List of chunks
|
||||
"""
|
||||
if overlap_size is None:
|
||||
return self.chunk(text)
|
||||
if overlap_size < 0:
|
||||
raise ValidationError("overlap_size must be non-negative")
|
||||
if overlap_size >= self.chunk_size:
|
||||
raise ValidationError("overlap_size must be less than chunk_size")
|
||||
|
||||
original_overlap = self.overlap
|
||||
original_stride = self.stride
|
||||
|
||||
try:
|
||||
if overlap_size is not None:
|
||||
self.overlap = overlap_size
|
||||
self.stride = self.chunk_size - self.overlap
|
||||
return self.chunk(text)
|
||||
finally:
|
||||
self.overlap = original_overlap
|
||||
self.stride = original_stride
|
||||
|
||||
chunks = self.chunk(text)
|
||||
|
||||
# Restore original overlap
|
||||
self.overlap = original_overlap
|
||||
self.stride = self.chunk_size - self.overlap
|
||||
|
||||
return chunks
|
||||
|
||||
@@ -43,7 +43,7 @@ from contextlib import contextmanager
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, TextIO, Tuple, Union
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
from .logging import get_logger
|
||||
|
||||
@@ -144,37 +144,14 @@ class ProgressDisplay(ABC):
|
||||
class ConsoleProgressDisplay(ProgressDisplay):
|
||||
"""Console progress display with real-time updates."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
use_emoji: bool = True,
|
||||
update_interval: float = 0.1,
|
||||
stream: Optional[TextIO] = None,
|
||||
):
|
||||
"""Initialize the console display.
|
||||
|
||||
Args:
|
||||
use_emoji: Whether to decorate output with emoji.
|
||||
update_interval: Minimum seconds between redraws.
|
||||
stream: Where progress is written. Defaults to ``sys.stderr``.
|
||||
|
||||
Progress is diagnostic output, so stderr is the correct stream
|
||||
for it, and stdout must stay clean for programs that carry a
|
||||
machine-readable protocol on it — the stdio MCP servers put
|
||||
newline-delimited JSON-RPC there, and a progress bar on stdout
|
||||
corrupts that stream.
|
||||
|
||||
Left as ``None``, the stream is resolved on each write rather
|
||||
than captured here, so a later rebinding of ``sys.stderr``
|
||||
(pytest capture, for instance) is honoured.
|
||||
"""
|
||||
def __init__(self, use_emoji: bool = True, update_interval: float = 0.1):
|
||||
self.use_emoji = use_emoji
|
||||
self._stream = stream
|
||||
|
||||
# Check if the target stream supports emojis (especially on Windows)
|
||||
|
||||
# Check if stdout supports emojis (especially on Windows)
|
||||
if self.use_emoji:
|
||||
try:
|
||||
# Try encoding a test emoji with the stream's encoding
|
||||
encoding = getattr(self.stream, "encoding", None)
|
||||
# Try encoding a test emoji with stdout's encoding
|
||||
encoding = getattr(sys.stdout, "encoding", None)
|
||||
if encoding:
|
||||
"🧠".encode(encoding)
|
||||
except (UnicodeEncodeError, LookupError, AttributeError):
|
||||
@@ -185,11 +162,6 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
self.current_lines: Dict[str, str] = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
@property
|
||||
def stream(self) -> TextIO:
|
||||
"""The stream progress is written to; ``sys.stderr`` unless overridden."""
|
||||
return self._stream if self._stream is not None else sys.stderr
|
||||
|
||||
def _should_update(self) -> bool:
|
||||
"""Check if enough time has passed for update."""
|
||||
now = time.time()
|
||||
@@ -287,16 +259,15 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
return f"{base_msg}: {message}"
|
||||
|
||||
def _safe_write(self, text: str) -> None:
|
||||
"""Safely write text to the progress stream handling encoding errors."""
|
||||
stream = self.stream
|
||||
"""Safely write text to stdout handling encoding errors."""
|
||||
try:
|
||||
stream.write(text)
|
||||
sys.stdout.write(text)
|
||||
except UnicodeEncodeError:
|
||||
# Fallback: encode with replacement and write decoded
|
||||
# Use ascii as safe fallback if encoding is unknown or caused error
|
||||
encoding = getattr(stream, "encoding", None) or "ascii"
|
||||
encoding = getattr(sys.stdout, "encoding", None) or "ascii"
|
||||
safe_text = text.encode(encoding, errors="replace").decode(encoding)
|
||||
stream.write(safe_text)
|
||||
sys.stdout.write(safe_text)
|
||||
|
||||
def update(self, item: ProgressItem) -> None:
|
||||
"""Update console progress display."""
|
||||
@@ -331,11 +302,11 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
self._display_item_line(pipeline_item)
|
||||
self._safe_write("\n")
|
||||
|
||||
self.stream.flush()
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
# Original single-item display
|
||||
self._display_item_line(item)
|
||||
self.stream.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
def _display_item_line(self, item: ProgressItem) -> None:
|
||||
"""Display a single progress item line."""
|
||||
@@ -497,13 +468,13 @@ class ConsoleProgressDisplay(ProgressDisplay):
|
||||
f"Completed: {completed} | Failed: {failed} | Total Time: {total_time:.2f}s\n"
|
||||
)
|
||||
self._safe_write("=" * 80 + "\n")
|
||||
self.stream.flush()
|
||||
sys.stdout.flush()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""Clear console display."""
|
||||
with self.lock:
|
||||
self._safe_write("\r" + " " * 100 + "\r")
|
||||
self.stream.flush()
|
||||
sys.stdout.flush()
|
||||
self.current_lines.clear()
|
||||
|
||||
|
||||
|
||||
@@ -66,32 +66,12 @@ class FAISSIndex:
|
||||
self.metadata: Dict[str, Dict[str, Any]] = {}
|
||||
|
||||
def add_vectors(self, vectors: np.ndarray, ids: Optional[List[str]] = None):
|
||||
"""
|
||||
Add vectors to index.
|
||||
|
||||
Skips any id already present in vector_ids rather than appending a
|
||||
second physical vector under the same id. FAISS indices here don't
|
||||
support removing or replacing a single vector in place, so an
|
||||
"update" isn't possible; without this check, re-running an add for
|
||||
ids that already exist (e.g. retrying an interrupted migration)
|
||||
would silently duplicate vectors under the same id on every retry.
|
||||
"""
|
||||
"""Add vectors to index."""
|
||||
if ids is None:
|
||||
ids = [f"vec_{i}" for i in range(len(vectors))]
|
||||
|
||||
new_rows = []
|
||||
new_ids = []
|
||||
existing = set(self.vector_ids)
|
||||
for row, vec_id in zip(vectors, ids):
|
||||
if vec_id in existing:
|
||||
continue
|
||||
new_rows.append(row)
|
||||
new_ids.append(vec_id)
|
||||
existing.add(vec_id)
|
||||
|
||||
if new_rows:
|
||||
self.index.add(np.array(new_rows, dtype=np.float32))
|
||||
self.vector_ids.extend(new_ids)
|
||||
self.index.add(vectors.astype(np.float32))
|
||||
self.vector_ids.extend(ids)
|
||||
|
||||
def search(
|
||||
self, query_vectors: np.ndarray, k: int = 10
|
||||
@@ -325,12 +305,6 @@ class FAISSStore:
|
||||
"""
|
||||
Add vectors to index.
|
||||
|
||||
Any id that already exists in the index is skipped rather than
|
||||
stored as a second physical vector under the same id (see
|
||||
FAISSIndex.add_vectors), so calling this again with ids from a
|
||||
previous call is safe and doesn't accumulate duplicates. Metadata
|
||||
for those ids is still updated.
|
||||
|
||||
Args:
|
||||
vectors: List of vectors or numpy array
|
||||
ids: Vector IDs
|
||||
@@ -338,8 +312,7 @@ class FAISSStore:
|
||||
**options: Additional options
|
||||
|
||||
Returns:
|
||||
List of vector IDs (including ids that were already present
|
||||
and therefore not re-added as new vectors)
|
||||
List of vector IDs
|
||||
"""
|
||||
num_vectors = len(vectors) if isinstance(vectors, (list, np.ndarray)) else 1
|
||||
tracking_id = self.progress_tracker.start_tracking(
|
||||
@@ -553,30 +526,6 @@ class FAISSStore:
|
||||
|
||||
return results
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors in insertion order.
|
||||
|
||||
Args:
|
||||
offset: Number of vectors to skip
|
||||
limit: Maximum number of vectors to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if self.index is None or limit <= 0:
|
||||
return []
|
||||
|
||||
ids_page = self.index.vector_ids[offset:offset + limit]
|
||||
return [
|
||||
{
|
||||
"id": vector_id,
|
||||
"metadata": self.get_metadata(vector_id) or {},
|
||||
"vector": self.get_vector(vector_id),
|
||||
}
|
||||
for vector_id in ids_page
|
||||
]
|
||||
|
||||
def get_stats(self) -> Dict[str, Any]:
|
||||
"""Get index statistics."""
|
||||
if self.index is None:
|
||||
|
||||
@@ -656,52 +656,6 @@ class PgVectorStore:
|
||||
self.logger.warning(f"Failed to get metadata for {vector_id}: {e}")
|
||||
return None
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors ordered by id.
|
||||
|
||||
Args:
|
||||
offset: Number of rows to skip
|
||||
limit: Maximum number of rows to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if not PSYCOPG3_AVAILABLE and not PSYCOPG2_AVAILABLE:
|
||||
raise ProcessingError(
|
||||
"Neither psycopg3 nor psycopg2 is available. "
|
||||
"Install with: pip install psycopg[binary] or psycopg2-binary"
|
||||
)
|
||||
|
||||
if limit <= 0:
|
||||
return []
|
||||
|
||||
scan_sql = psycopg_sql.SQL("""
|
||||
SELECT id, vector, metadata
|
||||
FROM {}
|
||||
ORDER BY id
|
||||
LIMIT %s OFFSET %s
|
||||
""").format(psycopg_sql.Identifier(self.table_name))
|
||||
|
||||
with self._get_connection() as conn:
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(scan_sql, (limit, offset))
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
vec_id, vec, meta = row
|
||||
results.append({
|
||||
"id": vec_id,
|
||||
"metadata": meta if isinstance(meta, dict) else json.loads(meta) if meta else {},
|
||||
"vector": np.array(vec) if vec is not None else None,
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
raise ProcessingError(f"Failed to scan vectors: {str(e)}") from e
|
||||
|
||||
def filter_by_metadata(
|
||||
self, filters: Dict[str, Any], limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -616,49 +616,6 @@ class SQLiteVecStore:
|
||||
self.logger.warning(f"Failed to get metadata for {vector_id}: {e}")
|
||||
return None
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors ordered by id.
|
||||
|
||||
Args:
|
||||
offset: Number of rows to skip
|
||||
limit: Maximum number of rows to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if limit <= 0:
|
||||
return []
|
||||
|
||||
query_sql = f"""
|
||||
SELECT id, embedding, metadata
|
||||
FROM {self.table_name}
|
||||
ORDER BY id
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
|
||||
with self._lock, self._get_connection() as conn:
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute(query_sql, (limit, offset))
|
||||
rows = cur.fetchall()
|
||||
cur.close()
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
vec_id, embedding_blob, meta_json = row
|
||||
vec = None
|
||||
if embedding_blob:
|
||||
vec = np.frombuffer(embedding_blob, dtype=np.float32).copy()
|
||||
results.append({
|
||||
"id": vec_id,
|
||||
"metadata": json.loads(meta_json) if meta_json else {},
|
||||
"vector": vec,
|
||||
})
|
||||
return results
|
||||
except Exception as e:
|
||||
raise ProcessingError(f"Failed to scan vectors: {str(e)}") from e
|
||||
|
||||
def filter_by_metadata(
|
||||
self, filters: Dict[str, Any], limit: int = 10
|
||||
) -> List[Dict[str, Any]]:
|
||||
|
||||
@@ -824,64 +824,6 @@ class VectorStore:
|
||||
else:
|
||||
raise NotImplementedError(f"Backend store {type(self._backend_store).__name__} does not implement get_metadata")
|
||||
|
||||
def scan_vectors(self, offset: int = 0, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Page through stored vectors, backend-agnostic.
|
||||
|
||||
Follows the get_vector()/get_metadata() precedent (#843): the inmemory
|
||||
backend pages its local dict directly, a persistent backend delegates
|
||||
to a scan_vectors() on the wrapped store when available, and one that
|
||||
cannot enumerate its contents raises NotImplementedError rather than
|
||||
silently returning an empty page.
|
||||
|
||||
Args:
|
||||
offset: Number of vectors to skip
|
||||
limit: Maximum number of vectors to return
|
||||
|
||||
Returns:
|
||||
List of result dicts with 'id', 'metadata', and 'vector'
|
||||
"""
|
||||
if limit <= 0:
|
||||
return []
|
||||
|
||||
if self.backend == "inmemory":
|
||||
ids_page = list(self.vectors.keys())[offset:offset + limit]
|
||||
return [
|
||||
{
|
||||
"id": vec_id,
|
||||
"metadata": self.metadata.get(vec_id, {}),
|
||||
"vector": self.vectors.get(vec_id),
|
||||
}
|
||||
for vec_id in ids_page
|
||||
]
|
||||
elif self._backend_store and hasattr(self._backend_store, "scan_vectors"):
|
||||
return self._backend_store.scan_vectors(offset=offset, limit=limit)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"Backend store {type(self._backend_store).__name__} does not "
|
||||
"implement scan_vectors(). Add a scan_vectors() method to the "
|
||||
"backend store adapter to enable enumeration for this backend."
|
||||
)
|
||||
|
||||
def iter_vectors(self, batch_size: int = 500):
|
||||
"""
|
||||
Iterate over every stored vector, one page at a time.
|
||||
|
||||
Args:
|
||||
batch_size: Number of vectors to fetch per underlying scan_vectors() call
|
||||
|
||||
Yields:
|
||||
Result dicts with 'id', 'metadata', and 'vector', in scan order
|
||||
"""
|
||||
offset = 0
|
||||
while True:
|
||||
page = self.scan_vectors(offset=offset, limit=batch_size)
|
||||
if not page:
|
||||
return
|
||||
for item in page:
|
||||
yield item
|
||||
offset += len(page)
|
||||
|
||||
def count(self) -> int:
|
||||
"""Return the number of vectors in the store, backend-agnostic.
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
"""AgentMemory.find_by_entity returns all matches by default (#1018).
|
||||
|
||||
The previous default limit of 10 silently truncated results, making erasure
|
||||
workflows incomplete for entities with more than 10 memories: a caller
|
||||
computing "what references this entity" from a truncated page would leave
|
||||
the remainder live. The unbounded default is deliberate — an erasure check
|
||||
cannot paginate — while callers that want a page still pass an explicit
|
||||
limit. (Previously lived in tests/test_seed_manager.py; moved to the
|
||||
AgentMemory area per review.)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
|
||||
|
||||
from semantica.context.agent_memory import AgentMemory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def memory_with_15():
|
||||
mem = AgentMemory()
|
||||
for i in range(15):
|
||||
mem.store(
|
||||
content=f"fact {i} about entity",
|
||||
entities=[{"id": "e1", "name": "Entity", "type": "thing"}],
|
||||
)
|
||||
return mem
|
||||
|
||||
|
||||
class TestFindByEntityLimit:
|
||||
def test_returns_all_matches_by_default(self, memory_with_15):
|
||||
results = memory_with_15.find_by_entity("e1")
|
||||
assert len(results) == 15, f"expected 15 (all), got {len(results)}"
|
||||
|
||||
def test_explicit_limit_still_works(self, memory_with_15):
|
||||
assert len(memory_with_15.find_by_entity("e1", limit=5)) == 5
|
||||
|
||||
def test_no_matches_returns_empty(self):
|
||||
assert AgentMemory().find_by_entity("nonexistent") == []
|
||||
@@ -5,23 +5,14 @@ This module tests the decision tracking data models including
|
||||
validation, serialization, and deserialization.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Any
|
||||
|
||||
from semantica.context.decision_models import (
|
||||
ApprovalChain,
|
||||
Decision,
|
||||
DecisionContext,
|
||||
Policy,
|
||||
PolicyException,
|
||||
Precedent,
|
||||
deserialize_decision,
|
||||
deserialize_policy,
|
||||
serialize_decision,
|
||||
serialize_policy,
|
||||
validate_decision,
|
||||
validate_policy,
|
||||
Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain,
|
||||
validate_decision, validate_policy, serialize_decision, deserialize_decision,
|
||||
serialize_policy, deserialize_policy
|
||||
)
|
||||
|
||||
|
||||
@@ -461,82 +452,5 @@ class TestSerializationFunctions:
|
||||
assert deserialized.metadata == original_policy.metadata
|
||||
|
||||
|
||||
class TestAutoGenerateIdContract:
|
||||
"""Test the auto_generate_id InitVar contract across all decision models.
|
||||
|
||||
Regression coverage for the InitVar fix: previously ``auto_generate_id``
|
||||
was a plain ``__post_init__`` parameter that dataclass-generated ``__init__``
|
||||
never forwarded, so the ``auto_generate_id=False`` branch was dead code and
|
||||
the "id is required" contract could never fire.
|
||||
"""
|
||||
|
||||
def _base_kwargs(self, cls):
|
||||
now = datetime.now()
|
||||
return {
|
||||
Decision: dict(
|
||||
decision_id="", category="c", scenario="s", reasoning="r",
|
||||
outcome="o", confidence=0.5, timestamp=now, decision_maker="m",
|
||||
),
|
||||
DecisionContext: dict(
|
||||
context_id="", decision_id="d", entity_snapshots={}, risk_factors=[],
|
||||
),
|
||||
Policy: dict(
|
||||
policy_id="", name="n", description="d", rules={}, category="c",
|
||||
version="1", created_at=now, updated_at=now,
|
||||
),
|
||||
PolicyException: dict(
|
||||
exception_id="", decision_id="d", policy_id="p", reason="r",
|
||||
approver="a", approval_timestamp=now, justification="j",
|
||||
),
|
||||
Precedent: dict(
|
||||
precedent_id="", source_decision_id="d", similarity_score=0.5,
|
||||
relationship_type="same_policy",
|
||||
),
|
||||
ApprovalChain: dict(
|
||||
approval_id="", decision_id="d", approver="a",
|
||||
approval_method="email", approval_context="x", timestamp=now,
|
||||
),
|
||||
}[cls]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cls",
|
||||
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
||||
)
|
||||
def test_auto_generate_id_is_not_a_field(self, cls):
|
||||
"""auto_generate_id must stay an InitVar, never a real dataclass field."""
|
||||
import dataclasses
|
||||
|
||||
names = [f.name for f in dataclasses.fields(cls)]
|
||||
assert "auto_generate_id" not in names
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cls",
|
||||
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
||||
)
|
||||
def test_default_auto_generates_id(self, cls):
|
||||
"""With defaults, an empty id is auto-populated and stays a non-field."""
|
||||
obj = cls(**self._base_kwargs(cls))
|
||||
id_field = [f.name for f in __import__("dataclasses").fields(cls)][0]
|
||||
assert getattr(obj, id_field)
|
||||
assert "auto_generate_id" not in vars(obj)
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cls",
|
||||
[Decision, DecisionContext, Policy, PolicyException, Precedent, ApprovalChain],
|
||||
)
|
||||
def test_required_id_when_auto_generate_disabled(self, cls):
|
||||
"""auto_generate_id=False with an empty id must raise ValueError."""
|
||||
with pytest.raises(ValueError):
|
||||
cls(auto_generate_id=False, **self._base_kwargs(cls))
|
||||
|
||||
def test_explicit_id_honored_with_auto_generate_disabled(self):
|
||||
"""A provided id is preserved when auto_generate_id=False."""
|
||||
kwargs = self._base_kwargs(Decision)
|
||||
kwargs["decision_id"] = "fixed-id"
|
||||
decision = Decision(auto_generate_id=False, **kwargs)
|
||||
assert decision.decision_id == "fixed-id"
|
||||
assert "auto_generate_id" not in decision.to_dict()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
|
||||
@@ -291,8 +291,8 @@ class TestDeduplication(unittest.TestCase):
|
||||
class TestProgressTrackerEncoding(unittest.TestCase):
|
||||
"""Regression tests for issue #531 — Unicode crash on cp1252 Windows consoles."""
|
||||
|
||||
def _make_cp1252_stream(self):
|
||||
"""Return a stream that raises UnicodeEncodeError for non-cp1252 chars."""
|
||||
def _make_cp1252_stdout(self):
|
||||
"""Return a stdout-like object that raises UnicodeEncodeError for non-cp1252 chars."""
|
||||
class CP1252Writer:
|
||||
encoding = "cp1252"
|
||||
def write(self, text):
|
||||
@@ -304,39 +304,39 @@ class TestProgressTrackerEncoding(unittest.TestCase):
|
||||
def test_safe_write_does_not_crash_on_cp1252(self):
|
||||
"""_safe_write must not raise UnicodeEncodeError on a cp1252 console."""
|
||||
display = ConsoleProgressDisplay()
|
||||
orig = sys.stderr
|
||||
sys.stderr = self._make_cp1252_stream()
|
||||
orig = sys.stdout
|
||||
sys.stdout = self._make_cp1252_stdout()
|
||||
try:
|
||||
display._safe_write("🧠 Semantica - 📊 Current Progress\n")
|
||||
except UnicodeEncodeError:
|
||||
self.fail("_safe_write raised UnicodeEncodeError on a cp1252 stream")
|
||||
self.fail("_safe_write raised UnicodeEncodeError on cp1252 stdout")
|
||||
finally:
|
||||
sys.stderr = orig
|
||||
sys.stdout = orig
|
||||
|
||||
def test_update_pipeline_header_does_not_crash_on_cp1252(self):
|
||||
"""update() pipeline header write must not crash on a cp1252 console (issue #531)."""
|
||||
from semantica.utils.progress_tracker import ProgressItem
|
||||
display = ConsoleProgressDisplay()
|
||||
display.use_emoji = True # force emoji path to exercise the fixed branch
|
||||
orig = sys.stderr
|
||||
sys.stderr = self._make_cp1252_stream()
|
||||
orig = sys.stdout
|
||||
sys.stdout = self._make_cp1252_stdout()
|
||||
try:
|
||||
display._safe_write("🧠 Semantica - 📊 Current Progress\n")
|
||||
display._safe_write("=" * 150 + "\n")
|
||||
except UnicodeEncodeError:
|
||||
self.fail("Pipeline header write raised UnicodeEncodeError on cp1252 stdout")
|
||||
finally:
|
||||
sys.stderr = orig
|
||||
sys.stdout = orig
|
||||
|
||||
def test_emoji_detection_disables_on_cp1252(self):
|
||||
"""ConsoleProgressDisplay should auto-disable emoji when the progress stream is cp1252."""
|
||||
orig = sys.stderr
|
||||
sys.stderr = self._make_cp1252_stream()
|
||||
"""ConsoleProgressDisplay should auto-disable emoji when stdout is cp1252."""
|
||||
orig = sys.stdout
|
||||
sys.stdout = self._make_cp1252_stdout()
|
||||
try:
|
||||
display = ConsoleProgressDisplay()
|
||||
self.assertFalse(display.use_emoji, "use_emoji should be False on a cp1252 progress stream")
|
||||
self.assertFalse(display.use_emoji, "use_emoji should be False on cp1252 stdout")
|
||||
finally:
|
||||
sys.stderr = orig
|
||||
sys.stdout = orig
|
||||
|
||||
|
||||
class TestResultLimiting(unittest.TestCase):
|
||||
|
||||
@@ -24,19 +24,14 @@ import math
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the imports below, which need that extra.
|
||||
pytest.importorskip("fastapi")
|
||||
from fastapi.testclient import TestClient
|
||||
from pydantic import ValidationError
|
||||
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
from pydantic import ValidationError # noqa: E402
|
||||
|
||||
from semantica.context.context_graph import ContextGraph # noqa: E402
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.routes.decisions import _node_to_decision # noqa: E402
|
||||
from semantica.explorer.schemas import DecisionResponse # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.routes.decisions import _node_to_decision
|
||||
from semantica.explorer.schemas import DecisionResponse
|
||||
from semantica.explorer.session import GraphSession
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -9,15 +9,16 @@ import networkx as nx
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1103,7 +1104,7 @@ class TestBidirectionalPathRoute:
|
||||
# _classify_distance unit tests — issue #472
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from semantica.utils.helpers import classify_path_distance # noqa: E402
|
||||
from semantica.utils.helpers import classify_path_distance
|
||||
|
||||
|
||||
class _FakeSimilarity:
|
||||
|
||||
@@ -13,15 +13,16 @@ browsers can't set custom headers on a WebSocket handshake.
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
def _build_sample_graph() -> ContextGraph:
|
||||
|
||||
@@ -15,14 +15,9 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the explorer imports below, which pull fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.context.context_graph import ContextGraph # noqa: E402
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
@@ -27,12 +27,7 @@ import threading
|
||||
|
||||
import pytest
|
||||
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.explorer.routes import ontology as ontology_mod # noqa: E402
|
||||
from semantica.explorer.routes import ontology as ontology_mod
|
||||
|
||||
|
||||
def _start_local_server():
|
||||
|
||||
@@ -21,12 +21,7 @@ from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.explorer.routes import ontology as ontology_mod # noqa: E402
|
||||
from semantica.explorer.routes import ontology as ontology_mod
|
||||
|
||||
|
||||
def _fake_getaddrinfo(host, *args, **kwargs):
|
||||
|
||||
@@ -6,16 +6,17 @@ from urllib.parse import quote
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.routes.ontology import OntologyEntry
|
||||
from semantica.explorer.session import GraphSession
|
||||
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.routes.ontology import OntologyEntry # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
|
||||
def _build_ontology_graph() -> ContextGraph:
|
||||
|
||||
@@ -5,18 +5,13 @@ Tests for ProvenanceManager wiring into Explorer routes and application startup.
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the starlette/explorer imports below, which need that extra.
|
||||
pytest.importorskip("fastapi")
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
|
||||
from semantica.context.context_graph import ContextGraph # noqa: E402
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
from semantica.provenance import ProvenanceManager # noqa: E402
|
||||
from semantica.provenance.storage import SQLiteStorage # noqa: E402
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
from semantica.provenance import ProvenanceManager
|
||||
from semantica.provenance.storage import SQLiteStorage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -2,14 +2,7 @@
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from semantica.explorer.routes.provenance import ( # noqa: E402
|
||||
from semantica.explorer.routes.provenance import (
|
||||
_add_chain_edges,
|
||||
_build_provenance,
|
||||
_render_markdown,
|
||||
|
||||
@@ -14,17 +14,18 @@ from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
from semantica.context.context_graph import ContextGraph
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
from semantica.explorer.app import create_app
|
||||
from semantica.explorer.session import GraphSession
|
||||
|
||||
from semantica.explorer.app import create_app # noqa: E402
|
||||
from semantica.explorer.session import GraphSession # noqa: E402
|
||||
try:
|
||||
from starlette.testclient import TestClient
|
||||
except ImportError:
|
||||
pytest.skip(
|
||||
"starlette TestClient is required for explorer tests. Install semantica[explorer].",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
from starlette.testclient import TestClient # noqa: E402
|
||||
|
||||
import semantica.explorer.routes.sparql as sparql_mod # noqa: E402
|
||||
import semantica.explorer.routes.sparql as sparql_mod
|
||||
|
||||
|
||||
def _build_sample_graph() -> ContextGraph:
|
||||
|
||||
@@ -2,19 +2,12 @@
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this module
|
||||
# must skip rather than fail collection when it is absent. The guard has to sit
|
||||
# above the import below, which pulls fastapi in transitively.
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from fastapi import FastAPI # noqa: E402
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
|
||||
from semantica.explorer.dependencies import get_session # noqa: E402
|
||||
from semantica.explorer.routes.vocabulary import router # noqa: E402
|
||||
from semantica.utils.skos import validate_skos_hierarchy # noqa: E402
|
||||
from semantica.explorer.dependencies import get_session
|
||||
from semantica.explorer.routes.vocabulary import router
|
||||
from semantica.utils.skos import validate_skos_hierarchy
|
||||
|
||||
app = FastAPI()
|
||||
app.include_router(router)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# tests/integrations/crewai package
|
||||
@@ -1 +0,0 @@
|
||||
# tests/integrations/langchain package
|
||||
@@ -1,366 +0,0 @@
|
||||
"""Tests for the RETE engine pattern matching (issue #300).
|
||||
|
||||
These tests verify that ``AlphaNode._matches`` and ``BetaNode._can_join`` no
|
||||
longer behave like the old always-``True`` stubs, and that the network as a
|
||||
whole only fires rules whose conditions actually unify with the facts.
|
||||
"""
|
||||
|
||||
import itertools
|
||||
import re
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from semantica.reasoning import rete_engine
|
||||
from semantica.reasoning.reasoner import Fact, Rule
|
||||
from semantica.reasoning.rete_engine import (
|
||||
AlphaNode,
|
||||
BetaNode,
|
||||
ReteEngine,
|
||||
unify_condition,
|
||||
)
|
||||
|
||||
|
||||
class TestUnifyCondition(unittest.TestCase):
|
||||
def test_single_variable_binds(self):
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
bindings = unify_condition("Person(?x)", fact)
|
||||
self.assertEqual(bindings, {"x": "John"})
|
||||
|
||||
def test_predicate_mismatch_returns_none(self):
|
||||
fact = Fact("f1", "Company", ["Google"])
|
||||
self.assertIsNone(unify_condition("Person(?x)", fact))
|
||||
|
||||
def test_two_arguments_bind(self):
|
||||
fact = Fact("f2", "Parent", ["John", "Mary"])
|
||||
bindings = unify_condition("Parent(?x, ?y)", fact)
|
||||
self.assertEqual(bindings, {"x": "John", "y": "Mary"})
|
||||
|
||||
def test_literal_argument_must_match(self):
|
||||
fact = Fact("f3", "Parent", ["John", "Mary"])
|
||||
self.assertIsNone(unify_condition("Parent(Bob, ?y)", fact))
|
||||
self.assertEqual(unify_condition("Parent(John, ?y)", fact), {"y": "Mary"})
|
||||
|
||||
def test_repeated_variable_requires_equal_values(self):
|
||||
loves_self = Fact("f4", "Loves", ["John", "John"])
|
||||
loves_other = Fact("f5", "Loves", ["John", "Mary"])
|
||||
self.assertEqual(unify_condition("Loves(?x, ?x)", loves_self), {"x": "John"})
|
||||
self.assertIsNone(unify_condition("Loves(?x, ?x)", loves_other))
|
||||
|
||||
def test_regex_error_logs_warning_and_returns_none(self):
|
||||
"""A regex compilation error is logged with context and yields None."""
|
||||
fact = Fact("f6", "Person", ["John"])
|
||||
with mock.patch.object(
|
||||
rete_engine.re,
|
||||
"match",
|
||||
side_effect=re.error("bad pattern"),
|
||||
), self.assertLogs("semantica.rete_engine", level="WARNING") as captured:
|
||||
result = unify_condition("Person(?x)", fact)
|
||||
self.assertIsNone(result)
|
||||
joined = "\n".join(captured.output)
|
||||
self.assertIn("Person(?x)", joined)
|
||||
self.assertIn("Person(John)", joined)
|
||||
self.assertIn("bad pattern", joined)
|
||||
|
||||
def test_unexpected_error_logs_warning_and_returns_none(self):
|
||||
"""An unexpected error is also logged and swallowed as None."""
|
||||
fact = Fact("f7", "Person", ["John"])
|
||||
with mock.patch.object(
|
||||
rete_engine.re,
|
||||
"match",
|
||||
side_effect=RuntimeError("boom"),
|
||||
), self.assertLogs("semantica.rete_engine", level="WARNING") as captured:
|
||||
result = unify_condition("Person(?x)", fact)
|
||||
self.assertIsNone(result)
|
||||
self.assertIn("boom", "\n".join(captured.output))
|
||||
|
||||
|
||||
class TestAlphaNode(unittest.TestCase):
|
||||
def test_matches_stores_bindings(self):
|
||||
node = AlphaNode("a1", "Person(?x)")
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
token = node.add_fact(fact)
|
||||
self.assertIsNotNone(token)
|
||||
assert token is not None # narrow type for the checker
|
||||
self.assertEqual(token.facts, [fact])
|
||||
self.assertEqual(token.bindings, {"x": "John"})
|
||||
self.assertIn(token, node.tokens)
|
||||
|
||||
def test_non_matching_fact_rejected(self):
|
||||
node = AlphaNode("a1", "Person(?x)")
|
||||
fact = Fact("f1", "Company", ["Google"])
|
||||
self.assertIsNone(node.add_fact(fact))
|
||||
self.assertEqual(node.tokens, [])
|
||||
|
||||
def test_uses_precompiled_regex(self):
|
||||
"""AlphaNode compiles its condition once and reuses it per fact."""
|
||||
node = AlphaNode("a1", "Person(?x)")
|
||||
self.assertIsNotNone(node._compiled)
|
||||
# Matching goes through the compiled matcher, not unify_condition.
|
||||
with mock.patch.object(rete_engine, "unify_condition") as unify:
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
token = node.add_fact(fact)
|
||||
unify.assert_not_called()
|
||||
self.assertIsNotNone(token)
|
||||
assert token is not None
|
||||
self.assertEqual(token.bindings, {"x": "John"})
|
||||
|
||||
def test_bad_condition_never_matches_and_logs(self):
|
||||
"""A condition that fails to compile logs a warning and never fires."""
|
||||
with mock.patch.object(
|
||||
rete_engine,
|
||||
"_build_condition_regex",
|
||||
return_value="(unbalanced",
|
||||
), self.assertLogs("semantica.rete_engine", level="WARNING") as captured:
|
||||
node = AlphaNode("bad", "Person(?x)")
|
||||
self.assertIsNone(node._compiled)
|
||||
self.assertIn("failed to compile", "\n".join(captured.output))
|
||||
fact = Fact("f1", "Person", ["John"])
|
||||
self.assertIsNone(node.add_fact(fact))
|
||||
self.assertEqual(node.tokens, [])
|
||||
|
||||
|
||||
class TestBetaNode(unittest.TestCase):
|
||||
def test_join_consistent_bindings(self):
|
||||
left = AlphaNode("a1", "Parent(?x, ?y)")
|
||||
right = AlphaNode("a2", "Person(?x)")
|
||||
beta = BetaNode("b1", left, right)
|
||||
|
||||
parent = Fact("f1", "Parent", ["John", "Mary"])
|
||||
person = Fact("f2", "Person", ["John"])
|
||||
left_token = left.add_fact(parent)
|
||||
right_token = right.add_fact(person)
|
||||
assert left_token is not None and right_token is not None
|
||||
|
||||
merged = beta.join(left_token, right_token)
|
||||
self.assertIsNotNone(merged)
|
||||
assert merged is not None # narrow type for the checker
|
||||
self.assertEqual(merged.bindings, {"x": "John", "y": "Mary"})
|
||||
# Facts are concatenated left-then-right in condition order.
|
||||
self.assertEqual(merged.facts, [parent, person])
|
||||
|
||||
def test_join_conflicting_bindings_rejected(self):
|
||||
left = AlphaNode("a1", "Parent(?x, ?y)")
|
||||
right = AlphaNode("a2", "Person(?x)")
|
||||
beta = BetaNode("b1", left, right)
|
||||
|
||||
parent = Fact("f1", "Parent", ["John", "Mary"])
|
||||
# ?x conflicts: John vs Alice
|
||||
person = Fact("f2", "Person", ["Alice"])
|
||||
left_token = left.add_fact(parent)
|
||||
right_token = right.add_fact(person)
|
||||
assert left_token is not None and right_token is not None
|
||||
|
||||
self.assertIsNone(beta.join(left_token, right_token))
|
||||
|
||||
|
||||
class TestReteEngineEndToEnd(unittest.TestCase):
|
||||
def test_only_matching_rule_fires(self):
|
||||
engine = ReteEngine()
|
||||
rule = Rule(
|
||||
rule_id="r1",
|
||||
name="person rule",
|
||||
conditions=["Person(?x)"],
|
||||
conclusion="Mortal(?x)",
|
||||
)
|
||||
engine.build_network([rule])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Company", ["Google"])) # should NOT fire
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0].bindings, {"x": "John"})
|
||||
|
||||
def test_multi_condition_join(self):
|
||||
engine = ReteEngine()
|
||||
rule = Rule(
|
||||
rule_id="r1",
|
||||
name="child rule",
|
||||
conditions=["Person(?x)", "Parent(?x, ?y)"],
|
||||
conclusion="Child(?y, ?x)",
|
||||
)
|
||||
engine.build_network([rule])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
# Unrelated parent whose ?x does not match any Person -> no activation.
|
||||
engine.add_fact(Fact("f3", "Parent", ["Bob", "Sue"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(matches[0].bindings, {"x": "John", "y": "Mary"})
|
||||
|
||||
def test_no_activation_when_join_inconsistent(self):
|
||||
engine = ReteEngine()
|
||||
rule = Rule(
|
||||
rule_id="r1",
|
||||
name="child rule",
|
||||
conditions=["Person(?x)", "Parent(?x, ?y)"],
|
||||
conclusion="Child(?y, ?x)",
|
||||
)
|
||||
engine.build_network([rule])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["Alice", "Mary"])) # ?x mismatch
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(matches, [])
|
||||
|
||||
|
||||
class TestThreeConditionChain(unittest.TestCase):
|
||||
"""Chained beta joins across three or more conditions (issue #300).
|
||||
|
||||
These exercise the Token model: a token must accumulate the ordered
|
||||
facts and the consistent bindings of every condition, so that deep
|
||||
chains neither drop bindings nor duplicate facts, and a conflict on the
|
||||
third condition correctly suppresses activation.
|
||||
"""
|
||||
|
||||
def _three_condition_rule(self):
|
||||
return Rule(
|
||||
rule_id="r1",
|
||||
name="location chain",
|
||||
conditions=[
|
||||
"Person(?x)",
|
||||
"Parent(?x, ?y)",
|
||||
"Located(?y, ?z)",
|
||||
],
|
||||
conclusion="LivesNear(?x, ?z)",
|
||||
)
|
||||
|
||||
def test_three_condition_valid_match(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
engine.add_fact(Fact("f3", "Located", ["Mary", "Paris"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
self.assertEqual(
|
||||
matches[0].bindings,
|
||||
{"x": "John", "y": "Mary", "z": "Paris"},
|
||||
)
|
||||
|
||||
def test_three_condition_third_level_conflict(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
# ?y is bound to Mary, so a Located fact about Bob must not join.
|
||||
engine.add_fact(Fact("f3", "Located", ["Bob", "Paris"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(matches, [])
|
||||
|
||||
def test_fact_insertion_order_independent(self):
|
||||
# Whatever order facts arrive, the same single match must result.
|
||||
base_facts = [
|
||||
Fact("f1", "Person", ["John"]),
|
||||
Fact("f2", "Parent", ["John", "Mary"]),
|
||||
Fact("f3", "Located", ["Mary", "Paris"]),
|
||||
]
|
||||
expected = {"x": "John", "y": "Mary", "z": "Paris"}
|
||||
|
||||
for order in itertools.permutations(base_facts):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
for fact in order:
|
||||
engine.add_fact(fact)
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1, f"order={order}")
|
||||
self.assertEqual(matches[0].bindings, expected)
|
||||
|
||||
def test_match_facts_complete_in_condition_order(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
person = Fact("f1", "Person", ["John"])
|
||||
parent = Fact("f2", "Parent", ["John", "Mary"])
|
||||
located = Fact("f3", "Located", ["Mary", "Paris"])
|
||||
engine.add_fact(person)
|
||||
engine.add_fact(parent)
|
||||
engine.add_fact(located)
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 1)
|
||||
# All three facts preserved, in condition order, no duplicates.
|
||||
self.assertEqual(matches[0].facts, [person, parent, located])
|
||||
|
||||
def test_multiple_left_tokens_join_one_right_fact(self):
|
||||
# Two Person/Parent chains sharing the same Located(?y, ?z) fact.
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
engine.add_fact(Fact("f3", "Person", ["Alice"]))
|
||||
engine.add_fact(Fact("f4", "Parent", ["Alice", "Mary"]))
|
||||
# One right fact should join with both accumulated left tokens.
|
||||
engine.add_fact(Fact("f5", "Located", ["Mary", "Paris"]))
|
||||
|
||||
matches = engine.match_patterns()
|
||||
self.assertEqual(len(matches), 2)
|
||||
result = {m.bindings["x"]: m.bindings["z"] for m in matches}
|
||||
self.assertEqual(result, {"John": "Paris", "Alice": "Paris"})
|
||||
|
||||
def test_matches_reasoner_match_rule(self):
|
||||
from semantica.reasoning.reasoner import Reasoner
|
||||
|
||||
rule = self._three_condition_rule()
|
||||
facts = [
|
||||
Fact("f1", "Person", ["John"]),
|
||||
Fact("f2", "Parent", ["John", "Mary"]),
|
||||
Fact("f3", "Located", ["Mary", "Paris"]),
|
||||
]
|
||||
|
||||
# Reasoner works over stringified facts and returns
|
||||
# (conclusion, matched_facts, bindings) tuples from self.facts.
|
||||
reasoner = Reasoner()
|
||||
for fact in facts:
|
||||
reasoner.add_fact(str(fact))
|
||||
reasoner_matches = reasoner._match_rule(rule)
|
||||
|
||||
engine = ReteEngine()
|
||||
engine.build_network([rule])
|
||||
for fact in facts:
|
||||
engine.add_fact(fact)
|
||||
rete_matches = engine.match_patterns()
|
||||
|
||||
# Both engines must agree on the number of activations.
|
||||
self.assertEqual(len(rete_matches), len(reasoner_matches))
|
||||
self.assertEqual(len(rete_matches), 1)
|
||||
self.assertEqual(
|
||||
rete_matches[0].bindings,
|
||||
{"x": "John", "y": "Mary", "z": "Paris"},
|
||||
)
|
||||
# The RETE match must carry the instantiated conclusion facts too.
|
||||
conclusion, _, _ = reasoner_matches[0]
|
||||
self.assertEqual(conclusion, "LivesNear(John, Paris)")
|
||||
|
||||
def test_reset_clears_all_token_memory(self):
|
||||
engine = ReteEngine()
|
||||
engine.build_network([self._three_condition_rule()])
|
||||
|
||||
engine.add_fact(Fact("f1", "Person", ["John"]))
|
||||
engine.add_fact(Fact("f2", "Parent", ["John", "Mary"]))
|
||||
engine.add_fact(Fact("f3", "Located", ["Mary", "Paris"]))
|
||||
self.assertEqual(len(engine.match_patterns()), 1)
|
||||
|
||||
engine.reset()
|
||||
|
||||
# No stale facts, tokens or activations remain anywhere.
|
||||
self.assertEqual(engine.facts, [])
|
||||
for node in engine.network.values():
|
||||
if isinstance(node, AlphaNode):
|
||||
self.assertEqual(node.tokens, [])
|
||||
elif isinstance(node, BetaNode):
|
||||
self.assertEqual(node.left_tokens, [])
|
||||
self.assertEqual(node.right_tokens, [])
|
||||
self.assertEqual(engine.match_patterns(), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -60,11 +60,6 @@ class TestSlidingWindowChunker:
|
||||
with pytest.raises(ValidationError):
|
||||
SlidingWindowChunker(chunk_size=100, overlap=100)
|
||||
|
||||
@pytest.mark.parametrize("stride", [0, -1])
|
||||
def test_init_rejects_non_positive_stride(self, stride):
|
||||
with pytest.raises(ValidationError, match="stride must be positive"):
|
||||
SlidingWindowChunker(chunk_size=100, stride=stride)
|
||||
|
||||
def test_empty_text_returns_empty(self):
|
||||
chunker = SlidingWindowChunker(chunk_size=50, overlap=10)
|
||||
assert chunker.chunk("") == []
|
||||
@@ -102,49 +97,6 @@ class TestSlidingWindowChunker:
|
||||
chunks = chunker.chunk_with_overlap(text, overlap_size=15)
|
||||
assert len(chunks) >= 2
|
||||
assert chunker.overlap == 0
|
||||
assert chunker.stride == 50
|
||||
|
||||
@pytest.mark.parametrize("overlap_size", [-1, 50, 51])
|
||||
def test_chunk_with_overlap_rejects_invalid_override(self, overlap_size):
|
||||
chunker = SlidingWindowChunker(chunk_size=50)
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
chunker.chunk_with_overlap(
|
||||
"non-empty input", overlap_size=overlap_size
|
||||
)
|
||||
|
||||
def test_chunk_with_overlap_accepts_largest_valid_override(self):
|
||||
chunker = SlidingWindowChunker(chunk_size=5)
|
||||
|
||||
chunks = chunker.chunk_with_overlap("abcdefghij", overlap_size=4)
|
||||
|
||||
assert [chunk.start_index for chunk in chunks] == list(range(10))
|
||||
|
||||
def test_chunk_with_overlap_restores_custom_stride(self):
|
||||
chunker = SlidingWindowChunker(chunk_size=10, overlap=2, stride=3)
|
||||
|
||||
chunker.chunk_with_overlap(
|
||||
"abcdefghijklmnopqrstuvwxyz", overlap_size=4
|
||||
)
|
||||
|
||||
assert chunker.overlap == 2
|
||||
assert chunker.stride == 3
|
||||
|
||||
def test_chunk_with_overlap_restores_state_when_chunk_raises(
|
||||
self, monkeypatch
|
||||
):
|
||||
chunker = SlidingWindowChunker(chunk_size=10, overlap=2, stride=3)
|
||||
|
||||
def raise_error(text):
|
||||
raise RuntimeError("chunk failed")
|
||||
|
||||
monkeypatch.setattr(chunker, "chunk", raise_error)
|
||||
|
||||
with pytest.raises(RuntimeError, match="chunk failed"):
|
||||
chunker.chunk_with_overlap("non-empty input", overlap_size=4)
|
||||
|
||||
assert chunker.overlap == 2
|
||||
assert chunker.stride == 3
|
||||
|
||||
def test_boundary_preservation_avoids_mid_word_when_possible(self):
|
||||
text = (
|
||||
|
||||
@@ -1359,101 +1359,6 @@ class TestStore:
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate", "--from", "faiss"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
def test_migrate_refuses_unsupported_backend_pair(self, runner):
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "faiss", "--to", "qdrant"])
|
||||
assert result.exit_code != 0
|
||||
assert "faiss, pgvector, sqlite" in result.output
|
||||
|
||||
def _fake_migrate_store_module(self, source_items, stored, dest_configs=None):
|
||||
class _FakeBackendStore:
|
||||
def __init__(self, dimension=None):
|
||||
self.dimension = dimension
|
||||
|
||||
class _FakeStore:
|
||||
def __init__(self, backend, config=None, **kw):
|
||||
self.backend = backend
|
||||
self._config = config or {}
|
||||
dim = self._config.get("dimension")
|
||||
self._backend_store = _FakeBackendStore(dimension=dim)
|
||||
if dest_configs is not None:
|
||||
dest_configs[backend] = dict(self._config)
|
||||
|
||||
def iter_vectors(self, batch_size=500):
|
||||
if self.backend == "sqlite":
|
||||
yield from source_items
|
||||
return
|
||||
return
|
||||
yield # pragma: no cover - makes this a generator for other backends
|
||||
|
||||
def store_vectors(self, vectors, metadata, ids=None):
|
||||
for vec_id, meta in zip(ids, metadata):
|
||||
stored[vec_id] = meta
|
||||
|
||||
return _fake_module(VectorStore=_FakeStore)
|
||||
|
||||
def test_migrate_runs_between_supported_backends(self, runner, monkeypatch):
|
||||
source_items = [
|
||||
{"id": "a", "vector": [0.1, 0.2], "metadata": {"tag": "x"}},
|
||||
{"id": "b", "vector": [0.3, 0.4], "metadata": {}},
|
||||
]
|
||||
stored = {}
|
||||
fake_vs = self._fake_migrate_store_module(source_items, stored)
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "pgvector",
|
||||
"--namespace", "prod", "--json"])
|
||||
_ok(result)
|
||||
data = _json_output(result)
|
||||
assert data == {"from": "sqlite", "to": "pgvector", "migrated": 2}
|
||||
assert stored == {"a": {"tag": "x", "namespace": "prod"}, "b": {"namespace": "prod"}}
|
||||
|
||||
def test_migrate_reports_zero_for_empty_source(self, runner, monkeypatch):
|
||||
stored = {}
|
||||
fake_vs = self._fake_migrate_store_module([], stored)
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "pgvector", "--json"])
|
||||
_ok(result)
|
||||
assert _json_output(result)["migrated"] == 0
|
||||
assert stored == {}
|
||||
|
||||
def test_migrate_inherits_source_dimension_into_dest(self, runner, monkeypatch):
|
||||
source_items = [{"id": "a", "vector": [0.1, 0.2, 0.3], "metadata": {}}]
|
||||
stored = {}
|
||||
dest_configs: dict = {}
|
||||
fake_vs = self._fake_migrate_store_module(source_items, stored, dest_configs)
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
monkeypatch.setattr(
|
||||
cli_module.Config, "to_dict",
|
||||
lambda self: {"vector_store": {"sqlite": {"dimension": 3}, "pgvector": {}}},
|
||||
)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "pgvector", "--json"])
|
||||
_ok(result)
|
||||
assert dest_configs["pgvector"].get("dimension") == 3
|
||||
|
||||
def test_migrate_faiss_source_requires_index_path(self, runner, monkeypatch):
|
||||
fake_vs = _fake_module(VectorStore=lambda **kw: MagicMock())
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "faiss", "--to", "sqlite"])
|
||||
assert result.exit_code != 0
|
||||
assert "index_path" in result.output
|
||||
|
||||
def test_migrate_faiss_dest_requires_index_path(self, runner, monkeypatch):
|
||||
fake_vs = _fake_module(VectorStore=lambda **kw: MagicMock())
|
||||
monkeypatch.setitem(__import__("sys").modules, "semantica.vector_store", fake_vs)
|
||||
|
||||
result = runner.invoke(cli_module.main, ["store", "migrate",
|
||||
"--from", "sqlite", "--to", "faiss"])
|
||||
assert result.exit_code != 0
|
||||
assert "index_path" in result.output
|
||||
|
||||
def test_flush_requires_confirm(self, runner):
|
||||
result = runner.invoke(cli_module.main, ["store", "flush"])
|
||||
assert result.exit_code != 0
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
"""Tests for the Anthropic LLM provider wrapper (semantica.llms.Anthropic)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Anthropic
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
"""Anthropic(...) should not crash and should remember what it was given."""
|
||||
claude = Anthropic(model="claude-sonnet-4-6", api_key="fake-key")
|
||||
assert claude.model == "claude-sonnet-4-6"
|
||||
assert claude.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
"""Without a real key, is_available() must be a real False, not truthy junk.
|
||||
|
||||
api_key=None alone isn't enough to prove this: AnthropicProvider falls
|
||||
back to the ANTHROPIC_API_KEY environment variable, so this test has to
|
||||
clear it too or it would pass/fail depending on whoever's machine or CI
|
||||
runner happens to run it.
|
||||
"""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
assert claude.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
"""generate() must fail loudly.
|
||||
|
||||
Clears ANTHROPIC_API_KEY for the same reason as test_is_available_false_with_no_key:
|
||||
otherwise this test flakes depending on whether the runner's environment has a key set.
|
||||
"""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Anthropic provider not available"):
|
||||
claude.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
"""When available, generate() must actually call through to the real provider."""
|
||||
claude = Anthropic(api_key="fake-key")
|
||||
|
||||
claude.provider = MagicMock()
|
||||
claude.provider.is_available.return_value = True
|
||||
claude.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = claude.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
claude.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
claude = Anthropic(api_key="fake-key")
|
||||
claude.provider = MagicMock()
|
||||
claude.provider.is_available.return_value = True
|
||||
claude.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = claude.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
claude.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
claude = Anthropic(api_key="fake-key")
|
||||
claude.provider = MagicMock()
|
||||
claude.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
claude.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = claude.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
claude.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
"""generate_structured() must fail loudly, same as generate()."""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Anthropic provider not available"):
|
||||
claude.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
"""generate_typed() must fail loudly, same as generate() and generate_structured()."""
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
claude = Anthropic(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Anthropic provider not available"):
|
||||
claude.generate_typed("hello", object())
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Tests for the DeepSeek LLM provider wrapper (semantica.llms.DeepSeek)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import DeepSeek
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
llm = DeepSeek(model="deepseek-chat", api_key="fake-key")
|
||||
assert llm.model == "deepseek-chat"
|
||||
assert llm.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
assert llm.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="DeepSeek provider not available"):
|
||||
llm.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
llm = DeepSeek(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = llm.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
llm.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
llm = DeepSeek(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = llm.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
llm.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
llm = DeepSeek(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
llm.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = llm.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
llm.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="DeepSeek provider not available"):
|
||||
llm.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("DEEPSEEK_API_KEY", raising=False)
|
||||
llm = DeepSeek(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="DeepSeek provider not available"):
|
||||
llm.generate_typed("hello", object())
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Tests for the Gemini LLM provider wrapper (semantica.llms.Gemini)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Gemini
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
gemini = Gemini(model="gemini-pro", api_key="fake-key")
|
||||
assert gemini.model == "gemini-pro"
|
||||
assert gemini.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
assert gemini.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Gemini provider not available"):
|
||||
gemini.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
gemini = Gemini(api_key="fake-key")
|
||||
gemini.provider = MagicMock()
|
||||
gemini.provider.is_available.return_value = True
|
||||
gemini.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = gemini.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
gemini.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
gemini = Gemini(api_key="fake-key")
|
||||
gemini.provider = MagicMock()
|
||||
gemini.provider.is_available.return_value = True
|
||||
gemini.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = gemini.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
gemini.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
gemini = Gemini(api_key="fake-key")
|
||||
gemini.provider = MagicMock()
|
||||
gemini.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
gemini.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = gemini.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
gemini.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Gemini provider not available"):
|
||||
gemini.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("GEMINI_API_KEY", raising=False)
|
||||
gemini = Gemini(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Gemini provider not available"):
|
||||
gemini.generate_typed("hello", object())
|
||||
@@ -1,80 +0,0 @@
|
||||
"""Tests for the Novita LLM provider wrapper (semantica.llms.Novita)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Novita
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_api_key():
|
||||
llm = Novita(model="deepseek/deepseek-v3.2", api_key="fake-key")
|
||||
assert llm.model == "deepseek/deepseek-v3.2"
|
||||
assert llm.api_key == "fake-key"
|
||||
|
||||
|
||||
def test_is_available_false_with_no_key(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
assert llm.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Novita provider not available"):
|
||||
llm.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
llm = Novita(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = llm.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
llm.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
llm = Novita(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = llm.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
llm.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
llm = Novita(api_key="fake-key")
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
llm.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = llm.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
llm.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Novita provider not available"):
|
||||
llm.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable(monkeypatch):
|
||||
monkeypatch.delenv("NOVITA_API_KEY", raising=False)
|
||||
llm = Novita(api_key=None)
|
||||
with pytest.raises(ProcessingError, match="Novita provider not available"):
|
||||
llm.generate_typed("hello", object())
|
||||
@@ -1,78 +0,0 @@
|
||||
"""Tests for the Ollama LLM provider wrapper (semantica.llms.Ollama)."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.llms import Ollama
|
||||
from semantica.utils.exceptions import ProcessingError
|
||||
|
||||
|
||||
def test_construction_stores_model_and_base_url():
|
||||
llm = Ollama(model="llama2", base_url="http://localhost:11434")
|
||||
assert llm.model == "llama2"
|
||||
assert llm.base_url == "http://localhost:11434"
|
||||
|
||||
|
||||
def test_is_available_false_without_a_running_server():
|
||||
"""No api_key here, Ollama has none. Without a real server (or the ollama
|
||||
package) reachable at base_url, this must be a real False."""
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
assert llm.is_available() is False
|
||||
|
||||
|
||||
def test_generate_raises_clear_error_when_unavailable():
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
with pytest.raises(ProcessingError, match="Ollama provider not available"):
|
||||
llm.generate("hello")
|
||||
|
||||
|
||||
def test_generate_forwards_to_the_real_provider_when_available():
|
||||
llm = Ollama()
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate.return_value = "a fake response"
|
||||
|
||||
result = llm.generate("hello", temperature=0.5)
|
||||
|
||||
assert result == "a fake response"
|
||||
llm.provider.generate.assert_called_once_with("hello", temperature=0.5)
|
||||
|
||||
|
||||
def test_generate_structured_forwards_to_the_real_provider():
|
||||
llm = Ollama()
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
llm.provider.generate_structured.return_value = {"key": "value"}
|
||||
|
||||
result = llm.generate_structured("hello")
|
||||
|
||||
assert result == {"key": "value"}
|
||||
llm.provider.generate_structured.assert_called_once_with("hello")
|
||||
|
||||
|
||||
def test_generate_typed_forwards_schema_and_max_retries():
|
||||
llm = Ollama()
|
||||
llm.provider = MagicMock()
|
||||
llm.provider.is_available.return_value = True
|
||||
fake_schema = object()
|
||||
llm.provider.generate_typed.return_value = "typed result"
|
||||
|
||||
result = llm.generate_typed("hello", fake_schema, max_retries=5)
|
||||
|
||||
assert result == "typed result"
|
||||
llm.provider.generate_typed.assert_called_once_with(
|
||||
"hello", fake_schema, max_retries=5
|
||||
)
|
||||
|
||||
|
||||
def test_generate_structured_raises_clear_error_when_unavailable():
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
with pytest.raises(ProcessingError, match="Ollama provider not available"):
|
||||
llm.generate_structured("hello")
|
||||
|
||||
|
||||
def test_generate_typed_raises_clear_error_when_unavailable():
|
||||
llm = Ollama(base_url="http://localhost:1")
|
||||
with pytest.raises(ProcessingError, match="Ollama provider not available"):
|
||||
llm.generate_typed("hello", object())
|
||||
@@ -24,20 +24,9 @@ import pytest
|
||||
# rdf:/rdfs: namespaces) and neither the code nor this test caught it,
|
||||
# since both had the same bug. Importing the real function makes that class
|
||||
# of drift impossible.
|
||||
# fastapi ships in the optional `explorer` extra, not in `dev`, so this import
|
||||
# fails on a plain dev install. Only the SPARQL class below needs it; the Cypher,
|
||||
# XXE, vector-serialization and SSRF classes in this module are independent, so
|
||||
# the skip is scoped to the one class rather than the whole file.
|
||||
try:
|
||||
from semantica.explorer.routes.sparql import _is_read_only_query
|
||||
except ImportError: # pragma: no cover - depends on the installed extras
|
||||
_is_read_only_query = None
|
||||
from semantica.explorer.routes.sparql import _is_read_only_query
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
_is_read_only_query is None,
|
||||
reason="requires semantica[explorer] (fastapi)",
|
||||
)
|
||||
class TestSparqlReadOnlyValidation:
|
||||
"""Regression tests for SPARQL injection prevention."""
|
||||
|
||||
|
||||
@@ -443,3 +443,4 @@ def test_export_seed_data(seed_manager, temp_data_dir):
|
||||
rows = list(reader)
|
||||
assert len(rows) == 1
|
||||
assert rows[0]["id"] == "1"
|
||||
|
||||
|
||||
@@ -1,184 +0,0 @@
|
||||
"""Console progress must be written to stderr, never to stdout.
|
||||
|
||||
Progress is diagnostic output. Writing it to stdout corrupts any program that
|
||||
carries a machine-readable protocol there — the stdio MCP servers put
|
||||
newline-delimited JSON-RPC on stdout, and a progress bar interleaved with a
|
||||
response body makes that response unparseable (#1134).
|
||||
|
||||
Both servers currently defend against this by setting
|
||||
SEMANTICA_DISABLE_PROGRESS, and a console display is only attached when the
|
||||
console is interactive. Those are containments, not the fix: they give up
|
||||
progress output entirely, and every future entry point has to remember them.
|
||||
Writing to the correct stream in the first place is what these tests pin.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import io
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def progress_module():
|
||||
"""Import the real progress_tracker, bypassing a mocked sys.modules entry.
|
||||
|
||||
tests/test_extractors_dispatch.py assigns a MagicMock over
|
||||
'semantica.utils.progress_tracker' at import time and never restores it, so
|
||||
a plain module-level import here returns mocks when that file has already
|
||||
run. Dropping the cached entry re-imports the real module.
|
||||
|
||||
Both bindings are restored afterwards: importing a submodule also rebinds it
|
||||
as an attribute of its parent package, so restoring only the sys.modules
|
||||
entry would leave `semantica.utils.progress_tracker` and
|
||||
`sys.modules["semantica.utils.progress_tracker"]` pointing at different
|
||||
objects for every test that follows.
|
||||
"""
|
||||
name = "semantica.utils.progress_tracker"
|
||||
attr = name.rsplit(".", 1)[1]
|
||||
parent = importlib.import_module("semantica.utils")
|
||||
|
||||
missing = object()
|
||||
saved_entry = sys.modules.get(name, missing)
|
||||
saved_attr = getattr(parent, attr, missing)
|
||||
|
||||
sys.modules.pop(name, None)
|
||||
try:
|
||||
module = importlib.import_module(name)
|
||||
assert hasattr(module, "__file__"), "expected the real module, got a stand-in"
|
||||
yield module
|
||||
finally:
|
||||
if saved_entry is missing:
|
||||
sys.modules.pop(name, None)
|
||||
else:
|
||||
sys.modules[name] = saved_entry
|
||||
|
||||
if saved_attr is missing:
|
||||
if hasattr(parent, attr):
|
||||
delattr(parent, attr)
|
||||
else:
|
||||
setattr(parent, attr, saved_attr)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def display_cls(progress_module):
|
||||
return progress_module.ConsoleProgressDisplay
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_item(progress_module):
|
||||
def _make(**overrides):
|
||||
defaults = dict(
|
||||
module="kg",
|
||||
submodule="Reasoner",
|
||||
message="Inferring facts",
|
||||
status="running",
|
||||
total_items=10,
|
||||
processed_items=3,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
return progress_module.ProgressItem(**defaults)
|
||||
|
||||
return _make
|
||||
|
||||
|
||||
class TestProgressStreamDefaults:
|
||||
def test_defaults_to_stderr(self, display_cls):
|
||||
assert display_cls().stream is sys.stderr
|
||||
|
||||
def test_default_is_not_stdout(self, display_cls):
|
||||
"""The whole point: stdout stays clean for protocol traffic."""
|
||||
assert display_cls().stream is not sys.stdout
|
||||
|
||||
def test_stream_follows_rebinding(self, display_cls, monkeypatch):
|
||||
"""Resolved per write, so pytest capture and later rebinds are honoured."""
|
||||
display = display_cls()
|
||||
replacement = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stderr", replacement)
|
||||
assert display.stream is replacement
|
||||
|
||||
def test_explicit_stream_overrides_the_default(self, display_cls):
|
||||
buffer = io.StringIO()
|
||||
assert display_cls(stream=buffer).stream is buffer
|
||||
|
||||
|
||||
class TestProgressWritesGoToTheStream:
|
||||
def test_update_writes_to_the_configured_stream(self, display_cls, make_item):
|
||||
buffer = io.StringIO()
|
||||
display = display_cls(stream=buffer, use_emoji=False, update_interval=0.0)
|
||||
|
||||
display.update(make_item())
|
||||
|
||||
assert buffer.getvalue(), "progress should have been rendered"
|
||||
|
||||
def test_update_writes_nothing_to_stdout(self, display_cls, make_item, monkeypatch):
|
||||
"""Regression guard for #1134: stdout must stay untouched."""
|
||||
fake_stdout = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stdout", fake_stdout)
|
||||
buffer = io.StringIO()
|
||||
|
||||
display = display_cls(stream=buffer, use_emoji=False, update_interval=0.0)
|
||||
display.update(make_item())
|
||||
display.clear()
|
||||
|
||||
assert fake_stdout.getvalue() == "", (
|
||||
f"console progress leaked to stdout: {fake_stdout.getvalue()!r}"
|
||||
)
|
||||
|
||||
def test_default_display_writes_nothing_to_stdout(
|
||||
self, display_cls, make_item, monkeypatch
|
||||
):
|
||||
"""Same guard without an explicit stream, i.e. the real default path."""
|
||||
fake_stdout = io.StringIO()
|
||||
fake_stderr = io.StringIO()
|
||||
monkeypatch.setattr(sys, "stdout", fake_stdout)
|
||||
monkeypatch.setattr(sys, "stderr", fake_stderr)
|
||||
|
||||
display = display_cls(use_emoji=False, update_interval=0.0)
|
||||
display.update(make_item())
|
||||
|
||||
assert fake_stdout.getvalue() == ""
|
||||
assert fake_stderr.getvalue(), "progress should have gone to stderr"
|
||||
|
||||
def test_clear_flushes_the_stream_not_stdout(self, display_cls, monkeypatch):
|
||||
flushed = []
|
||||
|
||||
class RecordingStream(io.StringIO):
|
||||
def flush(self):
|
||||
flushed.append("stream")
|
||||
|
||||
class ExplodingStdout(io.StringIO):
|
||||
def flush(self): # pragma: no cover - fails the test if reached
|
||||
raise AssertionError("progress must not flush stdout")
|
||||
|
||||
monkeypatch.setattr(sys, "stdout", ExplodingStdout())
|
||||
display = display_cls(
|
||||
stream=RecordingStream(), use_emoji=False, update_interval=0.0
|
||||
)
|
||||
|
||||
display.clear()
|
||||
|
||||
assert flushed == ["stream"]
|
||||
|
||||
|
||||
class TestEncodingFallback:
|
||||
def test_falls_back_when_the_stream_cannot_encode(self, display_cls):
|
||||
"""A cp1252-style console must not raise on emoji; it degrades instead."""
|
||||
|
||||
class AsciiOnly(io.StringIO):
|
||||
encoding = "ascii"
|
||||
|
||||
def write(self, text):
|
||||
text.encode("ascii") # raises UnicodeEncodeError on emoji
|
||||
return super().write(text)
|
||||
|
||||
buffer = AsciiOnly()
|
||||
display = display_cls(stream=buffer, use_emoji=True, update_interval=0.0)
|
||||
|
||||
display._safe_write("progress \U0001f504 bar\n")
|
||||
|
||||
assert "progress" in buffer.getvalue()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__])
|
||||
@@ -3,7 +3,7 @@ from unittest.mock import MagicMock
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from semantica.vector_store.faiss_store import FAISSIndex, FAISSStore
|
||||
from semantica.vector_store.faiss_store import FAISSIndex
|
||||
|
||||
|
||||
def test_get_vector_reconstructs_from_flat_l2_index():
|
||||
@@ -120,90 +120,3 @@ def test_get_vector_reconstructs_from_real_ivfflat_index_without_prior_direct_ma
|
||||
result = index.get_vector("vec_target")
|
||||
|
||||
np.testing.assert_allclose(result, vectors[3], atol=1e-6)
|
||||
|
||||
|
||||
def _store_with_fake_index(ids, metadata_by_id=None):
|
||||
backend_index = MagicMock()
|
||||
backend_index.reconstruct.side_effect = lambda idx: [float(idx)] * 3
|
||||
index = FAISSIndex(backend_index, dimension=3)
|
||||
index.vector_ids = list(ids)
|
||||
index.metadata = dict(metadata_by_id or {})
|
||||
|
||||
store = FAISSStore(dimension=3)
|
||||
store.index = index
|
||||
return store
|
||||
|
||||
|
||||
def test_scan_vectors_returns_all_across_pages():
|
||||
store = _store_with_fake_index(["a", "b", "c", "d", "e"])
|
||||
|
||||
seen_ids = []
|
||||
offset = 0
|
||||
while True:
|
||||
page = store.scan_vectors(offset=offset, limit=2)
|
||||
if not page:
|
||||
break
|
||||
seen_ids.extend(p["id"] for p in page)
|
||||
offset += len(page)
|
||||
|
||||
assert seen_ids == ["a", "b", "c", "d", "e"]
|
||||
|
||||
|
||||
def test_scan_vectors_includes_vector_and_metadata():
|
||||
store = _store_with_fake_index(["a"], {"a": {"tag": "only"}})
|
||||
|
||||
page = store.scan_vectors(offset=0, limit=10)
|
||||
|
||||
assert len(page) == 1
|
||||
assert page[0]["id"] == "a"
|
||||
assert page[0]["metadata"] == {"tag": "only"}
|
||||
np.testing.assert_array_equal(page[0]["vector"], np.array([0.0, 0.0, 0.0], dtype=np.float32))
|
||||
|
||||
|
||||
def test_scan_vectors_no_index_returns_empty_list():
|
||||
store = FAISSStore(dimension=3)
|
||||
assert store.scan_vectors(offset=0, limit=10) == []
|
||||
|
||||
|
||||
def test_scan_vectors_zero_limit_returns_empty_list():
|
||||
store = _store_with_fake_index(["a"])
|
||||
assert store.scan_vectors(offset=0, limit=0) == []
|
||||
|
||||
|
||||
def test_scan_vectors_offset_past_end_returns_empty_list():
|
||||
store = _store_with_fake_index(["a"])
|
||||
assert store.scan_vectors(offset=100, limit=10) == []
|
||||
|
||||
|
||||
def test_add_vectors_retry_with_same_ids_does_not_duplicate():
|
||||
"""Re-running add_vectors with ids already in the index (e.g. retrying
|
||||
an interrupted migration) must not create a second physical vector
|
||||
under the same id."""
|
||||
backend_index = MagicMock()
|
||||
store = FAISSStore(dimension=3)
|
||||
store.index = FAISSIndex(backend_index, dimension=3)
|
||||
|
||||
vectors = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]], dtype=np.float32)
|
||||
ids = ["a", "b", "c", "d"]
|
||||
|
||||
store.add_vectors(vectors, ids=ids, metadata=[{"i": i} for i in range(4)])
|
||||
assert store.count() == 4
|
||||
|
||||
store.add_vectors(vectors, ids=ids, metadata=[{"i": i} for i in range(4)])
|
||||
|
||||
assert store.count() == 4
|
||||
assert store.index.vector_ids == ids
|
||||
|
||||
|
||||
def test_add_vectors_retry_with_partial_overlap_only_adds_new_ids():
|
||||
backend_index = MagicMock()
|
||||
store = FAISSStore(dimension=3)
|
||||
store.index = FAISSIndex(backend_index, dimension=3)
|
||||
|
||||
store.add_vectors(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32), ids=["a", "b"])
|
||||
store.add_vectors(np.array([[1, 2, 3], [7, 8, 9]], dtype=np.float32), ids=["a", "c"])
|
||||
|
||||
assert store.index.vector_ids == ["a", "b", "c"]
|
||||
second_call_vectors = backend_index.add.call_args[0][0]
|
||||
assert second_call_vectors.shape[0] == 1
|
||||
np.testing.assert_array_equal(second_call_vectors[0], np.array([7, 8, 9], dtype=np.float32))
|
||||
|
||||
@@ -467,48 +467,6 @@ class TestPgVectorStoreDelete:
|
||||
assert success is True
|
||||
|
||||
|
||||
class TestPgVectorStoreScan:
|
||||
"""Test scan_vectors pagination."""
|
||||
|
||||
def test_scan_returns_all_vectors_across_pages(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32) for _ in range(5)]
|
||||
ids = store.add(vectors, [{"index": i} for i in range(5)])
|
||||
|
||||
seen_ids = []
|
||||
offset = 0
|
||||
while True:
|
||||
page = store.scan_vectors(offset=offset, limit=2)
|
||||
if not page:
|
||||
break
|
||||
seen_ids.extend(p["id"] for p in page)
|
||||
offset += len(page)
|
||||
|
||||
assert set(seen_ids) == set(ids)
|
||||
assert len(seen_ids) == 5
|
||||
|
||||
def test_scan_page_includes_vector_and_metadata(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32)]
|
||||
ids = store.add(vectors, [{"tag": "only"}])
|
||||
|
||||
page = store.scan_vectors(offset=0, limit=10)
|
||||
|
||||
assert len(page) == 1
|
||||
assert page[0]["id"] == ids[0]
|
||||
assert page[0]["metadata"] == {"tag": "only"}
|
||||
assert page[0]["vector"] is not None
|
||||
|
||||
def test_scan_empty_store_returns_empty_list(self, store):
|
||||
assert store.scan_vectors(offset=0, limit=10) == []
|
||||
|
||||
def test_scan_zero_limit_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=0, limit=0) == []
|
||||
|
||||
def test_scan_offset_past_end_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=100, limit=10) == []
|
||||
|
||||
|
||||
class TestPgVectorStoreIndex:
|
||||
"""Test index creation operations."""
|
||||
|
||||
|
||||
@@ -415,48 +415,6 @@ class TestSQLiteVecStoreStats:
|
||||
assert stats["vector_count"] == 4
|
||||
|
||||
|
||||
class TestSQLiteVecStoreScan:
|
||||
"""Test scan_vectors pagination."""
|
||||
|
||||
def test_scan_returns_all_vectors_across_pages(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32) for _ in range(5)]
|
||||
ids = store.add(vectors, [{"index": i} for i in range(5)])
|
||||
|
||||
seen_ids = []
|
||||
offset = 0
|
||||
while True:
|
||||
page = store.scan_vectors(offset=offset, limit=2)
|
||||
if not page:
|
||||
break
|
||||
seen_ids.extend(p["id"] for p in page)
|
||||
offset += len(page)
|
||||
|
||||
assert set(seen_ids) == set(ids)
|
||||
assert len(seen_ids) == 5
|
||||
|
||||
def test_scan_page_includes_vector_and_metadata(self, store):
|
||||
vectors = [np.random.rand(128).astype(np.float32)]
|
||||
ids = store.add(vectors, [{"tag": "only"}])
|
||||
|
||||
page = store.scan_vectors(offset=0, limit=10)
|
||||
|
||||
assert len(page) == 1
|
||||
assert page[0]["id"] == ids[0]
|
||||
assert page[0]["metadata"] == {"tag": "only"}
|
||||
assert page[0]["vector"] is not None
|
||||
|
||||
def test_scan_empty_store_returns_empty_list(self, store):
|
||||
assert store.scan_vectors(offset=0, limit=10) == []
|
||||
|
||||
def test_scan_zero_limit_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=0, limit=0) == []
|
||||
|
||||
def test_scan_offset_past_end_returns_empty_list(self, store):
|
||||
store.add([np.random.rand(128).astype(np.float32)])
|
||||
assert store.scan_vectors(offset=100, limit=10) == []
|
||||
|
||||
|
||||
class TestSQLiteVecStoreFilterByMetadata:
|
||||
"""Test filter_by_metadata, including list-valued metadata handling."""
|
||||
|
||||
|
||||
@@ -120,78 +120,6 @@ class VectorStoreCountTests(unittest.TestCase):
|
||||
self.assertIn("count()", msg)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VectorStore.scan_vectors() / iter_vectors() dispatch tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class _ScanningBackendStore:
|
||||
"""Fake persistent backend store that supports scan_vectors()."""
|
||||
|
||||
def __init__(self, items):
|
||||
self._items = items
|
||||
|
||||
def scan_vectors(self, offset=0, limit=100):
|
||||
return self._items[offset:offset + limit]
|
||||
|
||||
|
||||
class _NonScanningBackendStore:
|
||||
"""Fake persistent backend store without any scan capability."""
|
||||
|
||||
|
||||
class VectorStoreScanVectorsTests(unittest.TestCase):
|
||||
"""VectorStore.scan_vectors() / iter_vectors() backend-agnostic accessors."""
|
||||
|
||||
def setUp(self):
|
||||
self.vectors = [np.array([1.0, 0.0]), np.array([0.0, 1.0]), np.array([1.0, 1.0])]
|
||||
self.metadata = [{"type": "a"}, {"type": "b"}, {"type": "c"}]
|
||||
|
||||
def test_scan_inmemory_pages_through_all_vectors(self):
|
||||
store = VectorStore(backend="inmemory", dimension=2)
|
||||
ids = store.store_vectors(self.vectors, self.metadata)
|
||||
|
||||
page1 = store.scan_vectors(offset=0, limit=2)
|
||||
page2 = store.scan_vectors(offset=2, limit=2)
|
||||
|
||||
self.assertEqual([p["id"] for p in page1], ids[:2])
|
||||
self.assertEqual([p["id"] for p in page2], ids[2:])
|
||||
self.assertEqual(page2[0]["metadata"], {"type": "c"})
|
||||
|
||||
def test_scan_inmemory_empty_store(self):
|
||||
store = VectorStore(backend="inmemory", dimension=2)
|
||||
self.assertEqual(store.scan_vectors(offset=0, limit=10), [])
|
||||
|
||||
def test_scan_zero_limit_returns_empty_list(self):
|
||||
store = VectorStore(backend="inmemory", dimension=2)
|
||||
store.store_vectors(self.vectors, self.metadata)
|
||||
self.assertEqual(store.scan_vectors(offset=0, limit=0), [])
|
||||
|
||||
def test_scan_delegates_to_backend_store(self):
|
||||
items = [{"id": "a", "metadata": {}, "vector": None}]
|
||||
store = VectorStore(backend="inmemory", dimension=2)
|
||||
store.backend = "faiss"
|
||||
store._backend_store = _ScanningBackendStore(items)
|
||||
self.assertEqual(store.scan_vectors(offset=0, limit=10), items)
|
||||
|
||||
def test_scan_raises_not_implemented_without_backend_support(self):
|
||||
store = VectorStore(backend="inmemory", dimension=2)
|
||||
store.backend = "faiss"
|
||||
store._backend_store = _NonScanningBackendStore()
|
||||
with self.assertRaises(NotImplementedError):
|
||||
store.scan_vectors(offset=0, limit=10)
|
||||
|
||||
def test_iter_vectors_walks_every_page(self):
|
||||
store = VectorStore(backend="inmemory", dimension=2)
|
||||
ids = store.store_vectors(self.vectors, self.metadata)
|
||||
|
||||
collected = list(store.iter_vectors(batch_size=2))
|
||||
|
||||
self.assertEqual([item["id"] for item in collected], ids)
|
||||
|
||||
def test_iter_vectors_empty_store_yields_nothing(self):
|
||||
store = VectorStore(backend="inmemory", dimension=2)
|
||||
self.assertEqual(list(store.iter_vectors(batch_size=2)), [])
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# VectorManager tests — inmemory backend
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user