├── .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 | # {article["code"]} - {article["article"]} 76 | # {article["status"]} 77 | # {article["texte"]} 78 | # 79 | # ''' 80 | # yield 81 | 82 | yield end_results 83 | 84 | if __name__ == "__main__": 85 | # if os.environ.get("APP_LOCATION") == "heroku": 86 | # SSLify(app) 87 | # app.run(host="0.0.0.0", port=int(os.environ.get("PORT", 5000))) 88 | # else: 89 | app.run(host="localhost", port=8080, reloader=True) 90 | -------------------------------------------------------------------------------- /cgu.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Code is low - Conditions d'utilisation 8 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |

Code is low

24 |
Un programme expérimental par E. Netter 25 | (v. 0.8) - codeislow [at] email.enetter.fr
26 |

Source : DILA - Données Légifrance exploitées en 28 | temps réel sous licence ouverte 2.0.

31 | 32 |
33 | 34 | 54 |
55 | 56 |
57 |

Conditions d'utilisation

58 | 59 | 60 |
61 |

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 |
70 | 71 |

Politique de confidentialité

72 | 73 |
74 | 75 |

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 |
117 |
118 |
119 | 120 | 121 | -------------------------------------------------------------------------------- /check_validity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # coding: utf-8 3 | # filename: check_validity.py 4 | """ 5 | 6 | Module de vérification du status de l'article 7 | 8 | - multiple time_converters: 9 | - (date <-> datetime) 10 | - (date,datetime <-> str) 11 | - (epoch<-> datetime) 12 | - time_delta: définition de nouvelle dates à partir d'un nombre année 13 | - check validity: module qui définit le status de l'article en fonction d'une plage temporelle 14 | 15 | """ 16 | from datetime import datetime 17 | import datetime 18 | from dateutil.relativedelta import relativedelta 19 | 20 | 21 | 22 | ### TIME CONVERSION UTILS 23 | 24 | def convert_date_to_datetime(date): 25 | return datetime.datetime.fromordinal(date.toordinal()) 26 | 27 | 28 | def convert_datetime_to_date(date_time): 29 | date_time.replace(hour=0, minute=0, second=0) 30 | return date_time 31 | 32 | 33 | def convert_epoch_to_datetime(epoch): 34 | """convert epoch (seconds till 01/01/1970) to date""" 35 | return datetime.datetime.utcfromtimestamp(epoch / 1000) 36 | 37 | 38 | def convert_datetime_to_epoch(date_time): 39 | """convert datetime to epoch""" 40 | epoch = datetime.datetime.utcfromtimestamp(0) 41 | # return date_time - epoch 42 | return (date_time - epoch).total_seconds() * 1000 43 | 44 | 45 | def convert_date_to_str(date_time): 46 | """convert datetime into string format""" 47 | return datetime.datetime.strftime(date_time[:4], "%d/%m/%Y") 48 | 49 | 50 | def convert_datetime_to_str(date_time): 51 | """convert datetime into string format""" 52 | return datetime.datetime.strftime(date_time, "%d/%m/%Y %H:%M:%S") 53 | 54 | 55 | def convert_str_to_datetime(date_time): 56 | ''' 57 | convert string format into datetime 58 | Arguments: 59 | date_str: string representation of a date 60 | Returns: 61 | date_time: datetime 62 | ''' 63 | return datetime.datetime.strptime(date_time, "%d/%m/%Y %H:%M:%S") 64 | 65 | ### SPECIALS: plage de temps + status de l'article 66 | def time_delta(operator, year_nb): 67 | """ 68 | Calculer le différentiel de date selon l'opérator et le nombre d'années 69 | Arguments 70 | ---------- 71 | operator: str 72 | chaine de caractère qui représente l'opérateur: - ou + 73 | year_nb: int 74 | entier qui représente le nombre d'années 75 | Return 76 | ------- 77 | datetime_delta: datetime 78 | objet datetime représentant la nouvelle date 79 | """ 80 | if operator not in ["-", "+"]: 81 | raise ValueError("Wrong operator") 82 | if not isinstance(year_nb, int): 83 | raise TypeError("Year must be an integer") 84 | today = datetime.date.today() 85 | if operator == "-": 86 | return convert_date_to_datetime(today.replace(year=today.year - year_nb)) 87 | else: 88 | return convert_date_to_datetime(today.replace(year=today.year + year_nb)) 89 | 90 | 91 | def time_delta_to_epoch(operator, year_nb): 92 | """ 93 | Calculer le différentiel de date selon l'opérator et le nombre d'années 94 | 95 | Arguments 96 | ---------- 97 | operator: str 98 | chaine de caractère qui représente l'opérateur: - ou + 99 | year_nb: int 100 | entier qui représente le nombre d'années 101 | Return 102 | ------- 103 | epoch_delta: int 104 | timestamp/epoch représentant la nouvelle date 105 | """ 106 | 107 | 108 | return convert_datetime_to_epoch(time_delta(operator, year_nb)) 109 | 110 | def get_validity_status(start, end, year_before, year_after): 111 | """ 112 | Verifier la validité de l'article à partir d'une plage de temps 113 | 114 | Arguments 115 | --------- 116 | year_before: int 117 | Nombre d'année avant aujourd'hui 118 | start: datetime 119 | Date de création de l'article 120 | year_after:int 121 | Nombre d'année après aujourd'hui 122 | end: datetime 123 | Date d'expiration de l'article 124 | 125 | Returns 126 | -------- 127 | status_code: int 128 | Un code de status qui s'inspire d'HTTP 129 | response: str 130 | Un message de status 131 | color: str 132 | Couleur CSS du status: green, yellow, orange, red 133 | """ 134 | past_boundary = time_delta("-", year_before) 135 | future_boundary = time_delta("+", year_after) 136 | if start > past_boundary: 137 | return (301, "Modifié le {}".format(convert_datetime_to_str(start).split(" ")[0]), "yellow") 138 | if end < future_boundary: 139 | return (302, "Valable jusqu'au {}".format(convert_datetime_to_str(end).split(" ")[0]), "orange") 140 | if start < past_boundary and end > future_boundary: 141 | return (204, "Pas de modification", "green") 142 | 143 | -------------------------------------------------------------------------------- /code_list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code is low - Liste des codes 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |

Code is low

21 |
Un programme expérimental par E. Netter (v. 0.8) - codeislow [at] email.enetter.fr
22 |

Source : DILA - Données Légifrance exploitées en temps réel sous licence ouverte 2.0.

23 |
24 | 44 |
45 | 46 |
47 |

Codes actuellement testés [formes abrégées reconnues]

48 | 49 | 50 |
51 | 52 |

La forme longue est évidemment reconnue. La casse est indifférente. 53 | N'hésitez pas à demander l'ajout d'autres codes.

