inpython-package 1.1.7__tar.gz → 1.1.9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. inpython_package-1.1.9/PKG-INFO +79 -0
  2. inpython_package-1.1.9/README.md +41 -0
  3. inpython_package-1.1.9/inpython/injcd.py +15 -0
  4. {inpython_package-1.1.7/inpython/inxml → inpython_package-1.1.9/inpython/inqrcode}/__init__.py +1 -1
  5. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/inqrcode/paymentqrcode.py +20 -11
  6. inpython_package-1.1.9/inpython/inrest/__init__.py +6 -0
  7. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/inrest/in_jwt.py +55 -44
  8. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/intools/Servalux-devis.py +15 -11
  9. inpython_package-1.1.9/inpython/intools/__init__.py +6 -0
  10. inpython_package-1.1.9/inpython/intools/in_cron.py +66 -0
  11. inpython_package-1.1.9/inpython/intools/in_docx.py +162 -0
  12. inpython_package-1.1.7/inpython/intools/in_docx.py → inpython_package-1.1.9/inpython/intools/in_docx_extract.py +23 -16
  13. inpython_package-1.1.9/inpython/intools/in_docx_extract_canvas.py +257 -0
  14. inpython_package-1.1.9/inpython/intools/in_docx_protect.py +73 -0
  15. inpython_package-1.1.9/inpython/intools/in_geo_calc.py +25 -0
  16. inpython_package-1.1.9/inpython/intools/in_log.py +81 -0
  17. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/intools/in_projet.py +9 -7
  18. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/intools/in_test.py +1 -2
  19. inpython_package-1.1.9/inpython/intools/xls_to_csv.py +126 -0
  20. inpython_package-1.1.9/inpython/servalux/__init__.py +6 -0
  21. inpython_package-1.1.9/inpython/servalux/docx_ext_schema_svl.py +16 -0
  22. inpython_package-1.1.9/inpython/servalux/servalux-devis.py +202 -0
  23. inpython_package-1.1.9/inpython_package.egg-info/PKG-INFO +79 -0
  24. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython_package.egg-info/SOURCES.txt +10 -2
  25. inpython_package-1.1.9/inpython_package.egg-info/requires.txt +18 -0
  26. inpython_package-1.1.9/setup.py +41 -0
  27. inpython_package-1.1.7/PKG-INFO +0 -57
  28. inpython_package-1.1.7/README.md +0 -30
  29. inpython_package-1.1.7/inpython/inqrcode/__init__.py +0 -6
  30. inpython_package-1.1.7/inpython/inrest/__init__.py +0 -6
  31. inpython_package-1.1.7/inpython/intools/__init__.py +0 -6
  32. inpython_package-1.1.7/inpython/intools/xls_to_csv.py +0 -45
  33. inpython_package-1.1.7/inpython/inxml/validator.py +0 -43
  34. inpython_package-1.1.7/inpython_package.egg-info/PKG-INFO +0 -57
  35. inpython_package-1.1.7/inpython_package.egg-info/requires.txt +0 -5
  36. inpython_package-1.1.7/setup.py +0 -29
  37. {inpython_package-1.1.7 → inpython_package-1.1.9}/LICENSE.txt +0 -0
  38. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/__init__.py +0 -0
  39. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/__main__.py +0 -0
  40. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/aotools/__init__.py +0 -0
  41. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/aotools/aogeo.py +0 -0
  42. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/ingraph/__init__.py +0 -0
  43. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/ingraph/ingraph.py +0 -0
  44. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython/inqrcode/inqrcode.py +0 -0
  45. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython_package.egg-info/dependency_links.txt +0 -0
  46. {inpython_package-1.1.7 → inpython_package-1.1.9}/inpython_package.egg-info/top_level.txt +0 -0
  47. {inpython_package-1.1.7 → inpython_package-1.1.9}/setup.cfg +0 -0
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: inpython-package
3
+ Version: 1.1.9
4
+ Summary: # Infodata's IN-Tools U2Python Package
5
+ Home-page: https://bitbucket.org/infodata-dev/inpython
6
+ Author: infodata
7
+ Author-email: jcd@infodata.lu
8
+ License: MIT
9
+ Requires-Python: >=3.4
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE.txt
12
+ Requires-Dist: msal>=1.22
13
+ Requires-Dist: requests
14
+ Requires-Dist: qrcode[pil]
15
+ Requires-Dist: python-dotenv
16
+ Requires-Dist: lxml
17
+ Requires-Dist: openpyxl
18
+ Requires-Dist: latlon3
19
+ Requires-Dist: geopy
20
+ Requires-Dist: python-docx
21
+ Requires-Dist: docxcompose
22
+ Requires-Dist: docxtpl
23
+ Requires-Dist: pandas
24
+ Requires-Dist: cron-converter
25
+ Requires-Dist: cryptography
26
+ Requires-Dist: PyJWT
27
+ Requires-Dist: pywin32; platform_system == "Windows"
28
+ Dynamic: author
29
+ Dynamic: author-email
30
+ Dynamic: description
31
+ Dynamic: description-content-type
32
+ Dynamic: home-page
33
+ Dynamic: license
34
+ Dynamic: license-file
35
+ Dynamic: requires-dist
36
+ Dynamic: requires-python
37
+ Dynamic: summary
38
+
39
+ # README Production – inpython-package
40
+
41
+ Ce document explique comment installer et utiliser le package **inpython-package** en production.
42
+
43
+ ## Présentation
44
+
45
+ **inpython-package** regroupe des modules Python pour l'intégration MS-Graph, la génération de QR code pour les paiements via ayden, la gestion de fichiers, la manipulation de documents, etc., utilisés par Integrix.
46
+
47
+ ## Installation
48
+
49
+ Installer le package depuis PyPI :
50
+
51
+ ```bash
52
+ pip install inpython-package
53
+ ```
54
+
55
+ ## Structure du package
56
+
57
+ - `inpython.aotools` : outils avancés (géolocalisation, etc.)
58
+ - `inpython.ingraph` : gestion de l'authentification et des accès MS-Graph (utilise MSAL)
59
+ - `inpython.inqrcode` : génération de QR codes, gestion des liens de paiement, intégration de logos
60
+ - `inpython.inrest` : outils pour l'authentification JWT et REST
61
+ - `inpython.intools` : outils divers (conversion de fichiers, gestion de projets, cron, etc.)
62
+ - `inpython.inxml` : validation et manipulation de fichiers XML
63
+
64
+ ## Utilisation
65
+
66
+ Importer les modules nécessaires dans vos scripts Python :
67
+ ```python
68
+ from inpython.ingraph import ...
69
+ from inpython.inqrcode import ...
70
+ # etc.
71
+ ```
72
+
73
+ ## Configuration
74
+
75
+ Certains modules nécessitent un fichier `.env` pour les clés API et paramètres sensibles. Placez ce fichier à la racine de votre projet ou fournissez son chemin lors de l'appel des fonctions (pour inqrcode par exemple).
76
+
77
+ ## Support
78
+
79
+ Pour toute question ou problème en production, contactez le support Integrix ou consultez la documentation officielle sur PyPI.
@@ -0,0 +1,41 @@
1
+ # README Production – inpython-package
2
+
3
+ Ce document explique comment installer et utiliser le package **inpython-package** en production.
4
+
5
+ ## Présentation
6
+
7
+ **inpython-package** regroupe des modules Python pour l'intégration MS-Graph, la génération de QR code pour les paiements via ayden, la gestion de fichiers, la manipulation de documents, etc., utilisés par Integrix.
8
+
9
+ ## Installation
10
+
11
+ Installer le package depuis PyPI :
12
+
13
+ ```bash
14
+ pip install inpython-package
15
+ ```
16
+
17
+ ## Structure du package
18
+
19
+ - `inpython.aotools` : outils avancés (géolocalisation, etc.)
20
+ - `inpython.ingraph` : gestion de l'authentification et des accès MS-Graph (utilise MSAL)
21
+ - `inpython.inqrcode` : génération de QR codes, gestion des liens de paiement, intégration de logos
22
+ - `inpython.inrest` : outils pour l'authentification JWT et REST
23
+ - `inpython.intools` : outils divers (conversion de fichiers, gestion de projets, cron, etc.)
24
+ - `inpython.inxml` : validation et manipulation de fichiers XML
25
+
26
+ ## Utilisation
27
+
28
+ Importer les modules nécessaires dans vos scripts Python :
29
+ ```python
30
+ from inpython.ingraph import ...
31
+ from inpython.inqrcode import ...
32
+ # etc.
33
+ ```
34
+
35
+ ## Configuration
36
+
37
+ Certains modules nécessitent un fichier `.env` pour les clés API et paramètres sensibles. Placez ce fichier à la racine de votre projet ou fournissez son chemin lors de l'appel des fonctions (pour inqrcode par exemple).
38
+
39
+ ## Support
40
+
41
+ Pour toute question ou problème en production, contactez le support Integrix ou consultez la documentation officielle sur PyPI.
@@ -0,0 +1,15 @@
1
+ import sys
2
+ from inrest import in_jwt
3
+
4
+ def main(argv):
5
+ #
6
+ try:
7
+ token = "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJTd3JnRjIyV1FVaHZBZUxpQTBhQnhmV2hqNGgtX0hLSXFuVzRpd3JTWnFVIn0.eyJleHAiOjE3ODc0MjM5MzksImlhdCI6MTc4NzQyMzYzOSwiYXV0aF90aW1lIjoxNzg3NDE5OTc2LCJqdGkiOiJiZGUxNjZmZS0wMDc3LTIyNWMtNDRkMi0yOWJkNzlkZTBjZDEiLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjgwODAvcmVhbG1zL215cmVhbG0iLCJhdWQiOiJteWNsaWVudC13ZWIiLCJzdWIiOiI0MGM1MGI5MS1iYjlmLTQ5MjEtYjg2YS1hYzQ1YWZiYTE1NWYiLCJ0eXAiOiJJRCIsImF6cCI6Im15Y2xpZW50LXdlYiIsInNpZCI6InQwZi1qWm5nVjYxVklQT29RQTJWa3p1VCIsImF0X2hhc2giOiJNN1VoMG12SFZKczdDaDVfN1F1bDZRIiwiYWNyIjoiMCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6IkpDIERldyIsInByZWZlcnJlZF91c2VybmFtZSI6Im15dXNlciIsImdpdmVuX25hbWUiOiJKQyIsImZhbWlseV9uYW1lIjoiRGV3IiwiZW1haWwiOiJqY2RAaW5mb2RhdGEubHUifQ.AuQWfzPHZjHzdtwMipDsR_VrxR2dl1Q3aHD1Ym5h9hZfHwiVtPH3prZ9ptDAmXkagz29uphx_X--yfOmqGlZiC4ygHsGXBkmohNRteIHRO_hu72iBM9kEaU8QakX_HTZXmhC99HHrSNGjYcdl9koNz9cZSvCd9EysGLGF1Bs2NWV0hwQzQVfYIpeDCgm2vyvLSLhJYH4UJariM4H0D9KhX7tgi5qRFxEn2lYlSam1pBuDTrNFrXoUvE_iN68e-wMQql0ib_WWQKVTmeXvxoLzHM2K_544MqqssWO2gZ5JnnV2ZlB1KgxqHATQxFLyGGZzsWMLLdHb91yIb_lfBIV7Q"
8
+ claims = in_jwt.validate_keycloak_jwt(token, "http://localhost:8080/realms/myrealm","myclient-web")
9
+ print(claims)
10
+ except ValueError as e:
11
+ print(f"Erreur: {e}")
12
+
13
+ if __name__ == "__main__":
14
+ main(sys.argv[1:])
15
+
@@ -3,4 +3,4 @@
3
3
 
4
4
  # !! update the list when new modules are added !!
5
5
 
6
- __all__ = ["validator"]
6
+ __all__ = ["inqrcode","paymentqrcode"]
@@ -15,8 +15,8 @@ HEADERS = {
15
15
  "Content-Type": "application/json"
16
16
  }
17
17
 
18
- def create_payment_link_adyen(amount_cents: int, currency: str, reference: str, mode: str = 1, logo_path: str = None, env_path: str = None, qrcode_path: str = None ) -> str:
19
- """Crée un lien de paiement via l'API Adyen. Si mode=2, génère aussi un QR code (avec logo si fourni). Si env_path est fourni, charge les variables d'environnement depuis ce fichier."""
18
+ def create_payment_link_adyen(amount_cents: float, currency: str, reference: str, mode: str = 1, logo_path: str = None, env_path: str = None, qrcode_path: str = None, jpg_filename: str = None ) -> str:
19
+ """Crée un lien de paiement via l'API Adyen. Si mode=2, génère aussi un QR code (avec logo si fourni). Si env_path est fourni, charge les variables d'environnement depuis ce fichier. jpg_filename permet de choisir le nom du fichier jpg du QR code."""
20
20
  if env_path and os.path.exists(env_path):
21
21
  from dotenv import dotenv_values
