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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyfrctc
3
- Version: 0.2
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"
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
- flows_dict = post_res.json()
500
- logger.debug(f"Answer JSON: {flows_dict}")
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 = flows_dict.get('flowSyntax')
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 flows_dict
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
- return flows_dict
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