export command et enddpoint pour les villes

This commit is contained in:
Tykayn 2025-07-05 10:50:38 +02:00 committed by tykayn
parent c81affd3e3
commit 46d3b21cf6
4 changed files with 663 additions and 0 deletions

220
docs/api-stats-export.md Normal file
View file

@ -0,0 +1,220 @@
# API - Export des objets Stats
## Endpoint
```
GET /api/v1/stats/export
```
## Description
Cet endpoint permet d'exporter les objets Stats au format JSON avec leurs propriétés de nom et de décomptes, similaire à la commande `app:export-stats`.
## Paramètres de requête
| Paramètre | Type | Défaut | Description |
|-----------|------|--------|-------------|
| `zone` | string | - | Code INSEE spécifique à exporter (optionnel) |
| `pretty` | boolean | false | Formater le JSON avec indentation |
| `include_followups` | boolean | true | Inclure les données de followup |
| `include_places` | boolean | false | Inclure les données des lieux (peut être volumineux) |
## Exemples d'utilisation
### Export de toutes les zones
```bash
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export"
```
### Export avec formatage JSON
```bash
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true"
```
### Export d'une zone spécifique
```bash
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056"
```
### Export d'une zone avec formatage et sans followups
```bash
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056&pretty=true&include_followups=false"
```
### Export complet avec lieux
```bash
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true&include_places=true"
```
## Réponse
### Succès (200 OK)
```json
[
{
"id": 1,
"zone": "75056",
"name": "Paris",
"dateCreated": "2024-01-15 10:30:00",
"dateModified": "2024-01-20 14:45:00",
"population": 2161000,
"budgetAnnuel": "8500000000",
"siren": 200054781,
"codeEpci": 200054781,
"codesPostaux": "75001;75002;75003;...",
"decomptes": {
"placesCount": 1250,
"avecHoraires": 980,
"avecAdresse": 1200,
"avecSite": 850,
"avecAccessibilite": 450,
"avecNote": 320,
"completionPercent": 75,
"placesCountReal": 1250
},
"followups": [
{
"name": "fire_hydrant_count",
"measure": 1250,
"date": "2024-01-20 14:45:00"
},
{
"name": "fire_hydrant_completion",
"measure": 85.5,
"date": "2024-01-20 14:45:00"
}
],
"places": [
{
"id": 1,
"name": "Boulangerie du Centre",
"mainTag": "shop",
"osmId": 123456,
"osmKind": "node",
"email": "contact@boulangerie.fr",
"note": "Boulangerie artisanale",
"zipCode": "75001",
"siret": "12345678901234",
"lat": 48.8566,
"lon": 2.3522,
"hasOpeningHours": true,
"hasAddress": true,
"hasWebsite": true,
"hasWheelchair": false,
"hasNote": true,
"completionPercentage": 85
}
]
}
]
```
### Erreur - Zone non trouvée (404 Not Found)
```json
{
"error": "Aucun objet Stats trouvé",
"message": "Aucune zone trouvée pour le code INSEE: 99999"
}
```
### Erreur - Erreur serveur (500 Internal Server Error)
```json
{
"error": "Erreur lors de l'export",
"message": "Description de l'erreur"
}
```
## Headers de réponse
| Header | Description |
|--------|-------------|
| `Content-Type` | `application/json` |
| `Content-Disposition` | `attachment; filename="stats_export.json"` |
| `X-Export-Count` | Nombre d'objets exportés |
| `X-Export-Generated` | Date/heure de génération (format ISO 8601) |
| `X-Export-Zone` | Code INSEE si export d'une zone spécifique |
## Structure des données
### Informations générales
- `id` : Identifiant unique de l'objet Stats
- `zone` : Code INSEE de la zone
- `name` : Nom de la ville/zone
- `dateCreated` : Date de création
- `dateModified` : Date de dernière modification
### Données démographiques et administratives
- `population` : Population de la zone
- `budgetAnnuel` : Budget annuel de la collectivité
- `siren` : Code SIREN
- `codeEpci` : Code EPCI
- `codesPostaux` : Codes postaux de la zone
### Décomptes
- `placesCount` : Nombre de lieux enregistrés
- `avecHoraires` : Nombre de lieux avec horaires d'ouverture
- `avecAdresse` : Nombre de lieux avec adresse complète
- `avecSite` : Nombre de lieux avec site web
- `avecAccessibilite` : Nombre de lieux avec accessibilité PMR
- `avecNote` : Nombre de lieux avec note
- `completionPercent` : Pourcentage de complétion global
- `placesCountReal` : Nombre réel de lieux (comptage direct)
### Followups (si `include_followups=true`)
- `followups` : Tableau des mesures de suivi (CityFollowUp)
- `name` : Nom de la mesure
- `measure` : Valeur de la mesure
- `date` : Date de la mesure
### Places (si `include_places=true`)
- `places` : Tableau des lieux de la zone
- `id` : Identifiant du lieu
- `name` : Nom du lieu
- `mainTag` : Tag principal OSM
- `osmId` : ID OSM
- `osmKind` : Type OSM (node/way)
- `email` : Email de contact
- `note` : Note
- `zipCode` : Code postal
- `siret` : Numéro SIRET
- `lat` : Latitude
- `lon` : Longitude
- `hasOpeningHours` : A des horaires d'ouverture
- `hasAddress` : A une adresse complète
- `hasWebsite` : A un site web
- `hasWheelchair` : A des informations d'accessibilité
- `hasNote` : A une note
- `completionPercentage` : Pourcentage de complétion du lieu
## Cas d'usage
### Export pour analyse
```bash
# Export de toutes les villes avec formatage
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?pretty=true" > analyse_villes.json
# Export d'une ville spécifique
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?zone=75056&pretty=true" > paris.json
```
### Export pour traitement automatisé
```bash
# Export compact pour traitement par script
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export" > stats_compact.json
```
### Export avec données complètes
```bash
# Export avec tous les lieux (attention: peut être volumineux)
curl "https://osm-commerces.cipherbliss.com/api/v1/stats/export?include_places=true&pretty=true" > stats_complet.json
```
## Limitations
- L'option `include_places=true` peut générer des fichiers très volumineux pour les grandes villes
- Les requêtes avec `include_places=true` peuvent être plus lentes
- Le formatage JSON (`pretty=true`) augmente la taille de la réponse

