Resultats de l'analyse
43 |Référence (Code - Article) | 47 |Statut | 48 |Texte | 49 |Période de validité | 50 |
---|
├── .gitignore
├── .idea
└── .gitignore
├── LICENCE_fr.txt
├── Readme.md
├── app.py
├── cgu.html
├── check_validity.py
├── code_list.html
├── code_references.py
├── codeislow.py
├── docs
└── Licence_CeCILL_V2.1-fr.txt
├── documentation.html
├── dotenv.example
├── form.html
├── help.html
├── home.html
├── index.html
├── matching.py
├── parsing.py
├── pytest.ini
├── request_api.py
├── requesting.py
├── requirements.txt
├── result_templates.py
├── results.tpl
├── templates
├── about.html
├── base.html
├── cgu.html
├── codes.html
└── home.html
└── tests
├── index.html
├── matching.log
├── needles.csv
├── newtest.doc
├── newtest.docx
├── newtest.md
├── newtest.pdf
├── newtest2.pdf
├── parsing.md
├── test_001_parsing.py
├── test_002_code_references.py
├── test_003_matching.py
├── test_004_request.py
├── test_005_check_validity.py
├── test_006_integration.py
├── testnew.odt
├── testnew.pdf
└── testnew_highlighted.odt
/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | .env
3 | .idea/codeislow.iml
4 | .idea/inspectionProfiles/profiles_settings.xml
5 | .idea/modules.xml
6 | .idea/workspace.xml
7 | .idea/vcs.xml
8 | debug/codeislow-logs-1654119119213.txt
9 | debug/codeislow-logs-1654247407754.txt
10 | tmp/empty
11 | tmp/newtest.docx
12 | tmp/PUAM.docx
13 | tmp/test.docx
14 | .idea/misc.xml
15 | tmp/newtest.pdf
16 | tmp/article.pdf
17 | debug/analyse.txt
18 | debug/Résultats bug.odt
19 | debug/Résultats corrigés.odt
20 | tmp/PUAM-Les-garanties-indemnitaires.odt
21 | tmp/PUAM-Les-garanties-indemnitaires.pdf
22 | tmp/these2.pdf
23 | tmp/these.pdf
24 | .DS_Store
25 | .idea/.name
26 | .idea/inspectionProfiles/Project_Default.xml
27 |
28 | # Byte-compiled / optimized / DLL files
29 | __pycache__/
30 | *.py[cod]
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/LICENCE_fr.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/LICENCE_fr.txt
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | # Code is low
2 | Ce programme est conçu pour rechercher, à l'intérieur d'un fichier texte, des références à des articles de codes de droit français (comme le Code civil, le Code de commerce, le Code général des collectivités territoriales...). Il est écrit en Python.
3 |
4 | ## Formulaire utilisateur
5 |
6 | Une page d'accueil est générée en utilisant le [framework Bottle](https://bottlepy.org/docs/dev/). L'utilisateur y indique quel fichier il entend analyser. Les formats actuellement acceptés sont ODT, DOCX et PDF. La taille est limitée à 2 Mo, car le processus d'analyse est gourmand en ressources. Cela est suffisant pour soumettre même des thèses de doctorat aux formats ODT/DOCX. Le PDF doit être utilisé faute d'alternative, par exemple pour soumettre un article de tiers téléchargé en libre accès.
7 |
8 | L'utilisateur indique sur quelles périodes passée et future il convient de vérifier si l'article de code a connu ou connaîtra d'autres versions. Le champ demande des années mais accepte des nombres décimaux, ce qui permet par exemple une vérification sur les six derniers mois.
9 |
10 | ## Ouverture du fichier
11 |
12 | Le fichier est provisoirement enregistré sur le serveur puis passé à différentes libraries selon le format utilisé : [python-docx](https://python-docx.readthedocs.io/en/latest/), [odfpy](https://pypi.org/project/odfpy/) ou [PyPDF2](https://pypi.org/project/PyPDF2/). Dès que le fichier a été transformé en chaîne de caractères, il est supprimé du serveur.
13 |
14 | ## Expressions régulières
15 |
16 | Le programme confronte ensuite l'ensemble du texte à une expression régulière par code de droit français. Cette méthode est simple mais brutale et devrait être améliorée à l'avenir afin de consommer moins de ressources.
17 |
18 | Le résultat des expressions régulières est nettoyé au fur et à mesure, pour anticiper la requête qui sera envoyée à Légifrance. Par exemple, "L. 112-1" doit devenir "L112-1".
19 |
20 | Il ressort de ces opérations un dictionnaire de résultats, distinguant les différents codes. A l'intérieur de chaque code identifié, chaque article est à ce stade rattaché à une seule propriété, son numéro au sein du code.
21 |
22 | ## Interrogation de Légifrance
23 |
24 | La base de données [Légifrance](https://www.legifrance.gouv.fr/), gérée par la [DILA](https://www.dila.premier-ministre.gouv.fr/), dispose d'une API que le programme peut interroger, les données étant placées sous [licence ouverte 2.0](https://www.etalab.gouv.fr/wp-content/uploads/2017/04/ETALAB-Licence-Ouverte-v2.0.pdf).
25 |
26 | Interroger Légifrance suppose une authentification préalable à l'aide d'identifiants personnels. La [version accessible à tous de code is low](codeislow.enetter.fr) utilise les identifiants du développeur. Exécuter le programme par vos propres moyens implique l'obtention d'identifiants Légifrance (voir plus bas).
27 |
28 | Au moment de l'authentification, Légifrance accorde un jeton valable une heure seulement, et qui devra être présenté à chaque requête. Le programme est donc conçu pour demander un nouveau jeton à chaque utilisation.
29 |
30 | Pour chaque article de code, son identifiant est récupéré à l'aide d'une première requête. Si l'article existe (il n'y a pas d'erreur dans sa référence et il n'a pas été abrogé), une seconde requête permet de récupérer un vaste ensemble d'informations. On y récupère la date à laquelle a débuté la version de l'article actuellement en vigueur et, le cas échéant, la date à laquelle elle deviendra obsolète (abrogation avec effet différé, remplacement par une nouvelle version).
31 |
32 | ## Tri et affichage des résultats
33 |
34 | Les articles n'ayant pas renvoyé d'identifiant unique sont placés dans une liste de textes non trouvés.
35 |
36 | Pour les autres, la date de début et la date de fin de version d'article sont confrontées avec les périodes définies par l'utilisateur dans le formulaire initial. En fonction du résultat, l'article peut être classé comme ayant connu une version passée dans la période de référence pour le passé ET comme ayant vocation à changer dans la période de référence pour le futur. S'il n'a ni été modifié dans la période passée ni ne sera modifié dans la période future, il est classé dans la catégorie des articles correctements détectés mais ne présentant pas d'événement.
37 |
38 | A partir de ces différentes listes, une page de résultats est générée dynamiquement. Hormis les articles non trouvés, les textes sont cliquables et le lien conduit vers leur version sur Légifrance. Le lien est construit à partir d'une racine commune suivie de l'identifiant unique rapatrié au moment de la première requête.
39 |
40 | ## Exécuter le programme localement
41 |
42 | Une version du programme utilisable par tous est mise à disposition sur un serveur Heroku. Cela permet à l'utilisateur de profiter des identifiants du développeur sans qu'ils soient révélés.
43 |
44 | En dépit des précautions employées (connexion forcée en HTTPS, fichier supprimé avant la fin du script), il est déconseillé de soumettre à la version collective des fichiers contenant des données sensibles, confidentielles ou soumises à un secret professionnel. Le programme peut être exécuté sur votre machine et générera une page web identique purement locale. Seules les requêtes Légifrance sortiront vers l'extérieur. Cette solution requiert l'installation de Python, le clonage ou le téléchargement du dépôt Github, et [l'obtention d'identifiants pour l'API auprès de PISTE](https://developer.aife.economie.gouv.fr/).
45 |
46 | Il vous faudra alors créer un fichier intitulé .env, que vous placerez dans le même répertoire que le script codeislow.py, avec le contenu ci-dessous, en remplaçant évidemment "XXXX" par les valeurs qui vous auront été fournies par PISTE.
47 |
48 | CLIENT_ID = XXXX
49 | CLIENT_SECRET = XXXX
50 | si la version en cours de code is low utilise encore un mot de passe, vous devrez ajouter un champ PASSWORD = et y placer la valeur de votre choix.
51 |
52 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # coding: utf-8
3 | #filename: app.py
4 | """
5 | Main Bottle app
6 |
7 | """
8 | import os
9 | import json
10 |
11 | from bottle import Bottle
12 | from bottle import request, static_file
13 | from jinja2 import Environment, FileSystemLoader
14 | from code_references import CODE_REFERENCE, CODE_REGEX
15 | from codeislow import main, load_result
16 | from result_templates import start_results, end_results
17 |
18 | app = Bottle()
19 |
20 | environment = Environment(loader=FileSystemLoader("templates/"))
21 |
22 | @app.route("/")
23 | def home():
24 | template = environment.get_template("home.html")
25 | return template.render(code_names=list(CODE_REFERENCE.items()))
26 |
27 |
28 | @app.route("/cgu/")
29 | def cgu():
30 | template = environment.get_template("cgu.html")
31 | return template.render()
32 |
33 | @app.route("/about/")
34 | def about():
35 | template = environment.get_template("about.html")
36 | return template.render()
37 |
38 | @app.route("/codes/")
39 | def codes():
40 | code_full_list = []
41 | for short_code, long_code in CODE_REFERENCE.items():
42 | regex_c = CODE_REGEX[short_code]
43 | regex = f"{regex_c}
"
44 | comment = """?"""
45 | code_full_list.append((long_code, short_code, regex, comment))
46 | template = environment.get_template("codes.html")
47 | return template.render(codes_full_list=code_full_list)
48 |
49 | # @app.route('/ajax')
50 | # def ajax():
51 | # template = environment.get_template("results.html")
52 | # return template.render('results.html',
53 | # result=result)
54 | #https://stackoverflow.com/questions/69125397/call-function-with-arguments-from-user-input-in-python3-flask-jinja2-template
55 | #https://stackoverflow.com/questions/6036082/call-a-python-function-from-jinja2
56 |
57 | @app.route("/upload/", method="POST")
58 | def upload():
59 | upload = request.files.get('upload')
60 | name, ext = os.path.splitext(upload.filename)
61 |
62 | if ext not in ('.doc','.docx','.odt', '.pdf'):
63 | return 'Le format du fichier est incorrect'
64 | file_path = os.path.join("tmp", upload.filename)
65 | upload.save(file_path)
66 | past = int(request.forms.get('user_past'))
67 | future = int(request.forms.get('user_future'))
68 | selected_codes = [short_name for short_name in CODE_REFERENCE.keys() if request.forms.get(short_name) is not None]
69 | if len(selected_codes) == 0:
70 | selected_codes = None
71 | yield start_results
72 | for row in load_result(file_path, None, "article_code", past, future):
73 | yield row
74 | # row = f'''
75 | #
Source : DILA - Données Légifrance exploitées en 28 | temps réel sous licence ouverte 2.0.
31 | 32 |Le développeur propose, à titre gracieux et non-professionnel, un outil 62 | expérimental d'aide au traitement de textes juridiques. Il décline toute 63 | responsabilité pour le cas où les références à des articles de codes de droit français ne seraient 64 | pas correctement détectées. La responsabilité de l'exactitude des informations figurant sur la base 65 | de données Légifrance relève, quant à elle, 66 | exclusivement de la DILA. Pour le surplus, 67 | l'utilisation de ce logiciel est soumise à la licence CeCILL.
69 |L'application web est proposée par E. Netter, à titre individuel, sans impliquer l'université à 76 | laquelle il appartient. E. Netter est responsable du traitement des données à caractère personnel 77 | que vos documents pourraient contenir, au sens du règlement général sur la protection des 79 | données et de la loi 80 | informatique et libertés. Le serveur est opéré par Heroku, qui occupe une position de sous-traitant des données, 82 | rémunéré par E. Netter. Vos données sont traitées sur le fondement de la nécessité pour l'exécution 83 | d'une prestation de service dont vous demandez à bénéficier (art. 6, 85 | 1, b RGPD). 86 |
87 | 88 |Conformément au code 89 | source public, votre document est copié dans un fichier provisoire uniquement le temps 90 | d'être traité, puis supprimé (ligne "os.remove") avant même l'affichage des résultats. Au cas où la 91 | suppression échouerait en raison d'un bug, les fichiers sont automatiquement détruits par Heroku 92 | toutes les 24h, et le développeur s'engage à ne pas en prendre connaissance. Si vous souhaitez 93 | cependant qu'il examine votre document afin de comprendre pourquoi un examen a échoué, vous pouvez 94 | choisir de lui adresser, à l'adresse email située en haut de page. Il s'engage alors à ne 95 | l'exploiter à aucune autre fin puis à la détruire. L'échange entre votre navigateur et le serveur 96 | doit être chiffré. Si votre navigateur présente un cadenas ouvert ou indique que la connexion n'est 97 | pas sécurisée, il s'agit d'un dysfonctionnement et l'outil ne devrait pas être utilisé.
98 | 99 |Il est également possible de préférer à cette application web une exécution locale du code source sur votre propre système 101 | informatique. C'est le mode vivement recommandé pour le cas où votre document contiendrait des 102 | données confidentielles, particulièrement sensibles, ou soumises au secret professionnel. 103 | Rapprochez-vous de la personne responsable de vos systèmes d'information. L'ouverture d'un compte développeur PISTE (rapide et 105 | gratuite) sera alors nécessaire.
106 | 107 |Les droits qui vous sont reconnus par le RGPD et la LIL vous 109 | sont présentés par l'auteur sur son site de chercheur en droit.
110 | 111 |Si vous estimez que vos droits ont été méconnus, je vous invite à me contacter avant toute autre 112 | démarche, à l'adresse email figurant en haut de la page. Vous disposez du droit de déposer une 113 | réclamation auprès de la 114 | CNIL.
115 | 116 |Source : DILA - Données Légifrance exploitées en temps réel sous licence ouverte 2.0.
23 |La forme longue est évidemment reconnue. La casse est indifférente. 53 | N'hésitez pas à demander l'ajout d'autres codes.
54 | 55 |Source : DILA - Données Légifrance exploitées en 27 | temps réel sous licence ouverte 2.0.
30 | 31 |A partir d'un document (< 2 Mo au format docx
,
67 | odt
ou
68 | pdf
)
70 | le programme recherchera dans le texte les références à des articles de code
71 |selon le format d'annotation utilisé dans le texte:
72 |Il interrogera Légifrance à propos des articles détectés. 85 | En fonction de la plage temporelle choisie, 86 | vous saurez ainsi :
87 | 88 |Source : DILA - Données Légifrance exploitées en 27 | temps réel sous licence ouverte 2.0.
30 | 31 |Source : DILA - Données Légifrance exploitées en temps réel sous licence ouverte 2.0.
12 |Soumettez un texte au format DOCX ou ODT (de préférence) ou PDF . 41 | Actuellement, la taille du fichier est limitée à 2 Mo
42 |Le format PDF n'est pas toujours lisible. Les résultats peuvent être faussés.
43 |Le programme recherchera des références à des articles de code (ex : "art. 1240 C. civ.", "article L. 110-1 du Code de commerce"). Il interrogera Légifrance à propos des articles détectés. Vous saurez ainsi :
44 | 45 | La page du dépôt sur Github contient un fichier readme plus détaillé.
52 |
53 |
Le développeur propose, à titre gracieux et non-professionnel, un outil expérimental d'aide au traitement de textes juridiques. Il décline toute responsabilité pour le cas où les références à des articles de codes de droit français ne seraient pas correctement détectées. La responsabilité de l'exactitude des informations figurant sur la base de données Légifrance relève, quant à elle, exclusivement de la DILA. Pour le surplus, l'utilisation de ce logiciel est soumise à la licence CeCILL.
61 |L'application web est proposée par E. Netter, à titre individuel, sans impliquer l'université à laquelle il appartient. E. Netter est responsable du traitement des données à caractère personnel que vos documents pourraient contenir, au sens du règlement général sur la protection des données et de la loi informatique et libertés. Le serveur est opéré par Heroku, qui occupe une position de sous-traitant des données, rémunéré par E. Netter. Vos données sont traitées sur le fondement de la nécessité pour l'exécution d'une prestation de service dont vous demandez à bénéficier (art. 6, 1, b RGPD). 70 |
71 | 72 |Conformément au code source public, votre document est copié dans un fichier provisoire uniquement le temps d'être traité, puis supprimé (ligne "os.remove") avant même l'affichage des résultats. Au cas où la suppression échouerait en raison d'un bug, les fichiers sont automatiquement détruits par Heroku toutes les 24h, et le développeur s'engage à ne pas en prendre connaissance. Si vous souhaitez cependant qu'il examine votre document afin de comprendre pourquoi un examen a échoué, vous pouvez choisir de lui adresser, à l'adresse email située en haut de page. Il s'engage alors à ne l'exploiter à aucune autre fin puis à la détruire. L'échange entre votre navigateur et le serveur doit être chiffré. Si votre navigateur présente un cadenas ouvert ou indique que la connexion n'est pas sécurisée, il s'agit d'un dysfonctionnement et l'outil ne devrait pas être utilisé.
73 | 74 |Il est également possible de préférer à cette application web une exécution locale du code source sur votre propre système informatique. C'est le mode vivement recommandé pour le cas où votre document contiendrait des données confidentielles, particulièrement sensibles, ou soumises au secret professionnel. Rapprochez-vous de la personne responsable de vos systèmes d'information. L'ouverture d'un compte développeur PISTE (rapide et gratuite) sera alors nécessaire.
75 | 76 |Les droits qui vous sont reconnus par le RGPD et la LIL vous sont présentés par l'auteur sur son site de chercheur en droit.
77 | 78 |Si vous estimez que vos droits ont été méconnus, je vous invite à me contacter avant toute autre démarche, à l'adresse email figurant en haut de la page. Vous disposez du droit de déposer une réclamation auprès de la CNIL.
79 | 80 |La forme longue est évidemment reconnue. La casse est indifférente. N'hésitez pas à demander l'ajout d'autres codes.
89 | 90 |
85 | code = [k for k in qualified_needle.keys() if k not in ["ref", "art"]][0]
86 |
87 | ref = match.group("ref").strip()
88 | # split multiple articles of a same code
89 | refs = [
90 | n
91 | for n in re.split(r"(\set\s|,\s|\sdu)", ref)
92 | if n not in [" et ", ", ", " du", " ", ""]
93 | ]
94 | # normalize articles to remove dots, spaces, caret and 'alinea'
95 | refs = [
96 | "-".join(
97 | [
98 | r
99 | for r in re.split(r"\s|\.|-", ref)
100 | if r not in [" ", "", "al", "alinea", "alinéa"]
101 | ]
102 | )
103 | for ref in refs
104 | ]
105 | # clean caracters for everything but numbers and (L|A|R|D) and caret
106 | normalized_refs = []
107 | for ref in refs:
108 | # accepted caracters for article
109 | ref = "".join(
110 | [n for n in ref if (n.isdigit() or n in ["L", "A", "R", "D", "-"])]
111 | )
112 | if ref.endswith("-"):
113 | ref = ref[:-1]
114 | # remove caret separating article nb between first letter
115 | special_ref = ref.split("-", 1)
116 | if special_ref[0] in ["L", "A", "R", "D"]:
117 | normalized_refs.append("".join(special_ref))
118 | else:
119 | normalized_refs.append(ref)
120 |
121 | if code not in code_found:
122 | # append article references
123 | code_found[code] = normalized_refs
124 | else:
125 | # append article references to existing list
126 | code_found[code].extend(normalized_refs)
127 |
128 | return code_found
129 |
130 | def get_matching_result_item(full_text, selected_shortcodes=[], pattern_format="article_code"):
131 | """"
132 | Renvoie les références des articles détectés dans le texte
133 |
134 | Arguments
135 | -----------
136 | full_text: str
137 | a string of the full document normalized
138 | selected_shortcodes: array
139 | a list of selected codes in short format for filtering article detection. Default is an empty list (which stands for no filter)
140 | pattern_format: str
141 | a string representing the pattern format article_code or code_article. Defaut to article_code
142 |
143 | Yields
144 | --------
145 | code_short_name:str
146 |
147 | article_number:str
148 | """
149 | article_pattern = switch_pattern(selected_shortcodes, pattern_format)
150 | # normalisation des espaces dans le texte
151 | full_text = re.sub(r"\r|\n|\t|\f|\xa0", " ", " ".join(full_text))
152 | for i, match in enumerate(re.finditer(article_pattern, full_text)):
153 | needle = match.groupdict()
154 | qualified_needle = {
155 | key: value for key, value in needle.items() if value is not None
156 | }
157 | msg = f"#{i+1}\t{qualified_needle}"
158 | # logging.debug(msg)
159 | # get the code shortname based on regex group name
160 | code = [k for k in qualified_needle.keys() if k not in ["ref", "art"]][0]
161 |
162 | ref = match.group("ref").strip()
163 | # split multiple articles of a same code example: Article 22, 23 et 24 du Code
164 | refs = [
165 | n
166 | for n in re.split(r"(\set\s|,\s|\sdu)", ref)
167 | if n not in [" et ", ", ", " du", " ", ""]
168 | ]
169 | # normalize articles to remove dots, spaces, caret and 'alinea'
170 | refs = [
171 | "-".join(
172 | [
173 | r
174 | for r in re.split(r"\s|\.|-", ref)
175 | if r not in [" ", "", "al", "alinea", "alinéa"]
176 | ]
177 | )
178 | for ref in refs
179 | ]
180 | # clean caracters for everything but numbers and (L|A|R|D) and caret
181 | for ref in refs:
182 | # accepted caracters for article
183 | # exemple: 1224 du => Non 298 al 32 => Non R-288 => oui A-24-14=> oui
184 | ref = "".join(
185 | [n for n in ref if (n.isdigit() or n in ["L", "A", "R", "D", "-"])]
186 | )
187 | if ref.endswith("-"):
188 | ref = ref[:-1]
189 | # remove caret separating article nb between first letter
190 | # exemple: L-248-1 = > L248-1
191 | special_ref = ref.split("-", 1)
192 | if special_ref[0] in ["L", "A", "R", "D"]:
193 | yield(code, "".join(special_ref))
194 |
195 | else:
196 | yield(code, ref)
--------------------------------------------------------------------------------
/parsing.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # filename: parsing.py
3 |
4 | """
5 | Parsing file module:
6 |
7 | Load document with the accepted extensions and transform into list of text
8 |
9 | """
10 |
11 |
12 | import docx
13 | from PyPDF2 import PdfReader
14 | from odf import text, teletype
15 | from odf.opendocument import load
16 |
17 | ACCEPTED_EXTENSIONS = ("odt", "pdf", "docx", "doc")
18 |
19 |
20 | def parse_doc(file_path):
21 | """
22 | Parcourir le document pour en extraire le texte
23 | Arguments
24 | ----------
25 | file_path: str
26 | absolute filepath of the document
27 | Returns
28 | ----------
29 | full_text: array
30 | a list of sentences.
31 | Raises
32 | ----------
33 | Exception:
34 | Extension incorrecte. Les types de fichiers supportés sont odt, doc, docx, pdf
35 | FileNotFoundError:
36 | File has not been found. File_path must be incorrect
37 | """
38 |
39 | doc_name, doc_ext = file_path.split("/")[-1].split(".")
40 | if doc_ext not in ACCEPTED_EXTENSIONS:
41 | raise ValueError(
42 | "Extension incorrecte: les fichiers acceptés terminent par *.odt, *.docx, *.doc, *.pdf"
43 | )
44 |
45 | full_text = []
46 | if doc_ext == "pdf":
47 | with open(file_path, "rb") as f:
48 | reader = PdfReader(f)
49 |
50 | for i in range(len(reader.pages)):
51 | page = reader.pages[i]
52 | full_text.extend((page.extract_text()).split("\n"))
53 |
54 | elif doc_ext == "odt":
55 | with open(file_path, "rb") as f:
56 | document = load(f)
57 | paragraphs = document.getElementsByType(text.P)
58 | for i in range(len(paragraphs)):
59 | full_text.append((teletype.extractText(paragraphs[i])))
60 | else:
61 | # if doc_ext in ["docx", "doc"]:
62 | with open(file_path, "rb") as f:
63 | document = docx.Document(f)
64 | paragraphs = document.paragraphs
65 | for i in range(len(paragraphs)):
66 | full_text.append((paragraphs[i].text))
67 | full_text = [n for n in full_text if n not in ["\n", "", " "]]
68 | os.remove(file_path)
69 | return full_text
--------------------------------------------------------------------------------
/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | env_files =
3 | .env
4 | .test.env
5 | .deploy.env
--------------------------------------------------------------------------------
/request_api.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # filename: request_api.py
3 | """
4 | Module pour requeter l'API
5 |
6 | - authentification
7 | - get_article_id
8 | - get_article_content
9 | - get_article: module complet avec le status de l'article
10 | """
11 |
12 | import requests
13 | import time
14 | from dotenv import load_dotenv
15 | from code_references import get_code_full_name_from_short_code
16 | from check_validity import convert_epoch_to_datetime, convert_datetime_to_str, get_validity_status
17 |
18 | API_ROOT_URL = "https://sandbox-api.piste.gouv.fr/dila/legifrance-beta/lf-engine-app/"
19 | # API_ROOT_URL = "https://api.piste.gouv.fr/dila/legifrance-beta/lf-engine-app/",
20 |
21 |
22 |
23 | def get_legifrance_auth(client_id, client_secret):
24 | """
25 | Get authorization token from LEGIFRANCE API
26 |
27 | Arguments
28 | ---------
29 | client_id: str
30 | OAUTH CLIENT key provided by API
31 | client_secret: str
32 | OAUTH SECRET key provided by API
33 |
34 | Returns
35 | ---------
36 | authorization_header: dict
37 | a header composed of a json dict with access_token
38 |
39 | Raise
40 | ------
41 | Exception:
42 | No credentials have been set. Client_id or client_secret is None
43 | Exception:
44 | Invalid credentials. Request to authentication server failed with 400 or 401 error
45 | """
46 |
47 | TOKEN_URL = "https://sandbox-oauth.piste.gouv.fr/api/oauth/token"
48 | # TOKEN_URL = "https://sandbox-oauth.aife.economie.gouv.fr/api/oauth/token"
49 |
50 | if client_id is None or client_secret is None:
51 | # return HTTPError(401, "No credential have been set")
52 | raise ValueError(
53 | "No credential: client_id or/and client_secret are not set. \nPlease register your API at https://developer.aife.economie.gouv.fr/"
54 | )
55 | session = requests.Session()
56 | with session as s:
57 | res = s.post(
58 | TOKEN_URL,
59 | data={
60 | "grant_type": "client_credentials",
61 | "client_id": client_id,
62 | "client_secret": client_secret,
63 | "scope": "openid",
64 | },
65 | )
66 |
67 | if res.status_code in [400, 401]:
68 | # return HTTPError(res.status_code, "Unauthorized: invalid credentials")
69 | raise Exception(f"HTTP Error code: {res.status_code}: Invalid credentials")
70 | token = res.json()
71 | access_token = token["access_token"]
72 | return {"Authorization": f"Bearer {access_token}"}
73 |
74 |
75 | def get_article_uid(short_code_name, article_number, headers):
76 | """
77 | GET the article uid given by [Legifrance API](https://developer.aife.economie.gouv.fr/index.php?option=com_apiportal&view=apitester&usage=api&apitab=tests&apiName=L%C3%A9gifrance+Beta&apiId=426cf3c0-1c6d-46ba-a8b0-f79289086ed5&managerId=2&type=rest&apiVersion=1.6.2.5&Itemid=402&swaggerVersion=2.0&lang=fr)
78 |
79 | Arguments
80 | ---------
81 | code_name:str
82 | Nom du code de droit français (version courte)
83 | article_number: str
84 | Référence de l'article mentionné (version normalisée eg. L25-67)
85 |
86 | Returns
87 | --------
88 | article_uid: str
89 | Identifiant unique de l'article dans Legifrance LEGIART000xxxx or None
90 | Raises
91 | ------
92 | ValueError:
93 | Le nom du code est incorrect
94 | Exception:
95 | La requete a échoué response.status_code [400-500]
96 | """
97 | long_code = get_code_full_name_from_short_code(short_code_name)
98 | if long_code is None:
99 | raise ValueError(f"`{short_code_name}` not found in the supported Code List")
100 |
101 | session = requests.Session()
102 |
103 | today_epoch = int(time.time()) * 1000
104 | data = {
105 | "recherche": {
106 | "champs": [
107 | {
108 | "typeChamp": "NUM_ARTICLE",
109 | "criteres": [
110 | {
111 | "typeRecherche": "EXACTE",
112 | "valeur": article_number,
113 | "operateur": "ET",
114 | }
115 | ],
116 | "operateur": "ET",
117 | }
118 | ],
119 | "filtres": [
120 | {"facette": "NOM_CODE", "valeurs": [long_code]},
121 | {"facette": "DATE_VERSION", "singleDate": today_epoch},
122 | ],
123 | "pageNumber": 1,
124 | "pageSize": 10,
125 | "operateur": "ET",
126 | "sort": "PERTINENCE",
127 | "typePagination": "ARTICLE",
128 | },
129 | "fond": "CODE_DATE",
130 | }
131 | with session as s:
132 | response = s.post(
133 | "/".join([API_ROOT_URL, "search"]), headers=headers, json=data
134 | )
135 | if response.status_code > 399:
136 | # print(response)
137 | # return None
138 | raise Exception(f"Error {response.status_code}: {response.reason}")
139 |
140 | article_informations = response.json()
141 | if not article_informations["results"]:
142 | return None
143 |
144 | results = article_informations["results"]
145 | if len(results) == 0:
146 | return None
147 | else:
148 | # get the first result
149 | try:
150 | return results[0]["sections"][0]["extracts"][0]["id"]
151 | except IndexError:
152 | return None
153 |
154 |
155 |
156 | def get_article_content(article_id, headers):
157 | """
158 | GET article_content from LEGIFRANCE API using POST /consult/getArticle https://developer.aife.economie.gouv.fr/index.php?option=com_apiportal&view=apitester&usage=api&apitab=tests&apiName=L%C3%A9gifrance+Beta&apiId=426cf3c0-1c6d-46ba-a8b0-f79289086ed5&managerId=2&type=rest&apiVersion=1.6.2.5&Itemid=402&swaggerVersion=2.0&lang=fr
159 |
160 | Arguments
161 | ----------
162 | article_id: str
163 | article uid eg. LEGIARTI000006307920
164 | Returns
165 | -------
166 | article_content: dict
167 | a dictionnary with the full content of article
168 | Raise
169 | -------
170 | Exception
171 | response.status_code [400-500]
172 | """
173 | data = {"id": article_id}
174 | session = requests.Session()
175 | with session as s:
176 |
177 | response = s.post(
178 | "/".join([API_ROOT_URL, "consult", "getArticle"]),
179 | headers=headers,
180 | json=data,
181 | )
182 |
183 | if response.status_code > 399:
184 | raise Exception(f"Error {response.status_code}: {response.reason}")
185 | article_content = response.json()
186 | try:
187 | raw_article = article_content["article"]
188 | # FEATURE récupérer tous les titres et sections d'un article
189 | article = {
190 | "url": f"https://www.legifrance.gouv.fr/codes/article_lc/{article_id}"
191 | }
192 | for k in [
193 | "id",
194 | "num",
195 | "texte",
196 | "etat",
197 | "dateDebut",
198 | "dateFin",
199 | "articleVersions",
200 | ]:
201 | article[k] = raw_article[k]
202 | # FEATURE - integrer les différentes versions
203 | article["nb_versions"] = len(article["articleVersions"])
204 |
205 | return article
206 | except KeyError:
207 | return None
208 |
209 |
210 | def get_article_content_by_id_and_article_nb(article_id, article_num, headers):
211 | """
212 | Récupère un Article en fonction de son ID et Numéro article depuis API Legifrance GET /consult getArticleWithIdAndNum
213 | Arguments
214 | ---------
215 | article_id: str
216 | article uid eg. LEGIARTI000006307920
217 | article_num: str
218 | numéro de l'article standardisé eg. "3-45", "L214", "R25-64"
219 | Returns
220 | -------
221 | article_content: dict
222 | a dictionnary with the full content of article
223 | Raise
224 | -----
225 | Exception
226 | response.status_code [400-500]
227 | """
228 |
229 | data = {"id": article_id, "num": article_num}
230 |
231 | session = requests.Session()
232 | with session as s:
233 |
234 | response = s.post(
235 | "/".join([API_ROOT_URL, "consult", "getArticleWithIdandNum"]),
236 | headers=headers,
237 | json=data,
238 | )
239 | if response.status_code > 399:
240 | raise Exception(f"Error {response.status_code}: {response.reason}")
241 | article_content = response.json()
242 | return article_content["article"]
243 |
244 | def get_article(short_code_name, article_number, client_id, client_secret, past_year_nb=3, future_year_nb=3):
245 | """
246 | Accéder aux informations simplifiée de l'article
247 |
248 | Arguments
249 | ---------
250 | long_code_name: str
251 | Nom du code de loi française dans sa version longue
252 | article_number: str
253 | Numéro de l'article de loi normalisé ex. R25-67 L214 ou 2667-1-1
254 | Returns
255 | --------
256 | article: str
257 | Un dictionnaire json avec code (version courte), article (numéro), status, status_code, color, url, text, id, start_date, end_date, date_debut, date_fin
258 | """
259 |
260 | article = {
261 | "code": short_code_name,
262 | "code_full_name": get_code_full_name_from_short_code(short_code_name),
263 | "article": article_number,
264 | "status_code": 200,
265 | "status": "OK",
266 | "color": "secondary",
267 | "url": "",
268 | "texte": "",
269 | "date_debut": "",
270 | "date_fin": "",
271 | "id": get_article_uid(
272 | short_code_name, article_number, headers=get_legifrance_auth(client_id, client_secret)
273 | )
274 | }
275 | if article["id"] is None:
276 | article["color"] = "danger"
277 | article["status_code"] = 404
278 | article["status"] = "Indisponible"
279 | article["texte"] = "x"
280 | return article
281 | article_content = get_article_content(
282 | article["id"], headers=get_legifrance_auth(client_id, client_secret)
283 | )
284 | article["texte"] = article_content["texte"]
285 | article["url"] = article_content["url"]
286 | article["start_date"] = convert_epoch_to_datetime(article_content["dateDebut"])
287 | article["end_date"] = convert_epoch_to_datetime(article_content["dateFin"])
288 | article["status_code"], article["status"], article["color"] = get_validity_status(article["start_date"], article["end_date"], past_year_nb, future_year_nb)
289 | article["date_debut"] = convert_datetime_to_str(article["start_date"]).split(" ")[0]
290 | article["date_fin"] = convert_datetime_to_str(article["end_date"]).split(" ")[0]
291 | del article["start_date"]
292 | del article["end_date"]
293 | return article
294 |
295 |
296 |
--------------------------------------------------------------------------------
/requesting.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import time
4 | from dotenv import load_dotenv
5 | import pytest
6 |
7 | API_ROOT_URL = "https://sandbox-api.piste.gouv.fr/dila/legifrance-beta/lf-engine-app/"
8 | # API_ROOT_URL = "https://api.piste.gouv.fr/dila/legifrance-beta/lf-engine-app/",
9 |
10 | MAIN_CODELIST = {
11 | "CCIV": "Code civil",
12 | "CPRCIV": "Code de procédure civile",
13 | "CCOM": "Code de commerce",
14 | "CTRAV": "Code du travail",
15 | "CPI": "Code de la propriété intellectuelle",
16 | "CPEN": "Code pénal",
17 | "CPP": "Code de procédure pénale",
18 | "CASSUR": "Code des assurances",
19 | "CCONSO": "Code de la consommation",
20 | "CSI": "Code de la sécurité intérieure",
21 | "CSP": "Code de la santé publique",
22 | "CSS": "Code de la sécurité sociale",
23 | "CESEDA": "Code de l'entrée et du séjour des étrangers et du droit d'asile",
24 | "CGCT": "Code général des collectivités territoriales",
25 | "CPCE": "Code des postes et des communications électroniques",
26 | "CENV": "Code de l'environnement",
27 | "CJA": "Code de justice administrative",
28 | }
29 |
30 |
31 | def get_code_full_name_from_short_code(short_code):
32 | """
33 | Shortcut to get corresponding full_name from short_code
34 |
35 | Arguments:
36 | short_code: short form of Code eg. CCIV
37 | Returns:
38 | full_name: long form of code eg. Code Civil
39 | """
40 | try:
41 | return MAIN_CODELIST[short_code]
42 | except ValueError:
43 | return None
44 |
45 |
46 | def get_short_code_from_full_name(full_name):
47 | """
48 | Shortcut to get corresponding short_code from full_name
49 |
50 | Arguments:
51 | full_name: long form of code eg. Code Civil
52 | Returns:
53 | short_code: short form of Code eg. CCIV
54 | """
55 | keys = [k for k, v in MAIN_CODELIST.items() if v == full_name]
56 | if len(keys) > 0:
57 | return keys[0]
58 | else:
59 | return None
60 |
61 |
62 | def get_legifrance_auth(client_id, client_secret):
63 | """
64 | Get authorization token from LEGIFRANCE API
65 |
66 | Arguments:
67 | client_id: OAUTH CLIENT key provided by API
68 | client_secret: OAUTH SECRET key provided by API
69 |
70 | Returns:
71 | authorization_header: a header composed of a json dict with access_token
72 |
73 | Raise:
74 | Exception: No credentials have been set. Client_id or client_secret is None
75 | Exception: Invalid credentials. Request to authentication server failed with 400 or 401 error
76 | """
77 |
78 | TOKEN_URL = "https://sandbox-oauth.piste.gouv.fr/api/oauth/token"
79 | # TOKEN_URL = "https://sandbox-oauth.aife.economie.gouv.fr/api/oauth/token"
80 |
81 | if client_id is None or client_secret is None:
82 | # return HTTPError(401, "No credential have been set")
83 | raise ValueError(
84 | "No credential: client_id or/and client_secret are not set. \nPlease register your API at https://developer.aife.economie.gouv.fr/"
85 | )
86 | session = requests.Session()
87 | with session as s:
88 | res = s.post(
89 | TOKEN_URL,
90 | data={
91 | "grant_type": "client_credentials",
92 | "client_id": client_id,
93 | "client_secret": client_secret,
94 | "scope": "openid",
95 | },
96 | )
97 |
98 | if res.status_code in [400, 401]:
99 | # return HTTPError(res.status_code, "Unauthorized: invalid credentials")
100 | raise Exception(f"HTTP Error code: {res.status_code}: Invalid credentials")
101 | token = res.json()
102 | access_token = token["access_token"]
103 | return {"Authorization": f"Bearer {access_token}"}
104 |
105 |
106 | def get_article_uid(code_name, article_number, headers):
107 | """
108 | GET the article uid given by [Legifrance API](https://developer.aife.economie.gouv.fr/index.php?option=com_apiportal&view=apitester&usage=api&apitab=tests&apiName=L%C3%A9gifrance+Beta&apiId=426cf3c0-1c6d-46ba-a8b0-f79289086ed5&managerId=2&type=rest&apiVersion=1.6.2.5&Itemid=402&swaggerVersion=2.0&lang=fr)
109 |
110 | Arguments:
111 | code_name: Nom du code de droit français (version longue)
112 | article_number: Référence de l'article mentionné (version normalisée eg. L25-67)
113 |
114 | Returns:
115 | article: un dictionnaire qui contient l'identifiant unique de l'article dans legifrance, le nom du code en version courte et en version longue
116 | article_uid: Identifiant unique de l'article dans Legifrance or None
117 |
118 | """
119 | if code_name in list(MAIN_CODELIST.keys()):
120 | code_short = code_name
121 | code_long = MAIN_CODELIST[code_name]
122 | elif code_name in list(MAIN_CODELIST.values()):
123 | code_long = code_name
124 | code_short = get_short_code_from_full_name(code_long)
125 | else:
126 | raise ValueError(f"`{code_name}` not found in the supported Code List")
127 |
128 | session = requests.Session()
129 |
130 | today_epoch = int(time.time()) * 1000
131 | data = {
132 | "recherche": {
133 | "champs": [
134 | {
135 | "typeChamp": "NUM_ARTICLE",
136 | "criteres": [
137 | {
138 | "typeRecherche": "EXACTE",
139 | "valeur": article_number,
140 | "operateur": "ET",
141 | }
142 | ],
143 | "operateur": "ET",
144 | }
145 | ],
146 | "filtres": [
147 | {"facette": "NOM_CODE", "valeurs": [code_long]},
148 | {"facette": "DATE_VERSION", "singleDate": today_epoch},
149 | ],
150 | "pageNumber": 1,
151 | "pageSize": 10,
152 | "operateur": "ET",
153 | "sort": "PERTINENCE",
154 | "typePagination": "ARTICLE",
155 | },
156 | "fond": "CODE_DATE",
157 | }
158 | with session as s:
159 | response = s.post(
160 | "/".join([API_ROOT_URL, "search"]), headers=headers, json=data
161 | )
162 | if response.status_code > 399:
163 | # print(response)
164 | # return None
165 | raise Exception(f"Error {response.status_code}: {response.reason}")
166 |
167 | article_informations = response.json()
168 | if not article_informations["results"]:
169 | return None
170 |
171 | results = article_informations["results"]
172 | if len(results) == 0:
173 | return None
174 | else:
175 | # get the first results
176 | try:
177 | article_uid = results[0]["sections"][0]["extracts"][0]["id"]
178 | except IndexError:
179 | return None
180 | # article = {
181 | # "id": article_uid,
182 | # "code_name_short": code_short,
183 | # "code_name_long": code_long,
184 | # }
185 | return article_uid
186 |
187 |
188 | def get_article_content(article_id, headers):
189 | """
190 | GET article_content from LEGIFRANCE API using POST /consult/getArticle https://developer.aife.economie.gouv.fr/index.php?option=com_apiportal&view=apitester&usage=api&apitab=tests&apiName=L%C3%A9gifrance+Beta&apiId=426cf3c0-1c6d-46ba-a8b0-f79289086ed5&managerId=2&type=rest&apiVersion=1.6.2.5&Itemid=402&swaggerVersion=2.0&lang=fr
191 |
192 | Arguments:
193 | article_id: article uid eg. LEGIARTI000006307920
194 | Returns:
195 | article_content: a dictionnary with the full content of article
196 | Raise:
197 | Exception : response.status_code [400-500]
198 | """
199 | data = {"id": article_id}
200 | session = requests.Session()
201 | with session as s:
202 |
203 | response = s.post(
204 | "/".join([API_ROOT_URL, "consult", "getArticle"]),
205 | headers=headers,
206 | json=data,
207 | )
208 |
209 | if response.status_code > 399:
210 | raise Exception(f"Error {response.status_code}: {response.reason}")
211 | article_content = response.json()
212 | try:
213 | raw_article = article_content["article"]
214 | # FEATURE récupérer tous les titres et sections d'un article
215 | article = {
216 | "url": f"https://www.legifrance.gouv.fr/codes/article_lc/{article_id}"
217 | }
218 | for k in [
219 | "id",
220 | "num",
221 | "texte",
222 | "etat",
223 | "dateDebut",
224 | "dateFin",
225 | "articleVersions",
226 | ]:
227 | article[k] = raw_article[k]
228 | # FEATURE - integrer les différentes versions
229 | article["nb_versions"] = len(article["articleVersions"])
230 |
231 | return article
232 | except KeyError:
233 | return None
234 |
235 |
236 | def get_article_content_by_id_and_article_nb(article_id, article_num, headers):
237 | """
238 | Récupère un Article en fonction de son ID et Numéro article depuis API Legifrance GET /consult getArticleWithIdAndNum
239 | Arguments:
240 | article_id: article uid eg. LEGIARTI000006307920
241 | article_num: numéro de l'article standardisé eg. "3-45", "L214", "R25-64"
242 | Returns:
243 | article_content: a dictionnary with the full content of article
244 | Raise:
245 | Exception : response.status_code [400-500]
246 | """
247 |
248 | data = {"id": article_id, "num": article_num}
249 |
250 | session = requests.Session()
251 | with session as s:
252 |
253 | response = s.post(
254 | "/".join([API_ROOT_URL, "consult", "getArticleWithIdandNum"]),
255 | headers=headers,
256 | json=data,
257 | )
258 | if response.status_code > 399:
259 | raise Exception(f"Error {response.status_code}: {response.reason}")
260 | article_content = response.json()
261 | return article_content["article"]
262 |
263 |
264 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | bottle==0.12.23
2 | python-docx==0.8.11
3 | python-dotenv==0.20.0
4 | odfpy==1.4.1
5 | requests==2.27.1
6 | pdfminer.six==20220524
7 | urllib3~=1.26.11
8 | PyPDF2~=2.10.2
9 | pytest~=7.2.0
10 | pytest-dotenv==0.5.2
--------------------------------------------------------------------------------
/result_templates.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # coding: utf-8
3 |
4 | start_results="""
5 |
6 |
7 |
8 |
9 |
10 |
11 | Code is low - Accueil
12 |
13 |
14 |
15 |
16 |
17 |
40 |
41 |
42 | Resultats de l'analyse
43 |
44 |
45 |
46 | Référence (Code - Article)
47 | Statut
48 | Texte
49 | Période de validité
50 |
51 |
52 |
53 | """
54 | end_results="""
55 |
56 | Nouvelle Analyse
57 | Exporter au format csv
58 | Signaler une erreur
59 |
60 |
61 |
62 |
63 |
75 |
76 |
77 |
78 | """
--------------------------------------------------------------------------------
/results.tpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Code is low
5 |
6 |
7 |
8 |
9 |
10 |
11 | Code is low - Résultats
12 |
13 |
14 | Cliquez sur les numéros d'articles pour les ouvrir directement dans Légifrance et accéder à davantage d'informations (hors textes non trouvés).
15 |
16 |
17 | Textes non trouvés sur Légifrance
18 |
19 |
20 | % for result in articles_not_found:
21 | - {{!result}}
22 | % end
23 |
24 |
25 | Textes modifiés il y a moins de {{user_past}} an(s)
26 |
27 |
28 | % for result in articles_recently_modified:
29 | - {{!result}}
30 | % end
31 |
32 |
33 | Textes dont la version actuelle expire dans moins de {{user_future}} an(s)
34 |
35 |
36 | % for result in articles_changing_soon:
37 | - {{!result}}
38 | % end
39 |
40 |
41 | Textes détectés mais rien à signaler
42 |
43 |
44 | % for result in articles_without_event:
45 | - {{!result}}
46 | % end
47 |
48 |
49 |
--------------------------------------------------------------------------------
/templates/about.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% set active_page = "about" %}
3 |
4 | {% block page_title %}A propos{% endblock %}
5 | {% block content %}
6 | A propos
7 | {% endblock %}
8 |
--------------------------------------------------------------------------------
/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Code is low {% block page_title %}{% endblock %}
9 |
10 |
11 |
12 |
13 | {% set navigation_bar = [
14 | ('/', 'accueil', 'Accueil'),
15 | ('/cgu/', 'CGU', 'CGU'),
16 | ('/codes/', 'codes', 'Liste des codes'),
17 | ('/about/', 'about', 'A propos'),
18 | ] -%}
19 | {% set active_page = active_page|default('index') -%}
20 |
37 |
38 |
39 | {% block content %}{% endblock %}
40 |
41 |
42 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/templates/cgu.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% set active_page = "cgu" %}
3 |
4 | {% block page_title %}CGU{% endblock %}
5 | {% block content %}
6 | Conditions générales d'utilisation
7 |
8 | Le développeur propose, à titre gracieux et non-professionnel, un outil
9 | expérimental d'aide au traitement de textes juridiques. Il décline toute
10 | responsabilité pour le cas où les références à des articles de codes de droit français ne seraient
11 | pas correctement détectées. La responsabilité de l'exactitude des informations figurant sur la base
12 | de données Légifrance relève, quant à elle,
13 | exclusivement de la DILA. Pour le surplus,
14 | l'utilisation de ce logiciel est soumise à la licence CeCILL.
16 |
17 |
18 | L'application web est proposée par E. Netter, à titre individuel, sans impliquer l'université à
19 | laquelle il appartient. E. Netter est responsable du traitement des données à caractère personnel
20 | que vos documents pourraient contenir, au sens du règlement général sur la protection des
22 | données et de la loi
23 | informatique et libertés. Le serveur est opéré par Heroku, qui occupe une position de sous-traitant des données,
25 | rémunéré par E. Netter. Vos données sont traitées sur le fondement de la nécessité pour l'exécution
26 | d'une prestation de service dont vous demandez à bénéficier (art. 6,
28 | 1, b RGPD).
29 |
30 |
31 | Politique de confidentialité
32 | Conformément au code
33 | source public, votre document est copié dans un fichier provisoire uniquement le temps
34 | d'être traité, puis supprimé (ligne "os.remove") avant même l'affichage des résultats. Au cas où la
35 | suppression échouerait en raison d'un bug, les fichiers sont automatiquement détruits par Heroku
36 | toutes les 24h, et le développeur s'engage à ne pas en prendre connaissance. Si vous souhaitez
37 | cependant qu'il examine votre document afin de comprendre pourquoi un examen a échoué, vous pouvez
38 | choisir de lui adresser, à l'adresse email située en bas de page. Il s'engage alors à ne
39 | l'exploiter à aucune autre fin puis à la détruire. L'échange entre votre navigateur et le serveur
40 | doit être chiffré. Si votre navigateur présente un cadenas ouvert ou indique que la connexion n'est
41 | pas sécurisée, il s'agit d'un dysfonctionnement et l'outil ne devrait pas être utilisé.
42 |
43 | Il est également possible de préférer à cette application web une exécution locale du code source sur votre propre système
45 | informatique. C'est le mode vivement recommandé pour le cas où votre document contiendrait des
46 | données confidentielles, particulièrement sensibles, ou soumises au secret professionnel.
47 | Rapprochez-vous de la personne responsable de vos systèmes d'information. L'ouverture d'un compte développeur PISTE (rapide et
49 | gratuite) sera alors nécessaire.
50 |
51 | Les droits qui vous sont reconnus par le RGPD et la LIL vous
53 | sont présentés par l'auteur sur son site de chercheur en droit.
54 |
55 | Si vous estimez que vos droits ont été méconnus, je vous invite à me contacter avant toute autre
56 | démarche, à l'adresse email figurant en haut de la page. Vous disposez du droit de déposer une
57 | réclamation auprès de la
58 | CNIL.
59 | {% endblock %}
60 |
61 |
62 |
--------------------------------------------------------------------------------
/templates/codes.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% set active_page = "codes" %}
3 |
4 | {% block page_title %}Codes{% endblock %}
5 | {% block content %}
6 | Liste des codes actuellement supportés
7 |
8 |
9 | Actuellement
{{codes_full_list|length}}
codes de droit français
10 | sont supportés à la détection dans leur forme abbrégée comme dans leur nom complet. La casse est indifférente
11 | Proposer un ajout
12 |
13 |
14 |
15 |
16 |
17 |
18 | Nom du Code
19 | Abbréviation
20 | Expression rationnelle
21 | Commentaire
22 |
23 |
24 |
25 | {% for long_code, short_code, regex, comment in codes_full_list %}
26 |
27 | {{long_code}}
28 | {{short_code}}
29 | {{regex}}
30 | {{comment}}
31 |
32 | {%endfor%}
33 |
34 |
35 | {% endblock %}
36 |
--------------------------------------------------------------------------------
/templates/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% set active_page = "accueil" %}
3 |
4 | {% block page_title %}Accueil{% endblock %}
5 | {% block content %}
6 |
7 | Outil d'analyse de texte juridique
8 |
9 |
10 | Guide d'utilisation
11 | Soumettez le texte que vous souhaitez analyser
12 | au format DOC, DOCX ou ODT (de préférence) ou PDF.
13 | Le format PDF n'est pas toujours lisible. Les résultats peuvent être faussés.
14 |
15 | Actuellement, la taille du fichier est limitée à 2 Mospan>
16 |
17 | Définissez la plage temporelle de surveillance pour les articles
18 | Selectionnez les codes qui vous concernent
19 |
20 |
21 | Le programme recherchera des références à des articles de code
22 | (ex : "art. 1240 C. civ.", "article L. 110-1 du Code de commerce").
Il interrogera Légifrance à propos des articles détectés.
23 |
24 | Vous saurez ainsi :
25 | - Si le texte est introuvable sur Légifrance (abrogé, faute de frappe, erreur de détection, de mention...)
26 | - Si le texte a été récemment modifié (période définie par vous) (N.B. : seule la modification la plus récente est mentionnée)
27 | - Si le texte va être modifié prochainement (période définie par vous) N.B. : seule la version à venir la plus proche est mentionnée)
28 | - Si le texte n'a pas été modifié(pendant la période définie)
29 |
30 |
31 |
32 |
33 |
34 |
82 |
83 | {%endblock%}
84 |
--------------------------------------------------------------------------------
/tests/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Code is low - analyse de textes juridiques
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/tests/matching.log:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/matching.log
--------------------------------------------------------------------------------
/tests/needles.csv:
--------------------------------------------------------------------------------
1 | 1 {'art': 'article', 'ref': ' 1120 ', 'CCIV': 'du Code civil'}
2 | 2 {'art': 'art.', 'ref': ' 2288 C. ', 'CCIV': 'civ.'}
3 | 3 {'art': 'article', 'ref': ' 1240 al. 1 ', 'CCIV': 'C. civ.'}
4 | 4 {'art': 'article', 'ref': ' 1140. ', 'CCIV': 'civ.'}
5 | 5 {'art': 'articles', 'ref': ' 1 et 349 ', 'CCIV': 'du Code civil'}
6 | 6 {'art': 'article', 'ref': ' 39999 ', 'CCIV': 'C. civ.'}
7 | 7 {'art': 'Articles', 'ref': ' 3-12, 12-4-6, 14, 15 et 27 ', 'CCIV': 'C. civ.'}
8 | 8 {'art': 'Art.', 'ref': ' 1038 et 1289-2 ', 'CPRCIV': 'CPC'}
9 | 9 {'art': 'Art.', 'ref': ' L. 385-2, R. 343-4 et A421-13 ', 'CASSUR': 'C. assur.'}
10 | 10 {'art': 'articles', 'ref': ' L. 611-2 ', 'CCOM': 'C. com.'}
11 | 11 {'art': 'Articles', 'ref': ' L. 1111-1 ', 'CTRAV': 'C. trav.'}
12 | 12 {'art': 'Art.', 'ref': ' L. 112-1 ', 'CPI': 'C. pr. Int.'}
13 | 13 {'art': 'Article', 'ref': ' L. 331-4 ', 'CPI': 'du CPI'}
14 | 14 {'art': 'Articles', 'ref': ' 131-4 et 225-7-1 ', 'CPEN': 'c. pén.'}
15 | 15 {'art': 'Art.', 'ref': ' 694-4-1 et R57-6-1 ', 'CPP': 'CPP'}
16 | 16 {'art': 'Articles', 'ref': ' L. 121-14, R. 742-52 ', 'CCONSO': 'C. conso.'}
17 | 17 {'art': 'Articles', 'ref': ' L. 622-7 et R. 314-7 ', 'CSI': 'du CSI'}
18 | 18 {'art': 'Art.', 'ref': ' L. 173-8 ', 'CSS': 'du CSS'}
19 | 19 {'art': 'Art.', 'ref': ' L. 1110-1 ', 'CSP': 'du CSP'}
20 | 20 {'art': 'Articles', 'ref': ' L. 753-1 et 12', 'CENV': ' du CE'}
21 | 21 {'art': 'art.', 'ref': ' L. 1424-71 et L1 ', 'CGCT': 'CGCT'}
--------------------------------------------------------------------------------
/tests/newtest.doc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/newtest.doc
--------------------------------------------------------------------------------
/tests/newtest.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/newtest.docx
--------------------------------------------------------------------------------
/tests/newtest.md:
--------------------------------------------------------------------------------
1 | C’est la solution posée par l’article 1120 du Code civil. Voir aussi, dans le même sens, art. 2288 C. civ.
2 |
3 | Cette phrase ne contient pas le mot recherché.
4 |
5 | Cette autre phrase est également un piège.
6 |
7 | Toutefois, la prise en compte de l’article 1240 al. 1 C. civ. est de nature à changer la donne.
8 | C’est aussi le cas de l'article 1140. civ.
9 |
10 | Ce sont aussi les articles 1 et 349 du Code civil, l’article 39999 C. civ.
11 |
12 | Articles 3-12, 12-4-6, 14, 15 et 27 C. civ.
13 |
14 | Art. 1038 et 1289-2 CPC.
15 |
16 | Art. L. 385-2, R. 343-4 et A421-13 C. assur.
17 |
18 |
19 | Tenir compte des articles L. 611-2 C. com., L. 132-1 C. com. et R. 811-3 du Code de commerce.
20 |
21 | Articles L. 1111-1 C. trav. et R. 4512-15 du Code du travail.
22 |
23 | Art. L. 112-1 C. pr. Int. Article L. 331-4 du CPI.
24 |
25 | Articles 131-4 et 225-7-1 c. pén.
26 |
27 | Art. 694-4-1 et R57-6-1 CPP.
28 |
29 | Articles L. 121-14, R. 742-52 C. conso.
30 |
31 | Articles L. 622-7 et R. 314-7 du CSI.
32 |
33 | Art. L. 173-8 du CSS.
34 |
35 | Art. L. 1110-1 du CSP.
36 |
37 | Articles L. 753-1 et 12 du CESEDA.
38 |
39 | Les art. L. 1424-71 et L1 CGCT.
40 |
41 | Art. L. 121-2 CJA.
42 |
43 | Art. L.124-1 du Code de l'environnement.
--------------------------------------------------------------------------------
/tests/newtest.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/newtest.pdf
--------------------------------------------------------------------------------
/tests/newtest2.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/newtest2.pdf
--------------------------------------------------------------------------------
/tests/parsing.md:
--------------------------------------------------------------------------------
1 | #1 {'art': 'article', 'ref': ' 1120 du ', 'CCIV': 'Code civil'}
2 | #2 {'art': 'art.', 'ref': ' 2288 ', 'CCIV': 'C. civ.'}
3 | #3 {'art': 'article', 'ref': ' 1240 al. 1 ', 'CCIV': 'C. civ.'}
4 | #4 {'art': 'article', 'ref': ' 1140. ', 'CCIV': 'civ.'}
5 | #5 {'art': 'articles', 'ref': ' 1 et 349 du ', 'CCIV': 'Code civil'}
6 | #6 {'art': 'article', 'ref': ' 39999 ', 'CCIV': 'C. civ.'}
7 | #7 {'art': 'Articles', 'ref': ' 3-12, 12-4-6, 14, 15 et 27 ', 'CCIV': 'C. civ.'}
8 | #8 {'art': 'Art.', 'ref': ' 1038 et 1289-2 ', 'CPRCIV': 'CPC'}
9 | #9 {'art': 'Art.', 'ref': ' L. 385-2, R. 343-4 et A421-13 ', 'CASSUR': 'C. assur.'}
10 | #10 {'art': 'articles', 'ref': ' L. 611-2 ', 'CCOM': 'C. com.'}
11 | #11 {'art': 'Articles', 'ref': ' L. 1111-1 ', 'CTRAV': 'C. trav.'}
12 | #12 {'art': 'Art.', 'ref': ' L. 112-1 ', 'CPI': 'C. pr. Int.'}
13 | #13 {'art': 'Article', 'ref': ' L. 331-4 du ', 'CPI': 'CPI'}
14 | #14 {'art': 'Articles', 'ref': ' 131-4 et 225-7-1 ', 'CPEN': 'c. pén.'}
15 | #15 {'art': 'Art.', 'ref': ' 694-4-1 et R57-6-1 ', 'CPP': 'CPP'}
16 | #16 {'art': 'Articles', 'ref': ' L. 121-14, R. 742-52 ', 'CCONSO': 'C. conso.'}
17 | #17 {'art': 'Articles', 'ref': ' L. 622-7 et R. 314-7 du ', 'CSI': 'CSI'}
18 | #18 {'art': 'Art.', 'ref': ' L. 173-8 du ', 'CSS': 'CSS'}
19 | #19 {'art': 'Art.', 'ref': ' L. 1110-1 du ', 'CSP': 'CSP'}
20 | #20 {'art': 'Articles', 'ref': ' L. 753-1 et 12 du ', 'CESEDA': 'CESEDA'}
21 | #21 {'art': 'art.', 'ref': ' L. 1424-71 et L1 ', 'CGCT': 'CGCT'}
22 | #22 {'art': 'Art.', 'ref': ' L. 121-2 ', 'CJA': 'CJA'}
23 | #23 {'art': 'Art.', 'ref': ' L.124-1 du ', 'CENV': "Code de l'environnement"}
24 |
--------------------------------------------------------------------------------
/tests/test_001_parsing.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import pytest
4 | import os
5 |
6 | import docx
7 | from PyPDF2 import PdfReader
8 | from odf import text, teletype
9 | from odf.opendocument import load
10 |
11 | ACCEPTED_EXTENSIONS = ("odt", "pdf", "docx", "doc")
12 |
13 |
14 | def parse_doc(file_path):
15 | """
16 | Parsing document
17 | Arguments:
18 | file_path: a string representing the absolute filepath of the document
19 | Returns:
20 | full_text: a list of sentences
21 | Raises:
22 | Exception: Extension incorrecte. Les types de fichiers supportés sont odt, doc, docx, pdf
23 | FileNotFoundError: File has not been found. File_path must be incorrect
24 | """
25 | doc_name, doc_ext = file_path.split("/")[-1].split(".")
26 | if doc_ext not in ACCEPTED_EXTENSIONS:
27 | raise ValueError(
28 | "Extension incorrecte: les fichiers acceptés terminent par *.odt, *.docx, *.doc, *.pdf"
29 | )
30 |
31 | full_text = []
32 | if doc_ext == "pdf":
33 | with open(file_path, "rb") as f:
34 | reader = PdfReader(f)
35 |
36 | for i in range(len(reader.pages)):
37 | page = reader.pages[i]
38 | full_text.extend((page.extract_text()).split("\n"))
39 |
40 | elif doc_ext == "odt":
41 | with open(file_path, "rb") as f:
42 | document = load(f)
43 | paragraphs = document.getElementsByType(text.P)
44 | for i in range(len(paragraphs)):
45 | full_text.append((teletype.extractText(paragraphs[i])))
46 | else:
47 | # if doc_ext in ["docx", "doc"]:
48 | with open(file_path, "rb") as f:
49 | document = docx.Document(f)
50 | paragraphs = document.paragraphs
51 | for i in range(len(paragraphs)):
52 | full_text.append((paragraphs[i].text))
53 | full_text = [n for n in full_text if n not in ["\n", "", " "]]
54 | return full_text
55 |
56 |
57 | class TestFileParsing:
58 | def test_wrong_extension(self):
59 | """testing accepted extensions"""
60 | file_paths = ["document.rtf", "document.md", "document.xlsx"]
61 | with pytest.raises(ValueError) as e:
62 | for file_path in file_paths:
63 | parse_doc(file_path)
64 | assert e == "Extension incorrecte: les fichiers acceptés terminent par *.odt, *.docx, *.doc, *.pdf"
65 |
66 | def test_wrong_file_path(self):
67 | """testing FileNotFound Error"""
68 | filepath = "./document.doc"
69 | with pytest.raises(FileNotFoundError) as e:
70 | parse_doc(filepath)
71 | assert e == "", e
72 |
73 | def test_content(self):
74 | """test content text"""
75 | file_paths = ["newtest.doc", "newtest.docx", "newtest.pdf", "testnew.odt"]
76 | for file_path in file_paths:
77 | abspath = os.path.join(
78 | os.path.dirname(os.path.realpath(__file__)), file_path
79 | )
80 | full_text = parse_doc(abspath)
81 | doc_name, doc_ext = abspath.split("/")[-1].split(".")
82 | assert doc_name == "newtest" or doc_name == "testnew"
83 | if abspath.endswith(".pdf"):
84 | assert len(full_text) == 23, (len(full_text), abspath)
85 | else:
86 | assert len(full_text) == 22, (len(full_text), abspath)
87 | assert any("art." in _x for _x in full_text) is True
88 | assert any("Art." in _x for _x in full_text) is True
89 | assert any("Code" in _x for _x in full_text) is True
90 |
--------------------------------------------------------------------------------
/tests/test_002_code_references.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | import random
3 |
4 | CODE_REGEX = {
5 | "CCIV": r"(?PCode\scivil|C\.\sciv\.|Code\sciv\.|C\.civ\.|civ\.|CCIV)",
6 | "CPRCIV": r"(?PCode\sde\sprocédure\scivile|C\.\spr\.\sciv\.|CPC)",
7 | "CCOM": r"(?PCode\sde\scommerce|C\.\scom\.)",
8 | "CTRAV": r"(?PCode\sdu\stravail|C\.\strav\.)",
9 | "CPI": r"(?PCode\sde\sla\spropriété\sintellectuelle|CPI|C\.\spr\.\sint\.)",
10 | "CPEN": r"(?PCode\spénal|C\.\spén\.)",
11 | "CPP": r"(?PCode\sde\sprocédure\spénale|CPP)",
12 | "CASSUR": r"(?PCode\sdes\sassurances|C\.\sassur\.)",
13 | "CCONSO": r"(?PCode\sde\sla\sconsommation|C\.\sconso\.)",
14 | "CSI": r"(?PCode\sde\slasécurité\sintérieure|CSI)",
15 | "CSP": r"(?PCode\sde\slasanté\spublique|C\.\ssant\.\spub\.|CSP)",
16 | "CSS": r"(?PCode\sde\slasécurité\ssociale|C\.\ssec\.\ssoc\.|CSS)",
17 | "CESEDA": r"(?PCode\sde\sl'entrée\set\sdu\sséjour\sdes\sétrangers\set\sdu\sdroit\sd'asile|CESEDA)",
18 | "CGCT": r"(?PCode\sgénéral\sdes\scollectivités\sterritoriales|CGCT)",
19 | "CPCE": r"(?PCode\sdes\spostes\set\sdes\scommunications\sélectroniques|CPCE)",
20 | "CENV": r"(?PCode\sde\sl'environnement|C.\senvir.|CE\.)",
21 | "CJA": r"(?PCode\sde\sjustice\sadministrative|CJA)",
22 | }
23 |
24 | CODE_REFERENCE = {
25 | "CCIV": "Code civil",
26 | "CPRCIV": "Code de procédure civile",
27 | "CCOM": "Code de commerce",
28 | "CTRAV": "Code du travail",
29 | "CPI": "Code de la propriété intellectuelle",
30 | "CPEN": "Code pénal",
31 | "CPP": "Code de procédure pénale",
32 | "CASSUR": "Code des assurances",
33 | "CCONSO": "Code de la consommation",
34 | "CSI": "Code de la sécurité intérieure",
35 | "CSP": "Code de la santé publique",
36 | "CSS": "Code de la sécurité sociale",
37 | "CESEDA": "Code de l'entrée et du séjour des étrangers et du droit d'asile",
38 | "CGCT": "Code général des collectivités territoriales",
39 | "CPCE": "Code des postes et des communications électroniques",
40 | "CENV": "Code de l'environnement",
41 | "CJA": "Code de justice administrative",
42 | }
43 |
44 | def get_long_and_short_code(code_name):
45 | '''
46 | Accéder aux deux versions du nom du code: le nom complet et son abréviation
47 |
48 | Arguments:
49 | code_name: le nom du code (version longue ou courte)
50 | Returns:
51 | long_code: le nom complet du code
52 | short_code: l'abréviation du code
53 | Raises:
54 | Si le nom du code n'a pas été trouvé les valeurs sont nulles (None, None)
55 | '''
56 |
57 | if code_name in CODE_REFERENCE.keys():
58 | short_code = code_name
59 | long_code = CODE_REFERENCE[code_name]
60 | elif code_name in CODE_REFERENCE.values():
61 | long_code = code_name
62 | short_code_results = [k for k, v in CODE_REFERENCE.items() if v == code_name]
63 | if len(short_code_results) > 0:
64 | short_code = short_code_results[0]
65 | else:
66 | short_code = None
67 | else:
68 | short_code, long_code = None, None
69 | return(long_code, short_code)
70 |
71 |
72 | def get_code_full_name_from_short_code(short_code):
73 | """
74 | Shortcut to get corresponding full_name from short_code
75 |
76 | Arguments:
77 | short_code: short form of Code eg. CCIV
78 | Returns:
79 | full_name: long form of code eg. Code Civil
80 | """
81 | try:
82 | return CODE_REFERENCE[short_code]
83 |
84 | except KeyError:
85 | if get_short_code_from_full_name(short_code) is not None:
86 | return short_code
87 | else:
88 | return None
89 |
90 |
91 | def get_short_code_from_full_name(full_name):
92 | """
93 | Shortcut to get corresponding short_code from full_name
94 |
95 | Arguments:
96 | full_name: long form of code eg. Code Civil
97 | Returns:
98 | short_code: short form of Code eg. CCIV
99 | """
100 | keys = [k for k, v in CODE_REFERENCE.items() if v == full_name]
101 | if len(keys) > 0:
102 | return keys[0]
103 | else:
104 | return None
105 |
106 |
107 | def filter_code_regex(selected_codes):
108 | """
109 | Contruire l'expression régulière pour détecter les différents codes dans le document.
110 |
111 | Arguments:
112 | selected_codes: [short_code, ...]. Default: None (no filter)
113 | Returns:
114 | regex: string
115 | """
116 | if selected_codes is None:
117 | return "({})".format("|".join(list(CODE_REGEX.values())))
118 |
119 |
120 | if len(selected_codes) == 1:
121 | return CODE_REGEX[selected_codes[0]]
122 | else:
123 | selected_code_list = [CODE_REGEX[x] for x in sorted(selected_codes)]
124 | return "({})".format("|".join(selected_code_list))
125 |
126 | def filter_code_reference(selected_codes=None):
127 | """
128 | Filtrer le dictionnaire de référence des codes
129 |
130 | Arguments:
131 | selected_codes: [short_code, ...]
132 | Returns:
133 | code_reference_dict_filtered: filtered_CODE_REFERENCE
134 | """
135 | if selected_codes is None:
136 | return CODE_REFERENCE
137 | return {x: CODE_REFERENCE[x] for x in sorted(selected_codes)}
138 |
139 | class TestCodeFormats:
140 | @pytest.mark.parametrize("input", list(CODE_REFERENCE.keys()))
141 | def test_short2long_code(self, input):
142 |
143 | assert get_code_full_name_from_short_code(input) == CODE_REFERENCE[input], (
144 | input,
145 | CODE_REFERENCE[input],
146 | )
147 |
148 | @pytest.mark.parametrize("input", list(CODE_REFERENCE.values()))
149 | def test_long2short_code(self, input):
150 | print(input)
151 | result = get_short_code_from_full_name(input)
152 | expected = [k for k,v in CODE_REFERENCE.items() if v == input][0]
153 | assert result == expected, (result, expected)
154 |
155 | @pytest.mark.parametrize("input", list(CODE_REFERENCE.keys()))
156 | def test_get_long_and_short_code(self, input):
157 | result = get_long_and_short_code(input)
158 | expected = (CODE_REFERENCE[input], input)
159 | assert expected == result, result
160 |
161 |
162 | class TestFilterRegexCode:
163 | def test_code_regex_match_code_ref(self):
164 | assert set(CODE_REFERENCE) - set(CODE_REGEX) == set()
165 |
166 | def test_filter_regex_empty(self):
167 | result = filter_code_regex([])
168 | expected = "({})".format("|".join(list(CODE_REGEX.values())))
169 | assert result == expected, result
170 |
171 | def test_filter_regex_unique(self):
172 | result = filter_code_regex(["CJA"])
173 | expected = CODE_REGEX["CJA"]
174 | assert result == expected, (result, expected)
175 |
176 | def test_filter_code_regex_manual(self):
177 | code_list = sorted(["CJA", "CPP", "CCIV"])
178 | result = filter_code_regex(code_list)
179 | expected = "({})".format("|".join([CODE_REGEX[x] for x in code_list]))
180 | assert result == expected, (result, expected)
181 |
182 | def test_filter_code_regex_random(self):
183 | random_code_list = sorted(random.choices(list(CODE_REFERENCE), k=5))
184 | result = filter_code_regex(random_code_list)
185 | expected = "({})".format("|".join([CODE_REGEX[c] for c in random_code_list]))
186 | assert result == expected, result
187 |
188 | class TestFilterCodeReference:
189 | def test_filter_reference_empty(self):
190 | result = filter_code_reference([])
191 | assert result == CODE_REFERENCE
192 |
193 | def test_filter_reference_single(self):
194 | result = filter_code_reference(["CPP"])
195 | assert result == {"CPP": CODE_REFERENCE['CPP']}
196 |
197 | def test_filter_reference_manual(self):
198 | code_list = sorted(["CPP", "CPEN", "CENV"])
199 | result = filter_code_reference(code_list)
200 | expected = {x: CODE_REFERENCE[x] for x in code_list}
201 | assert result == expected, result
202 |
203 | def test_filter_reference_random(self):
204 | random_code_list = sorted(random.choices(list(CODE_REFERENCE), k=5))
205 | result = filter_code_reference(random_code_list)
206 | expected = {k: CODE_REFERENCE[k] for k in random_code_list}
207 | assert result == expected, result
--------------------------------------------------------------------------------
/tests/test_003_matching.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import pytest
4 |
5 | from test_001_parsing import parse_doc
6 | from test_002_code_references import filter_code_regex, CODE_REFERENCE, CODE_REGEX
7 |
8 | ARTICLE_REGEX = r"(?P(Articles?|Art\.))"
9 |
10 | # ARTICLE_REF = re.compile("\d+")
11 | # ARTICLE_ID = r"(L|R|A|D)?(\.|\s)?\d+(-\d+)?((\s(al\.|alinea)?\s\d+)?(\s|\.)"
12 |
13 |
14 |
15 |
16 | def switch_pattern(selected_codes=None, pattern="article_code"):
17 | """
18 | Build pattern recognition using pattern short code switch
19 |
20 | Arguments:
21 | selected_codes: a list of short codes to select. Default to None
22 | pattern: a string article_code or code_article. Default to article_code
23 | Returns:
24 | regex_pattern: compiled regex pattern
25 | Raise:
26 | ValueError: pattern name is wrong
27 | """
28 |
29 | code_regex = filter_code_regex(selected_codes)
30 |
31 | if pattern not in ["article_code", "code_article"]:
32 | raise ValueError(
33 | "Wrong pattern name: choose between 'article_code' or 'code_article'"
34 | )
35 | if pattern == "article_code":
36 | return re.compile(f"{ARTICLE_REGEX}(?P.*?){code_regex}", flags=re.I)
37 | # else:
38 | # #code_article
39 | # # return re.compile(f"{code_regex}.*?{ARTICLE_REGEX}(\s|\.)(?P.*?)(\.|\s)", flags=re.I)
40 | # return re.compile(f"{code_regex}.*?{ARTICLE_REGEX}.*?{ARTICLE_ID}", flags=re.I)
41 |
42 | def get_matching_results_dict(full_text, selected_short_codes=[], pattern_format="article_code"):
43 | """ "
44 | Une fonction qui revoie un dictionnaire de resultats: trié par code (version abbréviée) avec la liste des articles détectés lui appartenant.
45 |
46 | Arguments:
47 | full_text: a string of the full document normalized
48 | pattern_format: a string representing the pattern format article_code or code_article. Defaut to article_code
49 |
50 | Returns:
51 | code_found: a dict compose of short version of code as key and list of the detected articles references as values {code: [art_ref, art_ref2, ... ]}
52 | """
53 | article_pattern = switch_pattern(selected_short_codes, pattern_format)
54 | code_found = {}
55 |
56 | # normalisation
57 | full_text = re.sub(r"\r|\n|\t|\xa0", " ", " ".join(full_text))
58 | for i, match in enumerate(re.finditer(article_pattern, full_text)):
59 | needle = match.groupdict()
60 | qualified_needle = {
61 | key: value for key, value in needle.items() if value is not None
62 | }
63 | msg = f"#{i+1}\t{qualified_needle}"
64 | # logging.debug(msg)
65 | # get the code shortname based on regex group name
66 | code = [k for k in qualified_needle.keys() if k not in ["ref", "art"]][0]
67 |
68 | ref = match.group("ref").strip()
69 | # split multiple articles of a same code
70 | refs = [
71 | n
72 | for n in re.split(r"(\set\s|,\s|\sdu)", ref)
73 | if n not in [" et ", ", ", " du", " ", ""]
74 | ]
75 | # normalize articles to remove dots, spaces, caret and 'alinea'
76 | refs = [
77 | "-".join(
78 | [
79 | r
80 | for r in re.split(r"\s|\.|-", ref)
81 | if r not in [" ", "", "al", "alinea", "alinéa"]
82 | ]
83 | )
84 | for ref in refs
85 | ]
86 | # clean caracters for everything but numbers and (L|A|R|D) and caret
87 | normalized_refs = []
88 | for ref in refs:
89 | # accepted caracters for article
90 | ref = "".join(
91 | [n for n in ref if (n.isdigit() or n in ["L", "A", "R", "D", "-"])]
92 | )
93 | if ref.endswith("-"):
94 | ref = ref[:-1]
95 | # remove caret separating article nb between first letter
96 | special_ref = ref.split("-", 1)
97 | if special_ref[0] in ["L", "A", "R", "D"]:
98 | normalized_refs.append("".join(special_ref))
99 | else:
100 | normalized_refs.append(ref)
101 |
102 | if code not in code_found:
103 | # append article references
104 | code_found[code] = normalized_refs
105 | else:
106 | # append article references to existing list
107 | code_found[code].extend(normalized_refs)
108 |
109 | return code_found
110 |
111 | def get_matching_result_item(full_text, selected_shortcodes=[], pattern_format="article_code"):
112 | """"
113 | Générateur: renvoie la référence de l'article détecté dans le texte
114 |
115 | Arguments:
116 | full_text: a string of the full document normalized
117 | selected_shortcodes: a list of selected codes in short format for filtering article detection. Default is an empty list (which stands for no filter)
118 | pattern_format: a string representing the pattern format article_code or code_article. Defaut to article_code
119 |
120 | Yield:
121 | (code_short_name:str, article_number:str)
122 | """
123 | article_pattern = switch_pattern(selected_shortcodes, pattern_format)
124 | # normalisation des espaces dans le texte
125 | full_text = re.sub(r"\r|\n|\t|\f|\xa0", " ", " ".join(full_text))
126 | for i, match in enumerate(re.finditer(article_pattern, full_text)):
127 | needle = match.groupdict()
128 | qualified_needle = {
129 | key: value for key, value in needle.items() if value is not None
130 | }
131 | msg = f"#{i+1}\t{qualified_needle}"
132 | # logging.debug(msg)
133 | # get the code shortname based on regex group name
134 | code = [k for k in qualified_needle.keys() if k not in ["ref", "art"]][0]
135 |
136 | ref = match.group("ref").strip()
137 | # split multiple articles of a same code example: Article 22, 23 et 24 du Code
138 | refs = [
139 | n
140 | for n in re.split(r"(\set\s|,\s|\sdu)", ref)
141 | if n not in [" et ", ", ", " du", " ", ""]
142 | ]
143 | # normalize articles to remove dots, spaces, caret and 'alinea'
144 | refs = [
145 | "-".join(
146 | [
147 | r
148 | for r in re.split(r"\s|\.|-", ref)
149 | if r not in [" ", "", "al", "alinea", "alinéa"]
150 | ]
151 | )
152 | for ref in refs
153 | ]
154 | # clean caracters for everything but numbers and (L|A|R|D) and caret
155 | for ref in refs:
156 | # accepted caracters for article
157 | # exemple: 1224 du => Non 298 al 32 => Non R-288 => oui A-24-14=> oui
158 | ref = "".join(
159 | [n for n in ref if (n.isdigit() or n in ["L", "A", "R", "D", "-"])]
160 | )
161 | if ref.endswith("-"):
162 | ref = ref[:-1]
163 | # remove caret separating article nb between first letter
164 | # exemple: L-248-1 = > L248-1
165 | special_ref = ref.split("-", 1)
166 | if special_ref[0] in ["L", "A", "R", "D"]:
167 | yield(code, "".join(special_ref))
168 |
169 | else:
170 | yield(code, ref)
171 |
172 | class TestMatching:
173 | def test_matching_result_dict_codes_no_filter_pattern_article_code(self):
174 | # NO CPCE ref dans le doc
175 | code_reference_test = CODE_REGEX
176 | del code_reference_test["CPCE"]
177 |
178 | file_paths = ["newtest.doc", "newtest.docx", "newtest.pdf"]
179 | for file_path in file_paths:
180 | abspath = os.path.join(
181 | os.path.dirname(os.path.realpath(__file__)), file_path
182 | )
183 | full_text = parse_doc(abspath)
184 | results_dict = get_matching_results_dict(full_text, None, "article_code")
185 |
186 |
187 |
188 | # del code_reference_test["CPCE"]
189 | code_list = list(results_dict.keys())
190 | assert len(code_list) == len(code_reference_test), set(code_reference_test)- set(code_list)
191 | assert sorted(code_list) == sorted(code_reference_test), set(code_reference_test)- set(code_list)
192 | articles_detected = [
193 | item for sublist in results_dict.values() for item in sublist
194 | ]
195 | assert len(articles_detected) == 37, len(articles_detected)
196 | assert results_dict["CCIV"] == [
197 | "1120",
198 | "2288",
199 | "1240-1",
200 | "1140",
201 | "1",
202 | "349",
203 | "39999",
204 | "3-12",
205 | "12-4-6",
206 | "14",
207 | "15",
208 | "27",
209 | ], results_dict["CCIV"]
210 | assert results_dict["CPRCIV"] == ["1038", "1289-2"], results_dict["CPRCIV"]
211 | assert results_dict["CASSUR"] == ["L385-2", "R343-4", "A421-13"], results_dict[
212 | "CASSUR"
213 | ]
214 | assert results_dict["CCOM"] == ["L611-2"], results_dict["CCOM"]
215 | assert results_dict["CTRAV"] == ["L1111-1"], results_dict["CTRAV"]
216 | assert results_dict["CPI"] == ["L112-1", "L331-4"], results_dict["CPI"]
217 | assert results_dict["CPEN"] == ["131-4", "225-7-1"], results_dict["CPEN"]
218 | assert results_dict["CPP"] == ["694-4-1", "R57-6-1"], results_dict["CPP"]
219 | assert results_dict["CCONSO"] == ["L121-14", "R742-52"], results_dict["CCONSO"]
220 | assert results_dict["CSI"] == ["L622-7", "R314-7"], results_dict["CSI"]
221 | assert results_dict["CSS"] == ["L173-8"], results_dict["CSS"]
222 | assert results_dict["CSP"] == ["L1110-1"], results_dict["CSP"]
223 | assert results_dict["CENV"] == ["L124-1"], ("CENV", results_dict["CENV"])
224 | assert results_dict["CJA"] == ["L121-2"], ("CJA", results_dict["CJA"])
225 | assert results_dict["CGCT"] == ["L1424-71", "L1"], ("CGCT", results_dict["CGCT"])
226 | assert results_dict["CESEDA"] == ["L753-1", "12"], ("CESEDA", results_dict["CESEDA"])
227 |
228 | def test_matching_result_dict_codes_unique_filter_pattern_article_code(self):
229 | selected_codes = ["CASSUR"]
230 | file_paths = ["newtest.doc", "newtest.docx", "newtest.pdf"]
231 | for file_path in file_paths:
232 | abspath = os.path.join(
233 | os.path.dirname(os.path.realpath(__file__)), file_path
234 | )
235 | full_text = parse_doc(abspath)
236 | results_dict = get_matching_results_dict(full_text,selected_codes, "article_code")
237 | code_list = list(results_dict.keys())
238 | assert len(code_list) == len(selected_codes), len(code_list)
239 | assert sorted(code_list) == [
240 | "CASSUR"
241 | ], sorted(code_list)
242 | # articles_detected = [
243 | # item for sublist in results_dict.values() for item in sublist
244 | # ]
245 | # assert len(articles_detected) == 37, len(articles_detected)
246 | assert results_dict["CASSUR"] == ["L385-2", "R343-4", "A421-13"], results_dict[
247 | "CASSUR"
248 | ]
249 |
250 | def test_matching_result_dict_codes_multiple_filter_pattern_article_code(self):
251 | selected_codes = ["CASSUR", "CENV", "CSI", "CCIV"]
252 | file_paths = ["newtest.doc", "newtest.docx", "newtest.pdf"]
253 | for file_path in file_paths:
254 | abspath = os.path.join(
255 | os.path.dirname(os.path.realpath(__file__)), file_path
256 | )
257 | full_text = parse_doc(abspath)
258 | results_dict = get_matching_results_dict(full_text,selected_codes, "article_code")
259 | code_list = list(results_dict.keys())
260 | assert len(code_list) == len(selected_codes), len(code_list)
261 | assert sorted(code_list) == [
262 | "CASSUR",
263 | "CCIV",
264 | "CENV",
265 | "CSI",
266 | ], sorted(code_list)
267 | # articles_detected = [
268 | # item for sublist in results_dict.values() for item in sublist
269 | # ]
270 | # assert len(articles_detected) == 37, len(articles_detected)
271 | assert results_dict["CCIV"] == [
272 | "1120",
273 | "2288",
274 | "1240-1",
275 | "1140",
276 | "1",
277 | "349",
278 | "39999",
279 | "3-12",
280 | "12-4-6",
281 | "14",
282 | "15",
283 | "27",
284 | ], results_dict["CCIV"]
285 | # assert results_dict["CASSUR"] == ["L385-2", "R343-4", "A421-13"], results_dict[
286 | # "CASSUR"
287 | # ]
288 | # assert results_dict["CSI"] == ["L622-7", "R314-7"], results_dict["CSI"]
289 | assert results_dict["CENV"] == ["L124-1"], ("CENV", results_dict["CENV"])
290 |
291 |
--------------------------------------------------------------------------------
/tests/test_004_request.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import datetime
4 | import time
5 | from dotenv import load_dotenv
6 | import pytest
7 | from test_002_code_references import get_code_full_name_from_short_code
8 | from test_005_check_validity import convert_epoch_to_datetime, convert_datetime_to_str, get_validity_status, time_delta
9 |
10 | API_ROOT_URL = "https://sandbox-api.piste.gouv.fr/dila/legifrance-beta/lf-engine-app/"
11 | # API_ROOT_URL = "https://api.piste.gouv.fr/dila/legifrance-beta/lf-engine-app/",
12 |
13 |
14 |
15 | def get_legifrance_auth(client_id, client_secret):
16 | """
17 | Get authorization token from LEGIFRANCE API
18 |
19 | Arguments:
20 | client_id: OAUTH CLIENT key provided by API
21 | client_secret: OAUTH SECRET key provided by API
22 |
23 | Returns:
24 | authorization_header: a header composed of a json dict with access_token
25 |
26 | Raise:
27 | Exception: No credentials have been set. Client_id or client_secret is None
28 | Exception: Invalid credentials. Request to authentication server failed with 400 or 401 error
29 | """
30 |
31 | TOKEN_URL = "https://sandbox-oauth.piste.gouv.fr/api/oauth/token"
32 | # TOKEN_URL = "https://sandbox-oauth.aife.economie.gouv.fr/api/oauth/token"
33 |
34 | if client_id is None or client_secret is None:
35 | # return HTTPError(401, "No credential have been set")
36 | raise ValueError(
37 | "No credential: client_id or/and client_secret are not set. \nPlease register your API at https://developer.aife.economie.gouv.fr/"
38 | )
39 | session = requests.Session()
40 | with session as s:
41 | res = s.post(
42 | TOKEN_URL,
43 | data={
44 | "grant_type": "client_credentials",
45 | "client_id": client_id,
46 | "client_secret": client_secret,
47 | "scope": "openid",
48 | },
49 | )
50 |
51 | if res.status_code in [400, 401]:
52 | # return HTTPError(res.status_code, "Unauthorized: invalid credentials")
53 | raise Exception(f"HTTP Error code: {res.status_code}: Invalid credentials")
54 | token = res.json()
55 | access_token = token["access_token"]
56 | return {"Authorization": f"Bearer {access_token}"}
57 |
58 |
59 | def get_article_uid(short_code_name, article_number, headers):
60 | """
61 | GET the article uid given by [Legifrance API](https://developer.aife.economie.gouv.fr/index.php?option=com_apiportal&view=apitester&usage=api&apitab=tests&apiName=L%C3%A9gifrance+Beta&apiId=426cf3c0-1c6d-46ba-a8b0-f79289086ed5&managerId=2&type=rest&apiVersion=1.6.2.5&Itemid=402&swaggerVersion=2.0&lang=fr)
62 |
63 | Arguments:
64 | code_name: Nom du code de droit français (version longue)
65 | article_number: Référence de l'article mentionné (version normalisée eg. L25-67)
66 |
67 | Returns:
68 | article_uid: Identifiant unique de l'article dans Legifrance or None
69 |
70 | """
71 | long_code = get_code_full_name_from_short_code(short_code_name)
72 | if long_code is None:
73 | raise ValueError(f"`{short_code_name}` not found in the supported Code List")
74 |
75 | session = requests.Session()
76 |
77 | today_epoch = int(time.time()) * 1000
78 | data = {
79 | "recherche": {
80 | "champs": [
81 | {
82 | "typeChamp": "NUM_ARTICLE",
83 | "criteres": [
84 | {
85 | "typeRecherche": "EXACTE",
86 | "valeur": article_number,
87 | "operateur": "ET",
88 | }
89 | ],
90 | "operateur": "ET",
91 | }
92 | ],
93 | "filtres": [
94 | {"facette": "NOM_CODE", "valeurs": [long_code]},
95 | {"facette": "DATE_VERSION", "singleDate": today_epoch},
96 | ],
97 | "pageNumber": 1,
98 | "pageSize": 10,
99 | "operateur": "ET",
100 | "sort": "PERTINENCE",
101 | "typePagination": "ARTICLE",
102 | },
103 | "fond": "CODE_DATE",
104 | }
105 | with session as s:
106 | response = s.post(
107 | "/".join([API_ROOT_URL, "search"]), headers=headers, json=data
108 | )
109 | if response.status_code > 399:
110 | # print(response)
111 | # return None
112 | raise Exception(f"Error {response.status_code}: {response.reason}")
113 |
114 | article_informations = response.json()
115 | if not article_informations["results"]:
116 | return None
117 |
118 | results = article_informations["results"]
119 | if len(results) == 0:
120 | return None
121 | else:
122 | # get the first result
123 | try:
124 | return results[0]["sections"][0]["extracts"][0]["id"]
125 | except IndexError:
126 | return None
127 |
128 |
129 |
130 | def get_article_content(article_id, headers):
131 | """
132 | GET article_content from LEGIFRANCE API using POST /consult/getArticle https://developer.aife.economie.gouv.fr/index.php?option=com_apiportal&view=apitester&usage=api&apitab=tests&apiName=L%C3%A9gifrance+Beta&apiId=426cf3c0-1c6d-46ba-a8b0-f79289086ed5&managerId=2&type=rest&apiVersion=1.6.2.5&Itemid=402&swaggerVersion=2.0&lang=fr
133 |
134 | Arguments:
135 | article_id: article uid eg. LEGIARTI000006307920
136 | Returns:
137 | article_content: a dictionnary with the full content of article
138 | Raise:
139 | Exception : response.status_code [400-500]
140 | """
141 | data = {"id": article_id}
142 | session = requests.Session()
143 | with session as s:
144 |
145 | response = s.post(
146 | "/".join([API_ROOT_URL, "consult", "getArticle"]),
147 | headers=headers,
148 | json=data,
149 | )
150 |
151 | if response.status_code > 399:
152 | raise Exception(f"Error {response.status_code}: {response.reason}")
153 | article_content = response.json()
154 | try:
155 | raw_article = article_content["article"]
156 | # FEATURE récupérer tous les titres et sections d'un article
157 | article = {
158 | "url": f"https://www.legifrance.gouv.fr/codes/article_lc/{article_id}"
159 | }
160 | for k in [
161 | "id",
162 | "num",
163 | "texte",
164 | "etat",
165 | "dateDebut",
166 | "dateFin",
167 | "articleVersions",
168 | ]:
169 | article[k] = raw_article[k]
170 | # FEATURE - integrer les différentes versions
171 | article["nb_versions"] = len(article["articleVersions"])
172 |
173 | return article
174 | except KeyError:
175 | return None
176 |
177 |
178 | def get_article_content_by_id_and_article_nb(article_id, article_num, headers):
179 | """
180 | Récupère un Article en fonction de son ID et Numéro article depuis API Legifrance GET /consult getArticleWithIdAndNum
181 | Arguments:
182 | article_id: article uid eg. LEGIARTI000006307920
183 | article_num: numéro de l'article standardisé eg. "3-45", "L214", "R25-64"
184 | Returns:
185 | article_content: a dictionnary with the full content of article
186 | Raise:
187 | Exception : response.status_code [400-500]
188 | """
189 |
190 | data = {"id": article_id, "num": article_num}
191 |
192 | session = requests.Session()
193 | with session as s:
194 |
195 | response = s.post(
196 | "/".join([API_ROOT_URL, "consult", "getArticleWithIdandNum"]),
197 | headers=headers,
198 | json=data,
199 | )
200 | if response.status_code > 399:
201 | raise Exception(f"Error {response.status_code}: {response.reason}")
202 | article_content = response.json()
203 | return article_content["article"]
204 |
205 | def get_article(short_code_name, article_number, client_id, client_secret, past_year_nb=3, future_year_nb=3):
206 | """
207 | Accéder aux informations simplifiée de l'article
208 |
209 | Arguments:
210 | long_code_name: Nom du code de loi française dans sa version longue
211 | article_number: NUméro de l'article de loi normalisé ex. R25-67 L214 ou 2667-1-1
212 | Returns:
213 | article: Un dictionnaire json avec code (version courte), article (numéro), status, status_code, color, url, text, id, start_date, end_date, date_debut, date_fin
214 | """
215 |
216 | article = {
217 | "code": short_code_name,
218 | "code_full_name": get_code_full_name_from_short_code(short_code_name),
219 | "article": article_number,
220 | "status_code": 200,
221 | "status": "OK",
222 | "color": "grey",
223 | "url": "",
224 | "texte": "",
225 | "id": get_article_uid(
226 | short_code_name, article_number, headers=get_legifrance_auth(client_id, client_secret)
227 | )
228 | }
229 | if article["id"] is None:
230 | article["color"] = "red"
231 | article["status_code"] = 404
232 | article["status"] = "Indisponible"
233 | return article
234 | article_content = get_article_content(
235 | article["id"], headers=get_legifrance_auth(client_id, client_secret)
236 | )
237 | article["texte"] = article_content["texte"]
238 | article["url"] = article_content["url"]
239 | article["start_date"] = convert_epoch_to_datetime(article_content["dateDebut"])
240 | article["end_date"] = convert_epoch_to_datetime(article_content["dateFin"])
241 | article["status_code"], article["status"], article["color"] = get_validity_status(article["start_date"], article["end_date"], past_year_nb, future_year_nb)
242 | article["date_debut"] = convert_datetime_to_str(article["start_date"]).split(" ")[0]
243 | article["date_fin"] = convert_datetime_to_str(article["end_date"]).split(" ")[0]
244 | return article
245 |
246 |
247 | class TestOAuthLegiFranceAPI:
248 | def test_token_requests(self):
249 | load_dotenv()
250 | client_id = os.getenv("API_KEY")
251 | client_secret = os.getenv("API_SECRET")
252 | json_response = get_legifrance_auth(client_id, client_secret)
253 | assert "Authorization" in json_response
254 | assert json_response["Authorization"].startswith("Bearer")
255 |
256 | def test_token_requests_wrong_credentials(self):
257 | load_dotenv()
258 | client_id = os.getenv("API_KEY2")
259 | client_secret = os.getenv("API_SECRET2")
260 | with pytest.raises(ValueError) as exc_info:
261 | json_response = get_legifrance_auth(client_id, client_secret)
262 | assert (
263 | str(exc_info.value)
264 | == "No credential: client_id or/and client_secret are not set. \nPlease register your API at https://developer.aife.economie.gouv.fr/"
265 | ), str(exc_info.value)
266 |
267 |
268 | class TestGetArticleId:
269 | def test_get_article_uid(self):
270 | load_dotenv()
271 | client_id = os.getenv("API_KEY")
272 | client_secret = os.getenv("API_SECRET")
273 | headers = get_legifrance_auth(client_id, client_secret)
274 | article = get_article_uid("CCIV", "1120", headers)
275 | assert article == "LEGIARTI000032040861", article
276 |
277 | @pytest.mark.parametrize(
278 | "input_expected",
279 | [
280 | ("CCONSO", "L121-14", "LEGIARTI000032227262"),
281 | ("CCONSO", "R742-52", "LEGIARTI000032808914"),
282 | ("CSI", "L622-7", "LEGIARTI000043540586"),
283 | ("CSI", "R314-7", "LEGIARTI000037144520"),
284 | ],
285 | )
286 | def test_get_article_uid(self, input_expected):
287 | load_dotenv()
288 | client_id = os.getenv("API_KEY")
289 | client_secret = os.getenv("API_SECRET")
290 | code_name, art_num, expected = input_expected
291 | headers = get_legifrance_auth(client_id, client_secret)
292 | article_uid = get_article_uid(code_name, art_num, headers)
293 | assert expected == article_uid
294 |
295 | def test_get_article_uid_wrong_article_num(self):
296 | load_dotenv()
297 | client_id = os.getenv("API_KEY")
298 | client_secret = os.getenv("API_SECRET")
299 | headers = get_legifrance_auth(client_id, client_secret)
300 | article_uid = get_article_uid("CCIV", "11-20", headers)
301 | assert article_uid == None, article_uid
302 |
303 | def test_get_article_uid_wrong_code_name(self):
304 | load_dotenv()
305 | client_id = os.getenv("API_KEY")
306 | client_secret = os.getenv("API_SECRET")
307 | headers = get_legifrance_auth(client_id, client_secret)
308 |
309 | with pytest.raises(ValueError) as exc_info:
310 | # Note: Le code est sensible à la casse.
311 | # FEATURE: faire une base de référence insensible à la casse
312 | article_uid = get_article_uid("Code Civil", "1120", headers)
313 | assert str(exc_info.value) == "", str(exc_info.value)
314 | assert article_uid == None, article_uid
315 |
316 |
317 | class TestGetArticleContent:
318 | def test_get_article_full_content(
319 | self, input_id=("CCONSO", "L121-14", "LEGIARTI000032227262")
320 | ):
321 | load_dotenv()
322 | client_id = os.getenv("API_KEY")
323 | client_secret = os.getenv("API_SECRET")
324 | headers = get_legifrance_auth(client_id, client_secret)
325 | article_num, code, article_uid = input_id
326 | article_content = get_article_content(article_uid, headers)
327 | assert (
328 | article_content["url"]
329 | == "https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000032227262"
330 | ), article_content["url"]
331 | assert article_content["dateDebut"] == 1467331200000, article_content[
332 | "dateDebut"
333 | ]
334 | assert article_content["dateFin"] == 32472144000000, article_content["dateFin"]
335 | # assert article_content["nb_versions"] == 1, article_content["nb_versions"]
336 | assert article_content["articleVersions"][0] == {
337 | "dateDebut": 1467331200000,
338 | "dateFin": 32472144000000,
339 | "etat": "VIGUEUR",
340 | "id": "LEGIARTI000032227262",
341 | "numero": None,
342 | "ordre": None,
343 | "version": "1.0",
344 | }, article_content["articleVersions"][0]
345 |
346 | @pytest.mark.parametrize(
347 | "input_id",
348 | [
349 | ("CCONSO", "L121-14", "LEGIARTI000032227262"),
350 | ("CCONSO", "R742-52", "LEGIARTI000032808914"),
351 | ("CSI", "L622-7", "LEGIARTI000043540586"),
352 | ("CSI", "R314-7", "LEGIARTI000037144520"),
353 | ],
354 | )
355 | def test_get_article_content(self, input_id):
356 | load_dotenv()
357 | client_id = os.getenv("API_KEY")
358 | client_secret = os.getenv("API_SECRET")
359 | headers = get_legifrance_auth(client_id, client_secret)
360 | article_num, code, article_uid = input_id
361 | article_content = get_article_content(article_uid, headers)
362 | assert (
363 | article_content["url"]
364 | == f"https://www.legifrance.gouv.fr/codes/article_lc/{article_uid}"
365 | ), article_content["url"]
366 | assert type(article_content["dateDebut"]) == int
367 | assert type(article_content["dateFin"]) == int
368 | assert article_content["nb_versions"] >= 1, article_content["nb_versions"]
369 |
370 |
371 | class TestGetArticle:
372 | def test_get_single_article(self):
373 | load_dotenv()
374 | client_id = os.getenv("API_KEY")
375 | client_secret = os.getenv("API_SECRET")
376 | article = get_article("CCONSO", "L121-14", client_id, client_secret, past_year_nb=3, future_year_nb=3)
377 | assert article["start_date"] == datetime.datetime(2016, 7, 1, 0, 0), article[
378 | "start_date"
379 | ]
380 | assert article["end_date"] == datetime.datetime(2999, 1, 1, 0, 0), article[
381 | "end_date"
382 | ]
383 | assert (
384 | article["url"]
385 | == "https://www.legifrance.gouv.fr/codes/article_lc/LEGIARTI000032227262"
386 | ), article["url"]
387 | assert (
388 | article["texte"]
389 | == "Le paiement résultant d'une obligation législative ou réglementaire n'exige pas d'engagement exprès et préalable."
390 | )
391 | assert article["code"] == "CCONSO", article[
392 | "code"
393 | ]
394 | assert article["article"] == "L121-14"
395 | assert article["status"] == "Pas de modification"
396 | assert article["status_code"] == 204
397 | assert article["id"] == "LEGIARTI000032227262"
398 | assert article["color"] == "green"
399 | # assert article["nb_versions"] == 1, article["nb_versions"]
400 |
401 | @pytest.mark.parametrize(
402 | "input_expected",
403 | [
404 | ("CCONSO", "L121-14", "LEGIARTI000032227262",204),
405 | ("CCONSO", "R742-52", "LEGIARTI000032808914",204),
406 | ("CSI", "L622-7", "LEGIARTI000043540586", 301),
407 | ("CSI", "R314-7", "LEGIARTI000037144520",204),
408 | ("CGCT", "L1424-71", "LEGIARTI000028529379", 204),
409 | ("CJA", "L121-2", "LEGIARTI000043632528",301),
410 | ("CESEDA", "L753-1", "LEGIARTI000042774802", 301),
411 | ("CENV", "L124-1", "LEGIARTI000033140333", 204),
412 | ],
413 | )
414 | def test_get_multiple_articles(self, input_expected):
415 | load_dotenv()
416 | client_id = os.getenv("API_KEY")
417 | client_secret = os.getenv("API_SECRET")
418 | code_short_name, art_num, art_id, status_code = input_expected
419 | article = get_article(code_short_name, art_num, client_id, client_secret)
420 | assert article["id"] == art_id, (code_short_name, art_num, article["id"])
421 | # assert article["short_code"] == code_short_name, article["code_short_name"]
422 | # assert article["long_code"] == CODE_REFERENCE[code_short_name], article[
423 | # "long_code"
424 | # ]
425 | assert article["status_code"] == status_code, (status_code, code_short_name,art_num)
426 |
427 | @pytest.mark.parametrize(
428 | "input_expected",
429 | [
430 | (
431 | "CCONSO",
432 | "L121-14",
433 | datetime.datetime(2016, 7, 1, 0, 0),
434 | datetime.datetime(2999, 1, 1, 0, 0),
435 | ),
436 | (
437 | "CCONSO",
438 | "R742-52",
439 | datetime.datetime(2016, 7, 1, 0, 0),
440 | datetime.datetime(2999, 1, 1, 0, 0),
441 | ),
442 | (
443 | "CSI",
444 | "L622-7",
445 | datetime.datetime(2021, 5, 27, 0, 0),
446 | datetime.datetime(2022, 11, 26, 0, 0),
447 | ),
448 | (
449 | "CSI",
450 | "R314-7",
451 | datetime.datetime(2018, 8, 1, 0, 0),
452 | datetime.datetime(2999, 1, 1, 0, 0),
453 | ),
454 | (
455 | "CGCT",
456 | "L1424-71",
457 | datetime.datetime(2015, 1, 1, 0, 0),
458 | datetime.datetime(2999, 1, 1, 0, 0),
459 | ),
460 | (
461 | "CJA",
462 | "L121-2",
463 | datetime.datetime(2022, 1, 1, 0, 0),
464 | datetime.datetime(2999, 1, 1, 0, 0),
465 | ),
466 | (
467 | "CESEDA",
468 | "L753-1",
469 | datetime.datetime(2021, 5, 1, 0, 0),
470 | datetime.datetime(2999, 1, 1, 0, 0),
471 | ),
472 | (
473 | "CENV",
474 | "L124-1",
475 | datetime.datetime(2016, 1, 1, 0, 0),
476 | datetime.datetime(2999, 1, 1, 0, 0),
477 | ),
478 | ],
479 | )
480 | def test_get_multiple_articles_validity(self, input_expected):
481 | load_dotenv()
482 | client_id = os.getenv("API_KEY")
483 | client_secret = os.getenv("API_SECRET")
484 | code_short_name, art_num, start_date, end_date = input_expected
485 | article = get_article(code_short_name, art_num, client_id, client_secret)
486 | assert article["start_date"] == start_date, (
487 | article["start_date"],
488 | code_short_name,
489 | art_num,
490 | )
491 | assert article["end_date"] == end_date, (
492 | article["end_date"],
493 | code_short_name,
494 | art_num,
495 | )
496 |
497 | @pytest.mark.parametrize(
498 | "input_expected",
499 | [
500 | ("CCONSO", "R11-14", None),
501 | ("CCONSO", "742-52", None),
502 | ("CSI", "622-7", None),
503 | ("CSI", "314-7", None),
504 | ("CGCT", "1424-71", None),
505 | ("CJA", "121-2", None),
506 | ("CESEDA", "753-1", None),
507 | ("CENV", "124-1", None),
508 | ],
509 | )
510 | def test_get_not_found_articles(self, input_expected):
511 | load_dotenv()
512 | client_id = os.getenv("API_KEY")
513 | client_secret = os.getenv("API_SECRET")
514 | code_short_name, art_num, art_id = input_expected
515 | article = get_article(code_short_name, art_num, client_id, client_secret)
516 | assert article["id"] == art_id, (code_short_name, art_num, article["id"])
517 | # assert article["code_short_name"] == code_short_name, article["code_short_name"]
518 | # assert article["code_full_name"] == CODE_REFERENCE[code_short_name], article[
519 | # "code_full_name"
520 | # ]
521 | assert article["status_code"] == 404
522 |
523 |
524 | class TestTimeDelta:
525 | def test_time_delta_3(self):
526 | past_3, future_3 = time_delta("-", 3), time_delta("+", 3)
527 | today = datetime.date.today()
528 | assert past_3.year == today.year - 3, (past_3.year, past_3.month, past_3.day)
529 | assert future_3.year == today.year + 3, (
530 | future_3.year,
531 | future_3.month,
532 | future_3.day,
533 | )
534 |
535 | def test_time_delta_2(self):
536 | today = datetime.date.today()
537 | past_2, future_2 = time_delta("-", 2), time_delta("+", 2)
538 | assert past_2.year == (today.year) - 2, (past_2.year, past_2.month, past_2.day)
539 | assert future_2.year == (today.year) + 2, (
540 | future_2.year,
541 | future_2.month,
542 | future_2.day,
543 | )
544 |
545 | def test_time_delta_wrong_op(self):
546 |
547 | with pytest.raises(ValueError) as e:
548 | past_mul3 = time_delta("*", 3)
549 | assert e == "ValueError: Wrong operator", e
550 |
551 | def test_time_delta_wrong_nb(self):
552 | with pytest.raises(TypeError) as e:
553 | past_mul3 = time_delta("+", "9")
554 | assert e == "TypeError: Year must be an integer", e
555 |
556 |
557 | class TestValidityArticle:
558 | def test_validity_soon_deprecated(self):
559 | """ """
560 | year_nb = 2
561 | start = datetime.datetime(2018, 1, 1, 0, 0, 0)
562 | past_boundary = time_delta("-", year_nb)
563 | end = datetime.datetime(2023, 1, 1, 0, 0, 0)
564 | future_boundary = time_delta("+", year_nb)
565 | # QUESTION: avons nous besoin de différencier avant et après?
566 | status_code, status_msg, color = get_validity_status(start, end, year_nb, year_nb)
567 | assert end < future_boundary, (
568 | bool(end < future_boundary),
569 | end,
570 | future_boundary,
571 | )
572 | assert status_code == 302, status_code
573 | assert status_msg == "Valable jusqu'au 01/01/2023", status_msg
574 | assert color=="orange", color
575 | def test_validity_recently_modified(self):
576 | year_nb = 2
577 | start = datetime.datetime(2022, 8, 4, 0, 0, 0)
578 | past_boundary = time_delta("-", year_nb)
579 | end = datetime.datetime(2025, 1, 1, 0, 0, 0)
580 | future_boundary = time_delta("+", year_nb)
581 | # QUESTION: avons nous besoin de différencier avant et après?
582 | assert start > past_boundary, (start > past_boundary, start, past_boundary)
583 | status_code, status_msg, color = get_validity_status(start, end, year_nb, year_nb)
584 | assert status_code == 301, status_code
585 | assert status_msg == "Modifié le 04/08/2022", status_msg
586 | assert color=="yellow", color
587 | def test_validity_ras(self):
588 | year_nb = 2
589 | start = datetime.datetime(1801, 8, 4, 0, 0, 0)
590 | past_boundary = time_delta("-", year_nb)
591 | end = datetime.datetime(2040, 1, 1, 0, 0, 0)
592 | future_boundary = time_delta("+", year_nb)
593 | # QUESTION: avons nous besoin de différencier avant et après?
594 | assert start < past_boundary, (start < past_boundary, start, past_boundary)
595 | assert end > future_boundary, (end > future_boundary, end, future_boundary)
596 | status_code, status_msg, color = get_validity_status(start, end, year_nb, year_nb)
597 | assert status_code == 204, status_code
598 | assert status_msg == "Pas de modification", status_msg
599 | assert color=="green", color
--------------------------------------------------------------------------------
/tests/test_005_check_validity.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # coding: utf-8
3 | from datetime import datetime
4 | import os
5 | from re import T
6 | from dotenv import load_dotenv
7 | import datetime
8 | from dateutil.relativedelta import relativedelta
9 | import pytest
10 |
11 |
12 |
13 | ### TIME CONVERSION UTILS
14 |
15 | def convert_date_to_datetime(date):
16 | return datetime.datetime.fromordinal(date.toordinal())
17 |
18 |
19 | def convert_datetime_to_date(date_time):
20 | date_time.replace(hour=0, minute=0, second=0)
21 | return date_time
22 |
23 |
24 | def convert_epoch_to_datetime(epoch):
25 | """convert epoch (seconds till 01/01/1970) to date"""
26 | return datetime.datetime.utcfromtimestamp(epoch / 1000)
27 |
28 |
29 | def convert_datetime_to_epoch(date_time):
30 | """convert datetime to epoch"""
31 | epoch = datetime.datetime.utcfromtimestamp(0)
32 | # return date_time - epoch
33 | return (date_time - epoch).total_seconds() * 1000
34 |
35 |
36 | def convert_date_to_str(date_time):
37 | """convert datetime into string format"""
38 | return datetime.datetime.strftime(date_time[:4], "%d/%m/%Y")
39 |
40 |
41 | def convert_datetime_to_str(date_time):
42 | """convert datetime into string format"""
43 | return datetime.datetime.strftime(date_time, "%d/%m/%Y %H:%M:%S")
44 |
45 |
46 | def convert_str_to_datetime(date_time):
47 | '''
48 | convert string format into datetime
49 | Arguments:
50 | date_str: string representation of a date
51 | Returns:
52 | date_time: datetime
53 | '''
54 | return datetime.datetime.strptime(date_time, "%d/%m/%Y %H:%M:%S")
55 |
56 | ### SPECIALS: plage de temps + status de l'article
57 | def time_delta(operator, year_nb):
58 | """
59 | Calculer le différentiel de date selon l'opérator et le nombre d'années
60 | Arguments:
61 | operator: chaine de caractère qui représente l'opérateur: - ou +
62 | year_nb: entier qui représente le nombre d'années
63 | Return:
64 | datetime_delta: objet datetime représentant la nouvelle date
65 | """
66 | if operator not in ["-", "+"]:
67 | raise ValueError("Wrong operator")
68 | if not isinstance(year_nb, int):
69 | raise TypeError("Year must be an integer")
70 | today = datetime.date.today()
71 | if operator == "-":
72 | return convert_date_to_datetime(today.replace(year=today.year - year_nb))
73 | else:
74 | return convert_date_to_datetime(today.replace(year=today.year + year_nb))
75 |
76 |
77 | def time_delta_to_epoch(operator, year_nb):
78 | """
79 | Calculer le différentiel de date selon l'opérator et le nombre d'années
80 | Arguments:
81 | operator: chaine de caractère qui représente l'opérateur: - ou +
82 | year_nb: entier qui représente le nombre d'années
83 | Return:
84 | date_delta: timestamp représentant la nouvelle date
85 | """
86 |
87 | return convert_datetime_to_epoch(time_delta(operator, year_nb))
88 |
89 | def get_validity_status(start, end, year_before, year_after):
90 | """
91 | Verifier la validité de l'article à partir d'une plage de temps
92 | Arguments:
93 | year_before: Un entier qui correspond à un nombre d'année
94 | start: Un objet datetime représentant la date de création de l'article
95 | year_after: entier représentant un nombre d'année
96 | end: Un objet datetime représentant la date de fin de validité de l'article
97 |
98 | Returns:
99 | status_code: Un code de status qui s'inspire d'HTTP
100 | response: Un message de status
101 | color: status color
102 | """
103 | past_boundary = time_delta("-", year_before)
104 | future_boundary = time_delta("+", year_after)
105 | if start > past_boundary:
106 | return (301, "Modifié le {}".format(convert_datetime_to_str(start).split(" ")[0]), "yellow")
107 | if end < future_boundary:
108 | return (302, "Valable jusqu'au {}".format(convert_datetime_to_str(end).split(" ")[0]), "orange")
109 | if start < past_boundary and end > future_boundary:
110 | return (204, "Pas de modification", "green")
111 |
112 |
--------------------------------------------------------------------------------
/tests/test_006_integration.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 |
3 | import os
4 | from dotenv import load_dotenv
5 | from test_001_parsing import parse_doc
6 | from test_003_matching import get_matching_result_item
7 | from test_004_request import get_article
8 |
9 | def main(file_path, selected_codes=None, pattern_format="article_code", past=3, future=3):
10 | load_dotenv()
11 |
12 | client_id = os.getenv("API_KEY")
13 | client_secret = os.getenv("API_SECRET")
14 | #parse
15 | full_text = parse_doc(file_path)
16 | # matching_results = yield from get_matching_result_item(full_text,selected_codes, pattern_format)
17 | for code, article_nb in get_matching_result_item(full_text,selected_codes, pattern_format):
18 | #request and check validity
19 | # article = get_article(code, article_nb, client_id, client_secret, past_year_nb=past, future_year_nb=future)
20 | # print(article)
21 | yield get_article(code, article_nb, client_id, client_secret, past_year_nb=past, future_year_nb=future)
22 |
23 |
24 |
25 | class TestMainProcess:
26 | def test_main_default(self):
27 | #usert input set to defaults
28 | past=3
29 | future=3
30 | selected_codes = None
31 | pattern_format = "article_code"
32 | file_paths = ["newtest.doc", "newtest.docx", "newtest.pdf"]
33 | for file_path in file_paths:
34 | abspath = os.path.join(
35 | os.path.dirname(os.path.realpath(__file__)), file_path
36 | )
37 | for result in main(file_path):
38 | assert isinstance(result, dict), result
39 |
40 |
41 | if __name__ == "__main__":
42 | main("newtest.doc")
--------------------------------------------------------------------------------
/tests/testnew.odt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/testnew.odt
--------------------------------------------------------------------------------
/tests/testnew.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/testnew.pdf
--------------------------------------------------------------------------------
/tests/testnew_highlighted.odt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/emnetter/codeislow/37d84f271e0976b7b8684e5fdd271993efb47d8c/tests/testnew_highlighted.odt
--------------------------------------------------------------------------------