Metadata-Version: 2.4
Name: pyinternet
Version: 0.1.7
Summary: Python port of github.com/celsiainternet/elvis-python packages (cache, envar, event, nethttp, response, claim, middleware, router)
Author-email: Cesar Galvis Leon <cgalvisleon@gmail.com>
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: redis>=5.0
Requires-Dist: python-dotenv>=1.0
Requires-Dist: nats-py>=2.6
Requires-Dist: PyJWT>=2.8
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"

# elvis (Python)

Port a Python de paquetes Go de [`github.com/celsiainternet/elvis`](../elvis).
Los nombres de funciones y el orden/tipo de sus parámetros replican el
original 1:1, para que el código traducido desde Go se lea igual en Python.

Paquetes portados hasta ahora:

- **`cache`** ← [`elvis/cache`](../elvis/cache)
- **`envar`** ← [`elvis/envar`](../elvis/envar)
- **`event`** ← [`elvis/event`](../elvis/event)
- **`nethttp`** — no es un port de un paquete elvis; ver su sección abajo.
- **`response`** ← [`elvis/response`](../elvis/response)
- **`claim`** ← [`elvis/claim`](../elvis/claim)
- **`middleware`** ← [`elvis/middleware`](../elvis/middleware)
- **`router`** ← [`elvis/router`](../elvis/router) (parcial: `PublicRoute`, `ProtectRoute`)

```python
import envar
import cache
import event

cache.Load()                          # usa REDIS_HOST / REDIS_PASSWORD / REDIS_DB
cache.Set("greeting", "hello", 60)    # segundos, no time.Duration
print(cache.Get("greeting", ""))      # "hello"
cache.Close()

print(envar.GetInt(10, "REDIS_POOL_SIZE"))

event.Load()                                              # NATS_HOST / NATS_USER / NATS_PASSWORD
event.Subscribe("orders:created", lambda msg: print(msg.Data))
event.Publish("orders:created", {"order_id": "1"})
event.Close()

import claim
import nethttp
import response
import middleware

token = claim.NewToken("user-1", "myapp", "Jane", "jane", "web", 3600)  # 1h, in seconds

def hello(w: nethttp.ResponseWriter, r: nethttp.Request) -> None:
    response.JSON(w, r, 200, {"hello": claim.ClientId(r)})

chain = middleware.RequestID(middleware.Logger(middleware.Recoverer(middleware.Autentication(hello))))

r = nethttp.Request(method="GET", path="/hello")
r.header.Set("Authorization", f"Bearer {token}")
w = nethttp.ResponseWriter()
chain(w, r)
```

## Instalación

```bash
cd elvis
pip install -e .
# o, sin instalar el paquete:
pip install -r requirements.txt
```

## `cache`

Port de [`elvis/cache`](../elvis/cache) (`cache.go`, `connect.go`, `ctx.go`,
`handler.go`, `pubsub.go`).

### Variables de entorno

| Variable               | Uso                                                      | Default |
| ---------------------- | -------------------------------------------------------- | ------- |
| `REDIS_HOST`           | host:puerto de Redis                                     | —       |
| `REDIS_PASSWORD`       | password de Redis                                        | —       |
| `REDIS_DB`             | número de base de datos                                  | `0`     |
| `REDIS_POOL_SIZE`      | tamaño del pool de conexiones                            | `10`    |
| `REDIS_MIN_IDLE_CONNS` | leída por paridad; redis-py no tiene equivalente directo | `2`     |

### Mapeo Go -> Python