View file

@ -0,0 +1,142 @@
# Commande d'export des objets Stats
## Description
La commande `app:export-stats` permet d'exporter les objets Stats au format JSON avec leurs propriétés de nom et de décomptes.
## Utilisation
### Export de tous les objets Stats
```bash
php bin/console app:export-stats
```
### Export avec formatage JSON
```bash
php bin/console app:export-stats --pretty
```
### Export vers un fichier spécifique
```bash
php bin/console app:export-stats --output=mon_export.json
```
### Export d'une zone spécifique
```bash
php bin/console app:export-stats --zone=75056
```
### Export avec toutes les options
```bash
php bin/console app:export-stats --output=paris_stats.json --zone=75056 --pretty
```
### Export avec mode verbeux
```bash
php bin/console app:export-stats -v
```
## Options disponibles
- `--output, -o` : Fichier de sortie (défaut: `stats_export.json`)
- `--zone, -z` : Code INSEE spécifique à exporter (optionnel)
- `--pretty, -p` : Formater le JSON avec indentation
- `-v, --verbose` : Mode verbeux pour afficher un aperçu des données
## Structure des données exportées
Le fichier JSON contient un tableau d'objets avec la structure suivante :
```json
[
{
"id": 1,
"zone": "75056",
"name": "Paris",
"dateCreated": "2024-01-15 10:30:00",
"dateModified": "2024-01-20 14:45:00",
"population": 2161000,
"budgetAnnuel": "8500000000",
"siren": "200054781",
"codeEpci": "200054781",
"codesPostaux": "75001;75002;75003;...",
"decomptes": {
"placesCount": 1250,
"avecHoraires": 980,
"avecAdresse": 1200,
"avecSite": 850,
"avecAccessibilite": 450,
"avecNote": 320,
"completionPercent": 75,
"placesCountReal": 1250
},
"followups": [
{
"name": "fire_hydrant_count",
"measure": 1250,
"date": "2024-01-20 14:45:00"
},
{
"name": "fire_hydrant_completion",
"measure": 85.5,
"date": "2024-01-20 14:45:00"
}
]
}
]
```
## Propriétés exportées
### Informations générales
- `id` : Identifiant unique de l'objet Stats
- `zone` : Code INSEE de la zone
- `name` : Nom de la ville/zone
- `dateCreated` : Date de création
- `dateModified` : Date de dernière modification
### Données démographiques et administratives
- `population` : Population de la zone
- `budgetAnnuel` : Budget annuel de la collectivité
- `siren` : Code SIREN
- `codeEpci` : Code EPCI
- `codesPostaux` : Codes postaux de la zone
### Décomptes
- `placesCount` : Nombre de lieux enregistrés
- `avecHoraires` : Nombre de lieux avec horaires d'ouverture
- `avecAdresse` : Nombre de lieux avec adresse complète
- `avecSite` : Nombre de lieux avec site web
- `avecAccessibilite` : Nombre de lieux avec accessibilité PMR
- `avecNote` : Nombre de lieux avec note
- `completionPercent` : Pourcentage de complétion global
- `placesCountReal` : Nombre réel de lieux (comptage direct)
### Followups
- `followups` : Tableau des mesures de suivi (CityFollowUp)
- `name` : Nom de la mesure
- `measure` : Valeur de la mesure
- `date` : Date de la mesure
## Exemples d'utilisation
### Export pour analyse
```bash
# Export de toutes les villes avec formatage
php bin/console app:export-stats --pretty --output=analyse_villes.json
# Export d'une ville spécifique
php bin/console app:export-stats --zone=75056 --pretty --output=paris.json
```
### Export pour traitement automatisé
```bash
# Export compact pour traitement par script
php bin/console app:export-stats --output=stats_compact.json
```
### Vérification des données
```bash
# Export avec aperçu des données
php bin/console app:export-stats --pretty -v
```