22
22
  env_vars = dotenv_values(env_path)
@@ -33,7 +33,7 @@ def create_payment_link_adyen(amount_cents: int, currency: str, reference: str,
33
33
  }
34
34
  payload = {
35
35
  "amount": {
36
- "value": amount_cents,
36
+ "value": int(round(amount_cents)),
37
37
  "currency": currency
38
38
  },
39
39
  "reference": reference,
@@ -45,23 +45,31 @@ def create_payment_link_adyen(amount_cents: int, currency: str, reference: str,
45
45
  data = response.json()
46
46
  payment_url = data["url"]
47
47
  if mode == 2:
48
- qr_path = generate_qr_code(payment_url, reference, logo_path,qrcode_path)
48
+ qr_path = generate_qr_code(payment_url, reference, logo_path, qrcode_path, jpg_filename)
49
49
  return qr_path
50
50
  return payment_url
51
51
 
52
- def generate_qr_code(url: str, reference: str, logo_path: str = None, qrcode_path: str = None) -> None:
53
- """Génère un QR code dans le dossier ./qrcode/ (au même niveau que paymentqrcode.py), avec logo si fourni. Crée/écrase un fichier qrcode_logs.txt pour le diagnostic."""
52
+ def generate_qr_code(url: str, reference: str, logo_path: str = None, qrcode_path: str = None, jpg_filename: str = None) -> None:
53
+ """Génère un QR code dans le dossier ./qrcode/ (au même niveau que paymentqrcode.py), avec logo si fourni. Crée/écrase un fichier qrcode_logs.txt pour le diagnostic. jpg_filename permet de choisir le nom du fichier jpg du QR code (avec ou sans extension)."""
54
54
  import traceback
55
55
  base_dir = os.path.dirname(os.path.abspath(__file__))
56
56
  output_dir = os.path.join(base_dir, "qrcode")
57
57
 
58
58
  os.makedirs(output_dir, exist_ok=True)
59
59
  print("qrcode_path : ",qrcode_path)
60
+ if jpg_filename:
61
+ filename_base = jpg_filename
62
+ else:
63
+ filename_base = f"{reference}.jpg"
64
+ # Ajoute .jpg seulement si jpg_filename est vide ou ne contient pas déjà une extension
65
+ if jpg_filename:
66
+ if not (jpg_filename.lower().endswith('.jpg') or jpg_filename.lower().endswith('.jpeg')):
67
+ filename_base = jpg_filename + ".jpg"
60
68
  if qrcode_path:
61
- filename = os.path.join(qrcode_path, f"{reference}.jpg")
69
+ filename = os.path.join(qrcode_path, filename_base)
62
70
  log_file = os.path.join(qrcode_path, "qrcode_debug.log")
63
71
  else:
64
- filename = os.path.join(output_dir, f"{reference}.jpg")
72
+ filename = os.path.join(output_dir, filename_base)
65
73
  log_file = os.path.join(output_dir, "qrcode_debug.log")
66
74
 
67
75
  # Supprime le fichier s’il existe
@@ -115,15 +123,16 @@ def clean(s):
115
123
  if __name__ == "__main__":
116
124
  import sys
117
125
  try:
118
- # Utilisation : python paymentqrcode.py <mode> <montant> <devise> <reference> [chemin_logo] [chemin_env]
126
+ # Utilisation : python paymentqrcode.py <mode> <montant> <devise> <reference> [chemin_logo] [chemin_env] [qrcode_path] [jpg_filename]
119
127
  mode = int(clean(sys.argv[1]))
120
- amount = int(clean(sys.argv[2]))
128
+ amount = float(clean(sys.argv[2]))
121
129
  currency = sys.argv[3]
122
130
  reference = sys.argv[4]
123
131
  logo_path = sys.argv[5] if len(sys.argv) > 5 else ''
124
132
  env_path = sys.argv[6] if len(sys.argv) > 6 else ''
125
133
  qrcode_path = sys.argv[7] if len(sys.argv) > 7 else ''
126
- result = create_payment_link_adyen(amount, currency, reference, mode, logo_path, env_path,qrcode_path)
134
+ jpg_filename = sys.argv[8] if len(sys.argv) > 8 else ''
135
+ result = create_payment_link_adyen(amount, currency, reference, mode, logo_path, env_path, qrcode_path, jpg_filename if jpg_filename else None)
127
136
  print(result)
128
137
  except Exception as e:
129
138
  print("Erreur :", e)
@@ -0,0 +1,6 @@
1
+ # inpython.in_jwt init file
2
+ # executed when 'from inpython.in_jwt import *'
3
+
4
+ # !! update the list when new modules are added !!
5
+
6
+ __all__ = ["in_jwt"]
@@ -3,14 +3,15 @@
3
3
  #
4
4
  #--------------------------------------------------------
5
5
  # 22/08/2024 (JCD) : Creation fonction generateJwtClientAssertion
6
+ # 21/08/2026 (JCD) : ajout validate_keycloak_jwt et validate_microsoft_jwt
6
7
  #--------------------------------------------------------
7
8
  from cryptography.hazmat.primitives.serialization import pkcs12
8
9
  from cryptography.hazmat.primitives import serialization
9
10
  from cryptography.hazmat.backends import default_backend
10
11
  import jwt
11
12
  import datetime
12
- import sys
13
- import getopt
13
+ from jwt import PyJWKClient
14
+ import json
14
15
 
15
16
  # Generates a jwt client assertion from a private key
16
17
  def generateJwtClientAssertion(mode,path_certificate,private_key,client_id,audience,expirationTime=10):
@@ -84,48 +85,58 @@ def generateJwtClientAssertion(mode,path_certificate,private_key,client_id,audie
84
85
  except:
85
86
  raise # so that the caller receives the exception
86
87
 
87
- def main(argv):
88
- #mode = "1"
89
- #private_key = "C:\\TRF\\cleanup\\Cleanup1_private.key"
90
- #path_certificate = "C:\\TRF\\cleanup\\Cleanup1_request.crt"
91
- #client_id = "self_service_chaman_109660_shr5ppy83v"
92
-
93
- #mode = "2"
94
- #path_certificate = "/tmp/jcd/CermiaRest.pfx"
95
- #private_key = "2024infodd1648!" # Use b'' for an empty password
96
- #client_id = "self_service_chaman_108306_safnfiag3h"
97
- #audience = "https://services.socialsecurity.be/REST/oauth/v5/token"
98
- mode = ''
99
- private_key = ''
100
- path_certificate = ''
101
- client_id = ''
102
- audience = ''
103
- expirationTime = None
88
+ def validate_keycloak_jwt(token: str,keycloak_url: str,client_id: str):
89
+ """
90
+ Valide un JWT Keycloak.
91
+ """
104
92
  try:
105
- opts, args = getopt.getopt(argv, "m:k:x:c:a:e:", ["mode=", "private_key=", "path_certificate=", "client_id=", "audience=", "expirationTime="])
106
- except getopt.GetoptError:
107
- print('script.py -i <inputfile> -o <outputfile>')
108
- sys.exit(2)
109
-
110
- for opt, arg in opts:
111
- if opt in ("-m", "--mode"):
112
- mode = arg
113
- elif opt in ("-k", "--private_key"):
114
- private_key = arg
115
- elif opt in ("-x", "--path_certificate"):
116
- path_certificate = arg
117
- elif opt in ("-c", "--client_id"):
118
- client_id = arg
119
- elif opt in ("-a", "--audience"):
120
- audience = arg
121
- elif opt in ("-e", "--expirationTime"):
122
- if arg != "":
123
- expirationTime = arg
124
-
125
- result = generateJwtClientAssertion(mode,path_certificate,private_key,client_id,audience,expirationTime)
126
- print(result)
93
+ jwks_url = (f"{keycloak_url}/protocol/openid-connect/certs")
94
+ signing_key = PyJWKClient(jwks_url).get_signing_key_from_jwt(token)
95
+ payload = jwt.decode(
96
+ token,
97
+ signing_key.key,
98
+ algorithms=["RS256"],
99
+ issuer=keycloak_url,
100
+ audience=client_id
101
+ )
102
+ return json.dumps(payload)
103
+ except jwt.ExpiredSignatureError:
104
+ raise ValueError("JWT expiré")
105
+ except jwt.InvalidIssuerError:
106
+ raise ValueError("Issuer Keycloak incorrect")
107
+ except jwt.InvalidAudienceError:
108
+ raise ValueError("Audience Keycloak incorrect")
109
+ except jwt.InvalidTokenError as e:
110
+ raise ValueError(
111
+ f"JWT Keycloak invalide: {str(e)}"
112
+ )
113
+ except Exception as e:
114
+ raise ValueError(
115
+ f"Erreur validation Keycloak: {str(e)}"
116
+ )
127
117
 
128
- if __name__ == "__main__":
129
- main(sys.argv[1:])
130
-
118
+ def validate_microsoft_jwt(token: str, issuer: str = None, audience: str = None):
119
+ """
120
+ Valide un JWT Microsoft en récupérant les clés JWKS depuis:
121
+ https://login.microsoftonline.com/common/discovery/v2.0/keys
122
+ """
123
+ try:
124
+ jwks_url = "https://login.microsoftonline.com/common/discovery/v2.0/keys"
125
+ signing_key = PyJWKClient(jwks_url).get_signing_key_from_jwt(token)
131
126
 
127
+ options = {"verify_aud": bool(audience), "verify_iss": bool(issuer)}
128
+ claims = jwt.decode(
129
+ token,
130
+ signing_key.key,
131
+ algorithms=["RS256"],
132
+ audience=audience,
133
+ issuer=issuer,
134
+ options=options
135
+ )
136
+ return json.dumps(claims)
137
+ except jwt.ExpiredSignatureError:
138
+ raise ValueError("JWT Microsoft expiré")
139
+ except jwt.InvalidTokenError as e:
140
+ raise ValueError(f"JWT Microsoft invalide: {str(e)}")
141
+ except Exception as e:
142
+ raise ValueError(f"Erreur validation Microsoft: {str(e)}")
@@ -1,12 +1,14 @@
1
1
  from docx import Document
2
2
 
3
+
3
4
  def chk_br(par):
4
5
  for run in par.r_lst:
5
6
  if len(run.br_lst) > 0:
6
- return(True)
7
- return(False)
7
+ return (True)
8
+ return (False)
9
+
8
10
 
9
- def build_word_file(doc_directory,fab_filename,skipped_pg_br,keeped_pg_br):
11
+ def build_word_file(doc_directory, fab_filename, skipped_pg_br, keeped_pg_br):
10
12
  tmp_file = doc_directory + "/schemas.docx"
11
13
 
12
14
  print(fab_filename)
@@ -36,23 +38,25 @@ def build_word_file(doc_directory,fab_filename,skipped_pg_br,keeped_pg_br):
36
38
  if num_page_breaks == 3:
37
39
  state = 2
38
40
  doc_tmp.element.body.remove(elt)
39
- #doc_tmp.add_paragraph()
40
- #elif elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tbl":
41
- #table_copy(elt, doc_fab, doc_tmp)
41
+ # doc_tmp.add_paragraph()
42
+ # elif elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tbl":
43
+ # table_copy(elt, doc_fab, doc_tmp)
42
44
  elif state == 2:
43
45
  if elt.tag != "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}sectPr":
44
46
  doc_tmp.element.body.remove(elt)
45
-
47
+
46
48
  print("save " + tmp_file)
47
49
  doc_tmp.save(tmp_file)
48
50
 
49
- return(tmp_file)
51
+ return (tmp_file)
50
52
 
51
- def build_devis(doc_directory,fab_filename,startdoc,end_doc):
52
- tmp_doc = build_word_file(doc_directory,fab_filename,2,1)
53
+
54
+ def build_devis(doc_directory, fab_filename, startdoc, end_doc):
55
+ tmp_doc = build_word_file(doc_directory, fab_filename, 2, 1)
53
56
  print(tmp_doc)
54
57
 
58
+
55
59
  doc_dir = "Q:/is/chantier/1000/"
56
60
  fab_fn = "Q:/is/chantier/1000/FABRIQUANT/1000.docx"
57
61
 
58
- build_devis(doc_dir,fab_fn,"","")
62
+ build_devis(doc_dir, fab_fn, "", "")
@@ -0,0 +1,6 @@
1
+ # inpython.intools init file
2
+ # executed when 'from inpython.intools import *'
3
+
4
+ # !! update the list when new modules are added !!
5
+
6
+ __all__ = ["xls_to_csv", "in_projet", "in_docx", "in_log","in_geo_calc","in_cron"]
@@ -0,0 +1,66 @@
1
+ # compute cron value ; used by IN.JOB
2
+ # efv 01.12.2025 create
3
+ # ------------------------------------------
4
+ import sys
5
+ from cron_converter import Cron
6
+ from datetime import datetime
7
+
8
+
9
+ def next(cronexp: str, dtiso: str):
10
+ """Compute next datetime from crontab expression, starting from dtiso else now().
11
+
12
+ Args:
13
+ cronexp (str): a crontab expression string like : */5 * * * *
14
+ dtiso (str): a datetime iso format string like : 2025-12-01T09:00:23
15
+ if null use now()
16
+
17
+ Raises:
18
+ Exception: forward the exception to the caller
19
+
20
+ Returns:
21
+ _type_: datetime.isoformat YYYY-MM-DDTHH:MM:SS
22
+ """
23
+ try:
24
+ if len(cronexp) == 0:
25
+ raise Exception("cronexp is not provided.")
26
+ if len(dtiso) == 0:
27
+ reference = datetime.now()
28
+ else:
29
+ reference = datetime.fromisoformat(dtiso)
30
+ cron_instance = Cron() # Create a new instance
31
+ cron_instance.from_string(cronexp) # load the cron expression
32
+ schedule = cron_instance.schedule(reference) # load the datetime reference
33
+ thenext = schedule.next() # compute the next runtime datetime
34
+ return thenext.isoformat() # return isoformat YYYY-MM-DDTHH:MM:SS
35
+ except:
36
+ raise # raise the error to the parent caller
37
+
38
+ # ---------------------------------------------------------------------------------------
39
+
40
+ def validate(cronexp: str):
41
+ try:
42
+ if cronexp == '':
43
+ raise Exception("cronexp is not provided.")
44
+ cron_instance = Cron() # Create a new instance
45
+ cron_instance.from_string(cronexp)
46
+ return cron_instance.to_string()
47
+ except:
48
+ raise
49
+
50
+ # ---------------------------------------------------------------------------------------
51
+
52
+ if __name__ == "__main__":
53
+ """
54
+ test and sample of cron validate(), cron next()
55
+ python inpython\intools\in_cron.py '*/5 * * * *' '2025-12-01T09:00:23'
56
+ python inpython\intools\in_cron.py '*/5 * * * *'
57
+ """
58
+ cronexp = sys.argv[1]
59
+ dtiso = ''
60
+ if len(sys.argv) == 3:
61
+ dtiso = sys.argv[2]
62
+ res = validate(cronexp)
63
+ print(res, type(res))
64
+ res = next(cronexp, dtiso)
65
+ print(res, type(res))
66
+
@@ -0,0 +1,162 @@
1
+ # infodata > inpythom > intools
2
+ # module in_docx
3
+ # Author: [Th] 23/02/2024
4
+ from docx import Document
5
+ from docxcompose.composer import Composer
6
+ from docxtpl import DocxTemplate
7
+ import datetime as dt
8
+ import inpython.intools.in_log as inlog
9
+ import os
10
+
11
+
12
+ def chk_br(par):
13
+ # renvoi true en de présence d'un saut de page
14
+ for run in par.r_lst:
15
+ if len(run.br_lst) > 0:
16
+ return (True)
17
+ return (False)
18
+
19
+
20
+ def doc_replace_var(input_filename, output_filename, context_str):
21
+ import json
22
+
23
+ context = json.loads(context_str)
24
+ tpl = DocxTemplate(input_filename)
25
+ tpl.render(context)
26
+ tpl.save(output_filename)
27
+ return ("")
28
+
29
+
30
+ def doc_extract(doc_directory, extract_name, input_filename, skipped_pg_br, keeped_pg_br, final_br=True):
31
+ # permet d'extraire une partie d'un document word docx
32
+ # fonctionne à la manière d'une group extract
33
+ #
34
+ # doc_directory : chemin complet vers le répertoire où sera enregsitré le résultat
35
+ # extract_name : sera utilisé pour créér le nom du fichier final si "" on prend "extract"
36
+ # input_filename : chemin complet vers le fichier d'origine duquel sera fait l'extraction
37
+ # skipped_pg_br : nombre de sauts de page à passer avant de commencer l'extraction
38
+ # keeped_pg:br : nombre de sauts correspondant à l'extraction
39
+ #
40
+ # input : retourne le chemin complet vers le fichier résultat.
41
+ # le nom du fihier prend la forme yyyymmdd-hhmmss-extract_name.docx
42
+ inlog.init_log("in_docx", f"Extraction des pages entre les break {skipped_pg_br} et {keeped_pg_br}")
43
+
44
+ if len(extract_name) == 0:
45
+ extract_name = "extract"
46
+
47
+ tmp_file = f"{doc_directory}{dt.datetime.now().strftime('%Y%m%d-%H%M%S-')}{extract_name}.docx"
48
+
49
+ doc_fab = Document(input_filename)
50
+ doc_fab.save(tmp_file)
51
+ doc_tmp = Document(tmp_file)
52
+
53
+ # extraction des pages du docuemnt fabriquant
54
+ num_page_breaks = 0
55
+
56
+ state = 0
57
+
58
+ if type(skipped_pg_br) is str:
59
+ skipped_pg_br = int(skipped_pg_br)
60
+ if type(keeped_pg_br) is str:
61
+ keeped_pg_br = int(keeped_pg_br)
62
+
63
+ for elt_idx, elt in enumerate(doc_tmp.element.body):
64
+ if state == 0:
65
+ if elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p":
66
+ if chk_br(elt):
67
+ num_page_breaks += 1
68
+ if num_page_breaks == skipped_pg_br:
69
+ num_page_breaks = 0
70
+ doc_tmp.save(tmp_file)
71
+ state = 1
72
+ doc_tmp.element.body.remove(elt)
73
+ elif state == 1:
74
+ if elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p":
75
+ if chk_br(elt):
76
+ num_page_breaks += 1
77
+ if num_page_breaks == keeped_pg_br:
78
+ state = 2
79
+ if not (final_br):
80
+ doc_tmp.element.body.remove(elt)
81
+ # elif elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tbl":
82
+ # table_copy(elt, doc_fab, doc_tmp)
83
+ elif state == 2:
84
+ if elt.tag != "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}sectPr":
85
+ doc_tmp.element.body.remove(elt)
86
+
87
+ doc_tmp.save(tmp_file)
88
+
89
+ return (tmp_file)
90
+
91
+
92
+ def doc_br_count(input_filename):
93
+ input_doc = Document(input_filename)
94
+
95
+ # extraction des pages du docuemnt fabriquant
96
+ num_page_breaks = 0
97
+
98
+ state = 0
99
+
100
+ for elt_idx, elt in enumerate(input_doc.element.body):
101
+ if elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p":
102
+ if chk_br(elt):
103
+ num_page_breaks += 1
104
+
105
+ return (num_page_breaks)
106
+
107
+
108
+ def doc_str_find(input_filename, subString, occNumber):
109
+ inlog.init_log("in_docx", f"Recherche de {subString}")
110
+
111
+ input_doc = Document(input_filename)
112
+ occNumber = int(occNumber)
113
+ # extraction des pages du docuemnt fabriquant
114
+ num_page_breaks = 0
115
+
116
+ occurence = 0
117
+ found = False
118
+ for elt_idx, elt in enumerate(input_doc.element.body):
119
+ if found:
120
+ break
121
+ if elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}p":
122
+ if chk_br(elt):
123
+ num_page_breaks += 1
124
+ if subString in elt.text:
125
+ break
126
+ elif elt.tag == "{http://schemas.openxmlformats.org/wordprocessingml/2006/main}tbl":
127
+ for trlst in elt.tr_lst:
128
+ for tclst in trlst.tc_lst:
129
+ for plst in tclst.p_lst:
130
+ for rlst in plst.r_lst:
131
+ if subString in rlst.text:
132
+ occurence += 1
133
+ if occurence == occNumber:
134
+ found = True
135
+ inlog.debug(
136
+ f"Occurenc {occNumber} of \"{subString}\" found after {num_page_breaks} page breaks!")
137
+ break
138
+ if not (found):
139
+ num_page_breaks = 0
140
+ return (num_page_breaks)
141
+
142
+
143
+ def compose_doc(base_file, files_list, final_doc):
144
+ inlog.init_log("composer", "Assemblage des documents", True)
145
+ inlog.debug(os.path.normpath(base_file))
146
+ inlog.debug(files_list)
147
+ inlog.debug(final_doc)
148
+ files_list = files_list.split(";")
149
+ master = Document(os.path.normpath(base_file))
150
+ composer = Composer(master)
151
+ for filename in files_list:
152
+ doc_temp = Document(os.path.normpath(filename))
153
+ composer.append(doc_temp)
154
+ composer.save(os.path.normpath(final_doc))
155
+ return
156
+
157
+
158
+ if __name__ == '__main__':
159
+ flist = ['//winprod/commun/is/chantier/1000/schemas.docx', '//winprod/commun/is/chantier/1000/1000_DETAIL.docx',
160
+ '//winprod/commun/is/chantier/_doc_base/Formulaire_ALU_cgv.docx']
161
+ compose_doc('//winprod/commun/is/chantier/_doc_base/Formulaire_ALU_1-3.docx', flist,
162
+ '//winprod/commun/is/chantier/1000/Devis.docx')