normacare-sdk 2.0.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.
- normacare_sdk-2.0.0/PKG-INFO +122 -0
- normacare_sdk-2.0.0/README.md +104 -0
- normacare_sdk-2.0.0/normacare_sdk/__init__.py +39 -0
- normacare_sdk-2.0.0/normacare_sdk/cli.py +393 -0
- normacare_sdk-2.0.0/normacare_sdk/component.py +206 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/basic/README.md +8 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/basic/component.cmp +34 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/basic/component.py +21 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/basic/test_component.py +15 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/database/README.md +3 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/database/component.cmp +41 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/database/component.py +38 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/database/test_component.py +15 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/hl7/README.md +3 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/hl7/component.cmp +35 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/hl7/component.py +35 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/hl7/test_component.py +17 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/http/README.md +3 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/http/component.cmp +40 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/http/component.py +44 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/http/test_component.py +15 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/javascript/README.md +3 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/javascript/component.cmp +34 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/javascript/component.js +9 -0
- normacare_sdk-2.0.0/normacare_sdk/templates/javascript/test_component.py +15 -0
- normacare_sdk-2.0.0/normacare_sdk/testing.py +187 -0
- normacare_sdk-2.0.0/normacare_sdk.egg-info/PKG-INFO +122 -0
- normacare_sdk-2.0.0/normacare_sdk.egg-info/SOURCES.txt +32 -0
- normacare_sdk-2.0.0/normacare_sdk.egg-info/dependency_links.txt +1 -0
- normacare_sdk-2.0.0/normacare_sdk.egg-info/entry_points.txt +2 -0
- normacare_sdk-2.0.0/normacare_sdk.egg-info/requires.txt +3 -0
- normacare_sdk-2.0.0/normacare_sdk.egg-info/top_level.txt +1 -0
- normacare_sdk-2.0.0/pyproject.toml +41 -0
- normacare_sdk-2.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: normacare-sdk
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: SDK officiel pour développer, tester et publier des composants NormaCare.
|
|
5
|
+
Author-email: FDevelopment LTD <sdk@fdevelopment.eu>
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Project-URL: Homepage, https://normacare.app
|
|
8
|
+
Project-URL: Store, https://store.normacare.app
|
|
9
|
+
Project-URL: Documentation, https://docs.normacare.app/sdk
|
|
10
|
+
Keywords: normacare,hl7,fhir,interoperability,healthcare,integration
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Intended Audience :: Healthcare Industry
|
|
13
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Provides-Extra: protect
|
|
17
|
+
Requires-Dist: pyarmor>=8; extra == "protect"
|
|
18
|
+
|
|
19
|
+
# NormaCare SDK
|
|
20
|
+
|
|
21
|
+
Le SDK officiel pour développer, tester et publier des **composants** d'intégration
|
|
22
|
+
médicale, utilisables dans **NormaCare Studio** et exécutés par **NormaCare Engine**,
|
|
23
|
+
et distribuables sur le **NormaCare Store**.
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install normacare-sdk
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Développement local (depuis ce dossier) :
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Démarrage rapide (5 minutes)
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
normacare-sdk new mon_composant_hl7 --template hl7
|
|
41
|
+
cd mon_composant_hl7
|
|
42
|
+
normacare-sdk test . # exécute test_mon_composant_hl7.py
|
|
43
|
+
normacare-sdk validate . # vérifie le manifest .cmp
|
|
44
|
+
normacare-sdk build . # produit mon_composant_hl7-1.0.0.ncpkg
|
|
45
|
+
normacare-sdk login # connexion au Store
|
|
46
|
+
normacare-sdk publish . --price 0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Templates
|
|
50
|
+
|
|
51
|
+
| Template | Description |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `basic` | Composant Python minimal |
|
|
54
|
+
| `hl7` | Composant HL7 v2.x (extraction de segments) |
|
|
55
|
+
| `database` | Composant avec accès base de données |
|
|
56
|
+
| `javascript` | Composant JavaScript / Node.js |
|
|
57
|
+
| `http` | Composant appel API REST |
|
|
58
|
+
|
|
59
|
+
## Écrire un composant
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from normacare_sdk import NormaCareComponent, ComponentInfo, run
|
|
63
|
+
|
|
64
|
+
class Component(NormaCareComponent):
|
|
65
|
+
info = ComponentInfo(type="mon_comp", label="Mon Composant", author="Moi")
|
|
66
|
+
|
|
67
|
+
def process(self, data, vars, params):
|
|
68
|
+
# self._config = options du composant (définies dans le .cmp / le Studio)
|
|
69
|
+
seuil = self.get_option(self._config, "seuil", default=10)
|
|
70
|
+
self.log("Traitement en cours")
|
|
71
|
+
return {"result": data, "seuil": seuil}
|
|
72
|
+
|
|
73
|
+
run(Component)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
À l'exécution, l'Engine injecte le contexte du flux :
|
|
77
|
+
|
|
78
|
+
| Variable | Contenu |
|
|
79
|
+
|---|---|
|
|
80
|
+
| `_inputs` | sorties des nœuds parents |
|
|
81
|
+
| `_vars` | variables globales du flux |
|
|
82
|
+
| `_params` | paramètres du job Engine |
|
|
83
|
+
| `_config` | options du composant (du manifest `.cmp`) |
|
|
84
|
+
|
|
85
|
+
## Tester
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from normacare_sdk.testing import ComponentTester
|
|
89
|
+
from mon_comp import Component
|
|
90
|
+
|
|
91
|
+
t = ComponentTester(Component)
|
|
92
|
+
t.run(data={"raw": "MSH|..."}, config={"seuil": 5})
|
|
93
|
+
t.assert_success()
|
|
94
|
+
t.assert_output_key("result")
|
|
95
|
+
t.print_report()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Structure d'un composant
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
mon_composant/
|
|
102
|
+
├── mon_composant.cmp Manifest JSON (métadonnées, options, prix)
|
|
103
|
+
├── mon_composant.py Script (votre logique)
|
|
104
|
+
├── test_mon_composant.py Tests locaux
|
|
105
|
+
└── README.md Documentation (affichée sur le Store)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Niveaux de protection
|
|
109
|
+
|
|
110
|
+
| Niveau | Protection |
|
|
111
|
+
|---|---|
|
|
112
|
+
| Open source | aucune |
|
|
113
|
+
| `"protected": true` | obfuscation PyArmor (`pip install normacare-sdk[protect]`) |
|
|
114
|
+
| Payant | jeton d'achat requis (abonnement Publisher) |
|
|
115
|
+
|
|
116
|
+
## Support
|
|
117
|
+
|
|
118
|
+
- Documentation : https://docs.normacare.app/sdk
|
|
119
|
+
- Store : https://store.normacare.app
|
|
120
|
+
- Email : sdk@fdevelopment.eu
|
|
121
|
+
|
|
122
|
+
© FDevelopment LTD
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# NormaCare SDK
|
|
2
|
+
|
|
3
|
+
Le SDK officiel pour développer, tester et publier des **composants** d'intégration
|
|
4
|
+
médicale, utilisables dans **NormaCare Studio** et exécutés par **NormaCare Engine**,
|
|
5
|
+
et distribuables sur le **NormaCare Store**.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install normacare-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Développement local (depuis ce dossier) :
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Démarrage rapide (5 minutes)
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
normacare-sdk new mon_composant_hl7 --template hl7
|
|
23
|
+
cd mon_composant_hl7
|
|
24
|
+
normacare-sdk test . # exécute test_mon_composant_hl7.py
|
|
25
|
+
normacare-sdk validate . # vérifie le manifest .cmp
|
|
26
|
+
normacare-sdk build . # produit mon_composant_hl7-1.0.0.ncpkg
|
|
27
|
+
normacare-sdk login # connexion au Store
|
|
28
|
+
normacare-sdk publish . --price 0
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Templates
|
|
32
|
+
|
|
33
|
+
| Template | Description |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `basic` | Composant Python minimal |
|
|
36
|
+
| `hl7` | Composant HL7 v2.x (extraction de segments) |
|
|
37
|
+
| `database` | Composant avec accès base de données |
|
|
38
|
+
| `javascript` | Composant JavaScript / Node.js |
|
|
39
|
+
| `http` | Composant appel API REST |
|
|
40
|
+
|
|
41
|
+
## Écrire un composant
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from normacare_sdk import NormaCareComponent, ComponentInfo, run
|
|
45
|
+
|
|
46
|
+
class Component(NormaCareComponent):
|
|
47
|
+
info = ComponentInfo(type="mon_comp", label="Mon Composant", author="Moi")
|
|
48
|
+
|
|
49
|
+
def process(self, data, vars, params):
|
|
50
|
+
# self._config = options du composant (définies dans le .cmp / le Studio)
|
|
51
|
+
seuil = self.get_option(self._config, "seuil", default=10)
|
|
52
|
+
self.log("Traitement en cours")
|
|
53
|
+
return {"result": data, "seuil": seuil}
|
|
54
|
+
|
|
55
|
+
run(Component)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
À l'exécution, l'Engine injecte le contexte du flux :
|
|
59
|
+
|
|
60
|
+
| Variable | Contenu |
|
|
61
|
+
|---|---|
|
|
62
|
+
| `_inputs` | sorties des nœuds parents |
|
|
63
|
+
| `_vars` | variables globales du flux |
|
|
64
|
+
| `_params` | paramètres du job Engine |
|
|
65
|
+
| `_config` | options du composant (du manifest `.cmp`) |
|
|
66
|
+
|
|
67
|
+
## Tester
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
from normacare_sdk.testing import ComponentTester
|
|
71
|
+
from mon_comp import Component
|
|
72
|
+
|
|
73
|
+
t = ComponentTester(Component)
|
|
74
|
+
t.run(data={"raw": "MSH|..."}, config={"seuil": 5})
|
|
75
|
+
t.assert_success()
|
|
76
|
+
t.assert_output_key("result")
|
|
77
|
+
t.print_report()
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Structure d'un composant
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
mon_composant/
|
|
84
|
+
├── mon_composant.cmp Manifest JSON (métadonnées, options, prix)
|
|
85
|
+
├── mon_composant.py Script (votre logique)
|
|
86
|
+
├── test_mon_composant.py Tests locaux
|
|
87
|
+
└── README.md Documentation (affichée sur le Store)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Niveaux de protection
|
|
91
|
+
|
|
92
|
+
| Niveau | Protection |
|
|
93
|
+
|---|---|
|
|
94
|
+
| Open source | aucune |
|
|
95
|
+
| `"protected": true` | obfuscation PyArmor (`pip install normacare-sdk[protect]`) |
|
|
96
|
+
| Payant | jeton d'achat requis (abonnement Publisher) |
|
|
97
|
+
|
|
98
|
+
## Support
|
|
99
|
+
|
|
100
|
+
- Documentation : https://docs.normacare.app/sdk
|
|
101
|
+
- Store : https://store.normacare.app
|
|
102
|
+
- Email : sdk@fdevelopment.eu
|
|
103
|
+
|
|
104
|
+
© FDevelopment LTD
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NormaCare SDK — paquet public.
|
|
3
|
+
|
|
4
|
+
Développez des composants d'intégration médicale réutilisables, testables
|
|
5
|
+
et publiables sur le NormaCare Store, exécutés par NormaCare Engine.
|
|
6
|
+
|
|
7
|
+
Usage minimal :
|
|
8
|
+
|
|
9
|
+
from normacare_sdk import NormaCareComponent, ComponentInfo, run
|
|
10
|
+
|
|
11
|
+
class MonComposant(NormaCareComponent):
|
|
12
|
+
info = ComponentInfo(type="mon_comp", label="Mon Composant", author="Moi")
|
|
13
|
+
|
|
14
|
+
def process(self, data, vars, params):
|
|
15
|
+
return {"processed": True, "data": data}
|
|
16
|
+
|
|
17
|
+
run(MonComposant)
|
|
18
|
+
|
|
19
|
+
FDevelopment LTD
|
|
20
|
+
"""
|
|
21
|
+
from .component import (
|
|
22
|
+
NormaCareComponent,
|
|
23
|
+
ComponentInfo,
|
|
24
|
+
ComponentError,
|
|
25
|
+
run,
|
|
26
|
+
)
|
|
27
|
+
from .testing import ComponentTester, TestResult
|
|
28
|
+
|
|
29
|
+
__version__ = "2.0.0"
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"NormaCareComponent",
|
|
33
|
+
"ComponentInfo",
|
|
34
|
+
"ComponentError",
|
|
35
|
+
"run",
|
|
36
|
+
"ComponentTester",
|
|
37
|
+
"TestResult",
|
|
38
|
+
"__version__",
|
|
39
|
+
]
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NormaCare SDK — CLI
|
|
3
|
+
===================
|
|
4
|
+
Outil en ligne de commande pour créer, tester, valider, packager et publier
|
|
5
|
+
des composants NormaCare.
|
|
6
|
+
|
|
7
|
+
Commandes:
|
|
8
|
+
normacare-sdk new <nom> [--template T] Créer un composant depuis un template
|
|
9
|
+
normacare-sdk validate [dossier] Valider un composant avant publication
|
|
10
|
+
normacare-sdk test [dossier] Exécuter les tests locaux (test_*.py)
|
|
11
|
+
normacare-sdk build [dossier] Packager en bundle .ncpkg
|
|
12
|
+
normacare-sdk login Se connecter au Store NormaCare
|
|
13
|
+
normacare-sdk publish [dossier] [--price P] Publier sur le Store
|
|
14
|
+
|
|
15
|
+
FDevelopment LTD
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
import os
|
|
19
|
+
import sys
|
|
20
|
+
import json
|
|
21
|
+
import shutil
|
|
22
|
+
import zipfile
|
|
23
|
+
import hashlib
|
|
24
|
+
import argparse
|
|
25
|
+
import subprocess
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
|
|
29
|
+
STORE_URL = os.getenv("NORMACARE_STORE_URL", "https://store.normacare.app")
|
|
30
|
+
try:
|
|
31
|
+
from . import __version__ as SDK_VERSION
|
|
32
|
+
except Exception: # exécution hors paquet
|
|
33
|
+
SDK_VERSION = "2.0.0"
|
|
34
|
+
CONFIG_FILE = Path.home() / ".normacare" / "credentials.json"
|
|
35
|
+
# Templates embarqués dans le paquet (inclus par pip via package-data).
|
|
36
|
+
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
|
37
|
+
|
|
38
|
+
TEMPLATES = {
|
|
39
|
+
"basic": "Composant Python minimal — point de départ universel",
|
|
40
|
+
"hl7": "Composant spécialisé HL7 v2.x (segments MSH/PID/OBX…)",
|
|
41
|
+
"database": "Composant avec accès base de données",
|
|
42
|
+
"javascript": "Composant JavaScript / Node.js",
|
|
43
|
+
"http": "Composant appel API REST",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
def _ok(msg): print(f" ✓ {msg}")
|
|
49
|
+
def _info(msg): print(f" ℹ {msg}")
|
|
50
|
+
def _warn(msg): print(f" ⚠ {msg}")
|
|
51
|
+
def _err(msg): print(f" ✕ {msg}", file=sys.stderr)
|
|
52
|
+
|
|
53
|
+
def _load_credentials():
|
|
54
|
+
if CONFIG_FILE.exists():
|
|
55
|
+
try:
|
|
56
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
57
|
+
except Exception:
|
|
58
|
+
return None
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
def _save_credentials(data):
|
|
62
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
CONFIG_FILE.write_text(json.dumps(data, indent=2))
|
|
64
|
+
|
|
65
|
+
def _load_manifest(folder: Path) -> dict:
|
|
66
|
+
cmp_files = list(folder.glob("*.cmp"))
|
|
67
|
+
if not cmp_files:
|
|
68
|
+
_err(f"Aucun fichier .cmp trouvé dans {folder}")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
return json.loads(cmp_files[0].read_text(encoding="utf-8"))
|
|
71
|
+
|
|
72
|
+
def _slug(name: str) -> str:
|
|
73
|
+
return name.strip().lower().replace(" ", "_").replace("-", "_")
|
|
74
|
+
|
|
75
|
+
# ── Commande : new ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
def cmd_new(args):
|
|
78
|
+
template = args.template or "basic"
|
|
79
|
+
name = _slug(args.name)
|
|
80
|
+
target = Path(name)
|
|
81
|
+
|
|
82
|
+
if target.exists():
|
|
83
|
+
_err(f"Le dossier '{name}' existe déjà.")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
|
|
86
|
+
tpl_dir = TEMPLATES_DIR / template
|
|
87
|
+
if not tpl_dir.exists():
|
|
88
|
+
_err(f"Template '{template}' introuvable. Disponibles : {list(TEMPLATES)}")
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
shutil.copytree(tpl_dir, target)
|
|
92
|
+
|
|
93
|
+
# Le template utilise le radical « component » ; on le remplace par <name>.
|
|
94
|
+
is_js = template == "javascript"
|
|
95
|
+
old_stem = "component"
|
|
96
|
+
ext = ".js" if is_js else ".py"
|
|
97
|
+
|
|
98
|
+
# Renomme le script principal.
|
|
99
|
+
old_script = target / f"{old_stem}{ext}"
|
|
100
|
+
new_script = target / f"{name}{ext}"
|
|
101
|
+
if old_script.exists():
|
|
102
|
+
old_script.rename(new_script)
|
|
103
|
+
|
|
104
|
+
# Renomme et réécrit le manifest.
|
|
105
|
+
old_cmp = target / f"{old_stem}.cmp"
|
|
106
|
+
manifest = json.loads(old_cmp.read_text(encoding="utf-8"))
|
|
107
|
+
manifest["type"] = name
|
|
108
|
+
manifest["label"] = name.replace("_", " ").title()
|
|
109
|
+
manifest["script"] = f"{name}{ext}"
|
|
110
|
+
new_cmp = target / f"{name}.cmp"
|
|
111
|
+
new_cmp.write_text(json.dumps(manifest, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
112
|
+
if old_cmp.exists() and old_cmp != new_cmp:
|
|
113
|
+
old_cmp.unlink()
|
|
114
|
+
|
|
115
|
+
# Renomme le test et corrige son import (from component import ... → from <name>).
|
|
116
|
+
old_test = target / f"test_{old_stem}.py"
|
|
117
|
+
if old_test.exists():
|
|
118
|
+
txt = old_test.read_text(encoding="utf-8").replace(
|
|
119
|
+
f"from {old_stem} import", f"from {name} import"
|
|
120
|
+
).replace(f"import {old_stem}\n", f"import {name}\n")
|
|
121
|
+
new_test = target / f"test_{name}.py"
|
|
122
|
+
new_test.write_text(txt, encoding="utf-8")
|
|
123
|
+
if old_test != new_test:
|
|
124
|
+
old_test.unlink()
|
|
125
|
+
|
|
126
|
+
print(f"\n NormaCare SDK — composant créé : {name}/")
|
|
127
|
+
print(f" Template : {TEMPLATES.get(template, template)}\n")
|
|
128
|
+
print(" Structure :")
|
|
129
|
+
for f in sorted(target.rglob("*")):
|
|
130
|
+
if f.is_file():
|
|
131
|
+
print(f" {f.relative_to(target)}")
|
|
132
|
+
print("\n Prochaines étapes :")
|
|
133
|
+
print(f" 1. cd {name}")
|
|
134
|
+
print(f" 2. Éditez {name}{ext}")
|
|
135
|
+
print(" 3. normacare-sdk test .")
|
|
136
|
+
print(" 4. normacare-sdk build . (ou : publish . --price 0)\n")
|
|
137
|
+
|
|
138
|
+
# ── Commande : validate ───────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
def cmd_validate(args):
|
|
141
|
+
folder = Path(args.folder)
|
|
142
|
+
print(f"\n Validation de : {folder.resolve()}\n")
|
|
143
|
+
errors, warnings = [], []
|
|
144
|
+
|
|
145
|
+
manifest = _load_manifest(folder)
|
|
146
|
+
|
|
147
|
+
for field in ("type", "label", "version", "runtime", "script"):
|
|
148
|
+
if not manifest.get(field):
|
|
149
|
+
errors.append(f"Champ requis manquant : '{field}'")
|
|
150
|
+
if not manifest.get("author"):
|
|
151
|
+
warnings.append("Champ 'author' vide (requis pour publier sur le Store)")
|
|
152
|
+
|
|
153
|
+
if manifest.get("runtime") not in {"python", "javascript", "lua", "java"}:
|
|
154
|
+
errors.append(f"Runtime invalide : {manifest.get('runtime')!r} (python|javascript|lua|java)")
|
|
155
|
+
|
|
156
|
+
script_path = folder / manifest.get("script", "")
|
|
157
|
+
if manifest.get("script") and not script_path.exists():
|
|
158
|
+
errors.append(f"Script '{manifest.get('script')}' introuvable")
|
|
159
|
+
|
|
160
|
+
v = str(manifest.get("version", ""))
|
|
161
|
+
if not v or not all(p.isdigit() for p in v.split(".")):
|
|
162
|
+
warnings.append(f"Version '{v}' non standard (attendu X.Y.Z)")
|
|
163
|
+
|
|
164
|
+
if float(manifest.get("price", 0) or 0) < 0:
|
|
165
|
+
errors.append("Le prix ne peut pas être négatif")
|
|
166
|
+
|
|
167
|
+
if not (folder / "README.md").exists():
|
|
168
|
+
warnings.append("README.md manquant (recommandé pour le Store)")
|
|
169
|
+
|
|
170
|
+
if not (list(folder.glob("test_*.py")) + list(folder.glob("tests/*.py"))):
|
|
171
|
+
warnings.append("Aucun test trouvé (recommandé)")
|
|
172
|
+
|
|
173
|
+
for e in errors: _err(e)
|
|
174
|
+
for w in warnings: _warn(w)
|
|
175
|
+
|
|
176
|
+
if not errors:
|
|
177
|
+
_ok(f"Validation réussie — {manifest['label']} v{manifest.get('version','?')}")
|
|
178
|
+
if warnings:
|
|
179
|
+
print(f"\n {len(warnings)} avertissement(s) non bloquant(s)\n")
|
|
180
|
+
return True
|
|
181
|
+
print(f"\n {len(errors)} erreur(s) — corrigez avant de publier\n")
|
|
182
|
+
sys.exit(1)
|
|
183
|
+
|
|
184
|
+
# ── Commande : test ────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
def cmd_test(args):
|
|
187
|
+
folder = Path(args.folder)
|
|
188
|
+
tests = sorted(folder.glob("test_*.py")) + sorted(folder.glob("tests/test_*.py"))
|
|
189
|
+
if not tests:
|
|
190
|
+
_warn("Aucun test (test_*.py) trouvé.")
|
|
191
|
+
return
|
|
192
|
+
print(f"\n Tests de : {folder.resolve()}\n")
|
|
193
|
+
all_ok = True
|
|
194
|
+
for t in tests:
|
|
195
|
+
_info(f"→ {t.name}")
|
|
196
|
+
# cwd = dossier du composant pour que « from <name> import … » fonctionne.
|
|
197
|
+
r = subprocess.run([sys.executable, t.name], cwd=str(folder))
|
|
198
|
+
if r.returncode != 0:
|
|
199
|
+
all_ok = False
|
|
200
|
+
print()
|
|
201
|
+
if all_ok:
|
|
202
|
+
_ok("Tous les tests sont passés.")
|
|
203
|
+
else:
|
|
204
|
+
_err("Des tests ont échoué.")
|
|
205
|
+
sys.exit(1)
|
|
206
|
+
|
|
207
|
+
# ── Commande : build ───────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
def cmd_build(args):
|
|
210
|
+
folder = Path(args.folder)
|
|
211
|
+
manifest = _load_manifest(folder)
|
|
212
|
+
ctype = manifest["type"]
|
|
213
|
+
version = manifest.get("version", "1.0.0")
|
|
214
|
+
|
|
215
|
+
cmd_validate(argparse.Namespace(folder=str(folder)))
|
|
216
|
+
|
|
217
|
+
script_path = folder / manifest["script"]
|
|
218
|
+
script_code = script_path.read_text(encoding="utf-8")
|
|
219
|
+
|
|
220
|
+
if manifest.get("protected") and not getattr(args, "skip_protect", False):
|
|
221
|
+
try:
|
|
222
|
+
import pyarmor # noqa: F401
|
|
223
|
+
_info("Obfuscation PyArmor du script…")
|
|
224
|
+
result = subprocess.run(
|
|
225
|
+
["pyarmor", "gen", "--output", str(folder / "_dist"), str(script_path)],
|
|
226
|
+
capture_output=True, text=True,
|
|
227
|
+
)
|
|
228
|
+
_ok("Script obfusqué") if result.returncode == 0 else _warn("PyArmor indisponible — script non obfusqué")
|
|
229
|
+
except ImportError:
|
|
230
|
+
_warn("PyArmor non installé (pip install pyarmor) — script non obfusqué")
|
|
231
|
+
|
|
232
|
+
manifest["sha256"] = hashlib.sha256(script_code.encode()).hexdigest()
|
|
233
|
+
manifest["built_at"] = datetime.now(timezone.utc).isoformat()
|
|
234
|
+
|
|
235
|
+
out_name = f"{ctype}-{version}.ncpkg"
|
|
236
|
+
out_path = folder.parent / out_name
|
|
237
|
+
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
238
|
+
zf.writestr(f"{ctype}.cmp", json.dumps(manifest, indent=2, ensure_ascii=False))
|
|
239
|
+
zf.write(script_path, manifest["script"])
|
|
240
|
+
for opt in ("README.md", "CHANGELOG.md", "icon.png", "icon.svg"):
|
|
241
|
+
p = folder / opt
|
|
242
|
+
if p.exists():
|
|
243
|
+
zf.write(p, opt)
|
|
244
|
+
samples_dir = folder / "samples"
|
|
245
|
+
if samples_dir.exists():
|
|
246
|
+
for f in samples_dir.rglob("*"):
|
|
247
|
+
if f.is_file():
|
|
248
|
+
zf.write(f, f"samples/{f.relative_to(samples_dir)}")
|
|
249
|
+
|
|
250
|
+
size_kb = out_path.stat().st_size // 1024
|
|
251
|
+
_ok(f"Bundle créé : {out_path.name} ({size_kb} Ko)")
|
|
252
|
+
print(f" → {out_path.resolve()}\n")
|
|
253
|
+
return out_path
|
|
254
|
+
|
|
255
|
+
# ── Commande : login ───────────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
def cmd_login(args):
|
|
258
|
+
import getpass
|
|
259
|
+
print("\n Connexion au NormaCare Store\n")
|
|
260
|
+
email = input(" Email : ").strip()
|
|
261
|
+
try:
|
|
262
|
+
password = getpass.getpass(" Mot de passe : ")
|
|
263
|
+
except Exception:
|
|
264
|
+
password = input(" Mot de passe : ").strip()
|
|
265
|
+
|
|
266
|
+
import urllib.request
|
|
267
|
+
try:
|
|
268
|
+
req = urllib.request.Request(
|
|
269
|
+
f"{STORE_URL}/api/auth/login",
|
|
270
|
+
data=json.dumps({"email": email, "password": password}).encode(),
|
|
271
|
+
headers={"Content-Type": "application/json"},
|
|
272
|
+
method="POST",
|
|
273
|
+
)
|
|
274
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
275
|
+
data = json.loads(r.read())
|
|
276
|
+
_save_credentials({"email": email, "token": data["token"],
|
|
277
|
+
"publisher_id": data.get("publisher_id")})
|
|
278
|
+
_ok(f"Connecté en tant que {email}")
|
|
279
|
+
_info(f"Plan : {data.get('plan', '?')}")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
_err(f"Erreur de connexion : {e}")
|
|
282
|
+
sys.exit(1)
|
|
283
|
+
|
|
284
|
+
# ── Commande : publish ─────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
def cmd_publish(args):
|
|
287
|
+
folder = Path(args.folder)
|
|
288
|
+
manifest = _load_manifest(folder)
|
|
289
|
+
creds = _load_credentials()
|
|
290
|
+
|
|
291
|
+
if not creds:
|
|
292
|
+
_err("Non connecté. Lancez d'abord : normacare-sdk login")
|
|
293
|
+
sys.exit(1)
|
|
294
|
+
|
|
295
|
+
if args.price is not None:
|
|
296
|
+
manifest["price"] = float(args.price)
|
|
297
|
+
if args.visibility:
|
|
298
|
+
manifest["protected"] = args.visibility == "paid"
|
|
299
|
+
|
|
300
|
+
if float(manifest.get("price", 0) or 0) > 0:
|
|
301
|
+
cur = manifest.get("currency", "EUR")
|
|
302
|
+
print(f"\n Publication payante : {manifest['label']} à {manifest['price']} {cur}")
|
|
303
|
+
print(" Commission FDevelopment LTD : 25%")
|
|
304
|
+
print(f" Revenu net : {manifest['price'] * 0.75:.2f} {cur} / vente")
|
|
305
|
+
if input("\n Confirmer ? (oui/non) : ").strip().lower() != "oui":
|
|
306
|
+
_info("Publication annulée.")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
bundle_path = cmd_build(argparse.Namespace(folder=str(folder), skip_protect=False))
|
|
310
|
+
|
|
311
|
+
_info(f"Publication sur {STORE_URL}…")
|
|
312
|
+
import urllib.request
|
|
313
|
+
try:
|
|
314
|
+
bundle_data = Path(bundle_path).read_bytes()
|
|
315
|
+
metadata = json.dumps({"manifest": manifest,
|
|
316
|
+
"publisher_id": creds.get("publisher_id")}).encode()
|
|
317
|
+
boundary = "NormaCareBoundary"
|
|
318
|
+
body = f"--{boundary}\r\nContent-Disposition: form-data; name=\"metadata\"\r\n\r\n".encode()
|
|
319
|
+
body += metadata
|
|
320
|
+
body += (f"\r\n--{boundary}\r\nContent-Disposition: form-data; name=\"bundle\"; "
|
|
321
|
+
f"filename=\"{bundle_path.name}\"\r\nContent-Type: application/octet-stream\r\n\r\n").encode()
|
|
322
|
+
body += bundle_data
|
|
323
|
+
body += f"\r\n--{boundary}--\r\n".encode()
|
|
324
|
+
|
|
325
|
+
req = urllib.request.Request(
|
|
326
|
+
f"{STORE_URL}/api/components/publish",
|
|
327
|
+
data=body,
|
|
328
|
+
headers={"Authorization": f"Bearer {creds['token']}",
|
|
329
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}"},
|
|
330
|
+
method="POST",
|
|
331
|
+
)
|
|
332
|
+
with urllib.request.urlopen(req, timeout=60) as r:
|
|
333
|
+
result = json.loads(r.read())
|
|
334
|
+
_ok("Composant publié !")
|
|
335
|
+
_info(f"URL Store : {STORE_URL}/components/{manifest['type']}")
|
|
336
|
+
_info(f"ID : {result.get('id')}")
|
|
337
|
+
_info(f"Statut : {result.get('status', 'en révision')}")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
_err(f"Erreur de publication : {e}")
|
|
340
|
+
_info(f"Le bundle local est conservé : {bundle_path}")
|
|
341
|
+
sys.exit(1)
|
|
342
|
+
|
|
343
|
+
# ── CLI main ──────────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
def build_parser():
|
|
346
|
+
parser = argparse.ArgumentParser(
|
|
347
|
+
prog="normacare-sdk",
|
|
348
|
+
description=f"NormaCare SDK v{SDK_VERSION} — Créez et publiez des composants",
|
|
349
|
+
)
|
|
350
|
+
parser.add_argument("--version", action="version", version=f"normacare-sdk {SDK_VERSION}")
|
|
351
|
+
sub = parser.add_subparsers(dest="cmd", metavar="commande")
|
|
352
|
+
|
|
353
|
+
p_new = sub.add_parser("new", help="Créer un composant depuis un template")
|
|
354
|
+
p_new.add_argument("name", help="Nom du composant (snake_case)")
|
|
355
|
+
p_new.add_argument("--template", "-t", default="basic", choices=list(TEMPLATES),
|
|
356
|
+
help="Template à utiliser")
|
|
357
|
+
|
|
358
|
+
p_val = sub.add_parser("validate", help="Valider un composant")
|
|
359
|
+
p_val.add_argument("folder", nargs="?", default=".", help="Dossier du composant")
|
|
360
|
+
|
|
361
|
+
p_tst = sub.add_parser("test", help="Exécuter les tests locaux (test_*.py)")
|
|
362
|
+
p_tst.add_argument("folder", nargs="?", default=".", help="Dossier du composant")
|
|
363
|
+
|
|
364
|
+
p_bld = sub.add_parser("build", help="Packager le composant (.ncpkg)")
|
|
365
|
+
p_bld.add_argument("folder", nargs="?", default=".", help="Dossier du composant")
|
|
366
|
+
p_bld.add_argument("--skip-protect", action="store_true")
|
|
367
|
+
|
|
368
|
+
sub.add_parser("login", help="Se connecter au Store NormaCare")
|
|
369
|
+
|
|
370
|
+
p_pub = sub.add_parser("publish", help="Publier sur le Store NormaCare")
|
|
371
|
+
p_pub.add_argument("folder", nargs="?", default=".", help="Dossier du composant")
|
|
372
|
+
p_pub.add_argument("--price", type=float, default=None, help="Prix en EUR (0 = gratuit)")
|
|
373
|
+
p_pub.add_argument("--visibility", choices=["public", "paid"], default=None)
|
|
374
|
+
return parser
|
|
375
|
+
|
|
376
|
+
def main(argv=None):
|
|
377
|
+
parser = build_parser()
|
|
378
|
+
args = parser.parse_args(argv)
|
|
379
|
+
cmds = {
|
|
380
|
+
"new": cmd_new, "validate": cmd_validate, "test": cmd_test,
|
|
381
|
+
"build": cmd_build, "login": cmd_login, "publish": cmd_publish,
|
|
382
|
+
}
|
|
383
|
+
if args.cmd in cmds:
|
|
384
|
+
cmds[args.cmd](args)
|
|
385
|
+
else:
|
|
386
|
+
parser.print_help()
|
|
387
|
+
print("\n Templates disponibles :")
|
|
388
|
+
for k, v in TEMPLATES.items():
|
|
389
|
+
print(f" {k:<12} {v}")
|
|
390
|
+
print()
|
|
391
|
+
|
|
392
|
+
if __name__ == "__main__":
|
|
393
|
+
main()
|