orchestrator-core 4.4.0rc1__py3-none-any.whl → 4.4.0rc3__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.
orchestrator/__init__.py CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  """This is the orchestrator workflow engine."""
15
15
 
16
- __version__ = "4.4.0rc1"
16
+ __version__ = "4.4.0rc3"
17
17
 
18
18
  from orchestrator.app import OrchestratorCore
19
19
  from orchestrator.settings import app_settings
@@ -12,6 +12,7 @@
12
12
  # limitations under the License.
13
13
 
14
14
  """Module that implements subscription related API endpoints."""
15
+
15
16
  from http import HTTPStatus
16
17
  from typing import Any
17
18
  from uuid import UUID
@@ -13,11 +13,13 @@
13
13
 
14
14
 
15
15
  import logging
16
+ import time
16
17
 
17
18
  import typer
18
- from apscheduler.schedulers.blocking import BlockingScheduler
19
19
 
20
- from orchestrator.schedules.scheduler import jobstores, scheduler, scheduler_dispose_db_connections
20
+ from orchestrator.schedules.scheduler import (
21
+ get_paused_scheduler,
22
+ )
21
23
 
22
24
  log = logging.getLogger(__name__)
23
25
 
@@ -27,13 +29,11 @@ app: typer.Typer = typer.Typer()
27
29
  @app.command()
28
30
  def run() -> None:
29
31
  """Start scheduler and loop eternally to keep thread alive."""
30
- blocking_scheduler = BlockingScheduler(jobstores=jobstores)
32
+ with get_paused_scheduler() as scheduler:
33
+ scheduler.resume()
31
34
 
32
- try:
33
- blocking_scheduler.start()
34
- except (KeyboardInterrupt, SystemExit):
35
- scheduler.shutdown()
36
- scheduler_dispose_db_connections()
35
+ while True:
36
+ time.sleep(1)
37
37
 
38
38
 
39
39
  @app.command()
@@ -42,10 +42,8 @@ def show_schedule() -> None:
42
42
 
43
43
  in cli underscore is replaced by a dash `show-schedule`
