Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Premiers extracteurs NeTEx (StopPlaces) #4026

Merged
merged 16 commits into from
Aug 8, 2024
Merged

Conversation

thbar
Copy link
Contributor

@thbar thbar commented Jul 1, 2024

Cette PR introduit une couche fondamentale pour réaliser différentes choses plus tard:

  • apprendre à générer des méta-données (type nombre de StopPlace ou Quay etc dans un NeTEx...)
  • extraction d'éléments terrain pour les réflexions sur le registre d'arrêts
  • indexation d'arrêts pour la recherche par géographie dans l'API v2 ou le futur moteur de recherche

Les limitations sont documentées sur les modules directement, et le code est testé pour sa plus grande partie.

J'ai intégré un script qui me sert à parcourir la totalité des NeTEx du PAN, que je ferai évoluer.

TODOs

  • Code de traversal d'archive à rendre moins "soudé" au use-case stop places - ça attendra un prochain here
  • TestCase sur netex_archive_parser.ex (actuellement 0%, embêtant pour refactorer)
  • Doc sur le fait que c'est incomplet de façon générale en terme d'extraction stop places (des fichiers à 0 alors qu'il y en a), lié au fait que NeTEx
  • Expliquer ici les usages

@thbar thbar added the NeTEx label Jul 1, 2024
@thbar thbar self-assigned this Jul 1, 2024
@thbar thbar changed the title Premiers extracteurs NeTEx Premiers extracteurs NeTEx (StopPlaces) Jul 19, 2024
@thbar thbar marked this pull request as ready for review July 19, 2024 14:40
@thbar thbar requested a review from a team as a code owner July 19, 2024 14:40
@ptitfred
Copy link
Contributor

Ca faisait 15 ans que j'avais pas vu un parser Sax et je suis déçu de voir que c'est toujours aussi pénible à review et maintenir :/

@thbar
Copy link
Contributor Author

thbar commented Jul 29, 2024

@ptitfred je pense avoir des idées pour rendre le tout plus lisible dans un prochain tour (ou avec Saxy, ou avec d’autres librairies), donc ne perdons pas espoir 😊

Copy link
Member

@AntoineAugusti AntoineAugusti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beau boulot de découverte ! Pas facile à lire le code Saxy

Comment on lines +12 to +16
:zip.create(
zip_filename,
file_data
|> Enum.map(fn {name, content} -> {name |> to_charlist(), content} end)
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peut-être stocker en mémoire et non sur le disque ? Semble possible en lisant https://www.erlang.org/doc/apps/stdlib/zip.html#zip/2

Permettra de ne pas laisser des fichiers dans le tmp ou de devoir penser à nettoyer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peut-être stocker en mémoire et non sur le disque ? Semble possible en lisant https://www.erlang.org/doc/apps/stdlib/zip.html#zip/2

Merci pour la suggestion. En fait, notre "traversal de zip" est conçu pour travailler avec des fichiers sur disque uniquement, afin d'éviter une surcharge de la RAM en production (certains fichiers dépassent 500 MB). Le stockage en mémoire n'est donc pas adapté ici.

La codebase inclut plusieurs tests qui ne nettoient pas automatiquement les fichiers tmp. Un helper de nettoyage pourrait être utile à l'avenir (comme Dir.mktmpdir en Ruby), c'est une idée qu'on peut conserver !

Pour éviter tout souci, j'ai toutefois ajouté de l'unicité dans le commit e2ff1c4.

end

def handle_event(:characters, chars, state)
when state.current_tree == ["StopPlace", "Centroid", "Location", "Latitude"] do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On ne gère pas le cas d'une frame englobante ? Me semble qu'on n'aurait pas exactement ce tree alors

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci pour la question ; l'implémentation actuelle va bien gérer correctement tout ce qui est frame englobante au dessus (le StopPlace peut être à n'importe quel niveau des profondeur dans le XML), pas de souci du coup.

Pour mettre ce point plus clairement en évidence (et éviter les régressions), j'ai modulé un peu le test ici 8dba0bf.

tmp_file = System.tmp_dir!() |> Path.join("temp-netex.zip")
ZipCreator.create!(tmp_file, [{"arrets.xml", some_netex_content()}])

# given a zip netex archive containing 2 files, I want the output I expected
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seulement 1 fichier il me semble ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tout à fait, merci pour le catch. Corrigé dans 5858db6

Comment on lines +17 to +29
extension = Path.extname(file_name)

cond do
# Entry names ending with a slash `/` are directories. Skip them.
# https://github.com/akash-akya/unzip/blob/689a1ca7a134ab2aeb79c8c4f8492d61fa3e09a0/lib/unzip.ex#L69
String.ends_with?(file_name, "/") ->
[]

extension |> String.downcase() == ".zip" ->
raise "Insupported zip inside zip for file #{file_name}"

extension |> String.downcase() != ".xml" ->
raise "Insupported file extension (#{extension}) for file #{file_name}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peut-être déplacer ceci ailleurs, après Unzip.list_entries ? On risque d'avoir cette logique à d'autres endroits et ceci ne semble pas spécifique aux stop places

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci pour la suggestion. J'envisagerai ça quand on généralisera le code (pour l'instant très spécifique stop places), j'aurai davantage de recul à ce moment là.

