rfx-protocols-system 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rfx_protocols_system-0.2.0/.github/workflows/publish.yml +65 -0
- rfx_protocols_system-0.2.0/.gitignore +43 -0
- rfx_protocols_system-0.2.0/LICENSE +21 -0
- rfx_protocols_system-0.2.0/PKG-INFO +100 -0
- rfx_protocols_system-0.2.0/README.md +58 -0
- rfx_protocols_system-0.2.0/pyproject.toml +100 -0
- rfx_protocols_system-0.2.0/src/protocols_system/__init__.py +28 -0
- rfx_protocols_system-0.2.0/src/protocols_system/core/__init__.py +32 -0
- rfx_protocols_system-0.2.0/src/protocols_system/core/discovery.py +199 -0
- rfx_protocols_system-0.2.0/src/protocols_system/core/llm_support.py +194 -0
- rfx_protocols_system-0.2.0/src/protocols_system/core/registry.py +178 -0
- rfx_protocols_system-0.2.0/src/protocols_system/core/types.py +42 -0
- rfx_protocols_system-0.2.0/src/protocols_system/core/validation.py +13 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/__init__.py +40 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/cache.py +25 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/embedding.py +25 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/geo.py +46 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/health.py +28 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/image_library.py +105 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/llm.py +17 -0
- rfx_protocols_system-0.2.0/src/protocols_system/protocols/progress.py +20 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/__init__.py +53 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/google_places.py +335 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/huggingface_embedding.py +405 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/image_embedding.py +281 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/immich_client.py +540 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/llamacpp.py +392 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/local_cache.py +106 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/local_image.py +179 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/local_library.py +481 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/mux_image.py +298 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/ollama.py +260 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/ollama_embedding.py +136 -0
- rfx_protocols_system-0.2.0/src/protocols_system/providers/openai_llm.py +266 -0
- rfx_protocols_system-0.2.0/src/protocols_system/py.typed +0 -0
- rfx_protocols_system-0.2.0/src/protocols_system/webauth.py +98 -0
- rfx_protocols_system-0.2.0/tests/__init__.py +0 -0
- rfx_protocols_system-0.2.0/tests/test_discovery.py +138 -0
- rfx_protocols_system-0.2.0/tests/test_mux_image.py +113 -0
- rfx_protocols_system-0.2.0/tests/test_registry.py +172 -0
- rfx_protocols_system-0.2.0/tests/test_validation.py +94 -0
- rfx_protocols_system-0.2.0/tests/test_webauth.py +57 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: Build and Publish Wheel
|
|
2
|
+
|
|
3
|
+
# Builds the rfx-protocols-system wheel and publishes it so the consuming apps
|
|
4
|
+
# (PhotoStudio, Smart-Albums) can install a versioned artifact
|
|
5
|
+
# (rfx-protocols-system==0.2.x) instead of resolving a git SHA — which removes the
|
|
6
|
+
# need for `git` in every consumer Docker image and keeps both consumers
|
|
7
|
+
# locked to the same published version (SIMPLIFICATION 2.2 / 4.2).
|
|
8
|
+
#
|
|
9
|
+
# Triggers:
|
|
10
|
+
# * version tags (v*) -> release build, publishes to PyPI + attaches
|
|
11
|
+
# the wheel/sdist to a GitHub Release;
|
|
12
|
+
# * manual (workflow_dispatch) -> build + upload artifacts only (no publish).
|
|
13
|
+
on:
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
push:
|
|
16
|
+
tags:
|
|
17
|
+
- "v*"
|
|
18
|
+
|
|
19
|
+
jobs:
|
|
20
|
+
build:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
- name: Set up Python
|
|
26
|
+
uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: "3.12"
|
|
29
|
+
|
|
30
|
+
- name: Install build tooling
|
|
31
|
+
run: python -m pip install --upgrade build
|
|
32
|
+
|
|
33
|
+
- name: Build wheel and sdist
|
|
34
|
+
run: python -m build --outdir dist
|
|
35
|
+
|
|
36
|
+
- name: Upload build artifacts
|
|
37
|
+
uses: actions/upload-artifact@v4
|
|
38
|
+
with:
|
|
39
|
+
name: dist
|
|
40
|
+
path: dist/*
|
|
41
|
+
|
|
42
|
+
publish-pypi:
|
|
43
|
+
# Only publish from a version tag, and only after a successful build.
|
|
44
|
+
needs: build
|
|
45
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
environment: pypi
|
|
48
|
+
permissions:
|
|
49
|
+
# Required for PyPI Trusted Publishing (OIDC) — no stored API token.
|
|
50
|
+
id-token: write
|
|
51
|
+
contents: write
|
|
52
|
+
steps:
|
|
53
|
+
- name: Download build artifacts
|
|
54
|
+
uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
|
|
62
|
+
- name: Attach artifacts to the GitHub Release
|
|
63
|
+
uses: softprops/action-gh-release@v2
|
|
64
|
+
with:
|
|
65
|
+
files: dist/*
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
|
|
7
|
+
# Virtual environments
|
|
8
|
+
.venv/
|
|
9
|
+
.venv-verify/
|
|
10
|
+
venv/
|
|
11
|
+
env/
|
|
12
|
+
|
|
13
|
+
# Distribution / packaging
|
|
14
|
+
dist/
|
|
15
|
+
build/
|
|
16
|
+
*.egg-info/
|
|
17
|
+
*.egg
|
|
18
|
+
|
|
19
|
+
# IDE
|
|
20
|
+
.idea/
|
|
21
|
+
.vscode/
|
|
22
|
+
*.swp
|
|
23
|
+
*.swo
|
|
24
|
+
*~
|
|
25
|
+
|
|
26
|
+
# mypy
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
|
|
29
|
+
# pytest
|
|
30
|
+
.pytest_cache/
|
|
31
|
+
htmlcov/
|
|
32
|
+
.coverage
|
|
33
|
+
coverage.xml
|
|
34
|
+
|
|
35
|
+
# ruff
|
|
36
|
+
.ruff_cache/
|
|
37
|
+
|
|
38
|
+
# OS
|
|
39
|
+
.DS_Store
|
|
40
|
+
Thumbs.db
|
|
41
|
+
|
|
42
|
+
# Project-specific
|
|
43
|
+
*.log
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rfloresx
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: rfx-protocols-system
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A self-describing, pluggable service locator: decorator-based provider registration, singleton lifecycle management, and introspection-driven GUI discovery.
|
|
5
|
+
Project-URL: Homepage, https://github.com/rfloresx/ProtocolsSystem
|
|
6
|
+
Project-URL: Repository, https://github.com/rfloresx/ProtocolsSystem
|
|
7
|
+
Project-URL: Issues, https://github.com/rfloresx/ProtocolsSystem/issues
|
|
8
|
+
Author: Roberto Flores
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: discovery,plugin,protocols,registry,service-locator
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
25
|
+
Provides-Extra: heif
|
|
26
|
+
Requires-Dist: pillow-heif<1,>=0.16.0; extra == 'heif'
|
|
27
|
+
Provides-Extra: huggingface
|
|
28
|
+
Requires-Dist: torch>=2.0.0; extra == 'huggingface'
|
|
29
|
+
Requires-Dist: transformers<5,>=4.38.0; extra == 'huggingface'
|
|
30
|
+
Provides-Extra: openai
|
|
31
|
+
Requires-Dist: openai<3,>=1.0.0; extra == 'openai'
|
|
32
|
+
Provides-Extra: providers
|
|
33
|
+
Requires-Dist: googlemaps>=4.10; extra == 'providers'
|
|
34
|
+
Requires-Dist: httpx>=0.27; extra == 'providers'
|
|
35
|
+
Requires-Dist: immichpy<9,>=8.0; extra == 'providers'
|
|
36
|
+
Requires-Dist: ollama>=0.4; extra == 'providers'
|
|
37
|
+
Requires-Dist: pillow>=10.0; extra == 'providers'
|
|
38
|
+
Requires-Dist: tenacity<10,>=9.0; extra == 'providers'
|
|
39
|
+
Provides-Extra: webauth
|
|
40
|
+
Requires-Dist: bcrypt>=4.0; extra == 'webauth'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# Protocols System
|
|
44
|
+
|
|
45
|
+
A self-describing, pluggable service locator for Python.
|
|
46
|
+
|
|
47
|
+
Protocols System provides decorator-based provider registration, singleton
|
|
48
|
+
lifecycle management, and introspection-driven GUI discovery. Protocol and
|
|
49
|
+
provider additions automatically surface in any UI that consumes the discovery
|
|
50
|
+
layer.
|
|
51
|
+
|
|
52
|
+
It is shared across projects (PhotoStudio, Smart-Albums) as a standalone
|
|
53
|
+
package so provider implementations and the registry live in one place.
|
|
54
|
+
|
|
55
|
+
## Install
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Core only (registry, discovery, protocols). The importable package is
|
|
59
|
+
# `protocols_system`; the distribution name is `rfx-protocols-system`.
|
|
60
|
+
pip install rfx-protocols-system
|
|
61
|
+
|
|
62
|
+
# With built-in provider implementations (Immich, Ollama, llama.cpp, Google Places, ...)
|
|
63
|
+
pip install "rfx-protocols-system[providers]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For local development against a checkout:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install -e "../ProtocolsSystem[providers]"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Layout
|
|
73
|
+
|
|
74
|
+
- `protocols_system.core` — registry, discovery, types, validation
|
|
75
|
+
- `protocols_system.protocols` — protocol (interface) definitions: cache,
|
|
76
|
+
embedding, geo, health, image_library, llm, progress
|
|
77
|
+
- `protocols_system.providers` — concrete provider implementations that
|
|
78
|
+
self-register via the `@ProtocolsRegistry.register` decorator
|
|
79
|
+
|
|
80
|
+
## Usage
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from protocols_system import ProtocolsRegistry, get_protocol_slots
|
|
84
|
+
from protocols_system.providers import register_builtin_providers
|
|
85
|
+
|
|
86
|
+
# Import provider modules so they register themselves. Providers whose
|
|
87
|
+
# optional dependencies are missing are skipped silently.
|
|
88
|
+
register_builtin_providers()
|
|
89
|
+
|
|
90
|
+
registry = ProtocolsRegistry()
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install -e ".[dev,providers]"
|
|
97
|
+
pytest
|
|
98
|
+
mypy
|
|
99
|
+
ruff check
|
|
100
|
+
```
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Protocols System
|
|
2
|
+
|
|
3
|
+
A self-describing, pluggable service locator for Python.
|
|
4
|
+
|
|
5
|
+
Protocols System provides decorator-based provider registration, singleton
|
|
6
|
+
lifecycle management, and introspection-driven GUI discovery. Protocol and
|
|
7
|
+
provider additions automatically surface in any UI that consumes the discovery
|
|
8
|
+
layer.
|
|
9
|
+
|
|
10
|
+
It is shared across projects (PhotoStudio, Smart-Albums) as a standalone
|
|
11
|
+
package so provider implementations and the registry live in one place.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
# Core only (registry, discovery, protocols). The importable package is
|
|
17
|
+
# `protocols_system`; the distribution name is `rfx-protocols-system`.
|
|
18
|
+
pip install rfx-protocols-system
|
|
19
|
+
|
|
20
|
+
# With built-in provider implementations (Immich, Ollama, llama.cpp, Google Places, ...)
|
|
21
|
+
pip install "rfx-protocols-system[providers]"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For local development against a checkout:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install -e "../ProtocolsSystem[providers]"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Layout
|
|
31
|
+
|
|
32
|
+
- `protocols_system.core` — registry, discovery, types, validation
|
|
33
|
+
- `protocols_system.protocols` — protocol (interface) definitions: cache,
|
|
34
|
+
embedding, geo, health, image_library, llm, progress
|
|
35
|
+
- `protocols_system.providers` — concrete provider implementations that
|
|
36
|
+
self-register via the `@ProtocolsRegistry.register` decorator
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from protocols_system import ProtocolsRegistry, get_protocol_slots
|
|
42
|
+
from protocols_system.providers import register_builtin_providers
|
|
43
|
+
|
|
44
|
+
# Import provider modules so they register themselves. Providers whose
|
|
45
|
+
# optional dependencies are missing are skipped silently.
|
|
46
|
+
register_builtin_providers()
|
|
47
|
+
|
|
48
|
+
registry = ProtocolsRegistry()
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Development
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install -e ".[dev,providers]"
|
|
55
|
+
pytest
|
|
56
|
+
mypy
|
|
57
|
+
ruff check
|
|
58
|
+
```
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rfx-protocols-system"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "A self-describing, pluggable service locator: decorator-based provider registration, singleton lifecycle management, and introspection-driven GUI discovery."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Roberto Flores" },
|
|
10
|
+
]
|
|
11
|
+
keywords = ["service-locator", "registry", "protocols", "plugin", "discovery"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Typing :: Typed",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
# Built-in provider implementations pull optional third-party clients.
|
|
24
|
+
# The registry skips providers whose dependencies are missing, so these
|
|
25
|
+
# are opt-in per consumer.
|
|
26
|
+
providers = [
|
|
27
|
+
"httpx>=0.27",
|
|
28
|
+
"immichpy>=8.0,<9",
|
|
29
|
+
"tenacity>=9.0,<10",
|
|
30
|
+
"ollama>=0.4",
|
|
31
|
+
"googlemaps>=4.10",
|
|
32
|
+
"Pillow>=10.0",
|
|
33
|
+
]
|
|
34
|
+
# OpenAI-compatible LLM provider (openai_llm).
|
|
35
|
+
openai = [
|
|
36
|
+
"openai>=1.0.0,<3",
|
|
37
|
+
]
|
|
38
|
+
# HEIC/HEIF decoding for the vision image-preparation path (pillow-heif).
|
|
39
|
+
# Without it, HEIC input reports a clear error rather than sending
|
|
40
|
+
# undecodable bytes to a vision API.
|
|
41
|
+
heif = [
|
|
42
|
+
"pillow-heif>=0.16.0,<1",
|
|
43
|
+
]
|
|
44
|
+
# Local CLIP-family image embeddings (huggingface_embedding). Heavy — pulls
|
|
45
|
+
# torch — so kept separate from the core providers extra.
|
|
46
|
+
huggingface = [
|
|
47
|
+
"transformers>=4.38.0,<5",
|
|
48
|
+
"torch>=2.0.0",
|
|
49
|
+
]
|
|
50
|
+
# Shared web-auth primitives (protocols_system.webauth): bcrypt hashing and
|
|
51
|
+
# timing-safe verification used by consumer web GUIs.
|
|
52
|
+
webauth = [
|
|
53
|
+
"bcrypt>=4.0",
|
|
54
|
+
]
|
|
55
|
+
dev = [
|
|
56
|
+
"pytest>=8.0",
|
|
57
|
+
"pytest-asyncio>=0.23",
|
|
58
|
+
"pytest-cov>=5.0",
|
|
59
|
+
"ruff>=0.5",
|
|
60
|
+
"mypy>=1.10",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
[project.urls]
|
|
64
|
+
Homepage = "https://github.com/rfloresx/ProtocolsSystem"
|
|
65
|
+
Repository = "https://github.com/rfloresx/ProtocolsSystem"
|
|
66
|
+
Issues = "https://github.com/rfloresx/ProtocolsSystem/issues"
|
|
67
|
+
|
|
68
|
+
[build-system]
|
|
69
|
+
requires = ["hatchling"]
|
|
70
|
+
build-backend = "hatchling.build"
|
|
71
|
+
|
|
72
|
+
[tool.hatch.build.targets.wheel]
|
|
73
|
+
packages = ["src/protocols_system"]
|
|
74
|
+
|
|
75
|
+
[tool.ruff]
|
|
76
|
+
target-version = "py311"
|
|
77
|
+
src = ["src"]
|
|
78
|
+
line-length = 99
|
|
79
|
+
|
|
80
|
+
[tool.ruff.lint]
|
|
81
|
+
select = ["E", "F", "I", "N", "W", "UP"]
|
|
82
|
+
|
|
83
|
+
[tool.mypy]
|
|
84
|
+
python_version = "3.11"
|
|
85
|
+
strict = true
|
|
86
|
+
packages = ["protocols_system"]
|
|
87
|
+
mypy_path = ["src"]
|
|
88
|
+
|
|
89
|
+
# immichpy has no py.typed / stubs; ollama and googlemaps are suppressed
|
|
90
|
+
# inline via `type: ignore` at their import sites.
|
|
91
|
+
[[tool.mypy.overrides]]
|
|
92
|
+
module = [
|
|
93
|
+
"immichpy.*",
|
|
94
|
+
]
|
|
95
|
+
ignore_missing_imports = true
|
|
96
|
+
|
|
97
|
+
[tool.pytest.ini_options]
|
|
98
|
+
testpaths = ["tests"]
|
|
99
|
+
pythonpath = ["src"]
|
|
100
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Protocols System — A self-describing, pluggable service locator.
|
|
2
|
+
|
|
3
|
+
Provides decorator-based provider registration, singleton lifecycle management,
|
|
4
|
+
and introspection-driven GUI discovery. Protocol and provider additions
|
|
5
|
+
automatically surface in any UI that consumes the discovery layer.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core import (
|
|
9
|
+
ConfigParam,
|
|
10
|
+
ProtocolSlot,
|
|
11
|
+
ProtocolsRegistry,
|
|
12
|
+
classify_widget,
|
|
13
|
+
get_all_provider_schemas,
|
|
14
|
+
get_protocol_slots,
|
|
15
|
+
get_provider_schema,
|
|
16
|
+
set_excluded_protocols,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ProtocolsRegistry",
|
|
21
|
+
"get_protocol_slots",
|
|
22
|
+
"get_provider_schema",
|
|
23
|
+
"get_all_provider_schemas",
|
|
24
|
+
"classify_widget",
|
|
25
|
+
"set_excluded_protocols",
|
|
26
|
+
"ProtocolSlot",
|
|
27
|
+
"ConfigParam",
|
|
28
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Protocols System core — registry, discovery, and types."""
|
|
2
|
+
|
|
3
|
+
from .discovery import (
|
|
4
|
+
classify_widget,
|
|
5
|
+
get_all_provider_schemas,
|
|
6
|
+
get_protocol_slots,
|
|
7
|
+
get_provider_schema,
|
|
8
|
+
set_excluded_protocols,
|
|
9
|
+
)
|
|
10
|
+
from .llm_support import (
|
|
11
|
+
ImageConversionError,
|
|
12
|
+
detect_mime,
|
|
13
|
+
prepare_image_for_vision,
|
|
14
|
+
validate_against_schema,
|
|
15
|
+
)
|
|
16
|
+
from .registry import ProtocolsRegistry
|
|
17
|
+
from .types import ConfigParam, ProtocolSlot
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ProtocolsRegistry",
|
|
21
|
+
"get_protocol_slots",
|
|
22
|
+
"get_provider_schema",
|
|
23
|
+
"get_all_provider_schemas",
|
|
24
|
+
"classify_widget",
|
|
25
|
+
"set_excluded_protocols",
|
|
26
|
+
"ProtocolSlot",
|
|
27
|
+
"ConfigParam",
|
|
28
|
+
"validate_against_schema",
|
|
29
|
+
"prepare_image_for_vision",
|
|
30
|
+
"detect_mime",
|
|
31
|
+
"ImageConversionError",
|
|
32
|
+
]
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Introspection-driven discovery for dynamic GUI rendering.
|
|
2
|
+
|
|
3
|
+
This module provides the bridge between the ProtocolsRegistry catalog and a UI
|
|
4
|
+
framework. It introspects protocols (via docstrings) and providers (via __init__
|
|
5
|
+
signatures) so that settings forms can be rendered without any per-provider UI code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from inspect import Parameter, signature
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .registry import ProtocolsRegistry
|
|
15
|
+
from .types import ConfigParam, ProtocolSlot
|
|
16
|
+
|
|
17
|
+
# Protocols in this set won't appear in user-facing settings UI.
|
|
18
|
+
_EXCLUDED_PROTOCOLS: set[type] = set()
|
|
19
|
+
|
|
20
|
+
# Hints that indicate a parameter should be rendered as a password field.
|
|
21
|
+
_SECRET_HINTS = ("api_key", "password", "secret", "token", "credentials")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def set_excluded_protocols(protocols: set[type]) -> None:
|
|
25
|
+
"""Configure which protocols are excluded from the settings UI.
|
|
26
|
+
|
|
27
|
+
Call this once at app startup after defining your infrastructure protocols.
|
|
28
|
+
"""
|
|
29
|
+
global _EXCLUDED_PROTOCOLS
|
|
30
|
+
_EXCLUDED_PROTOCOLS = protocols
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_protocol_slots() -> list[ProtocolSlot]:
|
|
34
|
+
"""Discover all configurable protocol slots from the registry.
|
|
35
|
+
|
|
36
|
+
Returns a list of ProtocolSlot objects suitable for rendering a settings page.
|
|
37
|
+
Infrastructure protocols in the exclusion set are filtered out.
|
|
38
|
+
"""
|
|
39
|
+
slots: list[ProtocolSlot] = []
|
|
40
|
+
for protocol_cls in ProtocolsRegistry.get_protocols():
|
|
41
|
+
if protocol_cls in _EXCLUDED_PROTOCOLS:
|
|
42
|
+
continue
|
|
43
|
+
name = _derive_slot_name(protocol_cls)
|
|
44
|
+
label, description = _parse_protocol_doc(protocol_cls)
|
|
45
|
+
slots.append(ProtocolSlot(name, protocol_cls, label, description))
|
|
46
|
+
return slots
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_provider_schema(protocol: type, provider_name: str) -> list[ConfigParam]:
|
|
50
|
+
"""Introspect a provider's __init__ to produce a list of config parameters.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
protocol: The protocol type.
|
|
54
|
+
provider_name: The registered provider name.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of ConfigParam objects describing each constructor parameter.
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If the protocol or provider is not registered.
|
|
61
|
+
"""
|
|
62
|
+
providers = ProtocolsRegistry.get_providers(protocol)
|
|
63
|
+
if provider_name not in providers:
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"No provider named '{provider_name}' for protocol {protocol.__name__}"
|
|
66
|
+
)
|
|
67
|
+
cls = providers[provider_name]
|
|
68
|
+
return _introspect_init(cls)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_all_provider_schemas(protocol: type) -> dict[str, list[ConfigParam]]:
|
|
72
|
+
"""Return schemas for all providers registered under a protocol.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dict mapping provider_name -> list[ConfigParam].
|
|
76
|
+
"""
|
|
77
|
+
providers = ProtocolsRegistry.get_providers(protocol)
|
|
78
|
+
return {name: _introspect_init(cls) for name, cls in providers.items()}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def classify_widget(key: str, annotation: str, default: Any) -> str:
|
|
82
|
+
"""Classify a parameter into a UI widget kind.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
key: Parameter name.
|
|
86
|
+
annotation: Type annotation as string.
|
|
87
|
+
default: Default value (or Parameter.empty).
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
One of: "bool", "password", "float", "int", "str".
|
|
91
|
+
"""
|
|
92
|
+
# Bool detection
|
|
93
|
+
if "bool" in annotation.lower() or isinstance(default, bool):
|
|
94
|
+
return "bool"
|
|
95
|
+
|
|
96
|
+
# Secret-looking parameter names
|
|
97
|
+
if any(hint in key.lower() for hint in _SECRET_HINTS):
|
|
98
|
+
return "password"
|
|
99
|
+
|
|
100
|
+
# Numeric types
|
|
101
|
+
if "float" in annotation.lower() or isinstance(default, float):
|
|
102
|
+
return "float"
|
|
103
|
+
if "int" in annotation.lower() or isinstance(default, int):
|
|
104
|
+
return "int"
|
|
105
|
+
|
|
106
|
+
return "str"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# ------------------------------------------------------------------
|
|
110
|
+
# Internal helpers
|
|
111
|
+
# ------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _derive_slot_name(protocol_cls: type) -> str:
|
|
115
|
+
"""Derive a config-friendly slot name from a protocol class name.
|
|
116
|
+
|
|
117
|
+
IImageGenerationClient -> "image_generation"
|
|
118
|
+
ILLMClient -> "llm"
|
|
119
|
+
IStorageClient -> "storage"
|
|
120
|
+
ICacheManager -> "cache_manager"
|
|
121
|
+
"""
|
|
122
|
+
name = protocol_cls.__name__
|
|
123
|
+
|
|
124
|
+
# Strip leading "I"
|
|
125
|
+
if name.startswith("I") and len(name) > 1 and name[1].isupper():
|
|
126
|
+
name = name[1:]
|
|
127
|
+
|
|
128
|
+
# Strip trailing "Client" or "Manager" or "Provider"
|
|
129
|
+
for suffix in ("Client", "Manager", "Provider", "Service"):
|
|
130
|
+
if name.endswith(suffix) and len(name) > len(suffix):
|
|
131
|
+
name = name[: -len(suffix)]
|
|
132
|
+
break
|
|
133
|
+
|
|
134
|
+
# CamelCase to snake_case
|
|
135
|
+
name = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", name)
|
|
136
|
+
name = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", name)
|
|
137
|
+
return name.lower()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _parse_protocol_doc(protocol_cls: type) -> tuple[str, str]:
|
|
141
|
+
"""Parse protocol docstring into (label, description).
|
|
142
|
+
|
|
143
|
+
Expected format: "Label:Description text here."
|
|
144
|
+
Falls back to class name if docstring is missing or malformed.
|
|
145
|
+
"""
|
|
146
|
+
doc = protocol_cls.__doc__ or ""
|
|
147
|
+
first_line = doc.strip().split("\n")[0].strip()
|
|
148
|
+
|
|
149
|
+
if ":" in first_line:
|
|
150
|
+
label, description = first_line.split(":", 1)
|
|
151
|
+
return label.strip(), description.strip()
|
|
152
|
+
|
|
153
|
+
# Fallback: derive label from class name
|
|
154
|
+
label = _derive_slot_name(protocol_cls).replace("_", " ").title()
|
|
155
|
+
return label, first_line or f"Provider for {label}."
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _introspect_init(cls: type) -> list[ConfigParam]:
|
|
159
|
+
"""Extract ConfigParam list from a class's __init__ signature."""
|
|
160
|
+
try:
|
|
161
|
+
sig = signature(cls.__init__) # type: ignore[misc]
|
|
162
|
+
except (ValueError, TypeError):
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
params: list[ConfigParam] = []
|
|
166
|
+
for name, param in sig.parameters.items():
|
|
167
|
+
if name == "self":
|
|
168
|
+
continue
|
|
169
|
+
if param.kind in (Parameter.VAR_POSITIONAL, Parameter.VAR_KEYWORD):
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
# Resolve annotation
|
|
173
|
+
if param.annotation is not Parameter.empty:
|
|
174
|
+
annotation = (
|
|
175
|
+
param.annotation.__name__
|
|
176
|
+
if hasattr(param.annotation, "__name__")
|
|
177
|
+
else str(param.annotation)
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
annotation = "str"
|
|
181
|
+
|
|
182
|
+
# Resolve default
|
|
183
|
+
has_default = param.default is not Parameter.empty
|
|
184
|
+
default = param.default if has_default else None
|
|
185
|
+
required = not has_default
|
|
186
|
+
|
|
187
|
+
# Classify widget
|
|
188
|
+
widget_kind = classify_widget(name, annotation, default)
|
|
189
|
+
|
|
190
|
+
params.append(
|
|
191
|
+
ConfigParam(
|
|
192
|
+
key=name,
|
|
193
|
+
annotation=annotation,
|
|
194
|
+
default=default,
|
|
195
|
+
required=required,
|
|
196
|
+
widget_kind=widget_kind,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
return params
|