54 | 55 |
    56 |
  • Code des assurances [C. assur.]
  • 57 |
  • Code civil [C. civ.]
  • 58 |
  • Code de commerce [C. com.]
  • 59 |
  • Code de la consommation [C. conso.]
  • 60 |
  • Code de l'entrée et du séjour des étrangers et du droit d'asile [CESEDA]
  • 61 |
  • Code de l'environnement [C. envir., CE]
  • 62 |
  • Code général des collectivités territoriales [CGCT]
  • 63 |
  • Code de justice administrative [CJA]
  • 64 |
  • Code pénal [C. pén.]
  • 65 |
  • Code des postes et des communications électroniques [CPCE]
  • 66 |
  • Code de procédure civile [C. pr. civ., CPC]
  • 67 |
  • Code de procédure pénale [CPP]
  • 68 |
  • Code de la propriété intellectuelle [C. pr. int., CPI]
  • 69 |
  • Code de la santé publique [C. sant. pub., CSP]
  • 70 |
  • Code de la sécurité intérieure [CSI]
  • 71 |
  • Code de la sécurité sociale [CSS]
  • 72 |
  • Code du travail [C. trav.]
  • 73 |
74 |
75 |
-------------------------------------------------------------------------------- /code_references.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env 2 | # filename: code_references 3 | """ 4 | Code references module: 5 | 6 | - Build regex for codes 7 | - Get name and abbreviation for codes 8 | 9 | """ 10 | 11 | CODE_REGEX = { 12 | "CCIV": r"(?PCode\scivil|C\.\sciv\.|Code\sciv\.|C\.civ\.|civ\.|CCIV)", 13 | "CPRCIV": r"(?PCode\sde\sprocédure\scivile|C\.\spr\.\sciv\.|CPC)", 14 | "CCOM": r"(?PCode\sde\scommerce|C\.\scom\.)", 15 | "CTRAV": r"(?PCode\sdu\stravail|C\.\strav\.)", 16 | "CPI": r"(?PCode\sde\sla\spropriété\sintellectuelle|CPI|C\.\spr\.\sint\.)", 17 | "CPEN": r"(?PCode\spénal|C\.\spén\.)", 18 | "CPP": r"(?PCode\sde\sprocédure\spénale|CPP)", 19 | "CASSUR": r"(?PCode\sdes\sassurances|C\.\sassur\.)", 20 | "CCONSO": r"(?PCode\sde\sla\sconsommation|C\.\sconso\.)", 21 | "CSI": r"(?PCode\sde\slasécurité\sintérieure|CSI)", 22 | "CSP": r"(?PCode\sde\slasanté\spublique|C\.\ssant\.\spub\.|CSP)", 23 | "CSS": r"(?PCode\sde\slasécurité\ssociale|C\.\ssec\.\ssoc\.|CSS)", 24 | "CESEDA": r"(?PCode\sde\sl'entrée\set\sdu\sséjour\sdes\sétrangers\set\sdu\sdroit\sd'asile|CESEDA)", 25 | "CGCT": r"(?PCode\sgénéral\sdes\scollectivités\sterritoriales|CGCT)", 26 | "CPCE": r"(?PCode\sdes\spostes\set\sdes\scommunications\sélectroniques|CPCE)", 27 | "CENV": r"(?PCode\sde\sl'environnement|C.\senvir.|CE\.)", 28 | "CJA": r"(?PCode\sde\sjustice\sadministrative|CJA)", 29 | } 30 | 31 | CODE_REFERENCE = { 32 | "CCIV": "Code civil", 33 | "CPRCIV": "Code de procédure civile", 34 | "CCOM": "Code de commerce", 35 | "CTRAV": "Code du travail", 36 | "CPI": "Code de la propriété intellectuelle", 37 | "CPEN": "Code pénal", 38 | "CPP": "Code de procédure pénale", 39 | "CASSUR": "Code des assurances", 40 | "CCONSO": "Code de la consommation", 41 | "CSI": "Code de la sécurité intérieure", 42 | "CSP": "Code de la santé publique", 43 | "CSS": "Code de la sécurité sociale", 44 | "CESEDA": "Code de l'entrée et du séjour des étrangers et du droit d'asile", 45 | "CGCT": "Code général des collectivités territoriales", 46 | "CPCE": "Code des postes et des communications électroniques", 47 | "CENV": "Code de l'environnement", 48 | "CJA": "Code de justice administrative", 49 | } 50 | 51 | def get_long_and_short_code(code_name: str) -> (str,str): 52 | ''' 53 | Accéder aux deux versions du nom du code: le nom complet et son abréviation 54 | 55 | Parameters 56 | ---------- 57 | code_name : str 58 | le nom du code (version longue ou courte) 59 | 60 | Returns 61 | ---------- 62 | long_code: str 63 | le nom complet du code 64 | short_code: str 65 | l'abréviation du code 66 | 67 | Notes 68 | ---------- 69 | Si le nom du code n'a pas été trouvé les valeurs sont nulles (None, None) 70 | ''' 71 | 72 | if code_name in CODE_REFERENCE.keys(): 73 | short_code = code_name 74 | long_code = CODE_REFERENCE[code_name] 75 | elif code_name in CODE_REFERENCE.values(): 76 | long_code = code_name 77 | short_code_results = [k for k, v in CODE_REFERENCE.items() if v == code_name] 78 | if len(short_code_results) > 0: 79 | short_code = short_code_results[0] 80 | else: 81 | short_code = None 82 | else: 83 | short_code, long_code = None, None 84 | return(long_code, short_code) 85 | 86 | 87 | def get_code_full_name_from_short_code(short_code): 88 | """ 89 | Shortcut to get corresponding full_name from short_code 90 | 91 | Arguments 92 | ---------- 93 | short_code: str 94 | short form of Code eg. CCIV 95 | 96 | Returns 97 | ---------- 98 | full_name: str 99 | long form of code eg. Code Civil 100 | 101 | """ 102 | try: 103 | return CODE_REFERENCE[short_code] 104 | 105 | except KeyError: 106 | if get_short_code_from_full_name(short_code) is not None: 107 | return short_code 108 | else: 109 | return None 110 | 111 | 112 | def get_short_code_from_full_name(full_name): 113 | """ 114 | Shortcut to get corresponding short_code from full_name 115 | 116 | Arguments 117 | ---------- 118 | full_name: str 119 | long form of code eg. Code Civil 120 | 121 | Returns 122 | ---------- 123 | short_code: str 124 | short form of Code eg. CCIV 125 | """ 126 | keys = [k for k, v in CODE_REFERENCE.items() if v == full_name] 127 | if len(keys) > 0: 128 | return keys[0] 129 | else: 130 | return None 131 | 132 | 133 | def filter_code_regex(selected_codes): 134 | """ 135 | Contruire l'expression régulière pour détecter les différents codes dans le document. 136 | 137 | Arguments 138 | ---------- 139 | selected_codes: array 140 | [short_code, ...]. Default: None (no filter) 141 | 142 | Returns 143 | ---------- 144 | regex: str 145 | a regex expression to match codes 146 | """ 147 | if selected_codes is None: 148 | return "({})".format("|".join(list(CODE_REGEX.values()))) 149 | 150 | 151 | if len(selected_codes) == 1: 152 | return CODE_REGEX[selected_codes[0]] 153 | else: 154 | selected_code_list = [CODE_REGEX[x] for x in sorted(selected_codes)] 155 | return "({})".format("|".join(selected_code_list)) 156 | 157 | def filter_code_reference(selected_codes=None): 158 | """ 159 | Filtrer le dictionnaire de référence des codes 160 | 161 | Arguments 162 | ---------- 163 | selected_codes: array 164 | [short_code, ...]. Default None means no filter 165 | Returns 166 | ---------- 167 | code_reference_dict_filtered: dict 168 | The CODE_REFERENCE filtered with only the selected codes 169 | """ 170 | if selected_codes is None: 171 | return CODE_REFERENCE 172 | return {x: CODE_REFERENCE[x] for x in sorted(selected_codes)} 173 | 174 | -------------------------------------------------------------------------------- /codeislow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from dotenv import load_dotenv 5 | from parsing import parse_doc 6 | from matching import get_matching_result_item 7 | from request_api 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 | def load_result(file_path, selected_codes=None, pattern_format="article_code", past=3, future=3): 24 | ''' 25 | Load result in HTML 26 | 27 | Arguments 28 | --------- 29 | filepath: str 30 | le chemin du fichier 31 | selected_codes: array 32 | la liste des codes (version abbréviée) à détecter 33 | pattern_format: str 34 | le format des références: Article xxx du Code yyy ou Code yyyy Article xxx 35 | past: int 36 | nombre d'années dans le passé 37 | future: int 38 | nombre d'années dans le futur 39 | Yields 40 | ------ 41 | html_results: str 42 | resultat sous forme de cellule d'une table HTML 43 | ''' 44 | load_dotenv() 45 | client_id = os.getenv("API_KEY") 46 | client_secret = os.getenv("API_SECRET") 47 | #parse 48 | full_text = parse_doc(file_path) 49 | # matching_results = yield from get_matching_result_item(full_text,selected_codes, pattern_format) 50 | for code, article_nb in get_matching_result_item(full_text,selected_codes, pattern_format): 51 | #request and check validity 52 | article = get_article(code, article_nb, client_id, client_secret, past_year_nb=past, future_year_nb=future) 53 | row = f""" 54 | 55 | {article["code"]} - {article["article"]} 56 | {article["status"]} 57 | {article["texte"]} 58 | {article["date_debut"]}-{article["date_fin"]} 59 | 60 | """ 61 | yield(row) 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/Licence_CeCILL_V2.1-fr.txt: -------------------------------------------------------------------------------- 1 | 2 | CONTRAT DE LICENCE DE LOGICIEL LIBRE CeCILL 3 | 4 | Version 2.1 du 2013-06-21 5 | 6 | 7 | Avertissement 8 | 9 | Ce contrat est une licence de logiciel libre issue d'une concertation 10 | entre ses auteurs afin que le respect de deux grands principes préside à 11 | sa rédaction: 12 | 13 | * d'une part, le respect des principes de diffusion des logiciels 14 | libres: accès au code source, droits étendus conférés aux utilisateurs, 15 | * d'autre part, la désignation d'un droit applicable, le droit 16 | français, auquel elle est conforme, tant au regard du droit de la 17 | responsabilité civile que du droit de la propriété intellectuelle et 18 | de la protection qu'il offre aux auteurs et titulaires des droits 19 | patrimoniaux sur un logiciel. 20 | 21 | Les auteurs de la licence CeCILL (Ce[a] C[nrs] I[nria] L[ogiciel] L[ibre]) 22 | sont: 23 | 24 | Commissariat à l'énergie atomique et aux énergies alternatives - CEA, 25 | établissement public de recherche à caractère scientifique, technique et 26 | industriel, dont le siège est situé 25 rue Leblanc, immeuble Le Ponant 27 | D, 75015 Paris. 28 | 29 | Centre National de la Recherche Scientifique - CNRS, établissement 30 | public à caractère scientifique et technologique, dont le siège est 31 | situé 3 rue Michel-Ange, 75794 Paris cedex 16. 32 | 33 | Institut National de Recherche en Informatique et en Automatique - 34 | Inria, établissement public à caractère scientifique et technologique, 35 | dont le siège est situé Domaine de Voluceau, Rocquencourt, BP 105, 78153 36 | Le Chesnay cedex. 37 | 38 | 39 | Préambule 40 | 41 | Ce contrat est une licence de logiciel libre dont l'objectif est de 42 | conférer aux utilisateurs la liberté de modification et de 43 | redistribution du logiciel régi par cette licence dans le cadre d'un 44 | modèle de diffusion en logiciel libre. 45 | 46 | L'exercice de ces libertés est assorti de certains devoirs à la charge 47 | des utilisateurs afin de préserver ce statut au cours des 48 | redistributions ultérieures. 49 | 50 | L'accessibilité au code source et les droits de copie, de modification 51 | et de redistribution qui en découlent ont pour contrepartie de n'offrir 52 | aux utilisateurs qu'une garantie limitée et de ne faire peser sur 53 | l'auteur du logiciel, le titulaire des droits patrimoniaux et les 54 | concédants successifs qu'une responsabilité restreinte. 55 | 56 | A cet égard l'attention de l'utilisateur est attirée sur les risques 57 | associés au chargement, à l'utilisation, à la modification et/ou au 58 | développement et à la reproduction du logiciel par l'utilisateur étant 59 | donné sa spécificité de logiciel libre, qui peut le rendre complexe à 60 | manipuler et qui le réserve donc à des développeurs ou des 61 | professionnels avertis possédant des connaissances informatiques 62 | approfondies. Les utilisateurs sont donc invités à charger et tester 63 | l'adéquation du logiciel à leurs besoins dans des conditions permettant 64 | d'assurer la sécurité de leurs systèmes et/ou de leurs données et, plus 65 | généralement, à l'utiliser et l'exploiter dans les mêmes conditions de 66 | sécurité. Ce contrat peut être reproduit et diffusé librement, sous 67 | réserve de le conserver en l'état, sans ajout ni suppression de clauses. 68 | 69 | Ce contrat est susceptible de s'appliquer à tout logiciel dont le 70 | titulaire des droits patrimoniaux décide de soumettre l'exploitation aux 71 | dispositions qu'il contient. 72 | 73 | Une liste de questions fréquemment posées se trouve sur le site web 74 | officiel de la famille des licences CeCILL 75 | (http://www.cecill.info/index.fr.html) pour toute clarification qui 76 | serait nécessaire. 77 | 78 | 79 | Article 1 - DEFINITIONS 80 | 81 | Dans ce contrat, les termes suivants, lorsqu'ils seront écrits avec une 82 | lettre capitale, auront la signification suivante: 83 | 84 | Contrat: désigne le présent contrat de licence, ses éventuelles versions 85 | postérieures et annexes. 86 | 87 | Logiciel: désigne le logiciel sous sa forme de Code Objet et/ou de Code 88 | Source et le cas échéant sa documentation, dans leur état au moment de 89 | l'acceptation du Contrat par le Licencié. 90 | 91 | Logiciel Initial: désigne le Logiciel sous sa forme de Code Source et 92 | éventuellement de Code Objet et le cas échéant sa documentation, dans 93 | leur état au moment de leur première diffusion sous les termes du Contrat. 94 | 95 | Logiciel Modifié: désigne le Logiciel modifié par au moins une 96 | Contribution. 97 | 98 | Code Source: désigne l'ensemble des instructions et des lignes de 99 | programme du Logiciel et auquel l'accès est nécessaire en vue de 100 | modifier le Logiciel. 101 | 102 | Code Objet: désigne les fichiers binaires issus de la compilation du 103 | Code Source. 104 | 105 | Titulaire: désigne le ou les détenteurs des droits patrimoniaux d'auteur 106 | sur le Logiciel Initial. 107 | 108 | Licencié: désigne le ou les utilisateurs du Logiciel ayant accepté le 109 | Contrat. 110 | 111 | Contributeur: désigne le Licencié auteur d'au moins une Contribution. 112 | 113 | Concédant: désigne le Titulaire ou toute personne physique ou morale 114 | distribuant le Logiciel sous le Contrat. 115 | 116 | Contribution: désigne l'ensemble des modifications, corrections, 117 | traductions, adaptations et/ou nouvelles fonctionnalités intégrées dans 118 | le Logiciel par tout Contributeur, ainsi que tout Module Interne. 119 | 120 | Module: désigne un ensemble de fichiers sources y compris leur 121 | documentation qui permet de réaliser des fonctionnalités ou services 122 | supplémentaires à ceux fournis par le Logiciel. 123 | 124 | Module Externe: désigne tout Module, non dérivé du Logiciel, tel que ce 125 | Module et le Logiciel s'exécutent dans des espaces d'adressage 126 | différents, l'un appelant l'autre au moment de leur exécution. 127 | 128 | Module Interne: désigne tout Module lié au Logiciel de telle sorte 129 | qu'ils s'exécutent dans le même espace d'adressage. 130 | 131 | GNU GPL: désigne la GNU General Public License dans sa version 2 ou 132 | toute version ultérieure, telle que publiée par Free Software Foundation 133 | Inc. 134 | 135 | GNU Affero GPL: désigne la GNU Affero General Public License dans sa 136 | version 3 ou toute version ultérieure, telle que publiée par Free 137 | Software Foundation Inc. 138 | 139 | EUPL: désigne la Licence Publique de l'Union européenne dans sa version 140 | 1.1 ou toute version ultérieure, telle que publiée par la Commission 141 | Européenne. 142 | 143 | Parties: désigne collectivement le Licencié et le Concédant. 144 | 145 | Ces termes s'entendent au singulier comme au pluriel. 146 | 147 | 148 | Article 2 - OBJET 149 | 150 | Le Contrat a pour objet la concession par le Concédant au Licencié d'une 151 | licence non exclusive, cessible et mondiale du Logiciel telle que 152 | définie ci-après à l'article 5 <#etendue> pour toute la durée de 153 | protection des droits portant sur ce Logiciel. 154 | 155 | 156 | Article 3 - ACCEPTATION 157 | 158 | 3.1 L'acceptation par le Licencié des termes du Contrat est réputée 159 | acquise du fait du premier des faits suivants: 160 | 161 | * (i) le chargement du Logiciel par tout moyen notamment par 162 | téléchargement à partir d'un serveur distant ou par chargement à 163 | partir d'un support physique; 164 | * (ii) le premier exercice par le Licencié de l'un quelconque des 165 | droits concédés par le Contrat. 166 | 167 | 3.2 Un exemplaire du Contrat, contenant notamment un avertissement 168 | relatif aux spécificités du Logiciel, à la restriction de garantie et à 169 | la limitation à un usage par des utilisateurs expérimentés a été mis à 170 | disposition du Licencié préalablement à son acceptation telle que 171 | définie à l'article 3.1 <#acceptation-acquise> ci dessus et le Licencié 172 | reconnaît en avoir pris connaissance. 173 | 174 | 175 | Article 4 - ENTREE EN VIGUEUR ET DUREE 176 | 177 | 178 | 4.1 ENTREE EN VIGUEUR 179 | 180 | Le Contrat entre en vigueur à la date de son acceptation par le Licencié 181 | telle que définie en 3.1 <#acceptation-acquise>. 182 | 183 | 184 | 4.2 DUREE 185 | 186 | Le Contrat produira ses effets pendant toute la durée légale de 187 | protection des droits patrimoniaux portant sur le Logiciel. 188 | 189 | 190 | Article 5 - ETENDUE DES DROITS CONCEDES 191 | 192 | Le Concédant concède au Licencié, qui accepte, les droits suivants sur 193 | le Logiciel pour toutes destinations et pour la durée du Contrat dans 194 | les conditions ci-après détaillées. 195 | 196 | Par ailleurs, si le Concédant détient ou venait à détenir un ou 197 | plusieurs brevets d'invention protégeant tout ou partie des 198 | fonctionnalités du Logiciel ou de ses composants, il s'engage à ne pas 199 | opposer les éventuels droits conférés par ces brevets aux Licenciés 200 | successifs qui utiliseraient, exploiteraient ou modifieraient le 201 | Logiciel. En cas de cession de ces brevets, le Concédant s'engage à 202 | faire reprendre les obligations du présent alinéa aux cessionnaires. 203 | 204 | 205 | 5.1 DROIT D'UTILISATION 206 | 207 | Le Licencié est autorisé à utiliser le Logiciel, sans restriction quant 208 | aux domaines d'application, étant ci-après précisé que cela comporte: 209 | 210 | 1. 211 | 212 | la reproduction permanente ou provisoire du Logiciel en tout ou 213 | partie par tout moyen et sous toute forme. 214 | 215 | 2. 216 | 217 | le chargement, l'affichage, l'exécution, ou le stockage du Logiciel 218 | sur tout support. 219 | 220 | 3. 221 | 222 | la possibilité d'en observer, d'en étudier, ou d'en tester le 223 | fonctionnement afin de déterminer les idées et principes qui sont à 224 | la base de n'importe quel élément de ce Logiciel; et ceci, lorsque 225 | le Licencié effectue toute opération de chargement, d'affichage, 226 | d'exécution, de transmission ou de stockage du Logiciel qu'il est en 227 | droit d'effectuer en vertu du Contrat. 228 | 229 | 230 | 5.2 DROIT D'APPORTER DES CONTRIBUTIONS 231 | 232 | Le droit d'apporter des Contributions comporte le droit de traduire, 233 | d'adapter, d'arranger ou d'apporter toute autre modification au Logiciel 234 | et le droit de reproduire le logiciel en résultant. 235 | 236 | Le Licencié est autorisé à apporter toute Contribution au Logiciel sous 237 | réserve de mentionner, de façon explicite, son nom en tant qu'auteur de 238 | cette Contribution et la date de création de celle-ci. 239 | 240 | 241 | 5.3 DROIT DE DISTRIBUTION 242 | 243 | Le droit de distribution comporte notamment le droit de diffuser, de 244 | transmettre et de communiquer le Logiciel au public sur tout support et 245 | par tout moyen ainsi que le droit de mettre sur le marché à titre 246 | onéreux ou gratuit, un ou des exemplaires du Logiciel par tout procédé. 247 | 248 | Le Licencié est autorisé à distribuer des copies du Logiciel, modifié ou 249 | non, à des tiers dans les conditions ci-après détaillées. 250 | 251 | 252 | 5.3.1 DISTRIBUTION DU LOGICIEL SANS MODIFICATION 253 | 254 | Le Licencié est autorisé à distribuer des copies conformes du Logiciel, 255 | sous forme de Code Source ou de Code Objet, à condition que cette 256 | distribution respecte les dispositions du Contrat dans leur totalité et 257 | soit accompagnée: 258 | 259 | 1. 260 | 261 | d'un exemplaire du Contrat, 262 | 263 | 2. 264 | 265 | d'un avertissement relatif à la restriction de garantie et de 266 | responsabilité du Concédant telle que prévue aux articles 8 267 | <#responsabilite> et 9 <#garantie>, 268 | 269 | et que, dans le cas où seul le Code Objet du Logiciel est redistribué, 270 | le Licencié permette un accès effectif au Code Source complet du 271 | Logiciel pour une durée d'au moins 3 ans à compter de la distribution du 272 | logiciel, étant entendu que le coût additionnel d'acquisition du Code 273 | Source ne devra pas excéder le simple coût de transfert des données. 274 | 275 | 276 | 5.3.2 DISTRIBUTION DU LOGICIEL MODIFIE 277 | 278 | Lorsque le Licencié apporte une Contribution au Logiciel, les conditions 279 | de distribution du Logiciel Modifié en résultant sont alors soumises à 280 | l'intégralité des dispositions du Contrat. 281 | 282 | Le Licencié est autorisé à distribuer le Logiciel Modifié, sous forme de 283 | code source ou de code objet, à condition que cette distribution 284 | respecte les dispositions du Contrat dans leur totalité et soit 285 | accompagnée: 286 | 287 | 1. 288 | 289 | d'un exemplaire du Contrat, 290 | 291 | 2. 292 | 293 | d'un avertissement relatif à la restriction de garantie et de 294 | responsabilité du Concédant telle que prévue aux articles 8 295 | <#responsabilite> et 9 <#garantie>, 296 | 297 | et, dans le cas où seul le code objet du Logiciel Modifié est redistribué, 298 | 299 | 3. 300 | 301 | d'une note précisant les conditions d'accès effectif au code source 302 | complet du Logiciel Modifié, pendant une période d'au moins 3 ans à 303 | compter de la distribution du Logiciel Modifié, étant entendu que le 304 | coût additionnel d'acquisition du code source ne devra pas excéder 305 | le simple coût de transfert des données. 306 | 307 | 308 | 5.3.3 DISTRIBUTION DES MODULES EXTERNES 309 | 310 | Lorsque le Licencié a développé un Module Externe les conditions du 311 | Contrat ne s'appliquent pas à ce Module Externe, qui peut être distribué 312 | sous un contrat de licence différent. 313 | 314 | 315 | 5.3.4 COMPATIBILITE AVEC D'AUTRES LICENCES 316 | 317 | Le Licencié peut inclure un code soumis aux dispositions d'une des 318 | versions de la licence GNU GPL, GNU Affero GPL et/ou EUPL dans le 319 | Logiciel modifié ou non et distribuer l'ensemble sous les conditions de 320 | la même version de la licence GNU GPL, GNU Affero GPL et/ou EUPL. 321 | 322 | Le Licencié peut inclure le Logiciel modifié ou non dans un code soumis 323 | aux dispositions d'une des versions de la licence GNU GPL, GNU Affero 324 | GPL et/ou EUPL et distribuer l'ensemble sous les conditions de la même 325 | version de la licence GNU GPL, GNU Affero GPL et/ou EUPL. 326 | 327 | 328 | Article 6 - PROPRIETE INTELLECTUELLE 329 | 330 | 331 | 6.1 SUR LE LOGICIEL INITIAL 332 | 333 | Le Titulaire est détenteur des droits patrimoniaux sur le Logiciel 334 | Initial. Toute utilisation du Logiciel Initial est soumise au respect 335 | des conditions dans lesquelles le Titulaire a choisi de diffuser son 336 | oeuvre et nul autre n'a la faculté de modifier les conditions de 337 | diffusion de ce Logiciel Initial. 338 | 339 | Le Titulaire s'engage à ce que le Logiciel Initial reste au moins régi 340 | par le Contrat et ce, pour la durée visée à l'article 4.2 <#duree>. 341 | 342 | 343 | 6.2 SUR LES CONTRIBUTIONS 344 | 345 | Le Licencié qui a développé une Contribution est titulaire sur celle-ci 346 | des droits de propriété intellectuelle dans les conditions définies par 347 | la législation applicable. 348 | 349 | 350 | 6.3 SUR LES MODULES EXTERNES 351 | 352 | Le Licencié qui a développé un Module Externe est titulaire sur celui-ci 353 | des droits de propriété intellectuelle dans les conditions définies par 354 | la législation applicable et reste libre du choix du contrat régissant 355 | sa diffusion. 356 | 357 | 358 | 6.4 DISPOSITIONS COMMUNES 359 | 360 | Le Licencié s'engage expressément: 361 | 362 | 1. 363 | 364 | à ne pas supprimer ou modifier de quelque manière que ce soit les 365 | mentions de propriété intellectuelle apposées sur le Logiciel; 366 | 367 | 2. 368 | 369 | à reproduire à l'identique lesdites mentions de propriété 370 | intellectuelle sur les copies du Logiciel modifié ou non. 371 | 372 | Le Licencié s'engage à ne pas porter atteinte, directement ou 373 | indirectement, aux droits de propriété intellectuelle du Titulaire et/ou 374 | des Contributeurs sur le Logiciel et à prendre, le cas échéant, à 375 | l'égard de son personnel toutes les mesures nécessaires pour assurer le 376 | respect des dits droits de propriété intellectuelle du Titulaire et/ou 377 | des Contributeurs. 378 | 379 | 380 | Article 7 - SERVICES ASSOCIES 381 | 382 | 7.1 Le Contrat n'oblige en aucun cas le Concédant à la réalisation de 383 | prestations d'assistance technique ou de maintenance du Logiciel. 384 | 385 | Cependant le Concédant reste libre de proposer ce type de services. Les 386 | termes et conditions d'une telle assistance technique et/ou d'une telle 387 | maintenance seront alors déterminés dans un acte séparé. Ces actes de 388 | maintenance et/ou assistance technique n'engageront que la seule 389 | responsabilité du Concédant qui les propose. 390 | 391 | 7.2 De même, tout Concédant est libre de proposer, sous sa seule 392 | responsabilité, à ses licenciés une garantie, qui n'engagera que lui, 393 | lors de la redistribution du Logiciel et/ou du Logiciel Modifié et ce, 394 | dans les conditions qu'il souhaite. Cette garantie et les modalités 395 | financières de son application feront l'objet d'un acte séparé entre le 396 | Concédant et le Licencié. 397 | 398 | 399 | Article 8 - RESPONSABILITE 400 | 401 | 8.1 Sous réserve des dispositions de l'article 8.2 402 | <#limite-responsabilite>, le Licencié a la faculté, sous réserve de 403 | prouver la faute du Concédant concerné, de solliciter la réparation du 404 | préjudice direct qu'il subirait du fait du Logiciel et dont il apportera 405 | la preuve. 406 | 407 | 8.2 La responsabilité du Concédant est limitée aux engagements pris en 408 | application du Contrat et ne saurait être engagée en raison notamment: 409 | (i) des dommages dus à l'inexécution, totale ou partielle, de ses 410 | obligations par le Licencié, (ii) des dommages directs ou indirects 411 | découlant de l'utilisation ou des performances du Logiciel subis par le 412 | Licencié et (iii) plus généralement d'un quelconque dommage indirect. En 413 | particulier, les Parties conviennent expressément que tout préjudice 414 | financier ou commercial (par exemple perte de données, perte de 415 | bénéfices, perte d'exploitation, perte de clientèle ou de commandes, 416 | manque à gagner, trouble commercial quelconque) ou toute action dirigée 417 | contre le Licencié par un tiers, constitue un dommage indirect et 418 | n'ouvre pas droit à réparation par le Concédant. 419 | 420 | 421 | Article 9 - GARANTIE 422 | 423 | 9.1 Le Licencié reconnaît que l'état actuel des connaissances 424 | scientifiques et techniques au moment de la mise en circulation du 425 | Logiciel ne permet pas d'en tester et d'en vérifier toutes les 426 | utilisations ni de détecter l'existence d'éventuels défauts. L'attention 427 | du Licencié a été attirée sur ce point sur les risques associés au 428 | chargement, à l'utilisation, la modification et/ou au développement et à 429 | la reproduction du Logiciel qui sont réservés à des utilisateurs avertis. 430 | 431 | Il relève de la responsabilité du Licencié de contrôler, par tous 432 | moyens, l'adéquation du produit à ses besoins, son bon fonctionnement et 433 | de s'assurer qu'il ne causera pas de dommages aux personnes et aux biens. 434 | 435 | 9.2 Le Concédant déclare de bonne foi être en droit de concéder 436 | l'ensemble des droits attachés au Logiciel (comprenant notamment les 437 | droits visés à l'article 5 <#etendue>). 438 | 439 | 9.3 Le Licencié reconnaît que le Logiciel est fourni "en l'état" par le 440 | Concédant sans autre garantie, expresse ou tacite, que celle prévue à 441 | l'article 9.2 <#bonne-foi> et notamment sans aucune garantie sur sa 442 | valeur commerciale, son caractère sécurisé, innovant ou pertinent. 443 | 444 | En particulier, le Concédant ne garantit pas que le Logiciel est exempt 445 | d'erreur, qu'il fonctionnera sans interruption, qu'il sera compatible 446 | avec l'équipement du Licencié et sa configuration logicielle ni qu'il 447 | remplira les besoins du Licencié. 448 | 449 | 9.4 Le Concédant ne garantit pas, de manière expresse ou tacite, que le 450 | Logiciel ne porte pas atteinte à un quelconque droit de propriété 451 | intellectuelle d'un tiers portant sur un brevet, un logiciel ou sur tout 452 | autre droit de propriété. Ainsi, le Concédant exclut toute garantie au 453 | profit du Licencié contre les actions en contrefaçon qui pourraient être 454 | diligentées au titre de l'utilisation, de la modification, et de la 455 | redistribution du Logiciel. Néanmoins, si de telles actions sont 456 | exercées contre le Licencié, le Concédant lui apportera son expertise 457 | technique et juridique pour sa défense. Cette expertise technique et 458 | juridique est déterminée au cas par cas entre le Concédant concerné et 459 | le Licencié dans le cadre d'un protocole d'accord. Le Concédant dégage 460 | toute responsabilité quant à l'utilisation de la dénomination du 461 | Logiciel par le Licencié. Aucune garantie n'est apportée quant à 462 | l'existence de droits antérieurs sur le nom du Logiciel et sur 463 | l'existence d'une marque. 464 | 465 | 466 | Article 10 - RESILIATION 467 | 468 | 10.1 En cas de manquement par le Licencié aux obligations mises à sa 469 | charge par le Contrat, le Concédant pourra résilier de plein droit le 470 | Contrat trente (30) jours après notification adressée au Licencié et 471 | restée sans effet. 472 | 473 | 10.2 Le Licencié dont le Contrat est résilié n'est plus autorisé à 474 | utiliser, modifier ou distribuer le Logiciel. Cependant, toutes les 475 | licences qu'il aura concédées antérieurement à la résiliation du Contrat 476 | resteront valides sous réserve qu'elles aient été effectuées en 477 | conformité avec le Contrat. 478 | 479 | 480 | Article 11 - DISPOSITIONS DIVERSES 481 | 482 | 483 | 11.1 CAUSE EXTERIEURE 484 | 485 | Aucune des Parties ne sera responsable d'un retard ou d'une défaillance 486 | d'exécution du Contrat qui serait dû à un cas de force majeure, un cas 487 | fortuit ou une cause extérieure, telle que, notamment, le mauvais 488 | fonctionnement ou les interruptions du réseau électrique ou de 489 | télécommunication, la paralysie du réseau liée à une attaque 490 | informatique, l'intervention des autorités gouvernementales, les 491 | catastrophes naturelles, les dégâts des eaux, les tremblements de terre, 492 | le feu, les explosions, les grèves et les conflits sociaux, l'état de 493 | guerre... 494 | 495 | 11.2 Le fait, par l'une ou l'autre des Parties, d'omettre en une ou 496 | plusieurs occasions de se prévaloir d'une ou plusieurs dispositions du 497 | Contrat, ne pourra en aucun cas impliquer renonciation par la Partie 498 | intéressée à s'en prévaloir ultérieurement. 499 | 500 | 11.3 Le Contrat annule et remplace toute convention antérieure, écrite 501 | ou orale, entre les Parties sur le même objet et constitue l'accord 502 | entier entre les Parties sur cet objet. Aucune addition ou modification 503 | aux termes du Contrat n'aura d'effet à l'égard des Parties à moins 504 | d'être faite par écrit et signée par leurs représentants dûment habilités. 505 | 506 | 11.4 Dans l'hypothèse où une ou plusieurs des dispositions du Contrat 507 | s'avèrerait contraire à une loi ou à un texte applicable, existants ou 508 | futurs, cette loi ou ce texte prévaudrait, et les Parties feraient les 509 | amendements nécessaires pour se conformer à cette loi ou à ce texte. 510 | Toutes les autres dispositions resteront en vigueur. De même, la 511 | nullité, pour quelque raison que ce soit, d'une des dispositions du 512 | Contrat ne saurait entraîner la nullité de l'ensemble du Contrat. 513 | 514 | 515 | 11.5 LANGUE 516 | 517 | Le Contrat est rédigé en langue française et en langue anglaise, ces 518 | deux versions faisant également foi. 519 | 520 | 521 | Article 12 - NOUVELLES VERSIONS DU CONTRAT 522 | 523 | 12.1 Toute personne est autorisée à copier et distribuer des copies de 524 | ce Contrat. 525 | 526 | 12.2 Afin d'en préserver la cohérence, le texte du Contrat est protégé 527 | et ne peut être modifié que par les auteurs de la licence, lesquels se 528 | réservent le droit de publier périodiquement des mises à jour ou de 529 | nouvelles versions du Contrat, qui posséderont chacune un numéro 530 | distinct. Ces versions ultérieures seront susceptibles de prendre en 531 | compte de nouvelles problématiques rencontrées par les logiciels libres. 532 | 533 | 12.3 Tout Logiciel diffusé sous une version donnée du Contrat ne pourra 534 | faire l'objet d'une diffusion ultérieure que sous la même version du 535 | Contrat ou une version postérieure, sous réserve des dispositions de 536 | l'article 5.3.4 <#compatibilite>. 537 | 538 | 539 | Article 13 - LOI APPLICABLE ET COMPETENCE TERRITORIALE 540 | 541 | 13.1 Le Contrat est régi par la loi française. Les Parties conviennent 542 | de tenter de régler à l'amiable les différends ou litiges qui 543 | viendraient à se produire par suite ou à l'occasion du Contrat. 544 | 545 | 13.2 A défaut d'accord amiable dans un délai de deux (2) mois à compter 546 | de leur survenance et sauf situation relevant d'une procédure d'urgence, 547 | les différends ou litiges seront portés par la Partie la plus diligente 548 | devant les Tribunaux compétents de Paris. 549 | 550 | 551 | -------------------------------------------------------------------------------- /documentation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code is low - Guide d'utilisation 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