@@ -0,0 +1,46 @@
defmodule Transport.NeTEx.ArchiveParserTest do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

J'aurais tendance à mettre les "helper methods" après les tests pour mettre en évidence le fonctionnement testé

https://softwareengineering.stackexchange.com/q/186418

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je me souviens que tu as levé ce point quelques fois.

Le lien que tu as partagé montre que c'est une question de préférence personnelle.

(d'ailleurs personnellement je trouve ça plus simple cognitivement, dans les tests, où ce code n'est pas réutilisé ailleurs, d'avoir les helpers défini avant leur utilisation, ou encore mieux, dans des modules externes si on peut, plutôt qu'en bas).

Si ce point t'embête et vu que tu l'as évoqué quelques fois déjà, parlons-en tranquille à un point dév pour voir si ça vaut le coup d'homogénéiser les tests là dessus, si tu le souhaites.


netex =
df
|> Task.async_stream(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il semble qu'on avait un disk cache helper lors de précédentes PRs pour le script. Je confonds ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il y a bien un helper de ce type (ici: https://github.com/etalab/transport-site/blob/master/apps/shared/lib/req_custom_cache.ex) mais il n'est pas adapté à l'usage ici. Le cache évoqué dans ce lien stocke la totalité de l'objet réponse Elixir Req sur le disque, ce qui empêche d'avoir "juste le body zip" à part.

Pour faire plus simple pour le moment et reporter ce refactoring intéressant, j'ai préféré ne pas modifier le cache, et réimplémenter, le temps d'avoir + de recul et de pouvoir généraliser et modifier le cache (éventuellement).

Comment on lines +58 to +70
IO.puts("Processing file #{r.id}")

try do
count =
Transport.NeTEx.read_all_stop_places(r.local_path)
|> Enum.flat_map(fn {_file, stops} -> stops end)
# some stop places have no latitude in NeTEx
|> Enum.reject(fn p -> is_nil(p[:latitude]) end)
|> Enum.count()

IO.puts("#{count} StopPlaces detected")
rescue
e -> IO.puts("Som'thing bad happened")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tu aurais un output à partager ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il y a des cas variés, je préfère ne pas faire de détail pour l'instant car ça prendrait un brin de temps, une prochaine itération capturera ça en détail et j'aurai alors l'opportunité de partager ça (notamment pour améliorer le parser, certains fichiers ne passant pas).

Je contribuerai sur:

et j'en profiterai alors pour ou bien traiter ces cas, ou bien les afficher clairement.

Copy link
Contributor Author

@thbar thbar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci pour le retour détaillé @AntoineAugusti ; j'ai pris en compte ou répondu !

Comment on lines +17 to +29
extension = Path.extname(file_name)

cond do
# Entry names ending with a slash `/` are directories. Skip them.
# https://github.com/akash-akya/unzip/blob/689a1ca7a134ab2aeb79c8c4f8492d61fa3e09a0/lib/unzip.ex#L69
String.ends_with?(file_name, "/") ->
[]

extension |> String.downcase() == ".zip" ->
raise "Insupported zip inside zip for file #{file_name}"

extension |> String.downcase() != ".xml" ->
raise "Insupported file extension (#{extension}) for file #{file_name}"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci pour la suggestion. J'envisagerai ça quand on généralisera le code (pour l'instant très spécifique stop places), j'aurai davantage de recul à ce moment là.

end

def handle_event(:characters, chars, state)
when state.current_tree == ["StopPlace", "Centroid", "Location", "Latitude"] do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merci pour la question ; l'implémentation actuelle va bien gérer correctement tout ce qui est frame englobante au dessus (le StopPlace peut être à n'importe quel niveau des profondeur dans le XML), pas de souci du coup.

