arcane-execution-handler 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,3 @@
1
+ from .datastore_lgp import *
2
+ from .execution_handler import *
3
+ from .exception import *
@@ -0,0 +1,32 @@
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ import pytz
4
+
5
+ from google.cloud.datastore import Entity
6
+
7
+ from arcane.datastore import DATA_INGESTION_KIND, Client as DatastoreClient
8
+
9
+
10
+ def set_execution_info_as_start(
11
+ datastore_client: DatastoreClient,
12
+ entity: Entity,
13
+ kind: str = DATA_INGESTION_KIND,
14
+ current_time: Optional[datetime] = None
15
+ ):
16
+ """Set an entity's execution_info to `running` on the given Datastore kind.
17
+ """
18
+ entity_id = entity['id']
19
+ if not current_time:
20
+ current_time = datetime.now().replace(microsecond=0).astimezone(pytz.utc)
21
+ execution_info = {
22
+ **entity.get('execution_info', {}),
23
+ "status": "running",
24
+ "timestamp": current_time,
25
+ "errors": []
26
+ }
27
+ updated_properties = dict(
28
+ execution_info=datastore_client.convert_input_to_excluded_entity(
29
+ execution_info)
30
+ )
31
+ datastore_client.save_entity_with_transactions(
32
+ entity_id, updated_properties, kind)
@@ -0,0 +1,3 @@
1
+ class SkipExecutionError(Exception):
2
+ """Custom exception to indicate that an execution should be skipped due to outdated schedule information or concurrent execution."""
3
+ pass
@@ -0,0 +1,120 @@
1
+ from datetime import datetime, timedelta, timezone
2
+ import logging
3
+ from typing import Optional
4
+ from dateutil.parser import isoparse
5
+ import pytz
6
+
7
+ from google.cloud.datastore import Entity
8
+
9
+ from arcane.datastore import Client as DatastoreClient, DATA_INGESTION_KIND, DATA_PROCESSING_KIND
10
+ from arcane.pubsub import Client as PubSubClient
11
+
12
+ from .datastore_lgp import set_execution_info_as_start
13
+ from .exception import SkipExecutionError
14
+ from .types import ExecutionStartBody
15
+
16
+
17
+ def get_delta_time_for_ingestion_type(data_ingestion_type: str) -> timedelta:
18
+ """Delta time to consider a running ingestion still in progress"""
19
+ delta = timedelta(minutes=30)
20
+ # Because bigquery ingestion is expensive, we prevent overretrying
21
+ # Because HTTP ingestion can run during 30 minutes and can be retry 3 times
22
+ if data_ingestion_type == 'BIG_QUERY' or data_ingestion_type == 'HTTP':
23
+ delta = timedelta(minutes=90)
24
+ # Because SHOPIFY_API ingestion can run during several minutes and can be retry 5 times (5 bulk exec per token)
25
+ elif data_ingestion_type == 'SHOPIFY_API':
26
+ delta = timedelta(minutes=60)
27
+ return delta
28
+
29
+
30
+ def get_delta_time_for_processing() -> timedelta:
31
+ """Delta time for a running processing"""
32
+ return timedelta(minutes=90)
33
+
34
+
35
+ def _get_schedule_version(entity: Entity, kind: str) -> Optional[str]:
36
+ if kind == DATA_PROCESSING_KIND:
37
+ return entity.get('schedule_version')
38
+ return entity['parameters']['schedule_version']
39
+
40
+
41
+ def handle_start_execution(
42
+ body: ExecutionStartBody,
43
+ entity: Entity,
44
+ pubsub_client: PubSubClient,
45
+ datastore_client: DatastoreClient,
46
+ project_id: str,
47
+ pf_monitoring_topic: str,
48
+ delta_time: timedelta,
49
+ monitoring_step: str,
50
+ task_retry_count: int = 0,
51
+ *,
52
+ kind: str = DATA_INGESTION_KIND,
53
+ check_version: bool = True
54
+ ):
55
+
56
+ """
57
+ Handle the start of an execution for an entity.
58
+ This function validates the schedule version, checks if the entity is already running,
59
+ updates the execution status, and publishes a monitoring event for product flow services.
60
+ Args:
61
+ body (dict): The request body containing monitoring_id, entity_id, and schedule_version.
62
+ entity (Entity): The entity object containing parameters and execution information.
63
+ pubsub_client (PubSubClient): Client for publishing messages to Pub/Sub topics.
64
+ datastore_client (DatastoreClient): Client for interacting with the datastore.
65
+ project_id (str): The GCP project ID.
66
+ pf_monitoring_topic (str): The Pub/Sub topic name for product flow monitoring.
67
+ delta_time (timedelta): How long a running execution is still considered in progress
68
+ (anti over-retry window). Callers pass get_delta_time_for_ingestion_type(type) for
69
+ ingestions or get_delta_time_for_processing() for processings.
70
+ monitoring_step (str): The step name to include in the monitoring event.
71
+ task_retry_count (int): The value of the X-CloudTasks-TaskRetryCount header. If > 0, a running entity is treated as a Cloud Tasks retry and execution is allowed.
72
+ kind (str): Datastore kind of the entity (DATA_INGESTION_KIND by default).
73
+ check_version (bool): When False, skip the schedule_version comparison (used for the event-driven / OFF processing path that has no schedule_version). The `running` status is still set. Defaults to True.
74
+ Raises:
75
+ SkipExecutionError: If the schedule version is outdated (only when check_version) or if a previous execution
76
+ is still running (less than the allowed delta time).
77
+ """
78
+
79
+ monitoring_id = body["monitoring_id"]
80
+ entity_id = int(body["entity_id"])
81
+
82
+ schedule_version = body.get('schedule_version')
83
+ manual_execution = body.get('manual_execution', False)
84
+ if manual_execution:
85
+ logging.info(f'Manual execution for entity with id {entity_id}. Running without checking schedule version and execution status.')
86
+ else:
87
+ if entity['enabled'] is False:
88
+ logging.info(f'Entity with id {entity_id} is disabled. Skipping execution.')
89
+ raise SkipExecutionError('Entity is disabled')
90
+
91
+ if check_version:
92
+ current_schedule_version = _get_schedule_version(entity, kind)
93
+ if schedule_version != current_schedule_version:
94
+ logging.info(f'Entity with id {entity_id} has outdated schedule info. Previous version {schedule_version} and {current_schedule_version}')
95
+ raise SkipExecutionError(f'Schedule info is outdated. Previous version {schedule_version} and current version {current_schedule_version}')
96
+
97
+
98
+ if entity.get('execution_info') is not None and 'status' in entity['execution_info']:
99
+ if entity['execution_info']['status'] == 'running':
100
+ timestamp = entity['execution_info'].get('timestamp')
101
+ if timestamp and not isinstance(timestamp, datetime):
102
+ timestamp = isoparse(timestamp).astimezone(pytz.utc)
103
+ if timestamp and (datetime.now(timezone.utc) - timestamp).total_seconds() < delta_time.total_seconds():
104
+ if task_retry_count > 0:
105
+ logging.info(f'Entity with id {entity_id} is already running but this is a Cloud Tasks retry (retry_count={task_retry_count}). Allowing retry.')
106
+ else:
107
+ logging.info(f'Entity with id {entity_id} is already running since {timestamp}. Skipping execution.')
108
+ raise SkipExecutionError('Previous execution is still running')
109
+
110
+ set_execution_info_as_start(datastore_client, entity, kind)
111
+ if entity['service'] != 'datalab':
112
+ pubsub_client.pubsub_publish_pf_monitoring(
113
+ monitoring_id=monitoring_id,
114
+ entity_id=str(entity_id),
115
+ step=monitoring_step,
116
+ status='start',
117
+ project_id=project_id,
118
+ topic=pf_monitoring_topic
119
+ )
120
+
@@ -0,0 +1,10 @@
1
+ from typing import Any, NotRequired, Optional, TypedDict
2
+
3
+
4
+ class ExecutionStartBody(TypedDict):
5
+ """Expected body of an execution-start event (ingestion or processing)."""
6
+ monitoring_id: str
7
+ entity_id: Any
8
+ schedule_version: NotRequired[Optional[str]]
9
+ manual_execution: NotRequired[bool]
10
+ execution_time: NotRequired[str]
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.4
2
+ Name: arcane-execution-handler
3
+ Version: 1.0.0
4
+ Summary: Generic execution-start handling for scheduled entities (ingestion, processing, ...)
5
+ Author: Arcane
6
+ Author-email: product@wearcane.com
7
+ Requires-Python: >=3.11,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: arcane-datastore (>=1.1.15,<2.0.0)
14
+ Requires-Dist: arcane-pubsub (>=1.5.0,<2.0.0)
15
+ Requires-Dist: python-dateutil (>=2.7,<3.0)
16
+ Requires-Dist: pytz (>=2024.2,<2025.0)
17
+ Description-Content-Type: text/markdown
18
+
19
+ # Arcane execution-handler README
20
+
21
+
22
+ ## Release history
23
+ To see changes, please see CHANGELOG.md
24
+
@@ -0,0 +1,8 @@
1
+ arcane/execution_handler/__init__.py,sha256=X0nLDHH8U9nO2ghcEy8vAYVGLNT7mrgU-jvJxvy8kSo,87
2
+ arcane/execution_handler/datastore_lgp.py,sha256=gk-KaFD4Vh6XJOJB0NQCTpmkkw9PMShDqDWkinEA6_U,1002
3
+ arcane/execution_handler/exception.py,sha256=JYKI4WLLXg5Vw1luqsrFTwPZxTdAcsNx1hMXjIa27og,183
4
+ arcane/execution_handler/execution_handler.py,sha256=4yL1Lhf-sHiWYQAX8Pf7D1a-Pkdx-gnSq9NEAMELb-Q,5965
5
+ arcane/execution_handler/types.py,sha256=zeJhG6U880FIfLQLZw0CFCCOLmx6fEKkm9ocNmOJgRQ,343
6
+ arcane_execution_handler-1.0.0.dist-info/METADATA,sha256=Kr0j1SZXXGPD6k6g2Z02x2G9V2Nb4LNBauQUgZbhhwE,809
7
+ arcane_execution_handler-1.0.0.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88
8
+ arcane_execution_handler-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 2.4.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any