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.
Files changed (156) hide show
  1. {wsbuilder-1.3.0/src/wsbuilder.egg-info → wsbuilder-1.4.0}/PKG-INFO +35 -3
  2. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/README.md +30 -0
  3. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/api-map.md +25 -1
  4. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/cache.md +4 -0
  5. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/http-versions.md +4 -0
  6. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/http.md +9 -1
  7. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/ia.md +17 -0
  8. wsbuilder-1.4.0/docs/llm.md +181 -0
  9. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/persistence.md +8 -0
  10. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/proxy.md +2 -0
  11. wsbuilder-1.4.0/docs/rpc.md +182 -0
  12. wsbuilder-1.4.0/docs/tasks.md +107 -0
  13. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/mkdocs.yml +2 -0
  14. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/pyproject.toml +6 -3
  15. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/__init__.py +56 -13
  16. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/app.py +50 -6
  17. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/cache.py +11 -0
  18. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/caches.py +4 -1
  19. wsbuilder-1.4.0/src/wsbuilder/codec.py +555 -0
  20. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/db_replicas.py +57 -19
  21. wsbuilder-1.4.0/src/wsbuilder/gguf.py +790 -0
  22. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/hpack.py +71 -22
  23. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http.py +37 -16
  24. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http2.py +25 -7
  25. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http3.py +32 -16
  26. wsbuilder-1.4.0/src/wsbuilder/http3_client.py +536 -0
  27. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http3_server.py +121 -9
  28. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ia.py +227 -75
  29. wsbuilder-1.4.0/src/wsbuilder/llm.py +1151 -0
  30. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/metrics.py +6 -0
  31. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/orm.py +49 -20
  32. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/proxyi.py +4 -0
  33. wsbuilder-1.4.0/src/wsbuilder/quic/client.py +290 -0
  34. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/connection.py +76 -9
  35. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/frames.py +7 -1
  36. wsbuilder-1.4.0/src/wsbuilder/quic/tls.py +1175 -0
  37. wsbuilder-1.4.0/src/wsbuilder/rpc.py +588 -0
  38. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/server.py +28 -1
  39. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/tasks.py +174 -38
  40. {wsbuilder-1.3.0 → wsbuilder-1.4.0/src/wsbuilder.egg-info}/PKG-INFO +35 -3
  41. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/SOURCES.txt +14 -0
  42. wsbuilder-1.4.0/src/wsbuilder.egg-info/requires.txt +9 -0
  43. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_cache.py +29 -0
  44. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_caches.py +29 -2
  45. wsbuilder-1.4.0/tests/test_codec.py +244 -0
  46. wsbuilder-1.4.0/tests/test_gguf.py +224 -0
  47. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http.py +25 -1
  48. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3.py +22 -0
  49. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3_server.py +5 -0
  50. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3_streaming.py +29 -0
  51. wsbuilder-1.4.0/tests/test_ia.py +457 -0
  52. wsbuilder-1.4.0/tests/test_llm.py +676 -0
  53. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_metrics.py +13 -0
  54. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_optional_extras.py +33 -1
  55. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_orm_replicas.py +97 -0
  56. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_proxyi.py +8 -0
  57. wsbuilder-1.4.0/tests/test_quic_client.py +399 -0
  58. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_recovery.py +6 -2
  59. wsbuilder-1.4.0/tests/test_response_lifecycle.py +124 -0
  60. wsbuilder-1.4.0/tests/test_rpc.py +379 -0
  61. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_server.py +53 -0
  62. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_server_errors.py +118 -0
  63. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_tasks.py +193 -0
  64. wsbuilder-1.3.0/docs/tasks.md +0 -85
  65. wsbuilder-1.3.0/src/wsbuilder/quic/tls.py +0 -618
  66. wsbuilder-1.3.0/src/wsbuilder.egg-info/requires.txt +0 -6
  67. wsbuilder-1.3.0/tests/test_ia.py +0 -199
  68. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/LICENSE +0 -0
  69. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/MANIFEST.in +0 -0
  70. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/404.html +0 -0
  71. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/CNAME +0 -0
  72. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/advanced.md +0 -0
  73. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/ai-predict.md +0 -0
  74. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/app-http.md +0 -0
  75. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/architecture.md +0 -0
  76. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/assets/wsbuilder-mark.svg +0 -0
  77. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/beginners.md +0 -0
  78. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/cache-security.md +0 -0
  79. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/capabilities.md +0 -0
  80. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/data-orm.md +0 -0
  81. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/dns.md +0 -0
  82. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/full-project.md +0 -0
  83. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/getting-started.md +0 -0
  84. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/help/index.md +0 -0
  85. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/index.md +0 -0
  86. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/install.md +0 -0
  87. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/intermediate.md +0 -0
  88. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/mdns.md +0 -0
  89. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/network-edge.md +0 -0
  90. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/observability-tasks.md +0 -0
  91. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/observability.md +0 -0
  92. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/reference/index.md +0 -0
  93. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/security.md +0 -0
  94. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/stylesheets/extra.css +0 -0
  95. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/tls-certificates.md +0 -0
  96. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/docs/websocket.md +0 -0
  97. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/.gitignore +0 -0
  98. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/README.md +0 -0
  99. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/app.py +0 -0
  100. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/pyproject.toml +0 -0
  101. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/examples/full-project/upstream.py +0 -0
  102. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/setup.cfg +0 -0
  103. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/__main__.py +0 -0
  104. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/_coerce.py +0 -0
  105. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/_pragma.py +0 -0
  106. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/constants.py +0 -0
  107. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/cookies.py +0 -0
  108. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/dns.py +0 -0
  109. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/framework.py +0 -0
  110. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/headers.py +0 -0
  111. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http1.py +0 -0
  112. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/http3_websocket.py +0 -0
  113. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ia_audio.py +0 -0
  114. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ia_text.py +0 -0
  115. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/logs.py +0 -0
  116. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/mdns.py +0 -0
  117. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/pki.py +0 -0
  118. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/predicts.py +0 -0
  119. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/qpack.py +0 -0
  120. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/__init__.py +0 -0
  121. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/address.py +0 -0
  122. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/crypto.py +0 -0
  123. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/packet.py +0 -0
  124. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/recovery.py +0 -0
  125. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/quic/varint.py +0 -0
  126. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/security.py +0 -0
  127. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder/ws.py +0 -0
  128. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/dependency_links.txt +0 -0
  129. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/entry_points.txt +0 -0
  130. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/src/wsbuilder.egg-info/top_level.txt +0 -0
  131. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_app_threads.py +0 -0
  132. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_contributing_policy.py +0 -0
  133. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_demo_monitor.py +0 -0
  134. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_dns.py +0 -0
  135. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_docs.py +0 -0
  136. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_documentation_files.py +0 -0
  137. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_example_full_project.py +0 -0
  138. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_headers_cookies.py +0 -0
  139. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_hpack.py +0 -0
  140. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http1.py +0 -0
  141. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http2.py +0 -0
  142. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_http3_streaming_udp.py +0 -0
  143. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_ia_text_audio.py +0 -0
  144. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_logs.py +0 -0
  145. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_mdns.py +0 -0
  146. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_orm.py +0 -0
  147. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_pki.py +0 -0
  148. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_pr_identity_policy.py +0 -0
  149. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_predicts.py +0 -0
  150. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_proxyi_routing.py +0 -0
  151. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_address.py +0 -0
  152. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_crypto.py +0 -0
  153. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_protocol.py +0 -0
  154. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_quic_tls.py +0 -0
  155. {wsbuilder-1.3.0 → wsbuilder-1.4.0}/tests/test_security.py +0 -0
  156. {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.0
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>=42; extra == "tls"
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.