Pour mettre ce point plus clairement en évidence (et éviter les régressions), j'ai modulé un peu le test ici 8dba0bf.

@@ -0,0 +1,46 @@
defmodule Transport.NeTEx.ArchiveParserTest do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Je me souviens que tu as levé ce point quelques fois.

Le lien que tu as partagé montre que c'est une question de préférence personnelle.

(d'ailleurs personnellement je trouve ça plus simple cognitivement, dans les tests, où ce code n'est pas réutilisé ailleurs, d'avoir les helpers défini avant leur utilisation, ou encore mieux, dans des modules externes si on peut, plutôt qu'en bas).

Si ce point t'embête et vu que tu l'as évoqué quelques fois déjà, parlons-en tranquille à un point dév pour voir si ça vaut le coup d'homogénéiser les tests là dessus, si tu le souhaites.

tmp_file = System.tmp_dir!() |> Path.join("temp-netex.zip")
ZipCreator.create!(tmp_file, [{"arrets.xml", some_netex_content()}])

# given a zip netex archive containing 2 files, I want the output I expected
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tout à fait, merci pour le catch. Corrigé dans 5858db6

Comment on lines +58 to +70
IO.puts("Processing file #{r.id}")

try do
count =
Transport.NeTEx.read_all_stop_places(r.local_path)
|> Enum.flat_map(fn {_file, stops} -> stops end)
# some stop places have no latitude in NeTEx
|> Enum.reject(fn p -> is_nil(p[:latitude]) end)
|> Enum.count()

IO.puts("#{count} StopPlaces detected")
rescue
e -> IO.puts("Som'thing bad happened")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il y a des cas variés, je préfère ne pas faire de détail pour l'instant car ça prendrait un brin de temps, une prochaine itération capturera ça en détail et j'aurai alors l'opportunité de partager ça (notamment pour améliorer le parser, certains fichiers ne passant pas).

Je contribuerai sur:

et j'en profiterai alors pour ou bien traiter ces cas, ou bien les afficher clairement.


netex =
df
|> Task.async_stream(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Il y a bien un helper de ce type (ici: https://github.com/etalab/transport-site/blob/master/apps/shared/lib/req_custom_cache.ex) mais il n'est pas adapté à l'usage ici. Le cache évoqué dans ce lien stocke la totalité de l'objet réponse Elixir Req sur le disque, ce qui empêche d'avoir "juste le body zip" à part.

Pour faire plus simple pour le moment et reporter ce refactoring intéressant, j'ai préféré ne pas modifier le cache, et réimplémenter, le temps d'avoir + de recul et de pouvoir généraliser et modifier le cache (éventuellement).

Comment on lines +12 to +16
:zip.create(
zip_filename,
file_data
|> Enum.map(fn {name, content} -> {name |> to_charlist(), content} end)
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Peut-être stocker en mémoire et non sur le disque ? Semble possible en lisant https://www.erlang.org/doc/apps/stdlib/zip.html#zip/2

Merci pour la suggestion. En fait, notre "traversal de zip" est conçu pour travailler avec des fichiers sur disque uniquement, afin d'éviter une surcharge de la RAM en production (certains fichiers dépassent 500 MB). Le stockage en mémoire n'est donc pas adapté ici.

La codebase inclut plusieurs tests qui ne nettoient pas automatiquement les fichiers tmp. Un helper de nettoyage pourrait être utile à l'avenir (comme Dir.mktmpdir en Ruby), c'est une idée qu'on peut conserver !

Pour éviter tout souci, j'ai toutefois ajouté de l'unicité dans le commit e2ff1c4.

@thbar thbar enabled auto-merge August 7, 2024 16:13
@thbar thbar disabled auto-merge August 7, 2024 16:14
@thbar thbar requested a review from AntoineAugusti August 7, 2024 16:25
@thbar
Copy link
Contributor Author

thbar commented Aug 8, 2024

Merci @AntoineAugusti pour la review !

@thbar thbar added this pull request to the merge queue Aug 8, 2024
Merged via the queue into master with commit 63a5e48 Aug 8, 2024
4 checks passed
@thbar thbar deleted the implement-netex-extractors branch August 8, 2024 06:48
@ptitfred ptitfred mentioned this pull request Sep 18, 2024
23 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants