pyfrctc 0.2__tar.gz → 0.3__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.
- {pyfrctc-0.2 → pyfrctc-0.3}/PKG-INFO +6 -1
- {pyfrctc-0.2 → pyfrctc-0.3}/README.md +5 -0
- {pyfrctc-0.2 → pyfrctc-0.3}/pyfrctc/__init__.py +2 -2
- {pyfrctc-0.2 → pyfrctc-0.3}/pyfrctc/pyfrctc.py +85 -6
- {pyfrctc-0.2 → pyfrctc-0.3}/.flake8 +0 -0
- {pyfrctc-0.2 → pyfrctc-0.3}/.gitignore +0 -0
- {pyfrctc-0.2 → pyfrctc-0.3}/LICENSE +0 -0
- {pyfrctc-0.2 → pyfrctc-0.3}/pyproject.toml +0 -0
- {pyfrctc-0.2 → pyfrctc-0.3}/requirements.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyfrctc
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3
|
|
4
4
|
Summary: Helpers for eInvoicing and eReporting in France
|
|
5
5
|
Project-URL: Homepage, https://github.com/akretion/pyfrctc
|
|
6
6
|
Project-URL: Source, https://github.com/akretion/pyfrctc
|
|
@@ -43,3 +43,8 @@ This library is published under the [GNU Lesser General Public License v2.1](htt
|
|
|
43
43
|
* version 0.2 dated 2026-04-23
|
|
44
44
|
|
|
45
45
|
* Fixes in re-formatting of directory lines for B2G when SIRET has specific global properties
|
|
46
|
+
|
|
47
|
+
* version 0.3 dated 2026-04-30
|
|
48
|
+
|
|
49
|
+
* Add methods send\_flow\_parsed(), search\_flows\_parsed() and get\_flow\_metadata\_parsed()
|
|
50
|
+
* Add multi-page support in search\_flows()
|
|
@@ -23,3 +23,8 @@ This library is published under the [GNU Lesser General Public License v2.1](htt
|
|
|
23
23
|
* version 0.2 dated 2026-04-23
|
|
24
24
|
|
|
25
25
|
* Fixes in re-formatting of directory lines for B2G when SIRET has specific global properties
|
|
26
|
+
|
|
27
|
+
* version 0.3 dated 2026-04-30
|
|
28
|
+
|
|
29
|
+
* Add methods send\_flow\_parsed(), search\_flows\_parsed() and get\_flow\_metadata\_parsed()
|
|
30
|
+
* Add multi-page support in search\_flows()
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "0.
|
|
2
|
-
from .pyfrctc import get_session, healthcheck, get_directory_siren, get_directory_siren_parsed, get_directory_siret, get_directory_siret_parsed, get_directory_lines, get_directory_lines_parsed, send_flow, search_flows, get_flow
|
|
1
|
+
__version__ = "0.3"
|
|
2
|
+
from .pyfrctc import get_session, healthcheck, get_directory_siren, get_directory_siren_parsed, get_directory_siret, get_directory_siret_parsed, get_directory_lines, get_directory_lines_parsed, send_flow, send_flow_parsed, search_flows, search_flows_parsed, get_flow, get_flow_metadata_parsed
|
|
@@ -9,6 +9,8 @@ from stdnum.fr.siren import is_valid as siren_is_valid
|
|
|
9
9
|
from stdnum.fr.siret import is_valid as siret_is_valid
|
|
10
10
|
import json
|
|
11
11
|
import importlib
|
|
12
|
+
import datetime
|
|
13
|
+
import pytz
|
|
12
14
|
from io import BytesIO
|
|
13
15
|
|
|
14
16
|
VERSION = importlib.metadata.version("pyfrctc")
|
|
@@ -496,17 +498,22 @@ def send_flow(session, file_bin, filename, flow_syntax, processing_rule):
|
|
|
496
498
|
except Exception:
|
|
497
499
|
pass
|
|
498
500
|
raise RuntimeError(f"POST request on {url} failed ({status_code}). Error code: {error_code}. Error message: {error_msg}")
|
|
499
|
-
|
|
500
|
-
logger.debug(f"Answer JSON: {
|
|
501
|
+
flow_dict = post_res.json()
|
|
502
|
+
logger.debug(f"Answer JSON: {flow_dict}")
|
|
501
503
|
# We could check that the value received == value sent for processingRule and name
|
|
502
|
-
answer_flow_syntax =
|
|
504
|
+
answer_flow_syntax = flow_dict.get('flowSyntax')
|
|
503
505
|
if answer_flow_syntax and answer_flow_syntax != flow_syntax:
|
|
504
506
|
raise RuntimeError(f"Query had flowSyntax={flow_syntax} but answer has flowSyntax={answer_flow_syntax}")
|
|
505
|
-
return
|
|
507
|
+
return flow_dict
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def send_flow_parsed(session, file_bin, filename, flow_syntax, processing_rule):
|
|
511
|
+
flow_dict = send_flow(session, file_bin, filename, flow_syntax, processing_rule)
|
|
512
|
+
_parse_flow_dict(flow_dict)
|
|
513
|
+
return flow_dict
|
|
506
514
|
|
|
507
515
|
|
|
508
516
|
def search_flows(session, updated_after, flow_direction, flow_type, updated_before=None):
|
|
509
|
-
# TODO implement multi-page
|
|
510
517
|
# Pagination works with the updatedAfter property
|
|
511
518
|
# The comparison with current date is strict : updatedAt > updatedAfter
|
|
512
519
|
if not session:
|
|
@@ -551,6 +558,23 @@ def search_flows(session, updated_after, flow_direction, flow_type, updated_befo
|
|
|
551
558
|
if flow_direction:
|
|
552
559
|
query_json["where"]["flowDirection"] = flow_direction
|
|
553
560
|
url = f"{PLATFORM2BASE_URL[platform]}/afnor-flow/{AFNOR_API_VERSION}/flows/search"
|
|
561
|
+
next_page = True
|
|
562
|
+
res = []
|
|
563
|
+
while next_page:
|
|
564
|
+
res_single_call = _post_search_flows(session, url, query_json)
|
|
565
|
+
res += res_single_call
|
|
566
|
+
if len(res_single_call) < LIMIT:
|
|
567
|
+
next_page = False
|
|
568
|
+
else:
|
|
569
|
+
updated_after_list = [flow['updatedAt'] for flow in res_single_call if flow.get('updatedAt')]
|
|
570
|
+
if not updated_after_list:
|
|
571
|
+
raise RuntimeError(f"Key 'updatedAt' is not present in the key 'results' of the answer of {url}. This should not happen.")
|
|
572
|
+
next_updated_after = max(updated_after_list)
|
|
573
|
+
query_json['where']['updatedAfter'] = next_updated_after
|
|
574
|
+
return res
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _post_search_flows(session, url, query_json):
|
|
554
578
|
logger.info(f"Sending POST request on {url} (v{VERSION})")
|
|
555
579
|
try:
|
|
556
580
|
post_res = session.post(url, json=query_json, timeout=TIMEOUT)
|
|
@@ -568,7 +592,15 @@ def search_flows(session, updated_after, flow_direction, flow_type, updated_befo
|
|
|
568
592
|
raise RuntimeError(f"POST request on {url} failed ({status_code}). Error code: {error_code}. Error message: {error_msg}")
|
|
569
593
|
flows_dict = post_res.json()
|
|
570
594
|
logger.debug(f'Answer JSON: {flows_dict}')
|
|
571
|
-
|
|
595
|
+
res = flows_dict.get('results', [])
|
|
596
|
+
return res
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def search_flows_parsed(session, updated_after, flow_direction, flow_type, updated_before=None):
|
|
600
|
+
res = search_flows(session, updated_after, flow_direction, flow_type, updated_before=updated_before)
|
|
601
|
+
for flow_dict in res:
|
|
602
|
+
_parse_flow_dict(flow_dict)
|
|
603
|
+
return res
|
|
572
604
|
|
|
573
605
|
|
|
574
606
|
def get_flow(session, flow_id, doc_type=None):
|
|
@@ -618,3 +650,50 @@ def get_flow(session, flow_id, doc_type=None):
|
|
|
618
650
|
if not isinstance(file_bin, bytes):
|
|
619
651
|
raise RuntimeError(f"File retrieved from {url} is not a python bytes object")
|
|
620
652
|
return file_bin
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def get_flow_metadata_parsed(session, flow_id):
|
|
656
|
+
if not session:
|
|
657
|
+
raise ValueError("session argument has no value")
|
|
658
|
+
if not flow_id:
|
|
659
|
+
raise ValueError("flow_id argument has no value")
|
|
660
|
+
if not isinstance(flow_id, str):
|
|
661
|
+
raise ValueError("flow_id argument must be a string")
|
|
662
|
+
flow_dict = get_flow(session, flow_id, doc_type="Metadata")
|
|
663
|
+
_parse_flow_dict(flow_dict)
|
|
664
|
+
return flow_dict
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _parse_flow_dict(flow_dict):
|
|
668
|
+
state_map = {
|
|
669
|
+
'Pending': 'pending',
|
|
670
|
+
'Ok': 'done',
|
|
671
|
+
'Error': 'error',
|
|
672
|
+
}
|
|
673
|
+
if flow_dict.get('submittedAt'):
|
|
674
|
+
flow_dict['submitted_at'] = _timestamp_iso8601_to_utc_datetime(flow_dict['submittedAt'])
|
|
675
|
+
if flow_dict.get('updatedAt'):
|
|
676
|
+
flow_dict['updated_at'] = _timestamp_iso8601_to_utc_datetime(flow_dict['updatedAt'])
|
|
677
|
+
if flow_dict.get('acknowledgement'):
|
|
678
|
+
if flow_dict['acknowledgement'].get('status'):
|
|
679
|
+
flow_dict['state'] = state_map.get(flow_dict['acknowledgement']['status'], 'ap_unknown')
|
|
680
|
+
if flow_dict['acknowledgement'].get('details') and isinstance(flow_dict['acknowledgement']['details'], list):
|
|
681
|
+
messages = []
|
|
682
|
+
for detail in flow_dict['acknowledgement']['details']:
|
|
683
|
+
# the 4 fields are required, so the IF condition should always be ok
|
|
684
|
+
if detail.get('item') and detail.get('level') and detail.get('reasonCode') and detail.get('reasonMessage'):
|
|
685
|
+
msg = f"{detail['level']} on {detail['item']}: {detail['reasonMessage']} (code: {detail['reasonCode']})"
|
|
686
|
+
messages.append(msg)
|
|
687
|
+
flow_dict['ap_error_details'] = '\n'.join(messages) or False
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _timestamp_iso8601_to_utc_datetime(timestamp):
|
|
691
|
+
if not timestamp:
|
|
692
|
+
raise ValueError("timestamp argument has no value")
|
|
693
|
+
if not isinstance(timestamp, str):
|
|
694
|
+
raise ValueError("timestamp argument must be a string")
|
|
695
|
+
timestamp_dt = datetime.datetime.fromisoformat(timestamp)
|
|
696
|
+
# switch to UTC
|
|
697
|
+
timestamp_dt_utc = timestamp_dt.astimezone(pytz.utc)
|
|
698
|
+
timestamp_dt_utc_naive = timestamp_dt_utc.replace(tzinfo=None)
|
|
699
|
+
return timestamp_dt_utc_naive
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|