mirotaract 0.1.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.
@@ -0,0 +1,18 @@
1
+ node_modules
2
+ .turbo
3
+ .next
4
+ dist
5
+ storybook-static
6
+ test-results
7
+ playwright-report
8
+ coverage
9
+ .env
10
+ .env.local
11
+ *.log
12
+ postgres-data
13
+ redis-data
14
+ nats-data
15
+ .venv
16
+ __pycache__
17
+ .pytest_cache
18
+ *.egg-info
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Distrito 4845 de Rotaract
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,248 @@
1
+ Metadata-Version: 2.5
2
+ Name: mirotaract
3
+ Version: 0.1.0
4
+ Summary: SDK oficial de Mi Rotaract: API de datos institucionales e "Ingresar con Mi Rotaract" (OAuth 2.0 + OpenID Connect).
5
+ Author: Mi Rotaract
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3 :: Only
10
+ Classifier: Typing :: Typed
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: httpx<1,>=0.27
13
+ Requires-Dist: pyjwt[crypto]<3,>=2.8
14
+ Provides-Extra: dev
15
+ Requires-Dist: fastapi>=0.110; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Provides-Extra: fastapi
18
+ Requires-Dist: fastapi>=0.110; extra == 'fastapi'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # mirotaract (Python)
22
+
23
+ SDK oficial de **Mi Rotaract** para Python ≥ 3.10:
24
+
25
+ - **API de datos institucionales** (clubes, socios, autoridades, períodos,
26
+ permisos) con `client_credentials`: `MiRotaract` (síncrono) y
27
+ `AsyncMiRotaract` (asíncrono).
28
+ - **Ingresar con Mi Rotaract** (OAuth 2.0 + PKCE + OpenID Connect):
29
+ `MiRotaractAuth` y `AsyncMiRotaractAuth`, con verificación del `id_token`
30
+ contra el JWKS publicado.
31
+ - Dependencia para **FastAPI**: `mirotaract.fastapi.require_user`.
32
+ - **Webhooks**: `verify_webhook` verifica en una línea los avisos firmados
33
+ que Mi Rotaract le manda a tu servidor (solo biblioteca estándar).
34
+
35
+ Dependencias: `httpx` y `PyJWT[crypto]`.
36
+
37
+ ```bash
38
+ pip install mirotaract # o "mirotaract[fastapi]"
39
+ ```
40
+
41
+ La URL base es la de la API del kernel, que además es el _issuer_ OIDC:
42
+ `https://api.rotaract4845.com/api/kernel/v1`. El resto se descubre desde
43
+ `/.well-known/openid-configuration`.
44
+
45
+ ## 1. Cliente de servidor (API de datos)
46
+
47
+ ```python
48
+ import os
49
+ from mirotaract import MiRotaract
50
+
51
+ mr = MiRotaract(
52
+ os.environ["MIROTARACT_BASE_URL"],
53
+ os.environ["MIROTARACT_CLIENT_ID"], # mra_…
54
+ os.environ["MIROTARACT_CLIENT_SECRET"], # mrs_…
55
+ )
56
+
57
+ for club in mr.clubs.list(status="ACTIVE", type="CLUB"): # sigue nextCursor solo
58
+ print(club["name"])
59
+
60
+ socios = mr.members.list(club_id, status="ACTIVE").all()
61
+ club = mr.clubs.get(club_id)
62
+ personas = mr.persons.batch(ids) # de a 100, automático
63
+ membresias = mr.persons.memberships(person_id)
64
+ autoridades = mr.authorities.list(distrito_id, include_descendants=True)
65
+ periodos = mr.periods.list(club_id, status="ACTIVE")
66
+ decision = mr.permissions.check(
67
+ person_id=person_id, permission="meetings.meeting.create", organization_id=club_id
68
+ )
69
+ ```
70
+
71
+ Asíncrono, misma superficie:
72
+
73
+ ```python
74
+ from mirotaract import AsyncMiRotaract
75
+
76
+ async with AsyncMiRotaract(base_url, client_id, client_secret) as mr:
77
+ async for socio in mr.members.list(club_id, limit=100):
78
+ ...
79
+ club = await mr.clubs.get(club_id)
80
+ ```
81
+
82
+ | Recurso | Métodos |
83
+ |---|---|
84
+ | `clubs` (alias `organizations`) | `list(type=, status=, parent_id=, updated_since=, limit=, if_none_match=)`, `get(id)` |
85
+ | `members` | `list(organization_id, status=, updated_since=, limit=, if_none_match=)` |
86
+ | `persons` | `get(id)`, `batch(ids)`, `memberships(person_id)` |
87
+ | `authorities` | `list(organization_id, include_descendants=)` |
88
+ | `periods` | `list(organization_id, status=)` |
89
+ | `permissions` | `check(person_id=, permission=, organization_id=, scope_type=, period_id=, resource=)`, `check_many([...])` |
90
+
91
+ Otros: `get_access_token()`, `granted_scopes`, `clear_token()`, `discovery()`
92
+ y `request(method, path, params=, json=)`.
93
+
94
+ Las vistas son `TypedDict` con las claves tal como las devuelve la API
95
+ (`displayName`, `updatedAt`…): `OrganizationView`, `MemberView`,
96
+ `PersonView`, `AuthorityView`, `PeriodView`, `PersonMembershipView`,
97
+ `AuthorizationDecision`.
98
+
99
+ ### Comportamiento
100
+
101
+ - Token de servicio cacheado hasta 60 s antes de vencer (con lock, una sola
102
+ renovación aunque haya concurrencia); ante un 401 se renueva una vez.
103
+ - Reintentos (2 por defecto) con backoff exponencial y jitter ante
104
+ 429/502/503/504 y errores de red, respetando `Retry-After`. Nunca reintenta
105
+ un `POST` sin `Idempotency-Key`; las lecturas por POST (`persons.batch`,
106
+ `permissions.check`) mandan una clave propia.
107
+ - Paginadores: iteración, `.all(max=)`, `.pages()` y `.page(cursor)`.
108
+ - Podés pasar tu propio `httpx.Client` / `httpx.AsyncClient` (`http_client=`)
109
+ para proxies, timeouts o pruebas con `httpx.MockTransport`.
110
+
111
+ ### ETag (sincronización incremental)
112
+
113
+ ```python
114
+ page = mr.members.list(club_id).page()
115
+ guardar(page.items, page.etag)
116
+
117
+ again = mr.members.list(club_id, if_none_match=etag_guardado).page()
118
+ if again.not_modified: # 304: nada cambió
119
+ return
120
+ ```
121
+
122
+ `if_none_match` aplica a la primera página; con 304, `.page()` devuelve
123
+ `NotModified(etag=…)`, la iteración no produce elementos y
124
+ `paginator.not_modified` queda en `True`.
125
+
126
+ ### Errores
127
+
128
+ ```python
129
+ from mirotaract import MiRotaractApiError, MiRotaractOAuthError
130
+
131
+ try:
132
+ mr.members.list(otro_club).all()
133
+ except MiRotaractApiError as e: # Problem Details
134
+ e.status, e.code, e.title, e.detail, e.trace_id # 403, "KERNEL_HTTP_403", …
135
+ except MiRotaractOAuthError as e: # RFC 6749
136
+ e.error, e.error_description # "invalid_client", …
137
+ ```
138
+
139
+ ## 2. Ingresar con Mi Rotaract
140
+
141
+ ```python
142
+ from mirotaract import MiRotaractAuth
143
+
144
+ auth = MiRotaractAuth(
145
+ "https://api.rotaract4845.com/api/kernel/v1",
146
+ client_id="mra_…",
147
+ redirect_uri="https://mi-app.org/auth/callback",
148
+ client_secret="mrs_…", # None para apps PUBLIC
149
+ scope="openid profile email memberships",
150
+ )
151
+
152
+ # 1) Login: guardá code_verifier, state y nonce en la sesión del servidor
153
+ req = auth.authorization_url()
154
+ session["mr"] = {"verifier": req.code_verifier, "state": req.state, "nonce": req.nonce}
155
+ return redirect(req.url)
156
+
157
+ # 2) Callback
158
+ code = auth.parse_callback(str(request.url), session["mr"]["state"])
159
+ tokens = auth.exchange_code(code, session["mr"]["verifier"], nonce=session["mr"]["nonce"])
160
+ tokens.claims # id_token verificado: sub, name, email, memberships…
161
+ tokens.refresh_token
162
+
163
+ # 3) Después
164
+ auth.user_info(tokens.access_token)
165
+ nuevos = auth.refresh(tokens.refresh_token) # rota: guardá el nuevo
166
+ auth.revoke(nuevos.refresh_token)
167
+ ```
168
+
169
+ Métodos: `discovery()`, `authorization_url(scope=, state=, nonce=, redirect_uri=, extra_params=)`,
170
+ `parse_callback(url, state)`, `exchange_code(code, code_verifier, nonce=)`,
171
+ `refresh(refresh_token, scope=)`, `verify_id_token(id_token, nonce=, max_age=)`,
172
+ `verify_access_token(token)`, `user_info(access_token)`, `revoke(token)`.
173
+ `AsyncMiRotaractAuth` tiene los mismos métodos con `await`.
174
+
175
+ El JWKS se descarga con el mismo cliente `httpx` (no con `urllib`), se
176
+ construye con `jwt.PyJWKSet`, se cachea 10 minutos y se vuelve a pedir ante un
177
+ `kid` desconocido. Se verifica ES256, `iss`, `aud` (= `client_id`), `exp`,
178
+ `azp` y `nonce`.
179
+
180
+ ## 3. FastAPI
181
+
182
+ ```python
183
+ from fastapi import Depends, FastAPI
184
+ from mirotaract import AsyncMiRotaractAuth
185
+ from mirotaract.fastapi import require_user
186
+
187
+ auth = AsyncMiRotaractAuth(ISSUER, CLIENT_ID, REDIRECT_URI)
188
+ current_user = require_user(auth) # verify="userinfo" (por defecto) o "jwt"
189
+
190
+ app = FastAPI()
191
+
192
+ @app.get("/api/yo")
193
+ async def yo(user=Depends(current_user)):
194
+ return user
195
+ ```
196
+
197
+ - `verify="userinfo"`: consulta `/oauth/userinfo` (ve revocaciones al
198
+ instante); cachea `cache_ttl` segundos (60).
199
+ - `verify="jwt"`: verificación local de la firma, sin red por pedido.
200
+ - `authorize=lambda user: …` devuelve 403 si da `False`.
201
+ - Sin token o inválido: `401` con `WWW-Authenticate: Bearer error="invalid_token"`.
202
+
203
+ Para apps web con páginas del servidor conviene una sesión del lado del
204
+ servidor: hacé el login con `authorization_url` / `exchange_code` y guardá
205
+ `tokens.claims` en tu sesión.
206
+
207
+ ## 4. Webhooks
208
+
209
+ Mi Rotaract le avisa a tu servidor cuando pasa algo (socio activado, baja,
210
+ cargo asumido...) con un `POST` firmado. Guía completa:
211
+ [docs/developers/webhooks.md](../../docs/developers/webhooks.md); tipos:
212
+ [catálogo de eventos](../../docs/developers/catalogo-de-eventos.md).
213
+
214
+ ```python
215
+ import os
216
+ from mirotaract import MiRotaractWebhookError, verify_webhook
217
+
218
+ event = verify_webhook(
219
+ raw_body, # bytes crudos: await request.body() (FastAPI) / request.get_data() (Flask)
220
+ request.headers, # dict, Headers de Starlette o pares (bytes, bytes)
221
+ os.environ["MIROTARACT_WEBHOOK_SECRET"], # o [nuevo, viejo]
222
+ tolerance=300,
223
+ )
224
+ if event["type"] == "membership.activated.v1":
225
+ print(event["data"]["membership"]["person"]["displayName"])
226
+ ```
227
+
228
+ Devuelve el evento como `dict`. Si no es válido lanza
229
+ `MiRotaractWebhookError` con `code`: `missing_header`, `invalid_timestamp`,
230
+ `timestamp_out_of_tolerance`, `invalid_signature` o `invalid_payload`.
231
+
232
+ ## Conformidad
233
+
234
+ ```bash
235
+ python3 -m venv .venv && .venv/bin/pip install -e 'sdks/python[dev]'
236
+ KERNEL_DATABASE_URL=… MR_BASE_URL=http://127.0.0.1:3911/api/kernel/v1 node sdks/conformance/seed.mjs > /tmp/mr.env
237
+ .venv/bin/python sdks/conformance/run_python.py --env-file /tmp/mr.env
238
+ ```
239
+
240
+ `webhooks.signature_vectors` corre sin kernel (vectores compartidos con el SDK
241
+ de JavaScript en `sdks/conformance/webhook-vectors.json`).
242
+
243
+ ## Desarrollo
244
+
245
+ ```bash
246
+ .venv/bin/pip install -e 'sdks/python[dev]'
247
+ .venv/bin/python -m pytest sdks/python
248
+ ```
@@ -0,0 +1,228 @@
1
+ # mirotaract (Python)
2
+
3
+ SDK oficial de **Mi Rotaract** para Python ≥ 3.10:
4
+
5
+ - **API de datos institucionales** (clubes, socios, autoridades, períodos,
6
+ permisos) con `client_credentials`: `MiRotaract` (síncrono) y
7
+ `AsyncMiRotaract` (asíncrono).
8
+ - **Ingresar con Mi Rotaract** (OAuth 2.0 + PKCE + OpenID Connect):
9
+ `MiRotaractAuth` y `AsyncMiRotaractAuth`, con verificación del `id_token`
10
+ contra el JWKS publicado.
11
+ - Dependencia para **FastAPI**: `mirotaract.fastapi.require_user`.
12
+ - **Webhooks**: `verify_webhook` verifica en una línea los avisos firmados
13
+ que Mi Rotaract le manda a tu servidor (solo biblioteca estándar).
14
+
15
+ Dependencias: `httpx` y `PyJWT[crypto]`.
16
+
17
+ ```bash
18
+ pip install mirotaract # o "mirotaract[fastapi]"
19
+ ```
20
+
21
+ La URL base es la de la API del kernel, que además es el _issuer_ OIDC:
22
+ `https://api.rotaract4845.com/api/kernel/v1`. El resto se descubre desde
23
+ `/.well-known/openid-configuration`.
24
+
25
+ ## 1. Cliente de servidor (API de datos)
26
+
27
+ ```python
28
+ import os
29
+ from mirotaract import MiRotaract
30
+
31
+ mr = MiRotaract(
32
+ os.environ["MIROTARACT_BASE_URL"],
33
+ os.environ["MIROTARACT_CLIENT_ID"], # mra_…
34
+ os.environ["MIROTARACT_CLIENT_SECRET"], # mrs_…
35
+ )
36
+
37
+ for club in mr.clubs.list(status="ACTIVE", type="CLUB"): # sigue nextCursor solo
38
+ print(club["name"])
39
+
40
+ socios = mr.members.list(club_id, status="ACTIVE").all()
41
+ club = mr.clubs.get(club_id)
42
+ personas = mr.persons.batch(ids) # de a 100, automático
43
+ membresias = mr.persons.memberships(person_id)
44
+ autoridades = mr.authorities.list(distrito_id, include_descendants=True)
45
+ periodos = mr.periods.list(club_id, status="ACTIVE")
46
+ decision = mr.permissions.check(
47
+ person_id=person_id, permission="meetings.meeting.create", organization_id=club_id
48
+ )
49
+ ```
50
+
51
+ Asíncrono, misma superficie:
52
+
53
+ ```python
54
+ from mirotaract import AsyncMiRotaract
55
+
56
+ async with AsyncMiRotaract(base_url, client_id, client_secret) as mr:
57
+ async for socio in mr.members.list(club_id, limit=100):
58
+ ...
59
+ club = await mr.clubs.get(club_id)
60
+ ```
61
+
62
+ | Recurso | Métodos |
63
+ |---|---|
64
+ | `clubs` (alias `organizations`) | `list(type=, status=, parent_id=, updated_since=, limit=, if_none_match=)`, `get(id)` |
65
+ | `members` | `list(organization_id, status=, updated_since=, limit=, if_none_match=)` |
66
+ | `persons` | `get(id)`, `batch(ids)`, `memberships(person_id)` |
67
+ | `authorities` | `list(organization_id, include_descendants=)` |
68
+ | `periods` | `list(organization_id, status=)` |
69
+ | `permissions` | `check(person_id=, permission=, organization_id=, scope_type=, period_id=, resource=)`, `check_many([...])` |
70
+
71
+ Otros: `get_access_token()`, `granted_scopes`, `clear_token()`, `discovery()`
72
+ y `request(method, path, params=, json=)`.
73
+
74
+ Las vistas son `TypedDict` con las claves tal como las devuelve la API
75
+ (`displayName`, `updatedAt`…): `OrganizationView`, `MemberView`,
76
+ `PersonView`, `AuthorityView`, `PeriodView`, `PersonMembershipView`,
77
+ `AuthorizationDecision`.
78
+
79
+ ### Comportamiento
80
+
81
+ - Token de servicio cacheado hasta 60 s antes de vencer (con lock, una sola
82
+ renovación aunque haya concurrencia); ante un 401 se renueva una vez.
83
+ - Reintentos (2 por defecto) con backoff exponencial y jitter ante
84
+ 429/502/503/504 y errores de red, respetando `Retry-After`. Nunca reintenta
85
+ un `POST` sin `Idempotency-Key`; las lecturas por POST (`persons.batch`,
86
+ `permissions.check`) mandan una clave propia.
87
+ - Paginadores: iteración, `.all(max=)`, `.pages()` y `.page(cursor)`.
88
+ - Podés pasar tu propio `httpx.Client` / `httpx.AsyncClient` (`http_client=`)
89
+ para proxies, timeouts o pruebas con `httpx.MockTransport`.
90
+
91
+ ### ETag (sincronización incremental)
92
+
93
+ ```python
94
+ page = mr.members.list(club_id).page()
95
+ guardar(page.items, page.etag)
96
+
97
+ again = mr.members.list(club_id, if_none_match=etag_guardado).page()
98
+ if again.not_modified: # 304: nada cambió
99
+ return
100
+ ```
101
+
102
+ `if_none_match` aplica a la primera página; con 304, `.page()` devuelve
103
+ `NotModified(etag=…)`, la iteración no produce elementos y
104
+ `paginator.not_modified` queda en `True`.
105
+
106
+ ### Errores
107
+
108
+ ```python
109
+ from mirotaract import MiRotaractApiError, MiRotaractOAuthError
110
+
111
+ try:
112
+ mr.members.list(otro_club).all()
113
+ except MiRotaractApiError as e: # Problem Details
114
+ e.status, e.code, e.title, e.detail, e.trace_id # 403, "KERNEL_HTTP_403", …
115
+ except MiRotaractOAuthError as e: # RFC 6749
116
+ e.error, e.error_description # "invalid_client", …
117
+ ```
118
+
119
+ ## 2. Ingresar con Mi Rotaract
120
+
121
+ ```python
122
+ from mirotaract import MiRotaractAuth
123
+
124
+ auth = MiRotaractAuth(
125
+ "https://api.rotaract4845.com/api/kernel/v1",
126
+ client_id="mra_…",
127
+ redirect_uri="https://mi-app.org/auth/callback",
128
+ client_secret="mrs_…", # None para apps PUBLIC
129
+ scope="openid profile email memberships",
130
+ )
131
+
132
+ # 1) Login: guardá code_verifier, state y nonce en la sesión del servidor
133
+ req = auth.authorization_url()
134
+ session["mr"] = {"verifier": req.code_verifier, "state": req.state, "nonce": req.nonce}
135
+ return redirect(req.url)
136
+
137
+ # 2) Callback
138
+ code = auth.parse_callback(str(request.url), session["mr"]["state"])
139
+ tokens = auth.exchange_code(code, session["mr"]["verifier"], nonce=session["mr"]["nonce"])
140
+ tokens.claims # id_token verificado: sub, name, email, memberships…
141
+ tokens.refresh_token
142
+
143
+ # 3) Después
144
+ auth.user_info(tokens.access_token)
145
+ nuevos = auth.refresh(tokens.refresh_token) # rota: guardá el nuevo
146
+ auth.revoke(nuevos.refresh_token)
147
+ ```
148
+
149
+ Métodos: `discovery()`, `authorization_url(scope=, state=, nonce=, redirect_uri=, extra_params=)`,
150
+ `parse_callback(url, state)`, `exchange_code(code, code_verifier, nonce=)`,
151
+ `refresh(refresh_token, scope=)`, `verify_id_token(id_token, nonce=, max_age=)`,
152
+ `verify_access_token(token)`, `user_info(access_token)`, `revoke(token)`.
153
+ `AsyncMiRotaractAuth` tiene los mismos métodos con `await`.
154
+
155
+ El JWKS se descarga con el mismo cliente `httpx` (no con `urllib`), se
156
+ construye con `jwt.PyJWKSet`, se cachea 10 minutos y se vuelve a pedir ante un
157
+ `kid` desconocido. Se verifica ES256, `iss`, `aud` (= `client_id`), `exp`,
158
+ `azp` y `nonce`.
159
+
160
+ ## 3. FastAPI
161
+
162
+ ```python
163
+ from fastapi import Depends, FastAPI
164
+ from mirotaract import AsyncMiRotaractAuth
165
+ from mirotaract.fastapi import require_user
166
+
167
+ auth = AsyncMiRotaractAuth(ISSUER, CLIENT_ID, REDIRECT_URI)
168
+ current_user = require_user(auth) # verify="userinfo" (por defecto) o "jwt"
169
+
170
+ app = FastAPI()
171
+
172
+ @app.get("/api/yo")
173
+ async def yo(user=Depends(current_user)):
174
+ return user
175
+ ```
176
+
177
+ - `verify="userinfo"`: consulta `/oauth/userinfo` (ve revocaciones al
178
+ instante); cachea `cache_ttl` segundos (60).
179
+ - `verify="jwt"`: verificación local de la firma, sin red por pedido.
180
+ - `authorize=lambda user: …` devuelve 403 si da `False`.
181
+ - Sin token o inválido: `401` con `WWW-Authenticate: Bearer error="invalid_token"`.
182
+
183
+ Para apps web con páginas del servidor conviene una sesión del lado del
184
+ servidor: hacé el login con `authorization_url` / `exchange_code` y guardá
185
+ `tokens.claims` en tu sesión.
186
+
187
+ ## 4. Webhooks
188
+
189
+ Mi Rotaract le avisa a tu servidor cuando pasa algo (socio activado, baja,
190
+ cargo asumido...) con un `POST` firmado. Guía completa:
191
+ [docs/developers/webhooks.md](../../docs/developers/webhooks.md); tipos:
192
+ [catálogo de eventos](../../docs/developers/catalogo-de-eventos.md).
193
+
194
+ ```python
195
+ import os
196
+ from mirotaract import MiRotaractWebhookError, verify_webhook
197
+
198
+ event = verify_webhook(
199
+ raw_body, # bytes crudos: await request.body() (FastAPI) / request.get_data() (Flask)
200
+ request.headers, # dict, Headers de Starlette o pares (bytes, bytes)
201
+ os.environ["MIROTARACT_WEBHOOK_SECRET"], # o [nuevo, viejo]
202
+ tolerance=300,
203
+ )
204
+ if event["type"] == "membership.activated.v1":
205
+ print(event["data"]["membership"]["person"]["displayName"])
206
+ ```
207
+
208
+ Devuelve el evento como `dict`. Si no es válido lanza
209
+ `MiRotaractWebhookError` con `code`: `missing_header`, `invalid_timestamp`,
210
+ `timestamp_out_of_tolerance`, `invalid_signature` o `invalid_payload`.
211
+
212
+ ## Conformidad
213
+
214
+ ```bash
215
+ python3 -m venv .venv && .venv/bin/pip install -e 'sdks/python[dev]'
216
+ KERNEL_DATABASE_URL=… MR_BASE_URL=http://127.0.0.1:3911/api/kernel/v1 node sdks/conformance/seed.mjs > /tmp/mr.env
217
+ .venv/bin/python sdks/conformance/run_python.py --env-file /tmp/mr.env
218
+ ```
219
+
220
+ `webhooks.signature_vectors` corre sin kernel (vectores compartidos con el SDK
221
+ de JavaScript en `sdks/conformance/webhook-vectors.json`).
222
+
223
+ ## Desarrollo
224
+
225
+ ```bash
226
+ .venv/bin/pip install -e 'sdks/python[dev]'
227
+ .venv/bin/python -m pytest sdks/python
228
+ ```
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.21"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mirotaract"
7
+ version = "0.1.0"
8
+ description = "SDK oficial de Mi Rotaract: API de datos institucionales e \"Ingresar con Mi Rotaract\" (OAuth 2.0 + OpenID Connect)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Mi Rotaract" }]
13
+ dependencies = [
14
+ "httpx>=0.27,<1",
15
+ "PyJWT[crypto]>=2.8,<3",
16
+ ]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3 :: Only",
20
+ "Typing :: Typed",
21
+ ]
22
+
23
+ [project.optional-dependencies]
24
+ fastapi = ["fastapi>=0.110"]
25
+ dev = [
26
+ "pytest>=8",
27
+ "fastapi>=0.110",
28
+ ]
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/mirotaract"]
32
+
33
+ [tool.pytest.ini_options]
34
+ testpaths = ["tests"]
35
+ addopts = "-q"
@@ -0,0 +1,75 @@
1
+ """SDK oficial de Mi Rotaract.
2
+
3
+ - ``MiRotaract`` / ``AsyncMiRotaract``: API de datos institucionales
4
+ (client_credentials, solo servidor).
5
+ - ``MiRotaractAuth`` / ``AsyncMiRotaractAuth``: "Ingresar con Mi Rotaract"
6
+ (OAuth 2.0 + PKCE + OpenID Connect).
7
+ - ``mirotaract.fastapi.require_user``: dependencia para FastAPI.
8
+ - ``verify_webhook``: verifica la firma de un webhook de Mi Rotaract.
9
+ """
10
+
11
+ from ._http import SDK_VERSION
12
+ from .auth import (
13
+ KERNEL_AUDIENCE,
14
+ AsyncMiRotaractAuth,
15
+ MiRotaractAuth,
16
+ pkce_challenge,
17
+ random_token,
18
+ )
19
+ from .client import AsyncMiRotaract, MiRotaract
20
+ from .errors import (
21
+ MiRotaractApiError,
22
+ MiRotaractConfigError,
23
+ MiRotaractError,
24
+ MiRotaractOAuthError,
25
+ )
26
+ from .webhooks import (
27
+ MiRotaractWebhookError,
28
+ verify_webhook,
29
+ )
30
+ from .pagination import AsyncPaginator, NotModified, Page, SyncPaginator
31
+ from .types import (
32
+ AuthorityView,
33
+ AuthorizationDecision,
34
+ AuthorizationRequest,
35
+ MemberView,
36
+ OrganizationView,
37
+ PeriodView,
38
+ PersonMembershipView,
39
+ PersonView,
40
+ TokenSet,
41
+ UserInfo,
42
+ )
43
+
44
+ __version__ = SDK_VERSION
45
+
46
+ __all__ = [
47
+ "SDK_VERSION",
48
+ "KERNEL_AUDIENCE",
49
+ "MiRotaract",
50
+ "AsyncMiRotaract",
51
+ "MiRotaractAuth",
52
+ "AsyncMiRotaractAuth",
53
+ "MiRotaractError",
54
+ "MiRotaractApiError",
55
+ "MiRotaractOAuthError",
56
+ "MiRotaractConfigError",
57
+ "MiRotaractWebhookError",
58
+ "verify_webhook",
59
+ "Page",
60
+ "NotModified",
61
+ "SyncPaginator",
62
+ "AsyncPaginator",
63
+ "AuthorizationRequest",
64
+ "TokenSet",
65
+ "OrganizationView",
66
+ "PersonView",
67
+ "MemberView",
68
+ "AuthorityView",
69
+ "PeriodView",
70
+ "PersonMembershipView",
71
+ "AuthorizationDecision",
72
+ "UserInfo",
73
+ "pkce_challenge",
74
+ "random_token",
75
+ ]