Skip to content

Commit

Permalink
Import les données Litteralis de Lons-le-Saunier
Browse files Browse the repository at this point in the history
  • Loading branch information
florimondmanca committed Jan 8, 2025
1 parent c0534af commit 58c096b
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 12 deletions.
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ APP_MEL_LITTERALIS_CREDENTIALS='testuser:testpass'
APP_MEL_ORG_ID=e0d93630-acf7-4722-81e8-ff7d5fa64b66 # DiaLog org
APP_FOUGERES_LITTERALIS_CREDENTIALS='testuser:testpass'
APP_FOUGERES_ORG_ID=e0d93630-acf7-4722-81e8-ff7d5fa64b66 # DiaLog org
APP_LONS_LE_SAUNIER_LITTERALIS_CREDENTIALS='testuser:testpass'
APP_LONS_LE_SAUNIER_ORG_ID=e0d93630-acf7-4722-81e8-ff7d5fa64b66 # DiaLog org
SYMFONY_DEPRECATIONS_HELPER=999999
PANTHER_ERROR_SCREENSHOT_DIR=./var/error-screenshots
MAX_ITEMS_PER_PAGE=1
Expand Down
67 changes: 67 additions & 0 deletions .github/workflows/litteralis_lons_le_saunier_import.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Litteralis Lons-le-Saunier Import

on:
workflow_dispatch:
schedule:
- cron: '15 16 * * 1' # Voir https://crontab.guru/ : tous les lundis à 16h08 GMT

jobs:
import:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1

- name: Setup PHP with PECL extension
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'

- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Install Scalingo CLI
run: curl -O https://cli-dl.scalingo.com/install && bash install

- name: Install SSH key
# Credit: https://stackoverflow.com/a/69234389
run: |
mkdir -p ~/.ssh
install -m 600 -D /dev/null ~/.ssh/id_rsa
echo "${{ secrets.GH_SCALINGO_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
- name: Add Scalingo as a known host
run: |
ssh-keyscan -H ssh.osc-fr1.scalingo.com >> ~/.ssh/known_hosts
- name: Init environment variables
run: |
echo "DATABASE_URL=${{ secrets.APP_LONS_LE_SAUNIER_IMPORT_DATABASE_URL }}" >> .env.local
echo "BDTOPO_DATABASE_URL=${{ secrets.BDTOPO_DATABASE_URL }}" >> .env.local
echo "APP_LONS_LE_SAUNIER_LITTERALIS_CREDENTIALS=${{ secrets.APP_LONS_LE_SAUNIER_LITTERALIS_CREDENTIALS }}" >> .env.local
echo "APP_LONS_LE_SAUNIER_ORG_ID=${{ vars.APP_LONS_LE_SAUNIER_ORG_ID }}" >> .env.local
- name: Run import
run: make ci_litteralis_lons_le_saunier_import BIN_PHP="php" BIN_CONSOLE="php bin/console" BIN_COMPOSER="composer"

- name: Get log file path
id: logfile
if: ${{ !cancelled() }}
run:
echo "path=$(find log/litteralis -type f -name '*.log' | head -n 1)" >> $GITHUB_OUTPUT

- uses: actions/upload-artifact@v3
if: ${{ !cancelled() }}
with:
name: litteralis_logfile
path: ${{ steps.logfile.outputs.path }}
retention-days: 21
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,12 @@ ci_litteralis_fougeres_import: ## Run CI steps for Litteralis Fougeres Import wo
./tools/scalingodbtunnel ${APP_FOUGERES_IMPORT_APP} --host-url --port 10000 & ./tools/wait-for-it.sh 127.0.0.1:10000
make console CMD="app:fougeres:import"

ci_litteralis_lons_le_saunier_import: ## Run CI steps for Litteralis Lons-le-Saunier Import workflow
make composer CMD="install -n --prefer-dist"
scalingo login --ssh --ssh-identity ~/.ssh/id_rsa
./tools/scalingodbtunnel dialog --host-url --port 10000 & ./tools/wait-for-it.sh 127.0.0.1:10000
make console CMD="app:lons_le_saunier:import"

ci_bdtopo_migrate: ## Run CI steps for BD TOPO Migrate workflow
make composer CMD="install -n --prefer-dist"
make bdtopo_migrate
Expand Down
2 changes: 2 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ services:
$melCredentials: '%env(APP_MEL_LITTERALIS_CREDENTIALS)%' # format: 'user:pass'
$fougeresOrgId: '%env(APP_FOUGERES_ORG_ID)%'
$fougeresCredentials: '%env(APP_FOUGERES_LITTERALIS_CREDENTIALS)%' # format: 'user:pass'
$lonsLeSaunierOrgId: '%env(APP_LONS_LE_SAUNIER_ORG_ID)%'
$lonsLeSaunierCredentials: '%env(APP_LONS_LE_SAUNIER_LITTERALIS_CREDENTIALS)%' # format: 'user:pass'
$metabaseSiteUrl: '%env(APP_METABASE_SITE_URL)%'
$metabaseSecretKey: '%env(APP_METABASE_SECRET_KEY)%'
$mediaLocation: '%env(APP_MEDIA_LOCATION)%'
Expand Down
30 changes: 19 additions & 11 deletions docs/tools/litteralis.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,27 @@ L'intégration requête l'API WFS de Litteralis. Pour cela elle a besoin d'**ide