Code is low

23 |
Un programme expérimental par E. Netter 24 | (v. 0.8) - codeislow [at] email.enetter.fr
25 |

Source : DILA - Données Légifrance exploitées en 27 | temps réel sous licence ouverte 2.0.

30 | 31 |
32 | 33 | 53 |
54 | 55 |
56 |
57 | × 58 |

59 | 60 |

61 | Soyez informés du status des articles mentionnés dans votre document 62 |

63 |
64 |
65 |
66 |

A partir d'un document (< 2 Mo au format docx, 67 | odt ou 68 | pdf)

69 |

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 |
    73 |
  • ARTICLE - CODE: ex: "art. 1240 C. civ.", "article L. 110-1 du Code de commerce". 74 |
  • 75 |
  • CODE - ARTICLE: ex: "C. civ. art. 1240 ", "Code de commerce article L. 76 | 110-1". 77 | 78 |
  • 79 |
80 | 81 | Consultez la liste des codes du droit français supportés 82 | 83 |

84 |

Il interrogera Légifrance à propos des articles détectés. 85 | En fonction de la plage temporelle choisie, 86 | vous saurez ainsi :

87 | 88 |
    89 |
  • Si le texte est introuvable sur 90 | Légifrance 91 | (abrogé, faute de frappe, erreur de détection...)
  • 92 |
  • Si le texte a été récemment 93 | modifié 94 | (période définie par vous) (N.B. : seule la modification la plus récente est mentionnée) 95 |
  • 96 |
  • Si le texte va être modifié prochainement 97 | 98 | (période définie par vous) N.B. : seule la version à venir la plus proche est 99 | mentionnée)
  • 100 |
  • Si le texte n'a pas subi de modification
  • 101 |
102 |

103 |
104 |
105 |
106 |
107 | 108 | -------------------------------------------------------------------------------- /dotenv.example: -------------------------------------------------------------------------------- 1 | TOKEN_URL= 2 | API_ROOT_URL= 3 | API_KEY= 4 | API_SECRET= -------------------------------------------------------------------------------- /form.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 7 |
8 |
9 |
10 | 11 |
12 | 13 | 17 |
18 |
19 | 20 | 21 |
22 | 23 |
24 |
Sélectionner la plage temporelle pour surveiller la modification des articles 25 | cités: 26 |
27 |
28 |
29 | 30 |
31 |
32 | 3 an(s) 33 | 34 |
35 | 37 |
38 | dans le passé 39 |
40 |
41 | 42 |
43 |
44 | 3 an(s) 45 |
46 | 48 |
dans le futur 49 |
50 |
51 |
52 | 53 | 55 |
56 | 57 |
58 | 59 |
60 |
-------------------------------------------------------------------------------- /help.html: -------------------------------------------------------------------------------- 1 | 2 |
3 |
4 |
5 |
6 | × 7 |

8 | 9 |

10 | Soyez informés du status des articles mentionnés dans votre document 11 |

12 |
13 |
14 |
15 |

A partir d'un document (< 2 Mo au format docx, 16 | odt ou 17 | pdf)