| Archivo Go                              | Funciones                                                                                                                                                                                                                                                                                                                                                                                          | Archivo Python |
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `cache.go`                              | `Load`, `Close`, `IsLoad`, `HealthCheck`, `FromId`, tipo `Conn`                                                                                                                                                                                                                                                                                                                                    | `cache.py`     |
| `connect.go`                            | `ConnectTo`                                                                                                                                                                                                                                                                                                                                                                                        | `connect.py`   |
| `ctx.go`                                | `SetCtx`, `ExpireCtx`, `GetCtx`, `ExistsCtx`, `DeleteCtx`, `IncrCtx`, `DecrCtx`, `LPushCtx`, `LRemCtx`, `LRangeCtx`, `LTrimCtx`, `HSetCtx`, `HGetCtx`, `HDeleteCtx`, `IncrSetTTLScript`, `DecrKeepTTLScript`                                                                                                                                                                                       | `ctx.py`       |
| `handler.go`                            | `GenId`, `GenKey`, `Set`, `Get`, `Exists`, `Delete`, `Expire`, `Incr`, `Decr`, `LPush`, `LRem`, `LRange`, `LTrim`, `SetH/D/W/M/Y`, `Empty`, `More`, `HSet`, `HGet`, `HSetAtrib`, `HGetAtrib`, `HDelete`, `SetVerify`, `GetVerify`, `DeleteVerify`, `AllCache`, `GetInt`, `GetInt64`, `GetFloat`, `GetBool`, `GetJson`, `GetItem`, `GetItems`, `IsNil`, `HandlerAll`, `HandlerGet`, `HandlerDelete` | `handler.py`   |
| `pubsub.go`                             | tipo `Message`, métodos `Pub`, `Sub`, `Unsub` en `Conn`                                                                                                                                                                                                                                                                                                                                            | `pubsub.py`    |
| `et.Json/Item/Items/List` (dependencia) | tipos mínimos usados por `Set`/`GetJson`/`GetItem`/`GetItems`/`AllCache`                                                                                                                                                                                                                                                                                                                           | `_et.py`       |

### Desviaciones deliberadas frente al original en Go

- **Manejo de errores**: Go devuelve `(valor, error)`; Python lanza excepciones
  (`RuntimeError` cuando no hay conexión, igual que
  `msg.ERR_NOT_CACHE_SERVICE`). El resto de la firma (nombre y parámetros) no cambia.
- **`second time.Duration` -> `second: float`**: en Python se pasan segundos
  como `float`/`int` en vez de `time.Duration`.
- **`def` como nombre de parámetro**: es palabra reservada en Python; se usa
  `def_` (documentado en cada docstring).
- **`context.Context`**: las funciones `*Ctx` conservan `ctx` como primer
  parámetro por fidelidad de firma, pero no se usa — pásese `None`. El
  cliente `redis-py` síncrono no necesita cancelación/deadlines explícitos.
- **`cache.conn`**: en Go es una variable de paquete no exportada
  (minúscula); en Python tampoco se expone en `cache.__all__`. Use
  `IsLoad()` / `HealthCheck()`.
- **`HandlerAll` / `HandlerGet` / `HandlerDelete`**: en Go son
  `http.HandlerFunc` atados a chi/net-http. Python no tiene un framework web
  dominante único, así que aquí reciben un `dict` de query params (el
  equivalente de `response.GetQuery(r)`) y devuelven `(status_code, body)`
  en vez de escribir en un `http.ResponseWriter`.

## `envar`

Port completo de [`elvis/envar`](../elvis/envar) (`envar.go` +
`deprecate.go`, incluidas las funciones marcadas como deprecadas en Go —
se portaron igual, por completitud).

### Mapeo Go -> Python

| Archivo Go     | Funciones                                                                                                                                                                                  | Archivo Python |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------- |
| `envar.go`     | interfaz `Store`, `Load`, `MetaSet`, `SetStr`, `SetInt`, `SetInt64`, `SetBool`, `UpSetStr`, `UpSetInt`, `UpSetFloat`, `UpSetBool`, `GetStr`, `GetInt`, `GetInt64`, `GetFloat64`, `GetBool` | `envar.py`     |
| `deprecate.go` | `SetvarStr`, `SetvarInt`, `SetvarBool`, `EnvarStr`, `EnvarInt`, `EnvarInt64`, `EnvarNumber`, `EnvarBool`                                                                                   | `deprecate.py` |

