urban-json-db 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 black beard
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,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: urban-json-db
3
+ Version: 0.1.0
4
+ Summary: Plugin urban-json : lecture en base de données SQL (connectsql / readsql).
5
+ Author-email: Gael Beard <gaelgael5@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/gaelgael5/Jslt
8
+ Project-URL: Repository, https://github.com/gaelgael5/Jslt
9
+ Keywords: json,transform,template,jslt,sql,database,plugin
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Database
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: urban-json
26
+ Provides-Extra: postgresql
27
+ Requires-Dist: psycopg[binary]>=3; extra == "postgresql"
28
+ Provides-Extra: mysql
29
+ Requires-Dist: PyMySQL>=1; extra == "mysql"
30
+ Dynamic: license-file
31
+
32
+ # urban-json-db
33
+
34
+ Plugin [urban-json](https://github.com/gaelgael5/Jslt) : lecture en **base de données
35
+ SQL** depuis les templates. Port de `Bb.Jslt.Services.Sql.ServicesSql` (.NET).
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install urban-json-db # SQLite inclus (stdlib), aucune dépendance en plus
41
+ pip install "urban-json-db[postgresql]" # + connecteur PostgreSQL (psycopg)
42
+ pip install "urban-json-db[mysql]" # + connecteur MySQL (PyMySQL)
43
+ ```
44
+
45
+ `urban-json` découvre le plugin automatiquement via les entry points (`urban_json.functions`).
46
+
47
+ ## Utilisation
48
+
49
+ ```python
50
+ from urban_json import JsltTemplate
51
+
52
+ tpl = JsltTemplate.parse('''
53
+ {
54
+ "cx:": connectsql("sqlite", "app.db"),
55
+ "users": [ { "$source": readsql(cx:, "SELECT id, name FROM users"), "id": $.id, "n": $.name } ]
56
+ }
57
+ ''')
58
+ tpl.transform({})
59
+ # -> {"users": [{"id": 1, "n": "Alice"}, ...]}
60
+ ```
61
+
62
+ - `connectsql(client, connection)` → descripteur `{client, connection}` (à stocker
63
+ dans une variable).
64
+ - `readsql(connection, sql)` → tableau de lignes ; chaque ligne est un objet
65
+ `{"$_line": n, <colonnes...>}` (noms de colonnes assainis, comme en .NET).
66
+
67
+ ## Clients
68
+
69
+ | Client | Chaîne de connexion | Fourni par |
70
+ |---|---|---|
71
+ | `sqlite` | chemin du fichier `.db` | stdlib (toujours disponible) |
72
+ | `postgresql` | DSN psycopg | extra `postgresql` (à enregistrer) |
73
+ | `mysql` | DSN PyMySQL | extra `mysql` (à enregistrer) |
74
+
75
+ Ajouter un SGBD (connecteur DB-API 2.0) :
76
+
77
+ ```python
78
+ import psycopg
79
+ from urban_json_db import register_client
80
+ register_client("postgresql", psycopg.connect)
81
+ ```
82
+
83
+ ## Sécurité
84
+
85
+ `readsql` exécute la requête **telle quelle**. Ne construisez pas le SQL par
86
+ concaténation de données non fiables (risque d'injection) : le contenu de `sql`
87
+ relève de l'auteur du template.
88
+
89
+ ## Licence
90
+
91
+ Apache-2.0.
@@ -0,0 +1,60 @@
1
+ # urban-json-db
2
+
3
+ Plugin [urban-json](https://github.com/gaelgael5/Jslt) : lecture en **base de données
4
+ SQL** depuis les templates. Port de `Bb.Jslt.Services.Sql.ServicesSql` (.NET).
5
+
6
+ ## Installation
7
+
8
+ ```bash
9
+ pip install urban-json-db # SQLite inclus (stdlib), aucune dépendance en plus
10
+ pip install "urban-json-db[postgresql]" # + connecteur PostgreSQL (psycopg)
11
+ pip install "urban-json-db[mysql]" # + connecteur MySQL (PyMySQL)
12
+ ```
13
+
14
+ `urban-json` découvre le plugin automatiquement via les entry points (`urban_json.functions`).
15
+
16
+ ## Utilisation
17
+
18
+ ```python
19
+ from urban_json import JsltTemplate
20
+
21
+ tpl = JsltTemplate.parse('''
22
+ {
23
+ "cx:": connectsql("sqlite", "app.db"),
24
+ "users": [ { "$source": readsql(cx:, "SELECT id, name FROM users"), "id": $.id, "n": $.name } ]
25
+ }
26
+ ''')
27
+ tpl.transform({})
28
+ # -> {"users": [{"id": 1, "n": "Alice"}, ...]}
29
+ ```
30
+
31
+ - `connectsql(client, connection)` → descripteur `{client, connection}` (à stocker
32
+ dans une variable).
33
+ - `readsql(connection, sql)` → tableau de lignes ; chaque ligne est un objet
34
+ `{"$_line": n, <colonnes...>}` (noms de colonnes assainis, comme en .NET).
35
+
36
+ ## Clients
37
+
38
+ | Client | Chaîne de connexion | Fourni par |
39
+ |---|---|---|
40
+ | `sqlite` | chemin du fichier `.db` | stdlib (toujours disponible) |
41
+ | `postgresql` | DSN psycopg | extra `postgresql` (à enregistrer) |
42
+ | `mysql` | DSN PyMySQL | extra `mysql` (à enregistrer) |
43
+
44
+ Ajouter un SGBD (connecteur DB-API 2.0) :
45
+
46
+ ```python
47
+ import psycopg
48
+ from urban_json_db import register_client
49
+ register_client("postgresql", psycopg.connect)
50
+ ```
51
+
52
+ ## Sécurité
53
+
54
+ `readsql` exécute la requête **telle quelle**. Ne construisez pas le SQL par
55
+ concaténation de données non fiables (risque d'injection) : le contenu de `sql`
56
+ relève de l'auteur du template.
57
+
58
+ ## Licence
59
+
60
+ Apache-2.0.
@@ -0,0 +1,63 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "urban-json-db"
7
+ dynamic = ["version"]
8
+ description = "Plugin urban-json : lecture en base de données SQL (connectsql / readsql)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "Apache-2.0"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Gael Beard", email = "gaelgael5@gmail.com" }]
14
+ keywords = ["json", "transform", "template", "jslt", "sql", "database", "plugin"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Operating System :: OS Independent",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Topic :: Software Development :: Libraries :: Python Modules",
26
+ "Topic :: Database",
27
+ "Typing :: Typed",
28
+ ]
29
+ # SQLite est fourni par la stdlib : aucune dépendance au-delà du cœur.
30
+ dependencies = [
31
+ "urban-json",
32
+ ]
33
+
34
+ # SGBD additionnels en extras (connecteurs DB-API 2.0 à enregistrer via
35
+ # register_client). Ex. : pip install urban-json-db[postgresql]
36
+ [project.optional-dependencies]
37
+ postgresql = ["psycopg[binary]>=3"]
38
+ mysql = ["PyMySQL>=1"]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/gaelgael5/Jslt"
42
+ Repository = "https://github.com/gaelgael5/Jslt"
43
+
44
+ # Découverte automatique par urban-json : installer ce paquet suffit à exposer les fonctions.
45
+ [project.entry-points."urban_json.functions"]
46
+ db = "urban_json_db:register"
47
+
48
+ [tool.setuptools.dynamic]
49
+ version = { attr = "urban_json_db.__version__" }
50
+
51
+ [tool.setuptools.packages.find]
52
+ where = ["src"]
53
+
54
+ [tool.setuptools.package-data]
55
+ urban_json_db = ["py.typed"]
56
+
57
+ [tool.pytest.ini_options]
58
+ testpaths = ["tests"]
59
+ pythonpath = ["src"]
60
+
61
+ [tool.ruff]
62
+ line-length = 100
63
+ src = ["src", "tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,145 @@
1
+ """Plugin jslt : lecture en base de données SQL (``connectsql`` / ``readsql``).
2
+
3
+ Port de ``Bb.Jslt.Services.Sql.ServicesSql`` (.NET), déjà un assembly séparé côté
4
+ .NET. Deux fonctions de template :
5
+
6
+ * ``connectsql(client, connection)`` → descripteur de connexion
7
+ ``{"client": ..., "connection": ...}`` (à stocker dans une variable) ;
8
+ * ``readsql(connection, sql)`` → exécute la requête et renvoie un tableau de
9
+ lignes ; chaque ligne est un objet ``{"$_line": n, <colonnes...>}`` (noms de
10
+ colonnes assainis, comme en .NET).
11
+
12
+ Le .NET s'appuie sur ``DbProviderFactory`` (clé ``client``). Le portage utilise un
13
+ **registre de clients DB-API 2.0** : ``sqlite`` est fourni (stdlib, sans
14
+ dépendance). D'autres SGBD s'ajoutent via :func:`register_client` ou en extras
15
+ (PostgreSQL, MySQL…).
16
+
17
+ Sécurité : ``readsql`` exécute la requête SQL **telle quelle**. Ne construis pas la
18
+ requête par concaténation de données non fiables (risque d'injection) ; le contenu
19
+ de ``sql`` est sous la responsabilité de l'auteur du template.
20
+
21
+ Découvert automatiquement par ``jslt`` via l'entry point ``urban_json.functions``.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import sqlite3
27
+ import unicodedata
28
+ from collections.abc import Callable
29
+ from typing import Any
30
+
31
+ __version__ = "0.1.0"
32
+
33
+ __all__ = ["execute_connectsql", "execute_readsql", "register", "register_client"]
34
+
35
+ #: Connexion DB-API 2.0 : une callable ``(connection_string) -> connection``.
36
+ DbConnector = Callable[[str], Any]
37
+
38
+ #: Registre des clients SQL (clé insensible à la casse) -> connecteur DB-API.
39
+ _CLIENTS: dict[str, DbConnector] = {}
40
+
41
+
42
+ def register_client(name: str, connector: DbConnector) -> None:
43
+ """Enregistre un client SQL (ex. ``register_client("postgresql", psycopg.connect)``).
44
+
45
+ ``connector`` reçoit la chaîne de connexion et renvoie une connexion DB-API 2.0.
46
+ """
47
+ _CLIENTS[name.lower()] = connector
48
+
49
+
50
+ # Client intégré : SQLite (stdlib). Alias usuels, dont la forme « .NET-like ».
51
+ register_client("sqlite", sqlite3.connect)
52
+ register_client("sqlite3", sqlite3.connect)
53
+ register_client("system.data.sqlite", sqlite3.connect)
54
+
55
+
56
+ def _sanitize(name: str) -> str:
57
+ """Assainit un nom de colonne (même logique que urban-json-csv / ``ServiceCsv.GetLabel``)."""
58
+ decomposed = unicodedata.normalize("NFKD", name)
59
+ out: list[str] = []
60
+ for char in decomposed:
61
+ if unicodedata.combining(char):
62
+ continue
63
+ if char.isascii() and char.isalnum():
64
+ out.append(char)
65
+ elif char in (" ", "'", '"'):
66
+ out.append("_")
67
+ label = "".join(out)
68
+ while "___" in label:
69
+ label = label.replace("___", "_")
70
+ return label.replace("__", "_")
71
+
72
+
73
+ def execute_connectsql(
74
+ ctx: Any, client: str | None = None, connection: str | None = None
75
+ ) -> dict:
76
+ """Crée un descripteur de connexion SQL (port de ``connectsql``).
77
+
78
+ Args:
79
+ client: clé du client SQL (``sqlite``, ou un client enregistré).
80
+ connection: chaîne de connexion (pour SQLite : chemin du fichier ``.db``).
81
+
82
+ Returns:
83
+ Un descripteur ``{"client": client, "connection": connection}`` à passer à
84
+ :func:`execute_readsql` (typiquement via une variable de template).
85
+ """
86
+ if not client:
87
+ raise ValueError("connectsql: client est requis")
88
+ if client.lower() not in _CLIENTS:
89
+ raise ValueError(
90
+ f"connectsql: client '{client}' inconnu. Clients disponibles : "
91
+ f"{', '.join(sorted(_CLIENTS))}"
92
+ )
93
+ return {"client": client, "connection": connection}
94
+
95
+
96
+ def execute_readsql(ctx: Any, connection: Any = None, sql: str | None = None) -> list:
97
+ """Exécute une requête SQL et renvoie les lignes (port de ``readsql``).
98
+
99
+ Args:
100
+ connection: descripteur issu de :func:`execute_connectsql`.
101
+ sql: requête SQL à exécuter.
102
+
103
+ Returns:
104
+ La liste des lignes, chacune ``{"$_line": n, <colonne>: <valeur>, ...}``.
105
+ """
106
+ if not isinstance(connection, dict) or "client" not in connection:
107
+ raise ValueError("readsql: connection doit être le descripteur de connectsql")
108
+ if not sql:
109
+ raise ValueError("readsql: sql est requis")
110
+
111
+ client = connection["client"]
112
+ connector = _CLIENTS.get(str(client).lower())
113
+ if connector is None:
114
+ raise ValueError(f"readsql: client '{client}' inconnu")
115
+
116
+ conn = connector(connection.get("connection"))
117
+ try:
118
+ cursor = conn.cursor()
119
+ cursor.execute(sql)
120
+ if cursor.description is None: # requête sans jeu de résultats
121
+ return []
122
+ columns = [_sanitize(col[0]) for col in cursor.description]
123
+ rows: list[dict] = []
124
+ for line, record in enumerate(cursor.fetchall(), start=1):
125
+ obj: dict[str, Any] = {"$_line": line}
126
+ for name, value in zip(columns, record):
127
+ obj[name] = value
128
+ rows.append(obj)
129
+ return rows
130
+ finally:
131
+ conn.close()
132
+
133
+
134
+ def register(registry: Any) -> None:
135
+ """Enregistre les fonctions du plugin dans un registre jslt (entry point)."""
136
+ registry.register(
137
+ "connectsql",
138
+ execute_connectsql,
139
+ description="Crée un descripteur de connexion SQL (client + chaîne de connexion).",
140
+ )
141
+ registry.register(
142
+ "readsql",
143
+ execute_readsql,
144
+ description="Exécute une requête SQL et renvoie les lignes (tableau d'objets).",
145
+ )
File without changes
@@ -0,0 +1,91 @@
1
+ Metadata-Version: 2.4
2
+ Name: urban-json-db
3
+ Version: 0.1.0
4
+ Summary: Plugin urban-json : lecture en base de données SQL (connectsql / readsql).
5
+ Author-email: Gael Beard <gaelgael5@gmail.com>
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/gaelgael5/Jslt
8
+ Project-URL: Repository, https://github.com/gaelgael5/Jslt
9
+ Keywords: json,transform,template,jslt,sql,database,plugin
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Topic :: Database
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: urban-json
26
+ Provides-Extra: postgresql
27
+ Requires-Dist: psycopg[binary]>=3; extra == "postgresql"
28
+ Provides-Extra: mysql
29
+ Requires-Dist: PyMySQL>=1; extra == "mysql"
30
+ Dynamic: license-file
31
+
32
+ # urban-json-db
33
+
34
+ Plugin [urban-json](https://github.com/gaelgael5/Jslt) : lecture en **base de données
35
+ SQL** depuis les templates. Port de `Bb.Jslt.Services.Sql.ServicesSql` (.NET).
36
+
37
+ ## Installation
38
+
39
+ ```bash
40
+ pip install urban-json-db # SQLite inclus (stdlib), aucune dépendance en plus
41
+ pip install "urban-json-db[postgresql]" # + connecteur PostgreSQL (psycopg)
42
+ pip install "urban-json-db[mysql]" # + connecteur MySQL (PyMySQL)
43
+ ```
44
+
45
+ `urban-json` découvre le plugin automatiquement via les entry points (`urban_json.functions`).
46
+
47
+ ## Utilisation
48
+
49
+ ```python
50
+ from urban_json import JsltTemplate
51
+
52
+ tpl = JsltTemplate.parse('''
53
+ {
54
+ "cx:": connectsql("sqlite", "app.db"),
55
+ "users": [ { "$source": readsql(cx:, "SELECT id, name FROM users"), "id": $.id, "n": $.name } ]
56
+ }
57
+ ''')
58
+ tpl.transform({})
59
+ # -> {"users": [{"id": 1, "n": "Alice"}, ...]}
60
+ ```
61
+
62
+ - `connectsql(client, connection)` → descripteur `{client, connection}` (à stocker
63
+ dans une variable).
64
+ - `readsql(connection, sql)` → tableau de lignes ; chaque ligne est un objet
65
+ `{"$_line": n, <colonnes...>}` (noms de colonnes assainis, comme en .NET).
66
+
67
+ ## Clients
68
+
69
+ | Client | Chaîne de connexion | Fourni par |
70
+ |---|---|---|
71
+ | `sqlite` | chemin du fichier `.db` | stdlib (toujours disponible) |
72
+ | `postgresql` | DSN psycopg | extra `postgresql` (à enregistrer) |
73
+ | `mysql` | DSN PyMySQL | extra `mysql` (à enregistrer) |
74
+
75
+ Ajouter un SGBD (connecteur DB-API 2.0) :
76
+
77
+ ```python
78
+ import psycopg
79
+ from urban_json_db import register_client
80
+ register_client("postgresql", psycopg.connect)
81
+ ```
82
+
83
+ ## Sécurité
84
+
85
+ `readsql` exécute la requête **telle quelle**. Ne construisez pas le SQL par
86
+ concaténation de données non fiables (risque d'injection) : le contenu de `sql`
87
+ relève de l'auteur du template.
88
+
89
+ ## Licence
90
+
91
+ Apache-2.0.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/urban_json_db/__init__.py
5
+ src/urban_json_db/py.typed
6
+ src/urban_json_db.egg-info/PKG-INFO
7
+ src/urban_json_db.egg-info/SOURCES.txt
8
+ src/urban_json_db.egg-info/dependency_links.txt
9
+ src/urban_json_db.egg-info/entry_points.txt
10
+ src/urban_json_db.egg-info/requires.txt
11
+ src/urban_json_db.egg-info/top_level.txt
12
+ tests/test_readsql.py
@@ -0,0 +1,2 @@
1
+ [urban_json.functions]
2
+ db = urban_json_db:register
@@ -0,0 +1,7 @@
1
+ urban-json
2
+
3
+ [mysql]
4
+ PyMySQL>=1
5
+
6
+ [postgresql]
7
+ psycopg[binary]>=3
@@ -0,0 +1 @@
1
+ urban_json_db
@@ -0,0 +1,85 @@
1
+ """Tests du plugin urban-json-db (``connectsql`` / ``readsql``) avec SQLite.
2
+
3
+ Couvre : descripteur de connexion, lecture + ``$_line``, assainissement des noms
4
+ de colonnes, client inconnu, découverte via entry point, usage en template.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sqlite3
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+ from urban_json import FunctionRegistry, JsltTemplate
14
+
15
+ from urban_json_db import execute_connectsql, execute_readsql, register
16
+
17
+
18
+ def _make_db(tmp_path: Path) -> str:
19
+ path = tmp_path / "app.db"
20
+ conn = sqlite3.connect(path)
21
+ conn.execute("CREATE TABLE users (id INTEGER, name TEXT, active INTEGER)")
22
+ conn.executemany(
23
+ "INSERT INTO users VALUES (?, ?, ?)",
24
+ [(1, "Alice", 1), (2, "Bob", 0)],
25
+ )
26
+ conn.commit()
27
+ conn.close()
28
+ return str(path)
29
+
30
+
31
+ def test_connectsql_descriptor() -> None:
32
+ cx = execute_connectsql(None, "sqlite", "/tmp/x.db")
33
+ assert cx == {"client": "sqlite", "connection": "/tmp/x.db"}
34
+
35
+
36
+ def test_connectsql_unknown_client() -> None:
37
+ with pytest.raises(ValueError, match="inconnu"):
38
+ execute_connectsql(None, "oracle", "x")
39
+
40
+
41
+ def test_readsql_rows_and_line(tmp_path: Path) -> None:
42
+ cx = execute_connectsql(None, "sqlite", _make_db(tmp_path))
43
+ rows = execute_readsql(None, cx, "SELECT id, name FROM users ORDER BY id")
44
+ assert rows == [
45
+ {"$_line": 1, "id": 1, "name": "Alice"},
46
+ {"$_line": 2, "name": "Bob", "id": 2},
47
+ ]
48
+
49
+
50
+ def test_readsql_column_name_sanitized(tmp_path: Path) -> None:
51
+ cx = execute_connectsql(None, "sqlite", _make_db(tmp_path))
52
+ rows = execute_readsql(None, cx, 'SELECT name AS "Full Name" FROM users WHERE id = 1')
53
+ assert rows == [{"$_line": 1, "Full_Name": "Alice"}]
54
+
55
+
56
+ def test_readsql_requires_descriptor() -> None:
57
+ with pytest.raises(ValueError, match="descripteur"):
58
+ execute_readsql(None, "not-a-descriptor", "SELECT 1")
59
+
60
+
61
+ def test_register_into_custom_registry() -> None:
62
+ reg = FunctionRegistry()
63
+ register(reg)
64
+ assert "connectsql" in reg and "readsql" in reg
65
+
66
+
67
+ def test_discovered_via_entry_point() -> None:
68
+ from urban_json import default_registry, load_plugins
69
+
70
+ load_plugins(default_registry)
71
+ assert "connectsql" in default_registry and "readsql" in default_registry
72
+
73
+
74
+ def test_in_template(tmp_path: Path) -> None:
75
+ db = _make_db(tmp_path)
76
+ template = (
77
+ "{"
78
+ f' "cx:": connectsql("sqlite", "{db}"),'
79
+ ' "rows": [ { "$source": readsql(cx:, "SELECT id, name FROM users ORDER BY id"),'
80
+ ' "id": $.id, "n": $.name } ]'
81
+ "}"
82
+ )
83
+ assert JsltTemplate.parse(template).transform({}) == {
84
+ "rows": [{"id": 1, "n": "Alice"}, {"id": 2, "n": "Bob"}]
85
+ }