18 |

19 | le programme recherchera dans le texte les références à des articles de code

20 |

selon le format d'annotation utilisé dans le texte:

21 |
    22 |
  • ARTICLE - CODE: ex: "art. 1240 C. civ.", "article L. 110-1 du Code de commerce". 23 |
  • 24 |
  • CODE - ARTICLE: ex: "C. civ. art. 1240 ", "Code de commerce article L. 25 | 110-1". 26 | 27 |
  • 28 |
29 | 30 | Consultez la liste des codes du droit français supportés 31 | 32 |

33 |

Il interrogera Légifrance à propos des articles détectés. 34 | En fonction de la plage temporelle choisie, 35 | vous saurez ainsi :

36 | 37 |
    38 |
  • Si le texte est introuvable sur 39 | Légifrance 40 | (abrogé, faute de frappe, erreur de détection...)
  • 41 |
  • Si le texte a été récemment 42 | modifié 43 | (période définie par vous) (N.B. : seule la modification la plus récente est mentionnée) 44 |
  • 45 |
  • Si le texte va être modifié prochainement 46 | 47 | (période définie par vous) N.B. : seule la version à venir la plus proche est 48 | mentionnée)
  • 49 |
  • Si le texte n'a pas subi de modification
  • 50 |
51 |

52 |
53 |
54 |
55 |
56 |
57 | -------------------------------------------------------------------------------- /home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Code is low 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

Code is low

23 |
Un programme expérimental par E. Netter 24 | (v. 0.8) - codeislow [at] email.enetter.fr
25 |

Source : DILA - Données Légifrance exploitées en 27 | temps réel sous licence ouverte 2.0.

30 | 31 |
32 | 33 | 53 |
54 |
55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 |
63 |
64 | × 65 |

66 | 67 |

68 | Soyez informés du status des articles mentionnés dans votre document 69 |

70 |
71 |
72 |
73 |

A partir d'un document (< 2 Mo au format docx, 74 | odt ou 75 | pdf)

76 |

77 | le programme recherchera dans le texte les références à des articles de code

78 |

selon le format d'annotation utilisé dans le texte:

79 |
    80 |
  • ARTICLE - CODE: ex: "art. 1240 C. civ.", "article L. 110-1 du Code de commerce". 81 |
  • 82 |
  • CODE - ARTICLE: ex: "C. civ. art. 1240 ", "Code de commerce article L. 83 | 110-1". 84 | 85 |
  • 86 |
87 | 88 | Consultez la liste des codes du droit français supportés 89 | 90 |

91 |

Il interrogera Légifrance à propos des articles détectés. 92 | En fonction de la plage temporelle choisie, 93 | vous saurez ainsi :

94 | 95 |
    96 |
  • Si le texte est introuvable sur 97 | Légifrance 98 | (abrogé, faute de frappe, erreur de détection...)
  • 99 |
  • Si le texte a été récemment 100 | modifié 101 | (période définie par vous) (N.B. : seule la modification la plus récente est mentionnée) 102 |
  • 103 |
  • Si le texte va être modifié prochainement 104 | 105 | (période définie par vous) N.B. : seule la version à venir la plus proche est 106 | mentionnée)
  • 107 |
  • Si le texte n'a pas subi de modification
  • 108 |
109 |

110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | 118 |
119 |
120 | 122 | 123 |
124 |
125 | 126 |
127 |
128 |

Sélectionner une plage temporelle:

129 |
130 | 131 | 132 |
133 |
134 | 136 | 137 |
138 |
139 |
140 |
141 | 142 | 146 |
147 | 148 | 149 | 150 |
151 |
152 |
153 |
154 | 155 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Code is low - analyse de textes juridiques 5 | 6 | 7 | 8 |
9 |

Code is low

10 |
Un programme expérimental par E. Netter (v. 0.8) - codeislow [at] email.enetter.fr
11 |

Source : DILA - Données Légifrance exploitées en temps réel sous licence ouverte 2.0.

12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 |
20 | 21 | an(s) vers le passé
22 | 23 | an(s) vers l'avenir 24 |
25 | 28 |
29 |

En cliquant sur "soumettre", vous reconnaissez avoir pris connaissance des conditions d'utilisation et de la politique de confidentialité ci-dessous.

30 | 31 |
32 | 33 |
34 |
35 | 36 |
37 |

Guide d'utilisation

38 |
39 |
40 |

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 |
    46 |
  • Si le texte est introuvable sur Légifrance (abrogé, faute de frappe...)
  • 47 |
  • 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)
  • 48 |
  • 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)
  • 49 |