`cache` consume este paquete directamente (`cache/connect.py` hace
`import envar`) en vez de duplicar su propia copia — igual que en Go, donde
`elvis/cache` importa `elvis/envar`.

### Desviaciones deliberadas frente al original en Go

- **Autoload de `.env`**: Go carga `.env` mediante el import en blanco
  `_ "github.com/joho/godotenv/autoload"`; aquí se replica con
  `python-dotenv`'s `load_dotenv()`, ejecutado una vez al importar `envar`.
- **`Store` interface**: en Go es una `interface`; en Python es un
  `typing.Protocol` con el mismo método `Get(name, _default) -> str`. La
  firma real de `GetStr`/`GetBool` invoca `store.Get(_default, _var)` — el
  mismo orden (aparentemente invertido respecto a los nombres del
  parámetro) que el código Go original, replicado tal cual.
- **`EnvarStr`/`EnvarInt`/... (deprecadas)** leen `os.environ` directo y
  **no** consultan el `Store` fijado con `Load()`, igual que en Go — a
  diferencia de `GetStr` y compañía, que sí lo consultan.

## `event`

Port de [`elvis/event`](../elvis/event) (`event.go`, `connect.go`,
`emiter.go`, `handler.go`, `message.go`, `msg.go`, `workState.go`). Tiene
dos modos, igual que en Go:

- **Distribuido, vía NATS**: `event.Load()` / `event.Publish(channel, data)`
  / `event.Subscribe(channel, f)` / `event.Stack(channel, f)`.
- **Local, en el mismo proceso**: `event.On(channel, handler)` /
  `event.Emit(channel, data)`, vía `EventEmiter` — no toca NATS.

### Variables de entorno

| Variable        | Uso                                                          | Default          |
| --------------- | ------------------------------------------------------------ | ---------------- |
| `NATS_HOST`     | host/URL del servidor NATS                                   | —                |
| `NATS_USER`     | usuario NATS                                                 | —                |
| `NATS_PASSWORD` | password NATS                                                | —                |
| `STAGE`         | usada por `Publish` para el prefijo `pipe:<stage>:<channel>` | `local`          |
| `TIMEZONE`      | zona horaria para timestamps (`created_at`, etc.)            | `America/Bogota` |

### Mapeo Go -> Python

| Archivo Go     | Funciones                                                                                                                  | Archivo Python |
| -------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `event.go`     | constantes `EVENT*`, `Load`, `Close`, `Id`, `HealthCheck`, tipo `Conn`                                                     | `event.py`     |
| `connect.go`   | `ConnectTo`                                                                                                                | `connect.py`   |
| `emiter.go`    | `Handler`, `EventEmiter`, `NewEventEmiter`, `On`, `Emit`                                                                   | `emiter.py`    |
| `message.go`   | interfaz `Message`, `EvenMessage` (`Encode`, `ToString`, `ToJson`), `NewEvenMessage`, `DecodeMessage`                      | `message.py`   |
| `workState.go` | `WorkStatus` (`String`), `WorkStatusPending/Accepted/Processing/Completed/Failed`, `ToWorkStatus`                          | `workstate.py` |
| `handler.go`   | `Publish`, `Subscribe`, `Queue`, `Stack`, `Unsubscribe`, `Work`, `WorkState`, `Source`, `Log`, `Overflow`, `HttpEventWork` | `handler.py`   |
| `msg.go`       | constantes de error (`ERR_NOT_CONNECT`, `ERR_CHANNEL_REQUIRED`, ...)                                                       | `_msg.py`      |

### Desviaciones deliberadas frente al original en Go

- **Cliente NATS asíncrono**: `nats.go` (usado en Go) es síncrono; el
  cliente oficial en Python (`nats-py`) es exclusivamente `asyncio`. Para
  que `Load`, `Publish`, `Subscribe`, etc. sigan siendo llamadas bloqueantes
  normales (igual que en Go), cada `Conn` levanta un event loop en un hilo
  de fondo (`event/_loop.py`) y cada llamada a NATS se despacha ahí vía
  `asyncio.run_coroutine_threadsafe(...).result()`. Detalle interno, no
  cambia la firma pública.