44
44
  """
45
- scheduler.start(paused=True) # paused: avoid triggering jobs during CLI
46
- jobs = scheduler.get_jobs()
47
- scheduler.shutdown(wait=False)
48
- scheduler_dispose_db_connections()
45
+ with get_paused_scheduler() as scheduler:
46
+ jobs = scheduler.get_jobs()
49
47
 
50
48
  for job in jobs:
51
49
  typer.echo(f"[{job.id}] Next run: {job.next_run_time} | Trigger: {job.trigger}")
@@ -54,10 +52,8 @@ def show_schedule() -> None:
54
52
  @app.command()
55
53
  def force(job_id: str) -> None:
56
54
  """Force the execution of (a) scheduler(s) based on a job_id."""
57
- scheduler.start(paused=True) # paused: avoid triggering jobs during CLI
58
- job = scheduler.get_job(job_id)
59
- scheduler.shutdown(wait=False)
60
- scheduler_dispose_db_connections()
55
+ with get_paused_scheduler() as scheduler:
56
+ job = scheduler.get_job(job_id)
61
57
 
62
58
  if not job:
63
59
  typer.echo(f"Job '{job_id}' not found.")
@@ -12,9 +12,11 @@
12
12
  # limitations under the License.
13
13
 
14
14
 
15
+ from contextlib import contextmanager
15
16
  from datetime import datetime
16
- from typing import Any
17
+ from typing import Any, Generator
17
18
 
19
+ from apscheduler.executors.pool import ThreadPoolExecutor
18
20
  from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
19
21
  from apscheduler.schedulers.background import BackgroundScheduler
20
22
  from more_itertools import partition
@@ -28,14 +30,31 @@ from orchestrator.settings import app_settings
28
30
  from orchestrator.utils.helpers import camel_to_snake, to_camel
29
31
 
30
32
  jobstores = {"default": SQLAlchemyJobStore(url=str(app_settings.DATABASE_URI))}
33
+ executors = {
34
+ "default": ThreadPoolExecutor(1),
35
+ }
36
+ job_defaults = {
37
+ "coalesce": True,
38
+ }
31
39
 
32
- scheduler = BackgroundScheduler(jobstores=jobstores)
40
+ scheduler = BackgroundScheduler(jobstores=jobstores, executors=executors, job_defaults=job_defaults)
33
41
 
34
42
 
35
43
  def scheduler_dispose_db_connections() -> None:
36
44
  jobstores["default"].engine.dispose()
37
45
 
38
46
 
47
+ @contextmanager
48
+ def get_paused_scheduler() -> Generator[BackgroundScheduler, Any, None]:
49
+ scheduler.start(paused=True)
50
+
51
+ try:
52
+ yield scheduler
53
+ finally:
54
+ scheduler.shutdown()
55
+ scheduler_dispose_db_connections()
56
+
57
+
39
58
  class ScheduledTask(BaseModel):
40
59
  id: str
41
60
  name: str | None = None
@@ -131,10 +150,8 @@ def get_scheduler_tasks(
131
150
  sort_by: list[Sort] | None = None,
132
151
  error_handler: CallableErrorHandler = default_error_handler,
133
152
  ) -> tuple[list[ScheduledTask], int]:
134
- scheduler.start(paused=True)
135
- scheduled_tasks = scheduler.get_jobs()
136
- scheduler.shutdown(wait=False)
137
- scheduler_dispose_db_connections()
153
+ with get_paused_scheduler() as pauzed_scheduler:
154
+ scheduled_tasks = pauzed_scheduler.get_jobs()
138
155
 
139
156
  scheduled_tasks = filter_scheduled_tasks(scheduled_tasks, error_handler, filter_by)
140
157
  scheduled_tasks = sort_scheduled_tasks(scheduled_tasks, error_handler, sort_by)
@@ -12,6 +12,7 @@
12
12
  # limitations under the License.
13
13
 
14
14
  """Module that provides service functions on subscriptions."""
15
+
15
16
  import pickle # noqa: S403
16
17
  from collections import defaultdict
17
18
  from collections.abc import Sequence
@@ -506,6 +507,7 @@ TARGET_DEFAULT_USABLE_MAP: dict[Target, list[str]] = {
506
507
  Target.TERMINATE: ["active", "provisioning"],
507
508
  Target.SYSTEM: ["active"],
508
509
  Target.VALIDATE: ["active"],
510
+ Target.RECONCILE: ["active"],
509
511
  }
510
512
 
511
513
  WF_USABLE_MAP: dict[str, list[str]] = {}
@@ -532,6 +534,7 @@ def subscription_workflows(subscription: SubscriptionTable) -> dict[str, Any]:
532
534
  ... "terminate": [],
533
535
  ... "system": [],
534
536
  ... "validate": [],
537
+ ... "reconcile: [],
535
538
  ... }
536
539
 
537
540
  """
@@ -552,6 +555,7 @@ def subscription_workflows(subscription: SubscriptionTable) -> dict[str, Any]:
552
555
  "terminate": [],
553
556
  "system": [],
554
557
  "validate": [],
558
+ "reconcile": [],
555
559
  }
556
560
  for workflow in subscription.product.workflows:
557
561
  if workflow.name in WF_USABLE_WHILE_OUT_OF_SYNC or workflow.is_task:
orchestrator/targets.py CHANGED
@@ -23,3 +23,4 @@ class Target(strEnum):
23
23
  TERMINATE = "TERMINATE"
24
24
  SYSTEM = "SYSTEM"
25
25
  VALIDATE = "VALIDATE"
26
+ RECONCILE = "RECONCILE"
@@ -154,7 +154,7 @@ def _generate_modify_form(workflow_target: str, workflow_name: str) -> InputForm
154
154
 
155
155
 
156
156
  def wrap_modify_initial_input_form(initial_input_form: InputStepFunc | None) -> StateInputStepFunc | None:
157
- """Wrap initial input for modify and terminate workflows.
157
+ """Wrap initial input for modify, reconcile and terminate workflows.
158
158
 
159
159
  This is needed because the frontend expects all modify workflows to start with a page that only contains the
160
160
  subscription id. It also expects the second page to have some user visible inputs and the subscription id *again*.