View file

@ -0,0 +1,167 @@
<?php
namespace App\Command;
use App\Entity\Stats;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:export-stats',
description: 'Exporte les objets Stats au format JSON avec leurs propriétés de nom et de décomptes'
)]
class ExportStatsCommand extends Command
{
public function __construct(
private EntityManagerInterface $entityManager
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption(
'output',
'o',
InputOption::VALUE_REQUIRED,
'Fichier de sortie (par défaut: stats_export.json)',
'stats_export.json'
)
->addOption(
'zone',
'z',
InputOption::VALUE_REQUIRED,
'Code INSEE spécifique à exporter (optionnel)'
)
->addOption(
'pretty',
'p',
InputOption::VALUE_NONE,
'Formater le JSON avec indentation'
)
->setHelp('Cette commande exporte les objets Stats au format JSON avec leurs propriétés de nom et de décomptes.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$outputFile = $input->getOption('output');
$zone = $input->getOption('zone');
$pretty = $input->getOption('pretty');
$io->title('Export des objets Stats');
try {
// Construire la requête
$qb = $this->entityManager->getRepository(Stats::class)->createQueryBuilder('s');
if ($zone) {
$qb->where('s.zone = :zone')
->setParameter('zone', $zone);
$io->note("Export pour la zone INSEE: $zone");
}
$stats = $qb->getQuery()->getResult();
if (empty($stats)) {
$io->warning('Aucun objet Stats trouvé.');
return Command::SUCCESS;
}
$io->info(sprintf('Export de %d objet(s) Stats...', count($stats)));
// Préparer les données pour l'export
$exportData = [];
foreach ($stats as $stat) {
$statData = [
'id' => $stat->getId(),
'zone' => $stat->getZone(),
'name' => $stat->getName(),
'dateCreated' => $stat->getDateCreated() ? $stat->getDateCreated()->format('Y-m-d H:i:s') : null,
'dateModified' => $stat->getDateModified() ? $stat->getDateModified()->format('Y-m-d H:i:s') : null,
'population' => $stat->getPopulation(),
'budgetAnnuel' => $stat->getBudgetAnnuel(),
'siren' => $stat->getSiren(),
'codeEpci' => $stat->getCodeEpci(),
'codesPostaux' => $stat->getCodesPostaux(),
'decomptes' => [
'placesCount' => $stat->getPlacesCount(),
'avecHoraires' => $stat->getAvecHoraires(),
'avecAdresse' => $stat->getAvecAdresse(),
'avecSite' => $stat->getAvecSite(),
'avecAccessibilite' => $stat->getAvecAccessibilite(),
'avecNote' => $stat->getAvecNote(),
'completionPercent' => $stat->getCompletionPercent(),
'placesCountReal' => $stat->getPlaces()->count(),
],
'followups' => []
];
// Ajouter les followups si disponibles
foreach ($stat->getCityFollowUps() as $followup) {
$statData['followups'][] = [
'name' => $followup->getName(),
'measure' => $followup->getMeasure(),
'date' => $followup->getDate()->format('Y-m-d H:i:s')
];
}
$exportData[] = $statData;
}
// Préparer le JSON
$jsonOptions = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
if ($pretty) {
$jsonOptions |= JSON_PRETTY_PRINT;
}
$jsonContent = json_encode($exportData, $jsonOptions);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('Erreur lors de l\'encodage JSON: ' . json_last_error_msg());
}
// Écrire dans le fichier
$bytesWritten = file_put_contents($outputFile, $jsonContent);
if ($bytesWritten === false) {
throw new \Exception("Impossible d'écrire dans le fichier: $outputFile");
}
$io->success(sprintf(
'Export terminé avec succès ! %d objet(s) exporté(s) vers %s (%s octets)',
count($stats),
$outputFile,
number_format($bytesWritten, 0, ',', ' ')
));
// Afficher un aperçu des données
if ($io->isVerbose()) {
$io->section('Aperçu des données exportées');
foreach ($exportData as $index => $data) {
$io->text(sprintf(
'%d. %s (%s) - %d lieux, %d%% complété',
$index + 1,
$data['name'],
$data['zone'],
$data['decomptes']['placesCountReal'],
$data['decomptes']['completionPercent']
));
}
}
return Command::SUCCESS;
} catch (\Exception $e) {
$io->error('Erreur lors de l\'export: ' . $e->getMessage());
return Command::FAILURE;
}
}
}