- **Manejo de errores mixto, igual que en Go**: `Publish` (y todo lo que
  se apoya en ella: `Work`, `WorkState`, `Source`, `Log`, `Overflow`) hace
  _no-op_ silencioso si no hay conexión — así se comporta el Go original
  (`if conn == nil { return nil }`). En cambio `Subscribe`, `Queue`,
  `Stack` y `Unsubscribe` lanzan `RuntimeError(ERR_NOT_CONNECT)`, tal como
  el Go original devuelve `error` explícito en esos casos.
- **`go Publish(...)` → `threading.Thread(..., daemon=True).start()`**:
  `WorkState`, `Source`, `Log` y `Overflow` disparan la publicación en un
  hilo de fondo, igual que Go las dispara en una goroutine. `Work` en
  cambio publica de forma síncrona, igual que en Go (no usa `go`).
- **`HttpEventWork`**: recibe un `dict` (equivalente de
  `response.GetBody(r)`) y devuelve `(status_code, body)` en vez de
  `http.ResponseWriter`/`*http.Request` — misma convención que
  `cache.HandlerAll/Get/Delete`. Replica también el orden de validación
  del original: `Work(...)` se ejecuta _antes_ de comprobar que `event` y
  `data` no estén vacíos, así que ya se publicó el evento aunque la
  respuesta termine siendo 400.
- **`et.Object(m)` en `EvenMessage.ToJson`**: en Go usa reflexión genérica;
  aquí se construye el `dict` directamente a partir de los campos
  conocidos de `EvenMessage` (mismo resultado, sin necesitar reflexión).

## `nethttp`

**No es un port de un paquete de `elvis`.** `claim`, `response` y
`middleware` en Go se construyen directamente sobre la librería estándar
(`*http.Request`, `http.ResponseWriter`, `http.Header`) en vez de definir
sus propios tipos de request/response — no hay un paquete `elvis/nethttp`
que portar. Python no tiene un único framework HTTP dominante, así que
este módulo hace las veces de esa porción de `net/http` que los tres
paquetes comparten: `Header` (case-insensitive), `Request`, `ResponseWriter`
(ya con `Status()`/`BytesWritten()` incorporados) y los códigos de estado
usados (`StatusOK`, `StatusUnauthorized`, etc. + `StatusText`). Conéctelo a
Flask/FastAPI/Django/WSGI/ASGI según se necesite. También define
`Handler`/`Middleware` como los equivalentes de `http.Handler` y
`func(http.Handler) http.Handler`.

## `response`

Port de [`elvis/response`](../elvis/response) (`response.go`), construido
sobre `nethttp.Request`/`nethttp.ResponseWriter`.

### Mapeo Go -> Python

