wsbuilder 1.3.0__tar.gz → 1.4.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.
- {wsbuilder-1.3.0/src/wsbuilder.egg-info → wsbuilder-1.4.0}/PKG-INFO +35 -3
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/README.md +30 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/api-map.md +25 -1
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/cache.md +4 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/http-versions.md +4 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/http.md +9 -1
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/ia.md +17 -0
- wsbuilder-1.4.0/docs/llm.md +181 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/persistence.md +8 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/proxy.md +2 -0
- wsbuilder-1.4.0/docs/rpc.md +182 -0
- wsbuilder-1.4.0/docs/tasks.md +107 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/mkdocs.yml +2 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/pyproject.toml +6 -3
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/__init__.py +56 -13
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/app.py +50 -6
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/cache.py +11 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/caches.py +4 -1
- wsbuilder-1.4.0/src/wsbuilder/codec.py +555 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/db_replicas.py +57 -19
- wsbuilder-1.4.0/src/wsbuilder/gguf.py +790 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/hpack.py +71 -22
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http.py +37 -16
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http2.py +25 -7
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http3.py +32 -16
- wsbuilder-1.4.0/src/wsbuilder/http3_client.py +536 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http3_server.py +121 -9
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ia.py +227 -75
- wsbuilder-1.4.0/src/wsbuilder/llm.py +1151 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/metrics.py +6 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/orm.py +49 -20
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/proxyi.py +4 -0
- wsbuilder-1.4.0/src/wsbuilder/quic/client.py +290 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/connection.py +76 -9
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/frames.py +7 -1
- wsbuilder-1.4.0/src/wsbuilder/quic/tls.py +1175 -0
- wsbuilder-1.4.0/src/wsbuilder/rpc.py +588 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/server.py +28 -1
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/tasks.py +174 -38
- {wsbuilder-1.3.0 → wsbuilder-1.4.0/src/wsbuilder.egg-info}/PKG-INFO +35 -3
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/SOURCES.txt +14 -0
- wsbuilder-1.4.0/src/wsbuilder.egg-info/requires.txt +9 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_cache.py +29 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_caches.py +29 -2
- wsbuilder-1.4.0/tests/test_codec.py +244 -0
- wsbuilder-1.4.0/tests/test_gguf.py +224 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http.py +25 -1
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3.py +22 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3_server.py +5 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3_streaming.py +29 -0
- wsbuilder-1.4.0/tests/test_ia.py +457 -0
- wsbuilder-1.4.0/tests/test_llm.py +676 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_metrics.py +13 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_optional_extras.py +33 -1
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_orm_replicas.py +97 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_proxyi.py +8 -0
- wsbuilder-1.4.0/tests/test_quic_client.py +399 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_recovery.py +6 -2
- wsbuilder-1.4.0/tests/test_response_lifecycle.py +124 -0
- wsbuilder-1.4.0/tests/test_rpc.py +379 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_server.py +53 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_server_errors.py +118 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_tasks.py +193 -0
- wsbuilder-1.3.0/docs/tasks.md +0 -85
- wsbuilder-1.3.0/src/wsbuilder/quic/tls.py +0 -618
- wsbuilder-1.3.0/src/wsbuilder.egg-info/requires.txt +0 -6
- wsbuilder-1.3.0/tests/test_ia.py +0 -199
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/LICENSE +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/MANIFEST.in +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/404.html +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/CNAME +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/advanced.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/ai-predict.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/app-http.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/architecture.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/assets/wsbuilder-mark.svg +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/beginners.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/cache-security.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/capabilities.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/data-orm.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/dns.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/full-project.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/getting-started.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/help/index.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/index.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/install.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/intermediate.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/mdns.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/network-edge.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/observability-tasks.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/observability.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/reference/index.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/security.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/stylesheets/extra.css +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/tls-certificates.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/websocket.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/.gitignore +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/README.md +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/app.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/pyproject.toml +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/upstream.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/setup.cfg +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/__main__.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/_coerce.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/_pragma.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/constants.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/cookies.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/dns.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/framework.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/headers.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http1.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http3_websocket.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ia_audio.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ia_text.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/logs.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/mdns.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/pki.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/predicts.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/qpack.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/__init__.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/address.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/crypto.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/packet.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/recovery.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/varint.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/security.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ws.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/dependency_links.txt +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/entry_points.txt +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/top_level.txt +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_app_threads.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_contributing_policy.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_demo_monitor.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_dns.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_docs.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_documentation_files.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_example_full_project.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_headers_cookies.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_hpack.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http1.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http2.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3_streaming_udp.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_ia_text_audio.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_logs.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_mdns.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_orm.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_pki.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_pr_identity_policy.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_predicts.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_proxyi_routing.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_address.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_crypto.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_protocol.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_tls.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_security.py +0 -0
- {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_ws.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wsbuilder
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Lightweight HTTP + WebSocket framework for Python.
|
|
5
5
|
Author: wsbuilder Authors
|
|
6
6
|
License-Expression: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/jorgelsc-dev/wsbuilder
|
|
|
8
8
|
Project-URL: Documentation, https://wsbuilder.jorgelsc.dev/
|
|
9
9
|
Project-URL: Repository, https://github.com/jorgelsc-dev/wsbuilder
|
|
10
10
|
Project-URL: Issues, https://github.com/jorgelsc-dev/wsbuilder/issues
|
|
11
|
-
Keywords: websocket,http,framework,server,python
|
|
11
|
+
Keywords: websocket,http,http3,quic,rpc,cbor,llm,gguf,framework,server,python
|
|
12
12
|
Classifier: Development Status :: 3 - Alpha
|
|
13
13
|
Classifier: Intended Audience :: Developers
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
@@ -24,9 +24,11 @@ Requires-Python: >=3.11
|
|
|
24
24
|
Description-Content-Type: text/markdown
|
|
25
25
|
License-File: LICENSE
|
|
26
26
|
Provides-Extra: tls
|
|
27
|
-
Requires-Dist: cryptography>=
|
|
27
|
+
Requires-Dist: cryptography>=43; extra == "tls"
|
|
28
28
|
Provides-Extra: mdns
|
|
29
29
|
Requires-Dist: zeroconf>=0.131; extra == "mdns"
|
|
30
|
+
Provides-Extra: llm
|
|
31
|
+
Requires-Dist: llama-cpp-python>=0.3; extra == "llm"
|
|
30
32
|
Dynamic: license-file
|
|
31
33
|
|
|
32
34
|
# wsbuilder
|
|
@@ -44,7 +46,11 @@ Sirve para crear:
|
|
|
44
46
|
- rate limiting, ACL, listas de IP y bloqueo temporal.
|
|
45
47
|
- metricas JSON, stream NDJSON, logs y tareas en background.
|
|
46
48
|
- DNS local, mDNS y descubrimiento de servicios, reverse proxy y balanceo.
|
|
49
|
+
- comunicacion entre microservicios en binario (CBOR) sobre HTTP/3 cifrado,
|
|
50
|
+
con identidad de servicio por mTLS.
|
|
47
51
|
- prediccion simple y redes neuronales basicas desde Python puro.
|
|
52
|
+
- modelos de lenguaje (LLM): importar, guardar y descargar GGUF/safetensors,
|
|
53
|
+
ejecutarlos con llama.cpp u Ollama y servirlos con API compatible con OpenAI.
|
|
48
54
|
|
|
49
55
|
## Ruta de documentacion
|
|
50
56
|
|
|
@@ -64,6 +70,8 @@ Tambien puedes leer:
|
|
|
64
70
|
- [Arquitectura](docs/architecture.md)
|
|
65
71
|
- [API publica](docs/api-map.md)
|
|
66
72
|
- [mDNS y DNS-SD](docs/mdns.md) (extra opcional `wsbuilder[mdns]`)
|
|
73
|
+
- [Modelos de lenguaje (LLM)](docs/llm.md) (GGUF, Hugging Face, llama.cpp/Ollama; ejecucion local con `wsbuilder[llm]`)
|
|
74
|
+
- [RPC entre servicios](docs/rpc.md) (CBOR sobre HTTP/3; HTTP/3 usa el extra `wsbuilder[tls]`)
|
|
67
75
|
- [Proyecto integral](docs/full-project.md)
|
|
68
76
|
|
|
69
77
|
## Instalacion rapida
|
|
@@ -153,7 +161,31 @@ La demo incluye:
|
|
|
153
161
|
| Seguridad | `security.py` | `SecurityPolicy`, `ACLRule`, `SecurityDecision` |
|
|
154
162
|
| Observabilidad | `metrics.py`, `logs.py`, `tasks.py` | `AppMetrics`, `NDJSONLog`, `TaskManager` |
|
|
155
163
|
| Red y edge | `dns.py`, `mdns.py`, `proxyi.py` | `LocalDNSServer`, `LocalMDNSServer`, `discover_mdns_services`, `ProxyI` |
|
|
164
|
+
| Entre servicios | `rpc.py`, `codec.py`, `http3_client.py` | `App.rpc`, `RpcClient`, `RpcError`, `cbor_dumps`, `Http3Client` |
|
|
156
165
|
| IA y prediccion | `ia.py`, `predicts.py` | `DataSet`, `NeuralNetwork`, `Predictor` |
|
|
166
|
+
| Modelos de lenguaje | `llm.py`, `gguf.py` | `ModelStore`, `LanguageModel`, `GGUFReader`, `GGUFWriter`, `App.enable_llm` |
|
|
167
|
+
|
|
168
|
+
## Comunicacion entre servicios
|
|
169
|
+
|
|
170
|
+
Los servicios se llaman entre si con el payload en CBOR (binario, alrededor de
|
|
171
|
+
un 30 % mas pequeno que JSON y sin base64 para `bytes`) sobre una conexion
|
|
172
|
+
HTTP/3 reutilizada y cifrada con TLS 1.3:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from wsbuilder import App, RpcClient
|
|
176
|
+
|
|
177
|
+
app = App()
|
|
178
|
+
|
|
179
|
+
@app.rpc("/users.get")
|
|
180
|
+
def get_user(request, payload):
|
|
181
|
+
return {"id": payload["id"], "name": "Ada"}
|
|
182
|
+
|
|
183
|
+
# En otro servicio:
|
|
184
|
+
with RpcClient("https://users.internal:8443", trust=ca) as users:
|
|
185
|
+
user = users.call("/users.get", {"id": 7})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Detalles, mTLS y limites en [RPC entre servicios](docs/rpc.md).
|
|
157
189
|
|
|
158
190
|
## Ejemplo con datos
|
|
159
191
|
|
|
@@ -13,7 +13,11 @@ Sirve para crear:
|
|
|
13
13
|
- rate limiting, ACL, listas de IP y bloqueo temporal.
|
|
14
14
|
- metricas JSON, stream NDJSON, logs y tareas en background.
|
|
15
15
|
- DNS local, mDNS y descubrimiento de servicios, reverse proxy y balanceo.
|
|
16
|
+
- comunicacion entre microservicios en binario (CBOR) sobre HTTP/3 cifrado,
|
|
17
|
+
con identidad de servicio por mTLS.
|
|
16
18
|
- prediccion simple y redes neuronales basicas desde Python puro.
|
|
19
|
+
- modelos de lenguaje (LLM): importar, guardar y descargar GGUF/safetensors,
|
|
20
|
+
ejecutarlos con llama.cpp u Ollama y servirlos con API compatible con OpenAI.
|
|
17
21
|
|
|
18
22
|
## Ruta de documentacion
|
|
19
23
|
|
|
@@ -33,6 +37,8 @@ Tambien puedes leer:
|
|
|
33
37
|
- [Arquitectura](docs/architecture.md)
|
|
34
38
|
- [API publica](docs/api-map.md)
|
|
35
39
|
- [mDNS y DNS-SD](docs/mdns.md) (extra opcional `wsbuilder[mdns]`)
|
|
40
|
+
- [Modelos de lenguaje (LLM)](docs/llm.md) (GGUF, Hugging Face, llama.cpp/Ollama; ejecucion local con `wsbuilder[llm]`)
|
|
41
|
+
- [RPC entre servicios](docs/rpc.md) (CBOR sobre HTTP/3; HTTP/3 usa el extra `wsbuilder[tls]`)
|
|
36
42
|
- [Proyecto integral](docs/full-project.md)
|
|
37
43
|
|
|
38
44
|
## Instalacion rapida
|
|
@@ -122,7 +128,31 @@ La demo incluye:
|
|
|
122
128
|
| Seguridad | `security.py` | `SecurityPolicy`, `ACLRule`, `SecurityDecision` |
|
|
123
129
|
| Observabilidad | `metrics.py`, `logs.py`, `tasks.py` | `AppMetrics`, `NDJSONLog`, `TaskManager` |
|
|
124
130
|
| Red y edge | `dns.py`, `mdns.py`, `proxyi.py` | `LocalDNSServer`, `LocalMDNSServer`, `discover_mdns_services`, `ProxyI` |
|
|
131
|
+
| Entre servicios | `rpc.py`, `codec.py`, `http3_client.py` | `App.rpc`, `RpcClient`, `RpcError`, `cbor_dumps`, `Http3Client` |
|
|
125
132
|
| IA y prediccion | `ia.py`, `predicts.py` | `DataSet`, `NeuralNetwork`, `Predictor` |
|
|
133
|
+
| Modelos de lenguaje | `llm.py`, `gguf.py` | `ModelStore`, `LanguageModel`, `GGUFReader`, `GGUFWriter`, `App.enable_llm` |
|
|
134
|
+
|
|
135
|
+
## Comunicacion entre servicios
|
|
136
|
+
|
|
137
|
+
Los servicios se llaman entre si con el payload en CBOR (binario, alrededor de
|
|
138
|
+
un 30 % mas pequeno que JSON y sin base64 para `bytes`) sobre una conexion
|
|
139
|
+
HTTP/3 reutilizada y cifrada con TLS 1.3:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from wsbuilder import App, RpcClient
|
|
143
|
+
|
|
144
|
+
app = App()
|
|
145
|
+
|
|
146
|
+
@app.rpc("/users.get")
|
|
147
|
+
def get_user(request, payload):
|
|
148
|
+
return {"id": payload["id"], "name": "Ada"}
|
|
149
|
+
|
|
150
|
+
# En otro servicio:
|
|
151
|
+
with RpcClient("https://users.internal:8443", trust=ca) as users:
|
|
152
|
+
user = users.call("/users.get", {"id": 7})
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Detalles, mTLS y limites en [RPC entre servicios](docs/rpc.md).
|
|
126
156
|
|
|
127
157
|
## Ejemplo con datos
|
|
128
158
|
|
|
@@ -16,7 +16,7 @@ Para ejemplos completos por nivel, lee [Principiantes](beginners.md),
|
|
|
16
16
|
|
|
17
17
|
Metodos habituales:
|
|
18
18
|
|
|
19
|
-
- `App.view`, `App.api`, `App.route`, `App.ws`
|
|
19
|
+
- `App.view`, `App.api`, `App.rpc`, `App.route`, `App.ws`
|
|
20
20
|
- `App.enable_metrics`, `App.enable_security`, `App.enable_caches`,
|
|
21
21
|
`App.enable_logs`, `App.enable_docs`
|
|
22
22
|
- `App.dispatch`, `App.describe`, `App.run`, `App.close`
|
|
@@ -68,6 +68,17 @@ Filtros `QuerySet` documentados:
|
|
|
68
68
|
| Cabeceras | `normalize_header_name`, `get_header`, `has_header`, `set_header`, `add_header` |
|
|
69
69
|
| Cookies | `parse_cookie_header`, `get_cookie`, `build_set_cookie` |
|
|
70
70
|
|
|
71
|
+
## Comunicacion entre servicios
|
|
72
|
+
|
|
73
|
+
| Grupo | Exportaciones |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| RPC | `RpcClient`, `RpcError`, `RpcTransportError`, `RpcCodec`, `rpc_endpoint` |
|
|
76
|
+
| Codec CBOR | `cbor_dumps`, `cbor_loads`, `CBORTag`, `CBORSimple`, `CBORDecodeError`, `CBOREncodeError` |
|
|
77
|
+
| HTTP/3 cliente (extra `tls`) | `Http3Client`, `Http3Response`, `Http3Error` |
|
|
78
|
+
|
|
79
|
+
Metodo de la app: `App.rpc(path, methods=("POST",), *, codec="cbor")`. Guia
|
|
80
|
+
completa en [RPC entre servicios](rpc.md).
|
|
81
|
+
|
|
71
82
|
## DNS y proxy
|
|
72
83
|
|
|
73
84
|
| Grupo | Exportaciones |
|
|
@@ -101,6 +112,19 @@ Filtros `QuerySet` documentados:
|
|
|
101
112
|
| Redes | `DenseLayer`, `NeuralNetwork`, `submit_training_task` |
|
|
102
113
|
| Analisis | `describe_data`, `evaluate_errors` |
|
|
103
114
|
| Prediccion | `Predictor` |
|
|
115
|
+
| Persistencia de redes | `NeuralNetwork.save`, `NeuralNetwork.load` (GGUF) |
|
|
116
|
+
|
|
117
|
+
## Modelos de lenguaje
|
|
118
|
+
|
|
119
|
+
| Grupo | Exportaciones |
|
|
120
|
+
| --- | --- |
|
|
121
|
+
| Almacen | `ModelStore`, `ModelRecord` |
|
|
122
|
+
| Descargas | `download_file`, `huggingface_files`, `huggingface_url`, `DownloadError` |
|
|
123
|
+
| Ejecucion | `LanguageModel`, `LlamaCppBackend` (extra `llm`), `OpenAICompatibleBackend` |
|
|
124
|
+
| HTTP | `install_llm`, `App.enable_llm` |
|
|
125
|
+
| GGUF | `GGUFReader`, `GGUFWriter`, `GGMLType`, `GGUFError`, `read_gguf_metadata` |
|
|
126
|
+
|
|
127
|
+
Guia completa en [Modelos de lenguaje](llm.md).
|
|
104
128
|
|
|
105
129
|
## Modulos directos utiles
|
|
106
130
|
|
|
@@ -81,6 +81,10 @@ install_metrics(app, app_name="cache-demo")
|
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Cuando la cache esta activa, `App.dispatch()` puede responder con `X-WSBuilder-Cache: HIT`.
|
|
84
|
+
Las claves de respuestas distinguen el `Host` y el esquema del transporte
|
|
85
|
+
(`http` o `https`); las cabeceras reenviadas no cambian ese esquema.
|
|
86
|
+
`App.close()` cierra las caches instaladas, también cuando usan un `attr_name`
|
|
87
|
+
personalizado, y evita cerrar dos veces un store compartido.
|
|
84
88
|
|
|
85
89
|
## Lo que conviene recordar
|
|
86
90
|
|
|
@@ -105,6 +105,10 @@ threading.Thread(target=h3.serve_forever, daemon=True).start()
|
|
|
105
105
|
Un cliente no descubre HTTP/3 solo: llega por la cabecera `Alt-Svc` que anuncia
|
|
106
106
|
una respuesta HTTP/1 o HTTP/2.
|
|
107
107
|
|
|
108
|
+
Una peticion HTTP/3 con `Content-Length` debe usar digitos ASCII y coincidir con
|
|
109
|
+
los DATA recibidos al finalizar. Una longitud invalida termina ese stream con
|
|
110
|
+
`H3_MESSAGE_ERROR`, sin ejecutar el handler ni interrumpir las otras peticiones.
|
|
111
|
+
|
|
108
112
|
```python
|
|
109
113
|
@app.api("/algo")
|
|
110
114
|
def algo(request):
|
|
@@ -35,7 +35,10 @@ Response.stream(iter(["a", "b", "c"]), content_type="text/plain")
|
|
|
35
35
|
|
|
36
36
|
`Response.stream` devuelve una respuesta en streaming con soporte para generadores, iterables y objetos con `read()`.
|
|
37
37
|
Al terminar el envio (o si el cliente se desconecta) el servidor llama a
|
|
38
|
-
`close()` sobre el origen del stream cuando existe.
|
|
38
|
+
`close()` sobre el origen del stream cuando existe. Tambien lo cierra sin
|
|
39
|
+
consumirlo para `HEAD` y para los estados sin cuerpo (`1xx`, `204`, `304`).
|
|
40
|
+
Esto se aplica a HTTP/1, HTTP/2 y HTTP/3. Un `Content-Length` explicito en `HEAD`
|
|
41
|
+
o `304` describe la representacion; los estados `1xx` y `204` lo omiten.
|
|
39
42
|
|
|
40
43
|
`Response.headers` es un diccionario con un valor por nombre. Un valor puede ser
|
|
41
44
|
una lista o tupla para enviar el campo repetido, que es lo que necesita
|
|
@@ -127,3 +130,8 @@ def sum_view(request):
|
|
|
127
130
|
- `parse_http_request` para parseo de requests en bruto.
|
|
128
131
|
- `send_http_response` si necesitas escribir respuestas manualmente.
|
|
129
132
|
- `parse_query_string` para utilidades ligeras de query strings.
|
|
133
|
+
|
|
134
|
+
`send_http_response` devuelve `True` al completar el envio y `False` si falla
|
|
135
|
+
la escritura o el productor. En HTTP/1, una respuesta incompleta obliga a cerrar
|
|
136
|
+
la conexion antes de procesar otra peticion. Los errores de validacion de
|
|
137
|
+
cabeceras anteriores al envio siguen lanzando `ValueError`.
|
|
@@ -16,6 +16,8 @@ WSBuilder incluye herramientas sencillas de data science y redes neuronales hech
|
|
|
16
16
|
- `ByteTokenizer`
|
|
17
17
|
- `HashingTextVectorizer`
|
|
18
18
|
- `AudioData`, `load_wav`, `audio_features`
|
|
19
|
+
- `NeuralNetwork.save`, `NeuralNetwork.load` (GGUF)
|
|
20
|
+
- [Modelos de lenguaje](llm.md): `ModelStore`, `LanguageModel`, `GGUFReader`, `GGUFWriter`
|
|
19
21
|
|
|
20
22
|
## Texto, tokens y audio
|
|
21
23
|
|
|
@@ -29,6 +31,21 @@ Consulta los [ejemplos de texto y audio](ai-predict.md#texto-unicode-y-tokens).
|
|
|
29
31
|
Estos adaptadores preparan datos: no incorporan un modelo de conversacion,
|
|
30
32
|
traduccion, reconocimiento de voz, generacion de audio, imagen o video.
|
|
31
33
|
|
|
34
|
+
## Modelos de lenguaje y archivos GGUF
|
|
35
|
+
|
|
36
|
+
Para modelos de lenguaje (LLM) existe [`wsbuilder.llm`](llm.md). Permite
|
|
37
|
+
importar, guardar, exportar y descargar modelos GGUF y safetensors (por ejemplo
|
|
38
|
+
desde Hugging Face, con verificacion SHA-256), ejecutarlos con llama.cpp
|
|
39
|
+
(extra `wsbuilder[llm]`) o mediante un servidor compatible con OpenAI como
|
|
40
|
+
Ollama, y servirlos por HTTP.
|
|
41
|
+
|
|
42
|
+
Las redes propias tambien se guardan y cargan en GGUF:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
network.save("modelo.gguf", name="mi-red")
|
|
46
|
+
network = NeuralNetwork.load("modelo.gguf")
|
|
47
|
+
```
|
|
48
|
+
|
|
32
49
|
## Datos
|
|
33
50
|
|
|
34
51
|
```python
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Modelos de lenguaje (LLM)
|
|
2
|
+
|
|
3
|
+
`wsbuilder.llm` permite importar, guardar, descargar y ejecutar modelos de
|
|
4
|
+
lenguaje. Tambien puede servirlos por HTTP, con un endpoint compatible con la
|
|
5
|
+
API de OpenAI.
|
|
6
|
+
|
|
7
|
+
| Pieza | Para que sirve | Dependencias |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| `GGUFReader` / `GGUFWriter` | Leer y escribir archivos GGUF, el formato de llama.cpp y Ollama | Ninguna |
|
|
10
|
+
| `ModelStore` | Directorio de modelos: importar, descargar, exportar, verificar y borrar | Ninguna |
|
|
11
|
+
| `download_file`, `huggingface_files` | Descargas reanudables y verificadas con SHA-256 | Ninguna |
|
|
12
|
+
| `LanguageModel` + `OpenAICompatibleBackend` | Usar un modelo servido por Ollama, llama.cpp server, vLLM, LM Studio u OpenAI | Ninguna |
|
|
13
|
+
| `LanguageModel` + `LlamaCppBackend` | Ejecutar un GGUF local en este proceso | `pip install 'wsbuilder[llm]'` |
|
|
14
|
+
| `install_llm` / `App.enable_llm` | Exponer los modelos por HTTP | Ninguna |
|
|
15
|
+
|
|
16
|
+
Para ejecutar un LLM hace falta un motor de inferencia escrito en C/C++, y
|
|
17
|
+
wsbuilder no intenta serlo. Todo lo demas funciona solo con la biblioteca
|
|
18
|
+
estandar.
|
|
19
|
+
|
|
20
|
+
## Guardar modelos: el almacen
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
from wsbuilder import ModelStore
|
|
24
|
+
|
|
25
|
+
store = ModelStore() # $WSBUILDER_MODELS o ~/.cache/wsbuilder/models
|
|
26
|
+
record = store.import_file("descargas/qwen2.5-0.5b-instruct-q4_k_m.gguf")
|
|
27
|
+
print(record.id, record.summary["architecture"], record.summary["quantization"])
|
|
28
|
+
|
|
29
|
+
for model in store.list():
|
|
30
|
+
print(model.id, model.format, model.size, model.sha256)
|
|
31
|
+
|
|
32
|
+
store.export(record.id, "/backup/") # copia el archivo fuera del almacen
|
|
33
|
+
store.verify(record.id) # recalcula el SHA-256
|
|
34
|
+
store.rename(record.id, "Qwen 0.5B")
|
|
35
|
+
store.remove(record.id)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- Cada modelo vive en `<raiz>/<id>/` junto a un `manifest.json` con su nombre,
|
|
39
|
+
formato, tamano, SHA-256, origen y un resumen de sus metadatos. Como no hay
|
|
40
|
+
un indice compartido, no hay nada comun que se pueda corromper. Las
|
|
41
|
+
escrituras pasan por archivos temporales y `os.replace`.
|
|
42
|
+
- Se reconocen GGUF y safetensors. Otros formatos se rechazan salvo que se
|
|
43
|
+
pase `require_known_format=False`. Un archivo malformado nunca entra al
|
|
44
|
+
almacen.
|
|
45
|
+
- `import_file(..., move=True)` mueve el archivo en vez de copiarlo.
|
|
46
|
+
|
|
47
|
+
En un GGUF, el resumen incluye `name`, `architecture`, `parameters`,
|
|
48
|
+
`quantization` (por ejemplo `Q4_K_M`), `context_length`, `embedding_length`,
|
|
49
|
+
`block_count`, `attention_heads`, `tokenizer`, `vocabulary_size`,
|
|
50
|
+
`chat_template`, `license` y `file_size`.
|
|
51
|
+
|
|
52
|
+
## Descargar modelos
|
|
53
|
+
|
|
54
|
+
Desde Hugging Face:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from wsbuilder import ModelStore, huggingface_files
|
|
58
|
+
|
|
59
|
+
files = huggingface_files("Qwen/Qwen2.5-0.5B-Instruct-GGUF", suffix=".gguf")
|
|
60
|
+
# [{"path": "qwen2.5-0.5b-instruct-q4_k_m.gguf", "size": 491400032, "sha256": "..."}, ...]
|
|
61
|
+
|
|
62
|
+
store = ModelStore()
|
|
63
|
+
record = store.download_huggingface(
|
|
64
|
+
"Qwen/Qwen2.5-0.5B-Instruct-GGUF",
|
|
65
|
+
"qwen2.5-0.5b-instruct-q4_k_m.gguf",
|
|
66
|
+
progress=lambda done, total: print(done, total),
|
|
67
|
+
)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Desde cualquier URL HTTPS:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
record = store.download("https://example.com/modelo.gguf", sha256="...")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
- **Verificacion.** `download_huggingface` toma de la API del Hub el SHA-256
|
|
77
|
+
publicado y lo comprueba. Si no coincide, el archivo se borra y se lanza
|
|
78
|
+
`DownloadError`.
|
|
79
|
+
- **Reanudacion.** Si la descarga se corta, el siguiente intento continua desde
|
|
80
|
+
el archivo `.part` con una peticion `Range`. Solo se reanuda cuando hay un
|
|
81
|
+
SHA-256 con el que verificar el resultado.
|
|
82
|
+
- **Limites.** Solo se aceptan URLs `https://`. `max_bytes` acota el tamano
|
|
83
|
+
(200 GiB por defecto) y `cancel_event` permite cancelar la descarga.
|
|
84
|
+
- **Repositorios privados.** El token se toma de `token=` o de `HF_TOKEN` y se
|
|
85
|
+
envia solo al host original: si el Hub redirige a su CDN, la cabecera
|
|
86
|
+
`Authorization` no viaja. `HF_ENDPOINT` permite usar un mirror.
|
|
87
|
+
|
|
88
|
+
## Ejecutar modelos
|
|
89
|
+
|
|
90
|
+
Un GGUF local con llama.cpp (`pip install 'wsbuilder[llm]'`):
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
model = store.load(record.id, n_ctx=4096, n_gpu_layers=0) # se cachea por id
|
|
94
|
+
print(model.chat("Explica que es QUIC en una frase"))
|
|
95
|
+
for piece in model.stream_chat([{"role": "user", "content": "Hola"}], max_tokens=64):
|
|
96
|
+
print(piece, end="", flush=True)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Un modelo servido por Ollama u otro servidor compatible con OpenAI:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from wsbuilder import LanguageModel
|
|
103
|
+
|
|
104
|
+
model = LanguageModel.from_openai("http://localhost:11434/v1", "llama3.2")
|
|
105
|
+
print(model.chat("Hola"))
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
| Metodo | Devuelve |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `generate(prompt, **opciones)` | El texto completado. |
|
|
111
|
+
| `chat(messages, **opciones)` | La respuesta del asistente. `messages` puede ser un texto o una lista de `{"role", "content"}`. |
|
|
112
|
+
| `chat_completion(messages, **opciones)` | `{"text", "finish_reason", "usage"}`. |
|
|
113
|
+
| `stream_generate`, `stream_chat` | Generadores que van entregando el texto. |
|
|
114
|
+
|
|
115
|
+
Las opciones admitidas son `max_tokens`, `temperature`, `top_p`, `top_k`,
|
|
116
|
+
`stop`, `seed`, `repeat_penalty`, `presence_penalty` y `frequency_penalty`.
|
|
117
|
+
|
|
118
|
+
## Servir modelos por HTTP
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from wsbuilder import App, LanguageModel, ModelStore
|
|
122
|
+
|
|
123
|
+
app = App()
|
|
124
|
+
app.enable_security(...) # protege estas rutas antes de exponerlas
|
|
125
|
+
app.enable_llm(
|
|
126
|
+
ModelStore(),
|
|
127
|
+
models={"llama3.2": LanguageModel.from_openai("http://localhost:11434/v1", "llama3.2")},
|
|
128
|
+
backend_options={"n_ctx": 4096},
|
|
129
|
+
)
|
|
130
|
+
app.run("0.0.0.0", 8765)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| Ruta | Uso |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `GET /api/llm/models` | Lista los modelos del almacen y los remotos. |
|
|
136
|
+
| `GET /api/llm/model?id=` | Devuelve el manifiesto de un modelo. |
|
|
137
|
+
| `POST /api/llm/chat` | Recibe `{"model", "messages", "stream"}`. Con `stream`, responde en NDJSON (`{"content": ...}` por linea). |
|
|
138
|
+
| `POST /api/llm/generate` | Recibe `{"model", "prompt", "stream"}`. |
|
|
139
|
+
| `GET /v1/models`, `POST /v1/chat/completions` | API de OpenAI, con streaming SSE incluido. Cualquier cliente de OpenAI puede apuntar a la app. |
|
|
140
|
+
|
|
141
|
+
Las rutas que modifican el disco del servidor estan desactivadas por defecto:
|
|
142
|
+
|
|
143
|
+
- `allow_download=True` activa `POST /api/llm/download` (`{"repo", "filename"}`
|
|
144
|
+
o `{"url", "sha256"}`). La descarga corre como tarea en segundo plano y su
|
|
145
|
+
progreso se consulta en `GET /api/llm/tasks?id=`. `download_hosts`
|
|
146
|
+
restringe los hosts permitidos.
|
|
147
|
+
- `allow_import=True` activa `POST /api/llm/import` (`{"path"}`).
|
|
148
|
+
- `allow_delete=True` activa `POST /api/llm/delete` (`{"id"}`).
|
|
149
|
+
|
|
150
|
+
## Archivos GGUF y redes propias
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from wsbuilder import GGUFReader, GGUFWriter
|
|
154
|
+
|
|
155
|
+
with GGUFReader("modelo.gguf") as gguf:
|
|
156
|
+
gguf.metadata["general.architecture"]
|
|
157
|
+
gguf.summary()
|
|
158
|
+
gguf.tensor("token_embd.weight").shape
|
|
159
|
+
|
|
160
|
+
writer = GGUFWriter(architecture="mi-modelo")
|
|
161
|
+
writer.add_metadata("general.name", "demo")
|
|
162
|
+
writer.add_tensor("capa.0.weight", [0.1, 0.2, 0.3, 0.4], shape=(2, 2), ggml_type="F16")
|
|
163
|
+
writer.write("demo.gguf")
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- **Lectura sin cargar pesos.** El lector solo lee la cabecera y las tablas,
|
|
167
|
+
asi que abrir un modelo de varios GB es inmediato. Los arrays largos, como
|
|
168
|
+
el vocabulario, quedan como `GGUFArray` (tipo y longitud) salvo que se pase
|
|
169
|
+
`max_array_items=None`.
|
|
170
|
+
- **Decodificacion de tensores.** `tensor.values()` decodifica F32, F16, BF16,
|
|
171
|
+
F64, I8 a I64, Q8_0 y Q4_0. Las cuantizaciones K e IQ se leen como bytes
|
|
172
|
+
crudos.
|
|
173
|
+
- **Entrada no confiable.** Cada conteo y cada longitud se comprueban contra el
|
|
174
|
+
tamano del archivo antes de reservar memoria.
|
|
175
|
+
|
|
176
|
+
Las redes de `NeuralNetwork` tambien se guardan en GGUF:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
network.save("xor.gguf", name="xor") # dtype="F64" (exacto), "F32" o "F16"
|
|
180
|
+
same = NeuralNetwork.load("xor.gguf") # mismas predicciones
|
|
181
|
+
```
|
|
@@ -76,6 +76,10 @@ with db.transaction():
|
|
|
76
76
|
|
|
77
77
|
Si una excepcion rompe el bloque, la transaccion se revierte.
|
|
78
78
|
|
|
79
|
+
Fuera de un bloque explicito, `execute()` y `executemany()` revierten la
|
|
80
|
+
transaccion si falla su confirmacion automatica, tambien con restricciones
|
|
81
|
+
diferidas. La conexion queda disponible para operaciones posteriores.
|
|
82
|
+
|
|
79
83
|
## Replicas de lectura
|
|
80
84
|
|
|
81
85
|
La familia `db_replicas` agrega lectura optimizada:
|
|
@@ -94,6 +98,10 @@ db = Database("app.db", enable_wal=True, enable_replicas=True, replica_count=3)
|
|
|
94
98
|
rows = db.read_replica_fetchall("SELECT * FROM users")
|
|
95
99
|
```
|
|
96
100
|
|
|
101
|
+
`db.close()` es idempotente y cierra tambien las replicas. Las lecturas
|
|
102
|
+
posteriores lanzan `sqlite3.ProgrammingError`; para volver a operar, crea otra
|
|
103
|
+
instancia de la base.
|
|
104
|
+
|
|
97
105
|
En `QuerySet`, puedes forzar el camino de replica con:
|
|
98
106
|
|
|
99
107
|
```python
|
|
@@ -55,6 +55,8 @@ proxy.vhost("api.test.local", name="api-vhost").location("/api").upstream("http:
|
|
|
55
55
|
`%5c`, tambien `..;param`), se rechaza con `400` antes de evaluar reglas, para
|
|
56
56
|
que no escape de un prefijo ni del `base_path` del target.
|
|
57
57
|
- Las respuestas con `Content-Length` se leen con buffer bajo esos dos limites.
|
|
58
|
+
Si el upstream cierra antes de entregar la longitud declarada, se responde
|
|
59
|
+
`502`.
|
|
58
60
|
Las respuestas `chunked`, delimitadas por el cierre de la conexion o
|
|
59
61
|
`text/event-stream` se retransmiten como `Response.stream` por bloques, sin
|
|
60
62
|
cargarlas en memoria. En ese modo no hay plazo total ni limite de bytes: cada
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Comunicacion entre servicios (RPC)
|
|
2
|
+
|
|
3
|
+
`wsbuilder.rpc` sirve para que un servicio llame a otro gastando menos red. El
|
|
4
|
+
servicio que llama entrega un objeto Python. Ese objeto se codifica directamente
|
|
5
|
+
a bytes en formato CBOR (RFC 8949), viaja cifrado y el servicio de destino lo
|
|
6
|
+
recibe ya decodificado. En ningun punto del camino se escribe JSON como texto.
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
dict (servicio A) -> CBOR -> bytes cifrados por HTTP/3 -> CBOR -> dict (servicio B)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Si un servicio necesita responder JSON a un navegador, lo genera solo en su
|
|
13
|
+
borde. Entre servicios no hace falta.
|
|
14
|
+
|
|
15
|
+
## Por que ahorra red
|
|
16
|
+
|
|
17
|
+
- **Cuerpo mas pequeno.** CBOR escribe tipos y longitudes en binario, sin
|
|
18
|
+
comillas, sin escapes y sin numeros como texto. En registros tipicos el cuerpo
|
|
19
|
+
ocupa alrededor de un 30 % menos que el JSON compacto equivalente.
|
|
20
|
+
- **Bytes sin base64.** Un `bytes` (audio, imagen, firma) viaja tal cual. En
|
|
21
|
+
JSON tendria que ir en base64, que ocupa un 33 % mas.
|
|
22
|
+
- **Una conexion reutilizada.** Con HTTP/3 se mantiene abierta una conexion
|
|
23
|
+
QUIC y cada llamada abre un stream nuevo dentro de ella. Solo la primera
|
|
24
|
+
llamada paga el handshake y una respuesta lenta no bloquea a las demas.
|
|
25
|
+
- **Cifrado incluido.** QUIC exige TLS 1.3, asi que todo lo que viaja va
|
|
26
|
+
cifrado. No hace falta una capa de cifrado propia.
|
|
27
|
+
|
|
28
|
+
!!! note "CPU frente a red"
|
|
29
|
+
El codec CBOR esta escrito en Python puro para no anadir dependencias. El
|
|
30
|
+
modulo `json` de la biblioteca estandar esta escrito en C, de modo que
|
|
31
|
+
codificar y decodificar CBOR consume mas CPU que JSON: unas 3 a 5 veces
|
|
32
|
+
mas, alrededor de 1 ms por cada 20 KB de cuerpo. El ahorro esta en los
|
|
33
|
+
bytes que viajan. Si para un servicio pesa mas la CPU, usa `codec="json"`,
|
|
34
|
+
que mantiene el resto de ventajas (conexion reutilizada, errores
|
|
35
|
+
estructurados, mTLS).
|
|
36
|
+
|
|
37
|
+
## Servidor
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from wsbuilder import App, RpcError
|
|
41
|
+
|
|
42
|
+
app = App()
|
|
43
|
+
|
|
44
|
+
@app.rpc("/users.get")
|
|
45
|
+
def get_user(request, payload):
|
|
46
|
+
user = USERS.get(payload["id"])
|
|
47
|
+
if user is None:
|
|
48
|
+
raise RpcError("usuario inexistente", code="not_found", status=404,
|
|
49
|
+
details={"id": payload["id"]})
|
|
50
|
+
return {"id": user.id, "name": user.name, "avatar": user.avatar_bytes}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
- El handler recibe `request` y el `payload` ya decodificado. Si el cuerpo
|
|
54
|
+
llega vacio, `payload` es `None`.
|
|
55
|
+
- Lo que el handler devuelve se codifica con el mismo codec de la peticion, o
|
|
56
|
+
con el que pida la cabecera `Accept`. Si la peticion no indica ninguno, se
|
|
57
|
+
usa el valor de `codec`, que por defecto es `"cbor"`.
|
|
58
|
+
- Si el handler lanza `RpcError(message, code=..., status=..., details=...)`,
|
|
59
|
+
el servidor responde ese status con el cuerpo
|
|
60
|
+
`{"error": {"code", "message", "details"}}`.
|
|
61
|
+
- Cualquier otra excepcion produce un `500` con `code="internal"`, sin revelar
|
|
62
|
+
detalles internos. La excepcion se registra en los logs de la app.
|
|
63
|
+
- Un `Content-Type` desconocido produce un `415` y un cuerpo que no se puede
|
|
64
|
+
decodificar produce un `400`.
|
|
65
|
+
- `app.rpc()` admite las mismas opciones de workers que `app.route()`.
|
|
66
|
+
|
|
67
|
+
La misma app atiende RPC por HTTP/1.1, HTTP/2 y HTTP/3. Para HTTP/3 levanta un
|
|
68
|
+
`Http3Server` (UDP) junto al servidor TCP. Ambos pueden compartir el mismo
|
|
69
|
+
puerto (ver [versiones de HTTP](http-versions.md)):
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
import threading
|
|
73
|
+
|
|
74
|
+
from wsbuilder import CertificateAuthority, CertificateManager
|
|
75
|
+
from wsbuilder.http3_server import Http3Server
|
|
76
|
+
|
|
77
|
+
ca = CertificateAuthority.create("Servicios internos")
|
|
78
|
+
tls = CertificateManager(ca=ca, common_name="users.internal",
|
|
79
|
+
dns_names=["users.internal"])
|
|
80
|
+
h3 = Http3Server("0.0.0.0", 8443, app, tls)
|
|
81
|
+
threading.Thread(target=h3.serve_forever, daemon=True).start()
|
|
82
|
+
app.run("0.0.0.0", 8443, ssl_context=tls.ssl_context())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Cliente
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from wsbuilder import RpcClient, RpcError, RpcTransportError
|
|
89
|
+
|
|
90
|
+
with RpcClient("https://users.internal:8443", trust=ca) as users:
|
|
91
|
+
user = users.call("/users.get", {"id": 7})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
| Opcion | Uso |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `base_url` | `https://host:puerto` usa HTTP/3; `http://host:puerto` usa HTTP/1.1 sin cifrar, pensado para desarrollo local. |
|
|
97
|
+
| `transport` | Fuerza `"http3"` o `"http1"`. `"http1"` con `https://` usa TLS sobre TCP. |
|
|
98
|
+
| `codec` | `"cbor"` (por defecto) o `"json"`. |
|
|
99
|
+
| `trust` / `ca_pem` | CA que debe firmar el certificado del servidor: un `CertificateAuthority` o un PEM. Si no se indica, se usa el almacen del sistema. |
|
|
100
|
+
| `client_material` | Certificado del cliente (`TLSMaterial`) para mTLS. |
|
|
101
|
+
| `server_name` | Nombre que se verifica en el certificado cuando no coincide con el host (por ejemplo, al llamar a una IP). |
|
|
102
|
+
| `timeout` | Segundos por llamada (10 por defecto). |
|
|
103
|
+
| `headers` | Cabeceras que se envian en todas las llamadas. |
|
|
104
|
+
| `max_response_bytes` | Limite del cuerpo de cada respuesta (64 MiB por defecto). |
|
|
105
|
+
|
|
106
|
+
- `call(path, payload=None, *, headers=None, timeout=None)` devuelve el
|
|
107
|
+
resultado decodificado.
|
|
108
|
+
- Una respuesta de error lanza `RpcError` en el cliente con el mismo `status`,
|
|
109
|
+
`code`, `message` y `details` que envio el servidor.
|
|
110
|
+
- Si no llega ninguna respuesta (conexion rechazada, timeout, certificado no
|
|
111
|
+
valido, respuesta rota), se lanza `RpcTransportError`.
|
|
112
|
+
- El cliente es seguro entre hilos: todas las llamadas comparten la conexion
|
|
113
|
+
HTTP/3. Con HTTP/1.1 cada hilo usa su propia conexion keep-alive.
|
|
114
|
+
- `describe()` informa del transporte, el codec, el numero de llamadas y los
|
|
115
|
+
bytes enviados y recibidos.
|
|
116
|
+
|
|
117
|
+
HTTP/3 necesita el extra `tls` (`pip install 'wsbuilder[tls]'`). Sin ese extra,
|
|
118
|
+
`RpcClient` funciona con `http://` y `transport="http1"`.
|
|
119
|
+
|
|
120
|
+
## Identidad de cada servicio (mTLS)
|
|
121
|
+
|
|
122
|
+
Con TLS mutuo cada servicio demuestra quien es con un certificado de la CA
|
|
123
|
+
interna, y el servidor rechaza durante el handshake a quien no lo presente.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
# Servidor: exige certificados firmados por la CA interna.
|
|
127
|
+
tls = CertificateManager(ca=ca, common_name="users.internal",
|
|
128
|
+
dns_names=["users.internal"],
|
|
129
|
+
client_ca_pem=ca.certificate_pem,
|
|
130
|
+
require_client_cert=True)
|
|
131
|
+
|
|
132
|
+
@app.rpc("/users.get")
|
|
133
|
+
def get_user(request, payload):
|
|
134
|
+
caller = request.tls["peer_cert"] # verificado durante el handshake
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
# Cliente: presenta su propio certificado.
|
|
138
|
+
orders_cert = ca.issue("orders-service", client_auth=True, server_auth=False)
|
|
139
|
+
users = RpcClient("https://users.internal:8443", trust=ca,
|
|
140
|
+
client_material=orders_cert)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
La misma configuracion `client_ca_pem` / `require_client_cert` se aplica a
|
|
144
|
+
HTTP/1.1, HTTP/2 y HTTP/3. Por HTTP/3, `request.tls["peer_cert"]` es un
|
|
145
|
+
diccionario con `common_name`, `dns_names`, `ip_addresses`, `uris`, `subject`,
|
|
146
|
+
`issuer`, `serial_number`, `not_valid_after` y `fingerprint_sha256`. Por
|
|
147
|
+
TCP conserva el formato de `ssl.getpeercert()`.
|
|
148
|
+
|
|
149
|
+
La verificacion de cadenas usa el verificador WebPKI de `cryptography`. Para
|
|
150
|
+
certificados de cliente sobre HTTP/3 hace falta `cryptography>=43`.
|
|
151
|
+
|
|
152
|
+
## Codec CBOR directo
|
|
153
|
+
|
|
154
|
+
El codec tambien se puede usar fuera de RPC:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from wsbuilder import cbor_dumps, cbor_loads
|
|
158
|
+
|
|
159
|
+
data = cbor_dumps({"id": 7, "blob": b"\x00\x01", "tags": ["a", "b"]})
|
|
160
|
+
assert cbor_loads(data) == {"id": 7, "blob": b"\x00\x01", "tags": ["a", "b"]}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
- Tipos soportados: `None`, `bool`, `int` (de cualquier tamano), `float`,
|
|
164
|
+
`str`, `bytes`, `list`/`tuple` y `dict`. `CBORTag` y `CBORSimple` cubren
|
|
165
|
+
etiquetas y valores simples.
|
|
166
|
+
- Cada valor se escribe en su forma mas corta. Por ejemplo, un `float` usa 16,
|
|
167
|
+
32 o 64 bits segun haga falta para conservarlo exacto.
|
|
168
|
+
- La entrada se trata como no confiable. Se comprueban las longitudes antes de
|
|
169
|
+
reservar memoria, la profundidad esta limitada (`max_depth`, 256 por
|
|
170
|
+
defecto) y se rechazan los bytes sobrantes.
|
|
171
|
+
|
|
172
|
+
## Limites actuales
|
|
173
|
+
|
|
174
|
+
- No hay 0-RTT ni reanudacion de sesion: cada conexion QUIC nueva hace un
|
|
175
|
+
handshake completo de 1-RTT. Como la conexion se reutiliza, esto solo afecta
|
|
176
|
+
a la primera llamada y a cada rotacion.
|
|
177
|
+
- El servidor HTTP/3 de wsbuilder concede 100 streams por conexion. Al
|
|
178
|
+
agotarlos, el cliente abre otra conexion de forma transparente.
|
|
179
|
+
- En una red local sin perdidas, el transporte HTTP/1.1 con keep-alive puede
|
|
180
|
+
tener menos latencia por llamada que HTTP/3, porque su TLS esta escrito en C.
|
|
181
|
+
HTTP/3 compensa cuando hay muchas llamadas concurrentes, perdida de paquetes
|
|
182
|
+
o conexiones nuevas frecuentes.
|