50 | 51 |

La page du dépôt sur Github contient un fichier readme plus détaillé. 52 |
53 |

54 | 55 | 58 | 59 |
60 |

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 |
62 | 63 | 66 | 67 |
68 | 69 |

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 |
81 | 82 | 84 | 85 | 86 |
87 | 88 |

La forme longue est évidemment reconnue. La casse est indifférente. N'hésitez pas à demander l'ajout d'autres codes.

89 | 90 |
    91 |
  • Code des assurances [C. assur.]
  • 92 |
  • Code civil [C. civ.]
  • 93 |
  • Code de commerce [C. com.]
  • 94 |
  • Code de la consommation [C. conso.]
  • 95 |
  • Code de l'entrée et du séjour des étrangers et du droit d'asile [CESEDA]
  • 96 |
  • Code de l'environnement [C. envir., CE]
  • 97 |
  • Code général des collectivités territoriales [CGCT]
  • 98 |
  • Code de justice administrative [CJA]
  • 99 |
  • Code pénal [C. pén.]
  • 100 |
  • Code des postes et des communications électroniques [CPCE]
  • 101 |
  • Code de procédure civile [C. pr. civ., CPC]
  • 102 |
  • Code de procédure pénale [CPP]
  • 103 |
  • Code de la propriété intellectuelle [C. pr. int., CPI]
  • 104 |
  • Code de la santé publique [C. sant. pub., CSP]
  • 105 |
  • Code de la sécurité intérieure [CSI]
  • 106 |
  • Code de la sécurité sociale [CSS]
  • 107 |
  • Code du travail [C. trav.]
  • 108 |
109 |
110 | 111 | 112 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /matching.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | #filename: matching.py 3 | """ 4 | The matching module 5 | 6 | Ce module permet la detection des articles du code de droit français 7 | 8 | """ 9 | 10 | import re 11 | 12 | from parsing import parse_doc 13 | from code_references import filter_code_regex, CODE_REFERENCE, CODE_REGEX 14 | 15 | ARTICLE_REGEX = r"(?P(Articles?|Art\.))" 16 | 17 | # ARTICLE_REF = re.compile("\d+") 18 | # ARTICLE_ID = r"(L|R|A|D)?(\.|\s)?\d+(-\d+)?((\s(al\.|alinea)?\s\d+)?(\s|\.)" 19 | 20 | 21 | 22 | 23 | def switch_pattern(selected_codes=None, pattern="article_code"): 24 | """ 25 | Build pattern recognition using pattern short code switch 26 | 27 | Arguments 28 | --------- 29 | selected_codes: array 30 | a list of short codes to select. Default to None 31 | pattern: str 32 | a string article_code or code_article. Default to article_code 33 | Returns 34 | --------- 35 | regex_pattern: str 36 | a compiled regex pattern 37 | Raise 38 | -------- 39 | ValueError: 40 | pattern name is wrong 41 | """ 42 | 43 | code_regex = filter_code_regex(selected_codes) 44 | 45 | if pattern not in ["article_code", "code_article"]: 46 | raise ValueError( 47 | "Wrong pattern name: choose between 'article_code' or 'code_article'" 48 | ) 49 | if pattern == "article_code": 50 | return re.compile(f"{ARTICLE_REGEX}(?P.*?){code_regex}", flags=re.I) 51 | # else: 52 | # #code_article 53 | # # return re.compile(f"{code_regex}.*?{ARTICLE_REGEX}(\s|\.)(?P.*?)(\.|\s)", flags=re.I) 54 | # return re.compile(f"{code_regex}.*?{ARTICLE_REGEX}.*?{ARTICLE_ID}", flags=re.I) 55 | 56 | def get_matching_results_dict(full_text, selected_short_codes=[], pattern_format="article_code"): 57 | """ 58 | Une fonction qui renvoie un dictionnaire de resultats: trié par code (version abbréviée) avec la liste des articles détectés lui appartenant. 59 | 60 | Arguments 61 | ---------- 62 | full_text: str 63 | a string of the full document normalized 64 | pattern_format: str 65 | a string representing the pattern format article_code or code_article. Defaut to article_code 66 | 67 | Returns 68 | ---------- 69 | code_found: dict 70 | a dict compose of short version of code as key and list of the detected articles references as values {code: [art_ref, art_ref2, ... ]} 71 | """ 72 | article_pattern = switch_pattern(selected_short_codes, pattern_format) 73 | code_found = {} 74 | 75 | # normalisation 76 | full_text = re.sub(r"\r|\n|\t|\xa0", " ", " ".join(full_text)) 77 | for i, match in enumerate(re.finditer(article_pattern, full_text)): 78 | needle = match.groupdict() 79 | qualified_needle = { 80 | key: value for key, value in needle.items() if value is not None 81 | } 82 | msg = f"#{i+1}\t{qualified_needle}" 83 | # logging.debug(msg) 84 | # get the code shortname based on regex group name 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 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | """ 54 | end_results=""" 55 |
Référence (Code - Article)StatutTextePériode de validité
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 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for long_code, short_code, regex, comment in codes_full_list %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {%endfor%} 33 | 34 |
Nom du CodeAbbréviationExpression rationnelleCommentaire
{{long_code}}{{short_code}}{{regex}}{{comment}}
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 |
35 | 36 |
37 |
38 |
39 |
40 | 42 | 43 |
44 |
45 | 46 |
47 |
48 |

Sélectionner une plage temporelle:

49 |
50 |
51 |
52 | 53 | 54 |
55 |
56 | 58 | 59 |
60 |
61 |
62 |
63 | Sélectionner les codes à vérifier 64 | {% for short, name in code_names %} 65 |
66 | 67 | 68 |
69 | {%endfor%} 70 |
71 |
72 | 73 | 77 |
78 | 79 | 80 |
81 |
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 |
19 |
20 |
21 |
22 | 23 | 24 |
25 |
26 |
27 | 28 |
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 --------------------------------------------------------------------------------