| Función Go                                                                                           | Función Python                                                                                                                                                                                            |
| ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ScanBody`, `ScanStr`, `ScanJson`                                                                    | igual (`ScanBody` toma `bytes` en vez de `io.Reader`)                                                                                                                                                     |
| `GetBody`, `GetArray`, `GetQuery`, `GetHeader`, `GetParam`                                           | igual                                                                                                                                                                                                     |
| `WriteResponse`, `RESULT`, `ANY`, `JSON`, `ITEM`, `ITEMS`                                            | igual                                                                                                                                                                                                     |
| `HTTPError`, `HTTPAlert`, `Unauthorized`, `Forbidden`, `InternalServerError`, `PreconditionRequired` | igual                                                                                                                                                                                                     |
| `Stream`                                                                                             | igual; `DataFunction` es `Callable[[page, rows], (ok, items)]` en vez de devolver `et.Items`                                                                                                              |
| `HTTPApp`                                                                                            | **no portado** — sirve archivos estáticos vía `chi.Router`/`http.FileSystem`; es plomería de router/filesystem, no lógica de formateo de respuesta, y no hay un equivalente framework-agnóstico razonable |

## `claim`

Port de [`elvis/claim`](../elvis/claim) (`claim.go`, `msg.go`): emisión,
parseo y validación de JWT, respaldado por `cache` (Redis) para poder
invalidar tokens.

### Variables de entorno

| Variable | Uso                  | Default  |
| -------- | -------------------- | -------- |
| `SECRET` | clave de firma HS256 | `"1977"` |

### Desviaciones deliberadas frente al original en Go

- **`Duration` en segundos**, no el `time.Duration` (nanosegundos) crudo de
  Go — consistente con la convención `second: float` usada en todo este
  port (`cache`, `event`).
- **`context.Context` → `Request.context`**: los accesores
  (`Token`, `ServiceId`, `ClientId`, `GetClient`, ...) leen de
  `nethttp.Request.context` (un `dict` simple) en vez de un
  `context.Context` tipado por separado.
- **`mem` no se portó completo**: `ValidToken` usa una caché local L1 en
  memoria antes de golpear Redis; en vez de portar el paquete completo
  `elvis/mem` (750+ líneas, no solicitado), `claim/_mem.py` implementa solo
  el `Get`/`Set`/`Del` con TTL que `ValidToken` necesita.
- **`ValidToken` devuelve `None`, no lanza**, cuando el token no coincide
  con el guardado en caché — replica exactamente que el Go original
  retorna `(nil, nil)` en ese caso (el llamador debe comprobar
  `if clm is None`, igual que Go comprueba `if clm == nil`).

## `middleware`

Port de [`elvis/middleware`](../elvis/middleware) (los 9 archivos:
`autentication.go`, `autorization.go`, `cors.go`, `ephemeral.go`,
`logger.go`, `recoverer.go`, `request_id.go`, `telemetry.go`,
`wrap_write.go`). Cada middleware tiene la forma `nethttp.Middleware`
(`Callable[[Handler], Handler]`), igual que Go's `func(next http.Handler) http.Handler`.

### Mapeo Go -> Python

| Archivo Go         | Contenido                                                                                                   | Archivo Python     |
| ------------------ | ----------------------------------------------------------------------------------------------------------- | ------------------ |
| `autentication.go` | `GetAuthorization`, `Autentication` (nombre con el typo del Go original, preservado)                        | `autentication.py` |
| `autorization.go`  | `Store`, `SetAuthorizationStore`, `Authorization`                                                           | `autorization.py`  |
| `cors.go`          | `AllowAll`                                                                                                  | `cors.py`          |
| `ephemeral.go`     | `Ephemeral`                                                                                                 | `ephemeral.py`     |
| `logger.go`        | `Logger`, `RequestLogger`, `LogFormatter`, `LogEntry`, `GetLogEntry`, `WithLogEntry`, `DefaultLogFormatter` | `logger.py`        |
| `recoverer.go`     | `Recoverer`, `PrintPrettyStack`                                                                             | `recoverer.py`     |
| `request_id.go`    | `RequestID`, `RequestIDHeader`, `GetReqID`, `NextRequestID`                                                 | `request_id.py`    |
| `telemetry.go`     | `Metrics`, `Telemetry`, `NewMetric`, `NewRpcMetric`, `SetServiceName`, `PushTelemetry*`                     | `telemetry.py`     |
| `wrap_write.go`    | `WrapResponseWriter`, `NewWrapResponseWriter`                                                               | `wrap_write.py`    |

### Desviaciones deliberadas frente al original en Go

- **`cors.go`**: Go configura la librería externa `github.com/rs/cors`;
  aquí `AllowAll` implementa directamente la misma política que esa
  configuración expresa (orígenes permitidos o todos si la lista está
  vacía; `HEAD/GET/POST/PUT/PATCH/DELETE`; todos los headers; credenciales
  permitidas), como un `nethttp.Middleware` plano.
- **`wrap_write.go`**: `nethttp.ResponseWriter` ya trackea
  `Status()`/`BytesWritten()` nativamente (no envuelve un socket real), así
  que `NewWrapResponseWriter` es una función identidad; no se portan
  `Tee`/`Unwrap` ni la detección de `Flusher`/`Hijacker`/`ReaderFrom`
  (irrelevantes sin un socket que envolver).
- **`recoverer.go`**: `PrintPrettyStack` imprime el traceback nativo de
  Python (`traceback` de la stdlib) en vez de parsear el formato de texto
  específico de `debug.Stack()` de Go; `prettyStack.parse/decorateLine/...`
  no se portan por la misma razón (formato de stack ajeno, no aplica).
- **`logger.go`**: sin `stdrout` (no solicitado/portado), el color ANSI
  usa un helper mínimo propio (`middleware/_stdrout.py`, no un port). El
  campo `Logger LoggerInterface` de `DefaultLogFormatter` se cae — este
  port imprime directo a stdout. `DefaultLogger` (paquete-variable
  reconfigurable en Go) no se reexporta como `middleware.DefaultLogger`
  porque Python no tiene bindings "vivos" entre módulos: reasignar
  `middleware.DefaultLogger = x` no afectaría lo que `Logger()` realmente
  usa. Para personalizarlo, asigne `middleware.logger.DefaultLogger`
  directamente.
- **`autorization.go`**: se preserva tal cual el bug del original — el
  chequeo está invertido (`if store is not None: ...error...` en vez de
  `is None`), así que `Authorization` nunca puede autorizar exitosamente
  ninguna solicitud tal como está escrito en Go. Además, si `store` queda
  en `None` y el token es válido, Go entra en pánico al invocar un método
  sobre una interfaz `nil`; este port en cambio captura esa falla y
  responde 500 en vez de propagar/crashear — ver nota en
  `middleware/autorization.py`.
- **`ephemeral.go`**: usa `claim.ParceToken` (solo valida el JWT), no
  `claim.ValidToken` — no consulta Redis, igual que en Go.
- **Manejo de errores**: en general, excepciones (`ValueError`,
  `RuntimeError`) en vez de `(valor, error)`; los middlewares las capturan
  con `try/except` en los mismos puntos donde Go comprueba `if err != nil`.

## `router`

Port **parcial** de [`elvis/router`](../elvis/router) (`router.go`): solo
`PublicRoute` y `ProtectRoute`, más la plomería de registro en el api
gateway de la que ambas dependen (`PushApiGateway`,
`DeleteApiGatewayById`, `GetRoutes`, `TpHeader`). `EphemeralRoute`,
`AuthorizationRoute` y `With` no están portadas todavía — tienen la misma
forma que `ProtectRoute`, solo cambia el/los middleware(s) pasados a
`.With(...)`.

```python
import router
import nethttp