L'intégration est "générique" au sens où elle peut être réutilisée pour plusieurs collectivités. Chaque collectivité a donc un peu de code pour faire le pont entre des variables d'environnement contenant les informations ci-dessus et l'intégration générique.

## Organisations enregistrées

Le `code` est à utiliser en remplacement de `<INTG>` (pour "intégration") dans les commandes qui suivent

| Code | Nom complet |
|---|---|
| `mel` | Métropole Européenne de Lille (département Nord 59, région Hauts-de-France) |
| `fougeres` | Ville de Fougères (sous-préfecture département Ille-et-Villaine 35, région Bretagne) |
| `lons_le_saunier` | Ville de Lons-le-Saunier (préfecture département Jura 39, région Bourgogne-Franche-Comté) |

## Exécuter l'intégration

L'intégration peut être exécutée à l'aide de commandes Symfony spécifiques à chaque collectivité.

### MEL

**Pour l'import en prod** :

1. Récupérer le UUID de l'organisation "Métropole Européenne de Lille (MEL)" en prod. Pour cela demander à un super-admin : l'UUID est visible dans l'URL de la page de l'organisation dans l'admin.
1. Récupérer le UUID de l'organisation en prod. Pour cela demander à un super-admin : l'UUID est visible dans l'URL de la page de l'organisation dans l'admin.
2. Créer un fichier `.env.prod.local` vide, et y définir :
* `BDTOPO_DATABASE_URL`
* `APP_MEL_ORG_ID=ID`,`ID` est l'UUID de la MEL que vous venez de récupérer.
* `APP_MEL_LITTERALIS_CREDENTIALS` avec les identifiants MEL au format `user:password` (les demander à un membre de l'équipe dev)
* `APP_<INTG>_ORG_ID=<ID>``<ID>` est l'UUID de l'organisation que vous venez de récupérer.
* `APP_<INTG>_LITTERALIS_CREDENTIALS` avec les identifiants MEL au format `user:password` (les demander à un membre de l'équipe dev)
3. Ouvrir un [tunnel](./db.md#utiliser-une-db-scalingo-en-local) vers la DB de prod :

```bash
Expand All @@ -33,7 +41,7 @@ L'intégration peut être exécutée à l'aide de commandes Symfony spécifiques
5. Lancer cette commande :
```bash
make console CMD="app:mel:import --env=prod"
make console CMD="app:<INTG>:import --env=prod"
```
L'exécution prendra plusieurs minutes. Les logs d'exécution seront ajoutés au dossier `logs/litteralis/`. En cas d'exception la commande échouera. Un rapport final "pretty print" est affiché.
Expand All @@ -42,20 +50,20 @@ L'intégration peut être exécutée à l'aide de commandes Symfony spécifiques

## Déploiement périodique automatique

### MEL
Les données Litteralis des différentes organisations sont automatiquement intégrées en production tous les lundis à 16h00.

Les données la MEL sont automatiquement intégrées en production tous les lundis à 16h00.

Cette automatisation est réalisée au moyen de [GitHub Actions](./github_actions.md) via le workflow [`litteralis_mel_import.yml`](../../.github/workflows/litteralis_mel_import.yml).
Cette automatisation est réalisée au moyen de [GitHub Actions](./github_actions.md) via un workflow par organisation.

La configuration passe par diverses variables d'environnement listées ci-dessous :
| Variable d'environnement | Configuration | Description |
|---|---|---|
| `APP_MEL_IMPORT_APP` | [Variable](https://docs.github.com/fr/actions/learn-github-actions/variables) au sens GitHub Actions | L'application Scalingo cible (par exemple `dialog` pour la production) |
| `APP_MEL_LITTERALIS_CREDENTIALS` | [Secret](https://docs.github.com/fr/actions/security-guides/using-secrets-in-github-actions) au sens GitHub Actions | Les identifiants d'accès à l'API Litteralis de la MEL |
| `APP_MEL_IMPORT_DATABASE_URL` | Secret | L'URL d'accès à la base de données par la CI (`./tools/scalingodbtunnel APP --host-url`) |
| `APP_MEL_LITTERALIS_CREDENTIALS` | [Secret](https://docs.github.com/fr/actions/security-guides/using-secrets-in-github-actions) au sens GitHub Actions | Les identifiants d'accès à l'API Litteralis de la MEL |
| `APP_MEL_ORG_ID` | Variable | Le UUID de l'organisation "Métropole Européenne de Lille" dans l'environnement défini par `APP_MEL_IMPORT_APP` |
| `APP_FOUGERES_LITTERALIS_CREDENTIALS`, `APP_FOUGERES_ORG_ID` | Secrets | Idem que pour la MEL |
| `APP_LONS_LE_SAUNIER_LITTERALIS_CREDENTIALS`, `APP_LONS_LE_SAUNIER_ORG_ID` | Secrets | Idem que pour la MEL |
| `GH_SCALINGO_SSH_PRIVATE_KEY` | Secret | Clé SSH privée permettant l'accès à Scalingo par la CI |

## Références
Expand Down
2 changes: 1 addition & 1 deletion src/Infrastructure/IntegrationReport/ReportFormatter.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ public function format(array $records): string
foreach ($info['regulations'] as $id) {
$line = \sprintf(' %s', $id);

if (\array_key_exists($id, $info['urls'])) {
if (!empty($info['urls']) && \array_key_exists($id, $info['urls'])) {
$url = $info['urls'][$id];
$line = \sprintf('%s (%s)', $line, $url);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Litteralis\LonsLeSaunier;

use App\Infrastructure\IntegrationReport\Reporter;
use App\Infrastructure\Litteralis\LitteralisExecutor;

final class LonsLeSaunierExecutor
{
private const INTEGRATION_NAME = 'Litteralis Lons-le-Saunier';

public function __construct(
private LitteralisExecutor $executor,
private string $lonsLeSaunierOrgId,
string $lonsLeSaunierCredentials,
) {
$this->executor->configure($lonsLeSaunierCredentials);
}

public function execute(\DateTimeInterface $laterThan, Reporter $reporter): string
{
return $this->executor->execute(self::INTEGRATION_NAME, $this->lonsLeSaunierOrgId, $laterThan, $reporter);
}
}
48 changes: 48 additions & 0 deletions src/Infrastructure/Symfony/Command/LonsLeSaunierImportCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace App\Infrastructure\Symfony\Command;

use App\Application\DateUtilsInterface;
use App\Infrastructure\IntegrationReport\Reporter;
use App\Infrastructure\Litteralis\LonsLeSaunier\LonsLeSaunierExecutor;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

#[AsCommand(
name: 'app:lons_le_saunier:import',
description: 'Import Litteralis data of Lons-le-Saunier',
hidden: false,
)]
class LonsLeSaunierImportCommand extends Command
{
public function __construct(
private LoggerInterface $logger,
private LonsLeSaunierExecutor $executor,
private DateUtilsInterface $dateUtils,
) {
parent::__construct();
}

public function execute(InputInterface $input, OutputInterface $output): int
{
$reporter = new Reporter($this->logger);
$now = $this->dateUtils->getNow();

try {
$report = $this->executor->execute($now, $reporter);

$output->write($report);
} catch (\RuntimeException $exc) {
$output->writeln($exc->getMessage());

return Command::FAILURE;
}

return Command::SUCCESS;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace App\Tests\Integration\Infrastructure\Symfony\Command;

use App\Infrastructure\Symfony\Command\LonsLeSaunierImportCommand;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Tester\CommandTester;

final class LonsLeSaunierImportCommandTest extends KernelTestCase
{
public function testExecute(): void
{
self::bootKernel();
$container = static::getContainer();

$command = $container->get(LonsLeSaunierImportCommand::class);
$commandTester = new CommandTester($command);

$commandTester->execute([]);
$commandTester->assertCommandIsSuccessful($commandTester->getDisplay());

$output = $commandTester->getDisplay();
// These results depend on LitteralisMockHttpClient
$this->assertStringContainsString("Nombre total d'emprises dans Litteralis pour cette organisation : 2", $output);
$this->assertStringContainsString("Emprises d'intérêt récupérées dans Litteralis : 2", $output);
$this->assertStringContainsString("Emprises effectivement candidates à l'import : 2 (dans 1 arrêtés)", $output);
$this->assertStringContainsString('Emprises importées avec succès : 2 (dans 1 arrêtés)', $output);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace App\Tests\Unit\Infrastructure\Symfony\Command;

use App\Application\DateUtilsInterface;
use App\Infrastructure\Litteralis\LonsLeSaunier\LonsLeSaunierExecutor;
use App\Infrastructure\Symfony\Command\LonsLeSaunierImportCommand;
use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Tester\CommandTester;

class LonsLeSaunierImportCommandTest extends TestCase
{
private $logger;
private $executor;
private $dateUtils;

protected function setUp(): void
{
$this->logger = $this->createMock(LoggerInterface::class);
$this->executor = $this->createMock(LonsLeSaunierExecutor::class);
$this->dateUtils = $this->createMock(DateUtilsInterface::class);
}

public function testExecute()
{
$this->executor
->expects(self::once())
->method('execute');

$command = new LonsLeSaunierImportCommand($this->logger, $this->executor, $this->dateUtils);
$this->assertSame('app:lons_le_saunier:import', $command->getName());

$commandTester = new CommandTester($command);
$commandTester->execute([]);
$commandTester->assertCommandIsSuccessful();
}

public function testExecuteError()
{
$this->executor
->expects(self::once())
->method('execute')
->willThrowException(new \RuntimeException('Failed'));

$command = new LonsLeSaunierImportCommand($this->logger, $this->executor, $this->dateUtils);
$commandTester = new CommandTester($command);
$commandTester->execute([]);

$this->assertSame(Command::FAILURE, $commandTester->getStatusCode());
}
}

0 comments on commit 58c096b

Please sign in to comment.