View file

@ -198,4 +198,138 @@ class ApiController extends AbstractController
$response->headers->set('Content-Disposition', 'attachment; filename="places_'.$insee.'.csv"');
return $response;
}
#[Route('/api/v1/stats/export', name: 'api_stats_export', methods: ['GET'])]
public function statsExport(
StatsRepository $statsRepository,
\Symfony\Component\HttpFoundation\Request $request
): JsonResponse {
// Récupérer les paramètres de requête
$zone = $request->query->get('zone');
$pretty = $request->query->getBoolean('pretty', false);
$includeFollowups = $request->query->getBoolean('include_followups', true);
$includePlaces = $request->query->getBoolean('include_places', false);
try {
// Construire la requête
$qb = $statsRepository->createQueryBuilder('s');
if ($zone) {
$qb->where('s.zone = :zone')
->setParameter('zone', $zone);
}
$stats = $qb->getQuery()->getResult();
if (empty($stats)) {
return new JsonResponse([
'error' => 'Aucun objet Stats trouvé',
'message' => $zone ? "Aucune zone trouvée pour le code INSEE: $zone" : 'Aucune donnée disponible'
], Response::HTTP_NOT_FOUND);
}
// Préparer les données pour l'export
$exportData = [];
foreach ($stats as $stat) {
$statData = [
'id' => $stat->getId(),
'zone' => $stat->getZone(),
'name' => $stat->getName(),
'dateCreated' => $stat->getDateCreated() ? $stat->getDateCreated()->format('Y-m-d H:i:s') : null,
'dateModified' => $stat->getDateModified() ? $stat->getDateModified()->format('Y-m-d H:i:s') : null,
'population' => $stat->getPopulation(),
'budgetAnnuel' => $stat->getBudgetAnnuel(),
'siren' => $stat->getSiren(),
'codeEpci' => $stat->getCodeEpci(),
'codesPostaux' => $stat->getCodesPostaux(),
'decomptes' => [
'placesCount' => $stat->getPlacesCount(),
'avecHoraires' => $stat->getAvecHoraires(),
'avecAdresse' => $stat->getAvecAdresse(),
'avecSite' => $stat->getAvecSite(),
'avecAccessibilite' => $stat->getAvecAccessibilite(),
'avecNote' => $stat->getAvecNote(),
'completionPercent' => $stat->getCompletionPercent(),
'placesCountReal' => $stat->getPlaces()->count(),
],
];
// Ajouter les followups si demandé
if ($includeFollowups) {
$statData['followups'] = [];
foreach ($stat->getCityFollowUps() as $followup) {
$statData['followups'][] = [
'name' => $followup->getName(),
'measure' => $followup->getMeasure(),
'date' => $followup->getDate()->format('Y-m-d H:i:s')
];
}
}
// Ajouter les lieux si demandé
if ($includePlaces) {
$statData['places'] = [];
foreach ($stat->getPlaces() as $place) {
$statData['places'][] = [
'id' => $place->getId(),
'name' => $place->getName(),
'mainTag' => $place->getMainTag(),
'osmId' => $place->getOsmId(),
'osmKind' => $place->getOsmKind(),
'email' => $place->getEmail(),
'note' => $place->getNote(),
'zipCode' => $place->getZipCode(),
'siret' => $place->getSiret(),
'lat' => $place->getLat(),
'lon' => $place->getLon(),
'hasOpeningHours' => $place->hasOpeningHours(),
'hasAddress' => $place->hasAddress(),
'hasWebsite' => $place->hasWebsite(),
'hasWheelchair' => $place->hasWheelchair(),
'hasNote' => $place->hasNote(),
'completionPercentage' => $place->getCompletionPercentage(),
];
}
}
$exportData[] = $statData;
}
// Préparer le JSON
$jsonOptions = JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
if ($pretty) {
$jsonOptions |= JSON_PRETTY_PRINT;
}
$jsonContent = json_encode($exportData, $jsonOptions);
if (json_last_error() !== JSON_ERROR_NONE) {
return new JsonResponse([
'error' => 'Erreur lors de l\'encodage JSON',
'message' => json_last_error_msg()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
// Retourner la réponse
$response = new JsonResponse($exportData, Response::HTTP_OK);
$response->headers->set('Content-Type', 'application/json');
$response->headers->set('Content-Disposition', 'attachment; filename="stats_export.json"');
// Ajouter des métadonnées dans les headers
$response->headers->set('X-Export-Count', count($stats));
$response->headers->set('X-Export-Generated', (new \DateTime())->format('c'));
if ($zone) {
$response->headers->set('X-Export-Zone', $zone);
}
return $response;
} catch (\Exception $e) {
return new JsonResponse([
'error' => 'Erreur lors de l\'export',
'message' => $e->getMessage()
], Response::HTTP_INTERNAL_SERVER_ERROR);
}
}
}