def hello(w: nethttp.ResponseWriter, r: nethttp.Request) -> None:
    response.JSON(w, r, 200, {"hello": "world"})

router.PublicRoute(mux, router.Get, "/hello", hello, "myapp", "/api/v1", "http://localhost:8080")
router.ProtectRoute(mux, router.Get, "/private", hello, "myapp", "/api/v1", "http://localhost:8080")
```

### Mapeo Go -> Python

| Elemento Go                                                                       | Elemento Python                        |
| ---------------------------------------------------------------------------------- | --------------------------------------- |
| `TypeRoute`, `HTTP`/`REST`/`PROXY`                                                | `TpRoute` (`IntEnum`)                   |
| `TpHeader`, `IntToTpHeader`, `ToTpHeader`                                         | igual                                   |
| `PushApiGateway`, `DeleteApiGatewayById`, `GetRoutes`                            | igual                                   |
| `PublicRoute`, `ProtectRoute`                                                     | igual                                   |
| `*chi.Mux` (parámetro `r`)                                                        | `Mux`, un `typing.Protocol`             |

### Desviaciones deliberadas frente al original en Go

- **`*chi.Mux` → `Mux` (`Protocol`)**: igual razón que `nethttp` — no hay un
  router HTTP dominante único en Python. `Mux` describe solo la porción de
  la superficie de `chi.Router` que `PublicRoute`/`ProtectRoute` usan
  (`Get`/`Post`/`Put`/`Patch`/`Delete`/`Head`/`Options`/`HandleFunc`/
  `With`); conecte un adaptador sobre Flask/FastAPI/Django/lo que use.
- **`event.Stack` sin conexión NATS**: en Go, `resetApigateway` llama a
  `event.Stack` e ignora el `error` que devuelve, así que sin NATS
  conectado la suscripción simplemente no ocurre y todo lo demás sigue
  funcionando. El port de `event.Stack` lanza `RuntimeError` en ese caso
  (ver sección `event` arriba) en vez de devolver un error; `router`
  captura esa excepción en su equivalente interno de `resetApigateway`
  para reproducir el mismo comportamiento observable (no-op silencioso).
- **`EphemeralRoute`, `AuthorizationRoute`, `With` no portadas**: no
  solicitadas; añadirlas es mecánico una vez que `Mux`/`PublicRoute`/
  `ProtectRoute` existen (mismo `_mount` + `.With(middleware.X)` que
  `ProtectRoute`, o middlewares arbitrarios para `With`).

## Tests

```bash
pip install -e ".[dev]"
python -m pytest tests/ -v
```

- `tests/cache/` — tests de `cache`, con un cliente Redis falso en memoria
  (`tests/cache/fake_redis.py`), sin requerir un servidor Redis real.
- `tests/envar/` — tests de `envar`, usando `monkeypatch` de pytest para
  variables de entorno y `sys.argv`.
- `tests/event/` — tests de `event`, con un cliente NATS falso en memoria
  (`tests/event/fake_nats.py`) para `Load`/`Publish`/`Subscribe`, más
  cobertura directa de `EventEmiter`, `EvenMessage` y `WorkStatus`. No
  requiere un servidor NATS real.
- `tests/nethttp/`, `tests/response/` — tests de los tipos HTTP base y del
  formateo de respuestas.
- `tests/claim/`, `tests/middleware/` — tests de JWT y de la cadena de
  middlewares completa, reutilizando el cliente Redis falso de
  `tests/cache/fake_redis.py` vía fixtures (`cache_conn`) en sus
  respectivos `conftest.py`.

## Deploy

`deploy.sh` es un menú de herramientas de despliegue para el paquete
`pyinternet`, a semejanza de `version.sh` en `elvis`/`jdb`: en vez de
etiquetar un módulo Go, sube la versión en `pyproject.toml`, la etiqueta
en Git y publica el build en PyPI.

### Configuración inicial

Se necesita un `~/.pypirc` con credenciales de PyPI (usar un
[API token](https://pypi.org/manage/account/#api-tokens), no la
contraseña de la cuenta):

```ini
[pypi]
username = __token__
password = <su token de PyPI>
```

```bash
chmod 600 ~/.pypirc
```

### Uso

```bash
./deploy.sh         # abre un menú interactivo con las herramientas disponibles
./deploy.sh --p      # o --public: publica en PyPI
./deploy.sh --help   # lista las opciones
```

`--public`:

1. Verifica que `build` y `twine` estén instalados (los instala si faltan) y que exista `~/.pypirc`.
2. Incrementa la revisión (patch) de la versión en `pyproject.toml` (X.Y.Z -> X.Y.Z+1).
3. Commitea `pyproject.toml`, crea el tag `vX.Y.Z` y hace `git push` (rama + tags).
4. Reconstruye `dist/` (`python -m build`) y sube el paquete con `twine upload`.

Hoy la única herramienta del menú es la publicación en PyPI; el script
está pensado para agregar más opciones al menú según se necesiten.
