lex-api-mx-client 0.1.1__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,99 @@
1
+ # ----------------------------------------------------------------------
2
+ # Documentos internos: estrategia, roadmaps, contexto y dirección.
3
+ # Viven en internal/ y NO se suben. Respáldalos por tu cuenta.
4
+ # ----------------------------------------------------------------------
5
+ internal/
6
+ **/ROADMAP*.md
7
+ **/*GO_TO_MARKET*.md
8
+ **/*ESTRATEGIA*.md
9
+ **/*STRATEGY*.md
10
+ **/Contexto*.md
11
+ **/*direccion-oficial*
12
+ docs/source/
13
+ *.private.md
14
+ *.interno.md
15
+ NOTAS*.md
16
+ notes/
17
+
18
+ # ----------------------------------------------------------------------
19
+ # Secretos y configuración local
20
+ # ----------------------------------------------------------------------
21
+ .env
22
+ .env.*
23
+ !.env.example
24
+ *.pem
25
+ # Certificados intermedios públicos (sin llave privada) que el monitor necesita para TLS
26
+ !packages/ingestion/src/ingestion/certs/*.pem
27
+ *.key
28
+ credentials*.json
29
+ service-account*.json
30
+ .claude/settings.local.json
31
+
32
+ # ----------------------------------------------------------------------
33
+ # Desarrollo personal: borradores, pruebas manuales y temporales
34
+ # ----------------------------------------------------------------------
35
+ scratch/
36
+ sandbox/
37
+ playground-local/
38
+ tmp/
39
+ .tmp/
40
+ *.local.*
41
+ *.bak
42
+ *.orig
43
+ *.log
44
+ .tool-home/
45
+
46
+ # Bases de datos y volcados locales
47
+ *.sqlite
48
+ *.sqlite3
49
+ *.db
50
+ *.dump
51
+ *.sql.gz
52
+ pgdata/
53
+
54
+ # ----------------------------------------------------------------------
55
+ # Python y Node
56
+ # ----------------------------------------------------------------------
57
+ __pycache__/
58
+ *.pyc
59
+ .venv/
60
+ venv/
61
+ *.egg-info/
62
+ dist/
63
+ build/
64
+ node_modules/
65
+ .next/
66
+
67
+ # Pruebas, cobertura y linters
68
+ .pytest_cache/
69
+ .pytest-tmp/
70
+ .pytest-*/
71
+ .tmp-pytest-*/
72
+ .test-tmp-*/
73
+ pytest-cache-files-*/
74
+ .ruff_cache/
75
+ .mypy_cache/
76
+ .coverage
77
+ coverage.xml
78
+ htmlcov/
79
+ reports/validation/runtime_test_workspace_*/pytest/
80
+ # Salidas de scripts/run_local_e2e.py y del monitor (en Actions viajan como artefactos)
81
+ reports/e2e/
82
+ reports/monitor/
83
+
84
+ # Supabase local
85
+ supabase/.temp/
86
+ supabase/.branches/
87
+
88
+ # ----------------------------------------------------------------------
89
+ # Editores y sistema operativo
90
+ # ----------------------------------------------------------------------
91
+ .vscode/
92
+ .idea/
93
+ *.swp
94
+ *.swo
95
+ .DS_Store
96
+ Thumbs.db
97
+
98
+ # Documentos históricos archivados fuera del repositorio (se conservan en disco)
99
+ docs/archive/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lex API MX
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,63 @@
1
+ Metadata-Version: 2.5
2
+ Name: lex-api-mx-client
3
+ Version: 0.1.1
4
+ Summary: Cliente Python de Lex API MX: legislación federal mexicana vigente con fuente, checksum y vigencia
5
+ Project-URL: Homepage, https://lexapi.mx
6
+ Project-URL: Documentation, https://lexapi.mx/docs
7
+ Project-URL: Repository, https://github.com/Julio-Patron/Lex-API-Mx
8
+ Author-email: Lex API MX <admin@lexapi.mx>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,legal,legislacion,leyes,mexico,sdk
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Natural Language :: Spanish
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx>=0.28
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Lex API MX — cliente Python
26
+
27
+ Cliente mínimo de [Lex API MX](https://lexapi.mx) para consultar la legislación
28
+ federal mexicana vigente. Cada respuesta trae la fuente oficial, el checksum
29
+ SHA-256 del texto y su periodo de vigencia.
30
+
31
+ ```bash
32
+ pip install lex-api-mx-client
33
+ ```
34
+
35
+ ```python
36
+ from lex_api_mx import LexApiClient, LexApiError
37
+
38
+ with LexApiClient(base_url="https://api.lexapi.mx", api_key="tu_clave") as lex:
39
+ paquete = lex.resolve(query="Artículo 47 LFT", as_of="2026-10-08")
40
+ articulos = lex.search_articles(query="aguinaldo", norm="lft", jurisdiction="federal")
41
+ arbol = lex.norm_structure("lft")
42
+ ```
43
+
44
+ | Método | Endpoint |
45
+ |---|---|
46
+ | `resolve(query, norm=None, jurisdiction="federal", as_of=None, limit=10)` | `POST /api/v1/resolve` |
47
+ | `search_articles(query, norm=None, matter=None, jurisdiction=None, as_of=None, limit=20)` | `GET /api/v1/articulos/search` |
48
+ | `norm_structure(norm, as_of=None, include_text=False)` | `GET /api/v1/normas/{slug}/estructura` |
49
+ | `map(query, jurisdiction=None, as_of=None)` | `POST /api/v1/map` (requiere el permiso `map:read`) |
50
+
51
+ `resolve` y `map` aceptan `idempotency_key`: un reintento con la misma clave no
52
+ se cobra dos veces. Los errores HTTP se lanzan como `LexApiError` con
53
+ `status_code`, `detail`, `request_id` y `body`; `402` significa saldo agotado.
54
+
55
+ Para obtener una clave, crea tu cuenta en
56
+ [lexapi.mx/#cuenta](https://lexapi.mx/#cuenta). Referencia de la API:
57
+ [lexapi.mx/docs](https://lexapi.mx/docs).
58
+
59
+ Para usar Lex API MX desde Claude, Cursor u otro agente, instala el servidor MCP
60
+ [`lex-api-mx-mcp`](https://pypi.org/project/lex-api-mx-mcp/).
61
+
62
+ Código fuente: [github.com/Julio-Patron/Lex-API-Mx](https://github.com/Julio-Patron/Lex-API-Mx/tree/main/packages/lex-api-client).
63
+ Licencia MIT.
@@ -0,0 +1,39 @@
1
+ # Lex API MX — cliente Python
2
+
3
+ Cliente mínimo de [Lex API MX](https://lexapi.mx) para consultar la legislación
4
+ federal mexicana vigente. Cada respuesta trae la fuente oficial, el checksum
5
+ SHA-256 del texto y su periodo de vigencia.
6
+
7
+ ```bash
8
+ pip install lex-api-mx-client
9
+ ```
10
+
11
+ ```python
12
+ from lex_api_mx import LexApiClient, LexApiError
13
+
14
+ with LexApiClient(base_url="https://api.lexapi.mx", api_key="tu_clave") as lex:
15
+ paquete = lex.resolve(query="Artículo 47 LFT", as_of="2026-10-08")
16
+ articulos = lex.search_articles(query="aguinaldo", norm="lft", jurisdiction="federal")
17
+ arbol = lex.norm_structure("lft")
18
+ ```
19
+
20
+ | Método | Endpoint |
21
+ |---|---|
22
+ | `resolve(query, norm=None, jurisdiction="federal", as_of=None, limit=10)` | `POST /api/v1/resolve` |
23
+ | `search_articles(query, norm=None, matter=None, jurisdiction=None, as_of=None, limit=20)` | `GET /api/v1/articulos/search` |
24
+ | `norm_structure(norm, as_of=None, include_text=False)` | `GET /api/v1/normas/{slug}/estructura` |
25
+ | `map(query, jurisdiction=None, as_of=None)` | `POST /api/v1/map` (requiere el permiso `map:read`) |
26
+
27
+ `resolve` y `map` aceptan `idempotency_key`: un reintento con la misma clave no
28
+ se cobra dos veces. Los errores HTTP se lanzan como `LexApiError` con
29
+ `status_code`, `detail`, `request_id` y `body`; `402` significa saldo agotado.
30
+
31
+ Para obtener una clave, crea tu cuenta en
32
+ [lexapi.mx/#cuenta](https://lexapi.mx/#cuenta). Referencia de la API:
33
+ [lexapi.mx/docs](https://lexapi.mx/docs).
34
+
35
+ Para usar Lex API MX desde Claude, Cursor u otro agente, instala el servidor MCP
36
+ [`lex-api-mx-mcp`](https://pypi.org/project/lex-api-mx-mcp/).
37
+
38
+ Código fuente: [github.com/Julio-Patron/Lex-API-Mx](https://github.com/Julio-Patron/Lex-API-Mx/tree/main/packages/lex-api-client).
39
+ Licencia MIT.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "lex-api-mx-client"
3
+ version = "0.1.1"
4
+ description = "Cliente Python de Lex API MX: legislación federal mexicana vigente con fuente, checksum y vigencia"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [{ name = "Lex API MX", email = "admin@lexapi.mx" }]
9
+ requires-python = ">=3.11"
10
+ keywords = ["mexico", "legislacion", "leyes", "legal", "api", "sdk"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "Natural Language :: Spanish",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Topic :: Software Development :: Libraries",
21
+ ]
22
+ dependencies = [
23
+ "httpx>=0.28",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://lexapi.mx"
28
+ Documentation = "https://lexapi.mx/docs"
29
+ Repository = "https://github.com/Julio-Patron/Lex-API-Mx"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.targets.wheel]
36
+ packages = ["src/lex_api_mx"]
@@ -0,0 +1,4 @@
1
+ from .client import LexApiClient, LexApiError
2
+
3
+ __all__ = ["LexApiClient", "LexApiError"]
4
+ __version__ = "0.1.1"
@@ -0,0 +1,193 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import date
4
+ from typing import Any
5
+ from urllib.parse import quote
6
+
7
+ import httpx
8
+
9
+
10
+ class LexApiError(RuntimeError):
11
+ def __init__(
12
+ self,
13
+ status_code: int,
14
+ message: str,
15
+ request_id: str | None = None,
16
+ body: Any = None,
17
+ ):
18
+ self.status_code = status_code
19
+ self.request_id = request_id
20
+ self.detail = message
21
+ self.body = body
22
+ suffix = f" request_id={request_id}" if request_id else ""
23
+ super().__init__(f"Lex API MX returned {status_code}: {message}{suffix}")
24
+
25
+
26
+ class LexApiClient:
27
+ """Synchronous client for the stable Lex API MX integration surface."""
28
+
29
+ def __init__(
30
+ self,
31
+ *,
32
+ base_url: str,
33
+ api_key: str,
34
+ timeout: float = 30.0,
35
+ transport: httpx.BaseTransport | None = None,
36
+ user_agent: str | None = None,
37
+ ) -> None:
38
+ if not base_url.strip():
39
+ raise ValueError("base_url is required")
40
+ if not api_key.strip():
41
+ raise ValueError("api_key is required")
42
+ if user_agent is None:
43
+ from . import __version__ # importación tardía: el paquete importa este módulo
44
+
45
+ user_agent = f"lex-api-mx-python/{__version__}"
46
+ self._client = httpx.Client(
47
+ base_url=base_url.rstrip("/"),
48
+ timeout=timeout,
49
+ transport=transport,
50
+ headers={
51
+ "X-API-Key": api_key,
52
+ "Accept": "application/json",
53
+ "User-Agent": user_agent,
54
+ },
55
+ )
56
+
57
+ def __enter__(self) -> "LexApiClient":
58
+ return self
59
+
60
+ def __exit__(self, *_: object) -> None:
61
+ self.close()
62
+
63
+ def close(self) -> None:
64
+ self._client.close()
65
+
66
+ def health(self) -> dict[str, Any]:
67
+ return self._request("GET", "/health")
68
+
69
+ def resolve(
70
+ self,
71
+ *,
72
+ query: str,
73
+ norm: str | None = None,
74
+ jurisdiction: str = "federal",
75
+ as_of: date | str | None = None,
76
+ limit: int = 10,
77
+ idempotency_key: str | None = None,
78
+ ) -> dict[str, Any]:
79
+ payload: dict[str, Any] = {
80
+ "query": query,
81
+ "jurisdiction": jurisdiction,
82
+ "limit": limit,
83
+ }
84
+ if norm is not None:
85
+ payload["norm"] = norm
86
+ if as_of is not None:
87
+ payload["date"] = as_of.isoformat() if isinstance(as_of, date) else as_of
88
+ return self._request(
89
+ "POST",
90
+ "/api/v1/resolve",
91
+ json=payload,
92
+ idempotency_key=idempotency_key,
93
+ )
94
+
95
+ def map(
96
+ self,
97
+ *,
98
+ query: str,
99
+ jurisdiction: str | None = None,
100
+ as_of: date | str | None = None,
101
+ mode: str = "api",
102
+ idempotency_key: str | None = None,
103
+ ) -> dict[str, Any]:
104
+ payload: dict[str, Any] = {"query": query, "mode": mode}
105
+ if jurisdiction is not None:
106
+ payload["jurisdiction"] = jurisdiction
107
+ if as_of is not None:
108
+ payload["date"] = as_of.isoformat() if isinstance(as_of, date) else as_of
109
+ return self._request(
110
+ "POST",
111
+ "/api/v1/map",
112
+ json=payload,
113
+ idempotency_key=idempotency_key,
114
+ )
115
+
116
+ def search_articles(
117
+ self,
118
+ *,
119
+ query: str,
120
+ norm: str | None = None,
121
+ matter: str | None = None,
122
+ jurisdiction: str | None = None,
123
+ as_of: date | str | None = None,
124
+ limit: int = 20,
125
+ ) -> dict[str, Any]:
126
+ params: dict[str, Any] = {"q": query, "limit": limit}
127
+ if norm is not None:
128
+ params["norma"] = norm
129
+ if matter is not None:
130
+ params["materia"] = matter
131
+ if jurisdiction is not None:
132
+ params["jurisdiccion"] = jurisdiction
133
+ if as_of is not None:
134
+ params["fecha"] = as_of.isoformat() if isinstance(as_of, date) else as_of
135
+ return self._request("GET", "/api/v1/articulos/search", params=params)
136
+
137
+ def norm_structure(
138
+ self,
139
+ norm: str,
140
+ *,
141
+ as_of: date | str | None = None,
142
+ include_text: bool = False,
143
+ ) -> dict[str, Any]:
144
+ if not norm.strip():
145
+ raise ValueError("norm is required")
146
+ params: dict[str, Any] = {"include_text": str(include_text).lower()}
147
+ if as_of is not None:
148
+ params["fecha"] = as_of.isoformat() if isinstance(as_of, date) else as_of
149
+ return self._request(
150
+ "GET",
151
+ f"/api/v1/normas/{quote(norm.strip(), safe='')}/estructura",
152
+ params=params,
153
+ )
154
+
155
+ def _request(
156
+ self,
157
+ method: str,
158
+ path: str,
159
+ *,
160
+ json: dict[str, Any] | None = None,
161
+ params: dict[str, Any] | None = None,
162
+ idempotency_key: str | None = None,
163
+ ) -> dict[str, Any]:
164
+ headers = {"Idempotency-Key": idempotency_key} if idempotency_key else None
165
+ response = self._client.request(method, path, json=json, params=params, headers=headers)
166
+ if response.is_error:
167
+ body: Any = None
168
+ try:
169
+ body = response.json()
170
+ message = str(body.get("detail", body)) if isinstance(body, dict) else str(body)
171
+ except ValueError:
172
+ message = response.text or response.reason_phrase
173
+ raise LexApiError(
174
+ response.status_code,
175
+ message,
176
+ response.headers.get("X-Request-ID"),
177
+ body,
178
+ )
179
+ try:
180
+ value = response.json()
181
+ except ValueError as exc:
182
+ raise LexApiError(
183
+ response.status_code,
184
+ "response was not valid JSON",
185
+ response.headers.get("X-Request-ID"),
186
+ ) from exc
187
+ if not isinstance(value, dict):
188
+ raise LexApiError(
189
+ response.status_code,
190
+ "response JSON must be an object",
191
+ response.headers.get("X-Request-ID"),
192
+ )
193
+ return value