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.
- urban_json_db-0.1.0/LICENSE +21 -0
- urban_json_db-0.1.0/PKG-INFO +91 -0
- urban_json_db-0.1.0/README.md +60 -0
- urban_json_db-0.1.0/pyproject.toml +63 -0
- urban_json_db-0.1.0/setup.cfg +4 -0
- urban_json_db-0.1.0/src/urban_json_db/__init__.py +145 -0
- urban_json_db-0.1.0/src/urban_json_db/py.typed +0 -0
- urban_json_db-0.1.0/src/urban_json_db.egg-info/PKG-INFO +91 -0
- urban_json_db-0.1.0/src/urban_json_db.egg-info/SOURCES.txt +12 -0
- urban_json_db-0.1.0/src/urban_json_db.egg-info/dependency_links.txt +1 -0
- urban_json_db-0.1.0/src/urban_json_db.egg-info/entry_points.txt +2 -0
- urban_json_db-0.1.0/src/urban_json_db.egg-info/requires.txt +7 -0
- urban_json_db-0.1.0/src/urban_json_db.egg-info/top_level.txt +1 -0
- urban_json_db-0.1.0/tests/test_readsql.py +85 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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
|
+
}
|