@@ -355,6 +355,53 @@ def validate_workflow(description: str) -> Callable[[Callable[[], StepList]], Wo
355
355
  return _validate_workflow
356
356
 
357
357
 
358
+ def reconcile_workflow(
359
+ description: str,
360
+ additional_steps: StepList | None = None,
361
+ authorize_callback: Authorizer | None = None,
362
+ retry_auth_callback: Authorizer | None = None,
363
+ ) -> Callable[[Callable[[], StepList]], Workflow]:
364
+ """Similar to a modify_workflow but without required input user input to perform a sync with external systems based on the subscriptions existing configuration.
365
+
366
+ Use this for subscription reconcile workflows.
367
+
368
+ Example::
369
+
370
+ @reconcile_workflow("Reconcile l2vpn")
371
+ def reconcile_l2vpn() -> StepList:
372
+ return (
373
+ begin
374
+ >> update_l2vpn_in_external_systems
375
+ )
376
+ """
377
+
378
+ wrapped_reconcile_initial_input_form_generator = wrap_modify_initial_input_form(None)
379
+
380
+ def _reconcile_workflow(f: Callable[[], StepList]) -> Workflow:
381
+ steplist = (
382
+ init
383
+ >> store_process_subscription()
384
+ >> unsync
385
+ >> f()
386
+ >> (additional_steps or StepList())
387
+ >> resync
388
+ >> refresh_subscription_search_index
389
+ >> done
390
+ )
391
+
392
+ return make_workflow(
393
+ f,
394
+ description,
395
+ wrapped_reconcile_initial_input_form_generator,
396
+ Target.RECONCILE,
397
+ steplist,
398
+ authorize_callback=authorize_callback,
399
+ retry_auth_callback=retry_auth_callback,
400
+ )
401
+
402
+ return _reconcile_workflow
403
+
404
+
358
405
  def ensure_provisioning_status(modify_steps: Step | StepList) -> StepList:
359
406
  """Decorator to ensure subscription modifications are executed only during Provisioning status."""
360
407
  return (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orchestrator-core
3
- Version: 4.4.0rc1
3
+ Version: 4.4.0rc3
4
4
  Summary: This is the orchestrator workflow engine.
5
5
  Author-email: SURF <automation-beheer@surf.nl>
6
6
  Requires-Python: >=3.11,<3.14
@@ -1,11 +1,11 @@
1
- orchestrator/__init__.py,sha256=2VCmBMCQvlduTSR0AOWiSAcfGIGfm7kGJxYEKzqiXew,1066
1
+ orchestrator/__init__.py,sha256=4-pAbOsr1gG9e7wEsiJqmS0b4qyvGlnxbcG8JjrN-c8,1066
2
2
  orchestrator/app.py,sha256=7UrXKjBKNSEaSSXAd5ww_RdMFhFqE4yvfj8faS2MzAA,12089
3
3
  orchestrator/exception_handlers.py,sha256=UsW3dw8q0QQlNLcV359bIotah8DYjMsj2Ts1LfX4ClY,1268
4
4
  orchestrator/log_config.py,sha256=1tPRX5q65e57a6a_zEii_PFK8SzWT0mnA5w2sKg4hh8,1853
5
5
  orchestrator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  orchestrator/security.py,sha256=iXFxGxab54aav7oHEKLAVkTgrQMJGHy6IYLojEnD7gI,2422
7
7
  orchestrator/settings.py,sha256=2Kgc6m3qUCcSM3Z_IVUeehfgO0QphMFkLrS0RC3sU-U,4365
8
- orchestrator/targets.py,sha256=WizBgnp8hWX9YLFUIju7ewSubiwQqinCvyiYNcXHbHI,802
8
+ orchestrator/targets.py,sha256=d7Fyh_mWIWPivA_E7DTNFpZID3xFW_K0JlZ5nksVX7k,830
9
9
  orchestrator/types.py,sha256=qzs7xx5AYRmKbpYRyJJP3wuDb0W0bcAzefCN0RWLAco,15459
10
10
  orchestrator/version.py,sha256=b58e08lxs47wUNXv0jXFO_ykpksmytuzEXD4La4W-NQ,1366
11
11
  orchestrator/workflow.py,sha256=meDCPnyyX_n5PsMUaFy2wWb5EKNm1_ff7zRDBYrbcDg,45901
@@ -23,7 +23,7 @@ orchestrator/api/api_v1/endpoints/products.py,sha256=BfFtwu9dZXEQbtKxYj9icc73GKG
23
23
  orchestrator/api/api_v1/endpoints/resource_types.py,sha256=gGyuaDyOD0TAVoeFGaGmjDGnQ8eQQArOxKrrk4MaDzA,2145
24
24
  orchestrator/api/api_v1/endpoints/settings.py,sha256=5s-k169podZjgGHUbVDmSQwpY_3Cs_Bbf2PPtZIkBcw,6184
25
25
  orchestrator/api/api_v1/endpoints/subscription_customer_descriptions.py,sha256=1_6LtgQleoq3M6z_W-Qz__Bj3OFUweoPrUqHMwSH6AM,3288
26
- orchestrator/api/api_v1/endpoints/subscriptions.py,sha256=zn_LeVfmp2uw7CszK4BvQ5n37hZccy3K2htkoDgF1sI,9809
26
+ orchestrator/api/api_v1/endpoints/subscriptions.py,sha256=7KaodccUiMkcVnrFnK2azp_V_-hGudcIyhov5WwVGQY,9810
27
27
  orchestrator/api/api_v1/endpoints/translations.py,sha256=dIWh_fCnZZUxJoGiNeJ49DK_xpf75IpR_0EIMSvzIvY,963
28
28
  orchestrator/api/api_v1/endpoints/user.py,sha256=RyI32EXVu6I-IxWjz0XB5zQWzzLL60zKXLgLqLH02xU,1827
29
29
  orchestrator/api/api_v1/endpoints/workflows.py,sha256=_0vhGiQeu3-z16Zi0WmuDWBs8gmed6BzRNwYH_sF6AY,1977
@@ -36,7 +36,7 @@ orchestrator/cli/migrate_domain_models.py,sha256=WRXy_1OnziQwpsCFZXvjB30nDJtjj0i
36
36
  orchestrator/cli/migrate_tasks.py,sha256=bju8XColjSZD0v3rS4kl-24dLr8En_H4-6enBmqd494,7255
37
37
  orchestrator/cli/migrate_workflows.py,sha256=nxUpx0vgEIc_8aJrjAyrw3E9Dt8JmaamTts8oiQ4vHY,8923
38
38
  orchestrator/cli/migration_helpers.py,sha256=C5tpkP5WEBr7G9S-1k1hgSI8ili6xd9Z5ygc9notaK0,4110
39
- orchestrator/cli/scheduler.py,sha256=jdBbgqE7bNUOevFe6gZS5C-SyDTOow8baNJKEBTkS0A,2293
39
+ orchestrator/cli/scheduler.py,sha256=2q6xT_XVOodY3e_qzIV98MWNvKvrbFpOJajWesj1fcs,1911
40
40
  orchestrator/cli/domain_gen_helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  orchestrator/cli/domain_gen_helpers/fixed_input_helpers.py,sha256=uzpwsaau81hHSxNMOS9-o7kF-9_78R0f_UE0AvWooZQ,6775
42
42
  orchestrator/cli/domain_gen_helpers/helpers.py,sha256=tIPxn8ezED_xYZxH7ZAtQLwkDc6RNmLZVxWAoJ3a9lw,4203
@@ -249,7 +249,7 @@ orchestrator/migrations/versions/schema/2025-07-04_4b58e336d1bf_deprecating_work
249
249
  orchestrator/migrations/versions/schema/2025-07-28_850dccac3b02_update_description_of_resume_workflows_.py,sha256=R6Qoga83DJ1IL0WYPu0u5u2ZvAmqGlDmUMv_KtJyOhQ,812
250
250
  orchestrator/schedules/__init__.py,sha256=Zy0fTOBMGIRFoh5iVFDLF9_PRAFaONYDThGK9EsysWo,981
251
251
  orchestrator/schedules/resume_workflows.py,sha256=jRnVRWDy687pQu-gtk80ecwiLSdrvtL15tG3U2zWA6I,891
252
- orchestrator/schedules/scheduler.py,sha256=vze3xaZhUL5maKQB6a1gCvc9AcGw3jX-BHT3d5xvy6A,5430
252
+ orchestrator/schedules/scheduler.py,sha256=_Y6TB-GKNJM0Nk7CRLuMnw0djFEBrDm999GOOcBuBeQ,5880
253
253
  orchestrator/schedules/scheduling.py,sha256=_mbpHMhijey8Y56ebtJ4wVkrp_kPVRm8hoByzlQF4SE,2821
254
254
  orchestrator/schedules/task_vacuum.py,sha256=mxb7fsy1GphRwvUWi_lvwNaj51YAXUdIDlkOJd90AFI,874
255
255
  orchestrator/schedules/validate_products.py,sha256=zWFQeVn3F8LP3joExLiKdmHs008pZsO-RolcIXHjFyE,1322
@@ -276,7 +276,7 @@ orchestrator/services/resource_types.py,sha256=_QBy_JOW_X3aSTqH0CuLrq4zBJL0p7Q-U
276
276
  orchestrator/services/settings.py,sha256=HEWfFulgoEDwgfxGEO__QTr5fDiwNBEj1UhAeTAdbLQ,3159
277
277
  orchestrator/services/settings_env_variables.py,sha256=iPErQjqPQCxKs0sPhefB16d8SBBVUi6eiRnFBK5bgqA,2196
278
278
  orchestrator/services/subscription_relations.py,sha256=aIdyzwyyy58OFhwjRPCPgnQTUTmChu6SeSQRIleQoDE,13138
279
- orchestrator/services/subscriptions.py,sha256=nr2HI89nC0lYjzTh2j-lEQ5cPQK43LNZv3gvP6jbepw,27189
279
+ orchestrator/services/subscriptions.py,sha256=XhJ5ygAAyWUIZHULhKyi1uU5DwkKZhzdxxn9vdQZYiA,27281
280
280
  orchestrator/services/tasks.py,sha256=mR3Fj1VsudltpanJKI2PvrxersyhVQ1skp8H7r3XnYI,5288
281
281
  orchestrator/services/translations.py,sha256=GyP8soUFGej8AS8uulBsk10CCK6Kwfjv9AHMFm3ElQY,1713
282
282
  orchestrator/services/workflows.py,sha256=iEkt2OBuTwkDru4V6ZSKatnw0b96ZdPV-VQqeZ9EOgU,4015
@@ -312,14 +312,14 @@ orchestrator/workflows/__init__.py,sha256=NzIGGI-8SNAwCk2YqH6sHhEWbgAY457ntDwjO1
312
312
  orchestrator/workflows/modify_note.py,sha256=eXt5KQvrkOXf-3YEXCn2XbBLP9N-n1pUYRW2t8Odupo,2150
313
313
  orchestrator/workflows/removed_workflow.py,sha256=V0Da5TEdfLdZZKD38ig-MTp3_IuE7VGqzHHzvPYQmLI,909
314
314
  orchestrator/workflows/steps.py,sha256=CZxfzkG5ANJYwuYTkQ4da2RpQqIjXCtey_Uy1ezRAZ4,6479
315
- orchestrator/workflows/utils.py,sha256=bhX9vm3oc9k6RSaESl34v4Nrh40G4Ys91INoTjZ0XVM,13966
315
+ orchestrator/workflows/utils.py,sha256=VUCDoIl5XAKtIeAJpVpyW2pCIg3PoVWfwGn28BYlYhA,15424
316
316
  orchestrator/workflows/tasks/__init__.py,sha256=GyHNfEFCGKQwRiN6rQmvSRH2iYX7npjMZn97n8XzmLU,571
317
317
  orchestrator/workflows/tasks/cleanup_tasks_log.py,sha256=BfWYbPXhnLAHUJ0mlODDnjZnQQAvKCZJDVTwbwOWI04,1624
318
318
  orchestrator/workflows/tasks/resume_workflows.py,sha256=T3iobSJjVgiupe0rClD34kUZ7KF4pL5yK2AVeRLZog8,4313
319
319
  orchestrator/workflows/tasks/validate_product_type.py,sha256=paG-NAY1bdde3Adt8zItkcBKf5Pxw6f5ngGW6an6dYU,3192
320
320
  orchestrator/workflows/tasks/validate_products.py,sha256=GZJBoFF-WMphS7ghMs2-gqvV2iL1F0POhk0uSNt93n0,8510
321
321
  orchestrator/workflows/translations/en-GB.json,sha256=ST53HxkphFLTMjFHonykDBOZ7-P_KxksktZU3GbxLt0,846
322
- orchestrator_core-4.4.0rc1.dist-info/licenses/LICENSE,sha256=b-aA5OZQuuBATmLKo_mln8CQrDPPhg3ghLzjPjLn4Tg,11409
323
- orchestrator_core-4.4.0rc1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
324
- orchestrator_core-4.4.0rc1.dist-info/METADATA,sha256=as5RXg0Y5DUGPcpT-6raY0HmvEeDJfEMsg1dIQfqoqU,5967
325
- orchestrator_core-4.4.0rc1.dist-info/RECORD,,
322
+ orchestrator_core-4.4.0rc3.dist-info/licenses/LICENSE,sha256=b-aA5OZQuuBATmLKo_mln8CQrDPPhg3ghLzjPjLn4Tg,11409
323
+ orchestrator_core-4.4.0rc3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
324
+ orchestrator_core-4.4.0rc3.dist-info/METADATA,sha256=Xu_xQc9lhukT3Gj52AVnPxfBc8Q8Pk77IBbPbMkgBe8,5967
325
+ orchestrator_core-4.4.0rc3.dist-info/RECORD,,