forge-mvc-stats 1.0.0b7__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,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: forge-mvc-stats
3
+ Version: 1.0.0b7
4
+ Summary: Forge stats — événements génériques, schéma SQL, tracking et consultation.
5
+ Author: Roger Cauchon
6
+ License-Expression: LicenseRef-Forge-Proprietary
7
+ Project-URL: Homepage, https://github.com/caucrogeGit/Forge
8
+ Project-URL: Repository, https://github.com/caucrogeGit/Forge
9
+ Project-URL: Documentation, https://caucrogegit.github.io/Forge/
10
+ Keywords: python,mvc,forge,stats,statistiques
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: forge-mvc<2,>=1.0.0b5
19
+
20
+ # forge-mvc-stats
21
+
22
+ Module stats pour Forge — événements génériques, schéma SQL, tracking et consultation.
23
+
24
+ Extrait du core Forge depuis la version 2.8.0 (ADR-004).
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install forge-mvc-stats
30
+ # ou en mode developpement
31
+ pip install -e packages/forge-mvc-stats/
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from forge_mvc_stats import make_event, track_event
38
+
39
+ # Créer un événement
40
+ event = make_event(
41
+ "page_view",
42
+ label="Vue de page",
43
+ category="traffic",
44
+ metadata={"path": "/contact"},
45
+ )
46
+
47
+ # Enregistrer dans la base (db.execute est fourni par l'application)
48
+ track_event(db.execute, event)
49
+
50
+ # Raccourci direct par nom
51
+ track_event(db.execute, "contact_click", label="Clic contact")
52
+ ```
53
+
54
+ ## Consultation
55
+
56
+ ```python
57
+ from forge_mvc_stats import list_stats_events
58
+
59
+ # Lister les 50 derniers événements (fetch_all est fourni par l'application)
60
+ events = list_stats_events(my_fetch_all)
61
+
62
+ # Filtrer par nom ou catégorie
63
+ page_views = list_stats_events(my_fetch_all, name="page_view")
64
+ traffic = list_stats_events(my_fetch_all, category="traffic", limit=100)
65
+ ```
66
+
67
+ ## Schéma SQL
68
+
69
+ ```python
70
+ from forge_mvc_stats import get_stats_events_schema_sql, STATS_EVENTS_TABLE
71
+
72
+ print(STATS_EVENTS_TABLE) # "forge_stats_events"
73
+ sql = get_stats_events_schema_sql() # CREATE TABLE IF NOT EXISTS forge_stats_events (...)
74
+ ```
75
+
76
+ ## Cas d'usage
77
+
78
+ - Comptage de visites de pages
79
+ - Suivi de clics sur des liens ou boutons
80
+ - Mesure de soumissions de formulaires
81
+ - Traçage d'événements métier applicatifs
82
+
83
+ ## API publique
84
+
85
+ - `StatsEvent` — dataclass d'événement (name, label, category, metadata)
86
+ - `StatsEventError` — exception de validation
87
+ - `make_event(name, label, category, metadata)` — crée et valide un événement
88
+ - `validate_event(event)` — valide un événement existant
89
+ - `normalize_event_name(value)` — normalise en snake_case
90
+ - `validate_event_name(value)` — valide un nom normalisé
91
+ - `STATS_EVENTS_TABLE` — nom de la table SQL (`forge_stats_events`)
92
+ - `STATS_EVENTS_COLUMNS` — tuple des colonnes
93
+ - `get_stats_events_schema_sql()` — SQL `CREATE TABLE IF NOT EXISTS`
94
+ - `track_event(execute, event_or_name, ...)` — enregistre un événement en base
95
+ - `get_track_event_sql()` — SQL `INSERT` paramétré
96
+ - `prepare_track_event_values(event)` — tuple de paramètres prêts pour `execute`
97
+ - `list_stats_events(fetch_all, name, category, limit)` — liste des événements normalisés
98
+ - `get_stats_events_admin_sql(name, category, limit)` — SQL `SELECT` filtré
99
+ - `prepare_stats_events_admin_params(name, category, limit)` — paramètres du SELECT
100
+ - `normalize_stats_event_row(row)` — normalise une ligne brute (metadata JSON → dict)
101
+ - `StatsAdminError` — exception de consultation
@@ -0,0 +1,82 @@
1
+ # forge-mvc-stats
2
+
3
+ Module stats pour Forge — événements génériques, schéma SQL, tracking et consultation.
4
+
5
+ Extrait du core Forge depuis la version 2.8.0 (ADR-004).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ pip install forge-mvc-stats
11
+ # ou en mode developpement
12
+ pip install -e packages/forge-mvc-stats/
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```python
18
+ from forge_mvc_stats import make_event, track_event
19
+
20
+ # Créer un événement
21
+ event = make_event(
22
+ "page_view",
23
+ label="Vue de page",
24
+ category="traffic",
25
+ metadata={"path": "/contact"},
26
+ )
27
+
28
+ # Enregistrer dans la base (db.execute est fourni par l'application)
29
+ track_event(db.execute, event)
30
+
31
+ # Raccourci direct par nom
32
+ track_event(db.execute, "contact_click", label="Clic contact")
33
+ ```
34
+
35
+ ## Consultation
36
+
37
+ ```python
38
+ from forge_mvc_stats import list_stats_events
39
+
40
+ # Lister les 50 derniers événements (fetch_all est fourni par l'application)
41
+ events = list_stats_events(my_fetch_all)
42
+
43
+ # Filtrer par nom ou catégorie
44
+ page_views = list_stats_events(my_fetch_all, name="page_view")
45
+ traffic = list_stats_events(my_fetch_all, category="traffic", limit=100)
46
+ ```
47
+
48
+ ## Schéma SQL
49
+
50
+ ```python
51
+ from forge_mvc_stats import get_stats_events_schema_sql, STATS_EVENTS_TABLE
52
+
53
+ print(STATS_EVENTS_TABLE) # "forge_stats_events"
54
+ sql = get_stats_events_schema_sql() # CREATE TABLE IF NOT EXISTS forge_stats_events (...)
55
+ ```
56
+
57
+ ## Cas d'usage
58
+
59
+ - Comptage de visites de pages
60
+ - Suivi de clics sur des liens ou boutons
61
+ - Mesure de soumissions de formulaires
62
+ - Traçage d'événements métier applicatifs
63
+
64
+ ## API publique
65
+
66
+ - `StatsEvent` — dataclass d'événement (name, label, category, metadata)
67
+ - `StatsEventError` — exception de validation
68
+ - `make_event(name, label, category, metadata)` — crée et valide un événement
69
+ - `validate_event(event)` — valide un événement existant
70
+ - `normalize_event_name(value)` — normalise en snake_case
71
+ - `validate_event_name(value)` — valide un nom normalisé
72
+ - `STATS_EVENTS_TABLE` — nom de la table SQL (`forge_stats_events`)
73
+ - `STATS_EVENTS_COLUMNS` — tuple des colonnes
74
+ - `get_stats_events_schema_sql()` — SQL `CREATE TABLE IF NOT EXISTS`
75
+ - `track_event(execute, event_or_name, ...)` — enregistre un événement en base
76
+ - `get_track_event_sql()` — SQL `INSERT` paramétré
77
+ - `prepare_track_event_values(event)` — tuple de paramètres prêts pour `execute`
78
+ - `list_stats_events(fetch_all, name, category, limit)` — liste des événements normalisés
79
+ - `get_stats_events_admin_sql(name, category, limit)` — SQL `SELECT` filtré
80
+ - `prepare_stats_events_admin_params(name, category, limit)` — paramètres du SELECT
81
+ - `normalize_stats_event_row(row)` — normalise une ligne brute (metadata JSON → dict)
82
+ - `StatsAdminError` — exception de consultation
@@ -0,0 +1,51 @@
1
+ """Forge stats — événements génériques, schéma SQL, tracking et consultation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from forge_mvc_stats.admin import (
6
+ StatsAdminError,
7
+ get_stats_events_admin_sql,
8
+ list_stats_events,
9
+ normalize_stats_event_row,
10
+ prepare_stats_events_admin_params,
11
+ )
12
+ from forge_mvc_stats.events import (
13
+ StatsEvent,
14
+ StatsEventError,
15
+ make_event,
16
+ normalize_event_name,
17
+ validate_event,
18
+ validate_event_name,
19
+ )
20
+ from forge_mvc_stats.schema import (
21
+ STATS_EVENTS_COLUMNS,
22
+ STATS_EVENTS_TABLE,
23
+ get_stats_events_schema_sql,
24
+ )
25
+ from forge_mvc_stats.tracking import (
26
+ get_track_event_sql,
27
+ prepare_track_event_values,
28
+ track_event,
29
+ )
30
+
31
+ __version__ = "1.0.0b7"
32
+
33
+ __all__ = [
34
+ "StatsEvent",
35
+ "StatsEventError",
36
+ "normalize_event_name",
37
+ "validate_event_name",
38
+ "make_event",
39
+ "validate_event",
40
+ "STATS_EVENTS_TABLE",
41
+ "STATS_EVENTS_COLUMNS",
42
+ "get_stats_events_schema_sql",
43
+ "get_track_event_sql",
44
+ "prepare_track_event_values",
45
+ "track_event",
46
+ "StatsAdminError",
47
+ "get_stats_events_admin_sql",
48
+ "prepare_stats_events_admin_params",
49
+ "normalize_stats_event_row",
50
+ "list_stats_events",
51
+ ]
@@ -0,0 +1,139 @@
1
+ """API de consultation admin simple des événements statistiques Forge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Callable, Iterable
7
+
8
+ from .events import StatsEventError, validate_event_name
9
+ from .schema import STATS_EVENTS_TABLE
10
+
11
+ _DEFAULT_LIMIT = 50
12
+ _MAX_LIMIT = 500
13
+ _REQUIRED_COLUMNS = ("id", "name", "label", "category", "metadata", "created_at")
14
+
15
+
16
+ class StatsAdminError(ValueError):
17
+ pass
18
+
19
+
20
+ def _validate_limit(limit: int) -> int:
21
+ if not isinstance(limit, int) or isinstance(limit, bool):
22
+ raise StatsAdminError(
23
+ f"limit doit être un entier, reçu : {type(limit).__name__}."
24
+ )
25
+ if limit < 1:
26
+ raise StatsAdminError(f"limit doit être ≥ 1, reçu : {limit}.")
27
+ return min(limit, _MAX_LIMIT)
28
+
29
+
30
+ def get_stats_events_admin_sql(
31
+ name: str | None = None,
32
+ category: str | None = None,
33
+ limit: int = _DEFAULT_LIMIT,
34
+ ) -> str:
35
+ """Return the SELECT SQL for listing stats events with optional filters."""
36
+ parts = [
37
+ f"SELECT id, name, label, category, metadata, created_at"
38
+ f" FROM {STATS_EVENTS_TABLE}"
39
+ f" WHERE 1 = 1",
40
+ ]
41
+ if name is not None:
42
+ parts.append(" AND name = ?")
43
+ if category is not None:
44
+ parts.append(" AND category = ?")
45
+ parts.append(" ORDER BY created_at DESC, id DESC")
46
+ parts.append(" LIMIT ?")
47
+ return "".join(parts)
48
+
49
+
50
+ def prepare_stats_events_admin_params(
51
+ name: str | None = None,
52
+ category: str | None = None,
53
+ limit: int = _DEFAULT_LIMIT,
54
+ ) -> tuple:
55
+ """Return the SQL parameter tuple for a stats events query."""
56
+ if name is not None:
57
+ try:
58
+ name = validate_event_name(name)
59
+ except StatsEventError as exc:
60
+ raise StatsAdminError(str(exc)) from exc
61
+ if category is not None:
62
+ if not isinstance(category, str) or not category.strip():
63
+ raise StatsAdminError(
64
+ "category doit être une chaîne non vide."
65
+ )
66
+ category = category.strip()
67
+ effective_limit = _validate_limit(limit)
68
+ params: list[Any] = []
69
+ if name is not None:
70
+ params.append(name)
71
+ if category is not None:
72
+ params.append(category)
73
+ params.append(effective_limit)
74
+ return tuple(params)
75
+
76
+
77
+ def normalize_stats_event_row(row: dict[str, Any]) -> dict[str, Any]:
78
+ """Normalize a raw DB row into a clean stats event dict.
79
+
80
+ metadata is deserialized from JSON string to dict.
81
+ Missing required columns or invalid JSON raises StatsAdminError.
82
+ """
83
+ if not isinstance(row, dict):
84
+ raise StatsAdminError(
85
+ f"Une ligne dict est attendue, reçu : {type(row).__name__}."
86
+ )
87
+ missing = [col for col in _REQUIRED_COLUMNS if col not in row]
88
+ if missing:
89
+ raise StatsAdminError(
90
+ f"Colonnes manquantes dans la ligne : {', '.join(missing)}."
91
+ )
92
+ raw_metadata = row["metadata"]
93
+ if raw_metadata is None or raw_metadata == "":
94
+ metadata: dict[str, Any] = {}
95
+ elif isinstance(raw_metadata, dict):
96
+ metadata = raw_metadata
97
+ elif isinstance(raw_metadata, str):
98
+ try:
99
+ parsed = json.loads(raw_metadata)
100
+ except json.JSONDecodeError as exc:
101
+ raise StatsAdminError(
102
+ f"metadata JSON invalide : {exc}"
103
+ ) from exc
104
+ if not isinstance(parsed, dict):
105
+ raise StatsAdminError(
106
+ "metadata JSON doit représenter un objet, "
107
+ f"reçu : {type(parsed).__name__}."
108
+ )
109
+ metadata = parsed
110
+ else:
111
+ raise StatsAdminError(
112
+ f"metadata doit être une chaîne JSON ou None, "
113
+ f"reçu : {type(raw_metadata).__name__}."
114
+ )
115
+ return {
116
+ "id": row["id"],
117
+ "name": row["name"],
118
+ "label": row["label"],
119
+ "category": row["category"],
120
+ "metadata": metadata,
121
+ "created_at": row["created_at"],
122
+ }
123
+
124
+
125
+ def list_stats_events(
126
+ fetch_all: Callable[[str, tuple], Iterable[dict]],
127
+ name: str | None = None,
128
+ category: str | None = None,
129
+ limit: int = _DEFAULT_LIMIT,
130
+ ) -> list[dict[str, Any]]:
131
+ """List stats events from the database using the provided fetch_all executor.
132
+
133
+ Returns a list of normalized dicts. The caller must supply fetch_all —
134
+ Forge never accesses the database automatically.
135
+ """
136
+ sql = get_stats_events_admin_sql(name=name, category=category, limit=limit)
137
+ params = prepare_stats_events_admin_params(name=name, category=category, limit=limit)
138
+ rows = fetch_all(sql, params)
139
+ return [normalize_stats_event_row(row) for row in rows]
@@ -0,0 +1,106 @@
1
+ """Événements statistiques génériques pour Forge.
2
+
3
+ Les noms d'événements sont de simples chaînes snake_case définies par l'application.
4
+ Forge ne préconise aucune liste de noms — ce principe est conforme à la Charte Forge
5
+ (Principe 1 : le framework n'est pas l'application).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ from dataclasses import dataclass, field
12
+ from typing import Any
13
+
14
+
15
+ _VALID_NAME_RE = re.compile(r"^[a-z][a-z0-9_]*$")
16
+ _SAFE_NORMALIZE_RE = re.compile(r"[\s\-]+")
17
+ _UNSAFE_CHARS_RE = re.compile(r"[^a-z0-9_\s\-]")
18
+
19
+
20
+ class StatsEventError(ValueError):
21
+ pass
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class StatsEvent:
26
+ name: str
27
+ label: str = ""
28
+ category: str = "general"
29
+ metadata: dict[str, Any] = field(default_factory=dict)
30
+
31
+ def __post_init__(self) -> None:
32
+ validated_name = validate_event_name(self.name)
33
+ object.__setattr__(self, "name", validated_name)
34
+ if not self.label:
35
+ object.__setattr__(self, "label", validated_name)
36
+ if not self.category:
37
+ object.__setattr__(self, "category", "general")
38
+ if self.metadata is None:
39
+ object.__setattr__(self, "metadata", {})
40
+ if not isinstance(self.metadata, dict):
41
+ raise StatsEventError(
42
+ f"metadata doit être un dictionnaire, reçu : {type(self.metadata).__name__}."
43
+ )
44
+
45
+
46
+ def normalize_event_name(value: str) -> str:
47
+ """Convert a raw string to a valid snake_case event name.
48
+
49
+ Spaces and hyphens are converted to underscores.
50
+ Any other non-alphanumeric character raises StatsEventError.
51
+ """
52
+ if not isinstance(value, str):
53
+ raise StatsEventError("Le nom d'événement doit être une chaîne.")
54
+ lowered = value.strip().lower()
55
+ if _UNSAFE_CHARS_RE.search(lowered):
56
+ raise StatsEventError(
57
+ f"Le nom d'événement '{value}' contient des caractères non autorisés. "
58
+ "Utilisez uniquement des lettres, des chiffres, des espaces et des tirets."
59
+ )
60
+ normalized = _SAFE_NORMALIZE_RE.sub("_", lowered)
61
+ normalized = re.sub(r"_+", "_", normalized).strip("_")
62
+ return normalized
63
+
64
+
65
+ def validate_event_name(value: str) -> str:
66
+ """Validate and return a normalized event name, or raise StatsEventError."""
67
+ if not isinstance(value, str):
68
+ raise StatsEventError("Le nom d'événement doit être une chaîne.")
69
+ if not value or not value.strip():
70
+ raise StatsEventError("Le nom d'événement ne peut pas être vide.")
71
+ normalized = normalize_event_name(value)
72
+ if not normalized:
73
+ raise StatsEventError(
74
+ f"Le nom d'événement '{value}' est invalide après normalisation."
75
+ )
76
+ if not _VALID_NAME_RE.match(normalized):
77
+ raise StatsEventError(
78
+ f"Le nom d'événement '{value}' est invalide. "
79
+ "Format attendu : lettres minuscules, chiffres et tirets bas, "
80
+ "commençant par une lettre."
81
+ )
82
+ return normalized
83
+
84
+
85
+ def make_event(
86
+ name: str,
87
+ label: str = "",
88
+ category: str = "general",
89
+ metadata: dict[str, Any] | None = None,
90
+ ) -> StatsEvent:
91
+ """Create a validated StatsEvent."""
92
+ return StatsEvent(
93
+ name=name,
94
+ label=label,
95
+ category=category,
96
+ metadata=metadata if metadata is not None else {},
97
+ )
98
+
99
+
100
+ def validate_event(event: StatsEvent) -> StatsEvent:
101
+ """Validate an existing StatsEvent and return it, or raise StatsEventError."""
102
+ if not isinstance(event, StatsEvent):
103
+ raise StatsEventError(
104
+ f"Un StatsEvent est attendu, reçu : {type(event).__name__}."
105
+ )
106
+ return event
@@ -0,0 +1,33 @@
1
+ """Définition SQL générique de la table d'événements statistiques Forge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ STATS_EVENTS_TABLE = "forge_stats_events"
6
+
7
+ STATS_EVENTS_COLUMNS = (
8
+ "id",
9
+ "name",
10
+ "label",
11
+ "category",
12
+ "metadata",
13
+ "created_at",
14
+ )
15
+
16
+ _SQL = """\
17
+ CREATE TABLE IF NOT EXISTS forge_stats_events (
18
+ id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
19
+ name VARCHAR(100) NOT NULL,
20
+ label VARCHAR(150) NOT NULL,
21
+ category VARCHAR(100) NOT NULL DEFAULT 'general',
22
+ metadata JSON NULL,
23
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
24
+ PRIMARY KEY (id),
25
+ INDEX idx_forge_stats_events_name (name),
26
+ INDEX idx_forge_stats_events_category (category),
27
+ INDEX idx_forge_stats_events_created_at (created_at)
28
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;"""
29
+
30
+
31
+ def get_stats_events_schema_sql() -> str:
32
+ """Return the SQL CREATE TABLE statement for forge_stats_events."""
33
+ return _SQL
@@ -0,0 +1,73 @@
1
+ """Helper Python explicite de tracking statistique pour Forge."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Any, Callable
7
+
8
+ from .events import StatsEvent, StatsEventError, make_event, validate_event
9
+ from .schema import STATS_EVENTS_TABLE
10
+
11
+ _INSERT_SQL = (
12
+ f"INSERT INTO {STATS_EVENTS_TABLE}"
13
+ " (name, label, category, metadata)"
14
+ " VALUES (?, ?, ?, ?)"
15
+ )
16
+
17
+
18
+ def get_track_event_sql() -> str:
19
+ """Return the INSERT SQL for recording a stats event."""
20
+ return _INSERT_SQL
21
+
22
+
23
+ def _serialize_metadata(metadata: dict[str, Any]) -> str:
24
+ try:
25
+ return json.dumps(metadata, ensure_ascii=False, sort_keys=True)
26
+ except (TypeError, ValueError) as exc:
27
+ raise StatsEventError(
28
+ f"metadata non sérialisable en JSON : {exc}"
29
+ ) from exc
30
+
31
+
32
+ def prepare_track_event_values(event: StatsEvent) -> tuple[str, str, str, str]:
33
+ """Return the SQL parameter tuple for a StatsEvent."""
34
+ if not isinstance(event, StatsEvent):
35
+ raise StatsEventError(
36
+ f"Un StatsEvent est attendu, reçu : {type(event).__name__}."
37
+ )
38
+ return (
39
+ event.name,
40
+ event.label,
41
+ event.category,
42
+ _serialize_metadata(event.metadata),
43
+ )
44
+
45
+
46
+ def track_event(
47
+ execute: Callable,
48
+ event_or_name: StatsEvent | str,
49
+ label: str = "",
50
+ category: str = "general",
51
+ metadata: dict[str, Any] | None = None,
52
+ ) -> StatsEvent:
53
+ """Record a stats event by calling the provided SQL executor.
54
+
55
+ The executor must be callable as execute(sql, params).
56
+ Forge never calls this automatically — the developer must call it explicitly.
57
+ """
58
+ if isinstance(event_or_name, StatsEvent):
59
+ event = validate_event(event_or_name)
60
+ elif isinstance(event_or_name, str):
61
+ event = make_event(
62
+ name=event_or_name,
63
+ label=label,
64
+ category=category,
65
+ metadata=metadata,
66
+ )
67
+ else:
68
+ raise StatsEventError(
69
+ "event_or_name doit être un StatsEvent ou un nom d'événement, "
70
+ f"reçu : {type(event_or_name).__name__}."
71
+ )
72
+ execute(get_track_event_sql(), prepare_track_event_values(event))
73
+ return event
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: forge-mvc-stats
3
+ Version: 1.0.0b7
4
+ Summary: Forge stats — événements génériques, schéma SQL, tracking et consultation.
5
+ Author: Roger Cauchon
6
+ License-Expression: LicenseRef-Forge-Proprietary
7
+ Project-URL: Homepage, https://github.com/caucrogeGit/Forge
8
+ Project-URL: Repository, https://github.com/caucrogeGit/Forge
9
+ Project-URL: Documentation, https://caucrogegit.github.io/Forge/
10
+ Keywords: python,mvc,forge,stats,statistiques
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+ Requires-Dist: forge-mvc<2,>=1.0.0b5
19
+
20
+ # forge-mvc-stats
21
+
22
+ Module stats pour Forge — événements génériques, schéma SQL, tracking et consultation.
23
+
24
+ Extrait du core Forge depuis la version 2.8.0 (ADR-004).
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install forge-mvc-stats
30
+ # ou en mode developpement
31
+ pip install -e packages/forge-mvc-stats/
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ from forge_mvc_stats import make_event, track_event
38
+
39
+ # Créer un événement
40
+ event = make_event(
41
+ "page_view",
42
+ label="Vue de page",
43
+ category="traffic",
44
+ metadata={"path": "/contact"},
45
+ )
46
+
47
+ # Enregistrer dans la base (db.execute est fourni par l'application)
48
+ track_event(db.execute, event)
49
+
50
+ # Raccourci direct par nom
51
+ track_event(db.execute, "contact_click", label="Clic contact")
52
+ ```
53
+
54
+ ## Consultation
55
+
56
+ ```python
57
+ from forge_mvc_stats import list_stats_events
58
+
59
+ # Lister les 50 derniers événements (fetch_all est fourni par l'application)
60
+ events = list_stats_events(my_fetch_all)
61
+
62
+ # Filtrer par nom ou catégorie
63
+ page_views = list_stats_events(my_fetch_all, name="page_view")
64
+ traffic = list_stats_events(my_fetch_all, category="traffic", limit=100)
65
+ ```
66
+
67
+ ## Schéma SQL
68
+
69
+ ```python
70
+ from forge_mvc_stats import get_stats_events_schema_sql, STATS_EVENTS_TABLE
71
+
72
+ print(STATS_EVENTS_TABLE) # "forge_stats_events"
73
+ sql = get_stats_events_schema_sql() # CREATE TABLE IF NOT EXISTS forge_stats_events (...)
74
+ ```
75
+
76
+ ## Cas d'usage
77
+
78
+ - Comptage de visites de pages
79
+ - Suivi de clics sur des liens ou boutons
80
+ - Mesure de soumissions de formulaires
81
+ - Traçage d'événements métier applicatifs
82
+
83
+ ## API publique
84
+
85
+ - `StatsEvent` — dataclass d'événement (name, label, category, metadata)
86
+ - `StatsEventError` — exception de validation
87
+ - `make_event(name, label, category, metadata)` — crée et valide un événement
88
+ - `validate_event(event)` — valide un événement existant
89
+ - `normalize_event_name(value)` — normalise en snake_case
90
+ - `validate_event_name(value)` — valide un nom normalisé
91
+ - `STATS_EVENTS_TABLE` — nom de la table SQL (`forge_stats_events`)
92
+ - `STATS_EVENTS_COLUMNS` — tuple des colonnes
93
+ - `get_stats_events_schema_sql()` — SQL `CREATE TABLE IF NOT EXISTS`
94
+ - `track_event(execute, event_or_name, ...)` — enregistre un événement en base
95
+ - `get_track_event_sql()` — SQL `INSERT` paramétré
96
+ - `prepare_track_event_values(event)` — tuple de paramètres prêts pour `execute`
97
+ - `list_stats_events(fetch_all, name, category, limit)` — liste des événements normalisés
98
+ - `get_stats_events_admin_sql(name, category, limit)` — SQL `SELECT` filtré
99
+ - `prepare_stats_events_admin_params(name, category, limit)` — paramètres du SELECT
100
+ - `normalize_stats_event_row(row)` — normalise une ligne brute (metadata JSON → dict)
101
+ - `StatsAdminError` — exception de consultation
@@ -0,0 +1,12 @@
1
+ README.md
2
+ pyproject.toml
3
+ forge_mvc_stats/__init__.py
4
+ forge_mvc_stats/admin.py
5
+ forge_mvc_stats/events.py
6
+ forge_mvc_stats/schema.py
7
+ forge_mvc_stats/tracking.py
8
+ forge_mvc_stats.egg-info/PKG-INFO
9
+ forge_mvc_stats.egg-info/SOURCES.txt
10
+ forge_mvc_stats.egg-info/dependency_links.txt
11
+ forge_mvc_stats.egg-info/requires.txt
12
+ forge_mvc_stats.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ forge-mvc<2,>=1.0.0b5
@@ -0,0 +1 @@
1
+ forge_mvc_stats
@@ -0,0 +1,34 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "forge-mvc-stats"
7
+ version = "1.0.0b7"
8
+ description = "Forge stats — événements génériques, schéma SQL, tracking et consultation."
9
+ readme = { file = "README.md", content-type = "text/markdown" }
10
+ requires-python = ">=3.12"
11
+ authors = [
12
+ { name = "Roger Cauchon" },
13
+ ]
14
+ license = "LicenseRef-Forge-Proprietary"
15
+ keywords = ["python", "mvc", "forge", "stats", "statistiques"]
16
+ dependencies = [
17
+ "forge-mvc>=1.0.0b5,<2",
18
+ ]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ "Programming Language :: Python :: 3.13",
24
+ "Programming Language :: Python :: 3.14",
25
+ ]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/caucrogeGit/Forge"
29
+ Repository = "https://github.com/caucrogeGit/Forge"
30
+ Documentation = "https://caucrogegit.github.io/Forge/"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["."]
34
+ include = ["forge_mvc_stats*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+