orchestrator-core 4.1.0rc2__py3-none-any.whl → 4.2.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.
Files changed (32) hide show
  1. orchestrator/__init__.py +1 -1
  2. orchestrator/api/api_v1/endpoints/processes.py +6 -2
  3. orchestrator/api/api_v1/endpoints/subscriptions.py +25 -2
  4. orchestrator/cli/database.py +8 -1
  5. orchestrator/cli/domain_gen_helpers/helpers.py +44 -2
  6. orchestrator/cli/domain_gen_helpers/product_block_helpers.py +35 -15
  7. orchestrator/cli/domain_gen_helpers/resource_type_helpers.py +5 -5
  8. orchestrator/cli/domain_gen_helpers/types.py +7 -1
  9. orchestrator/cli/generator/templates/create_product.j2 +1 -2
  10. orchestrator/cli/migrate_domain_models.py +16 -5
  11. orchestrator/db/models.py +6 -3
  12. orchestrator/graphql/schemas/process.py +21 -2
  13. orchestrator/graphql/schemas/product.py +8 -9
  14. orchestrator/graphql/schemas/workflow.py +9 -0
  15. orchestrator/graphql/types.py +7 -1
  16. orchestrator/migrations/versions/schema/2025-07-01_93fc5834c7e5_changed_timestamping_fields_in_process_steps.py +65 -0
  17. orchestrator/migrations/versions/schema/2025-07-04_4b58e336d1bf_deprecating_workflow_target_in_.py +30 -0
  18. orchestrator/schemas/process.py +5 -1
  19. orchestrator/services/processes.py +11 -2
  20. orchestrator/services/settings_env_variables.py +3 -15
  21. orchestrator/settings.py +1 -1
  22. orchestrator/utils/enrich_process.py +4 -2
  23. orchestrator/utils/errors.py +2 -1
  24. orchestrator/workflow.py +7 -2
  25. orchestrator/workflows/modify_note.py +1 -1
  26. orchestrator/workflows/steps.py +14 -8
  27. orchestrator/workflows/utils.py +3 -3
  28. orchestrator_core-4.2.0.dist-info/METADATA +167 -0
  29. {orchestrator_core-4.1.0rc2.dist-info → orchestrator_core-4.2.0.dist-info}/RECORD +31 -29
  30. orchestrator_core-4.1.0rc2.dist-info/METADATA +0 -118
  31. {orchestrator_core-4.1.0rc2.dist-info → orchestrator_core-4.2.0.dist-info}/WHEEL +0 -0
  32. {orchestrator_core-4.1.0rc2.dist-info → orchestrator_core-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,65 @@
1
+ """Changed timestamping fields in process_steps.
2
+
3
+ Revision ID: 93fc5834c7e5
4
+ Revises: 4b58e336d1bf
5
+ Create Date: 2025-07-01 14:20:44.755694
6
+
7
+ """
8
+
9
+ import sqlalchemy as sa
10
+ from alembic import op
11
+
12
+ from orchestrator import db
13
+
14
+ # revision identifiers, used by Alembic.
15
+ revision = "93fc5834c7e5"
16
+ down_revision = "4b58e336d1bf"
17
+ branch_labels = None
18
+ depends_on = None
19
+
20
+
21
+ def upgrade() -> None:
22
+ # ### commands auto generated by Alembic - please adjust! ###
23
+ op.add_column(
24
+ "process_steps",
25
+ sa.Column(
26
+ "started_at",
27
+ db.UtcTimestamp(timezone=True),
28
+ server_default=sa.text("statement_timestamp()"),
29
+ nullable=False,
30
+ ),
31
+ )
32
+ op.alter_column("process_steps", "executed_at", new_column_name="completed_at")
33
+ # conn = op.get_bind()
34
+ # sa.select
35
+ # ### end Alembic commands ###
36
+ # Backfill started_at field correctly using proper aliasing
37
+ op.execute(
38
+ """
39
+ WITH backfill_started_at AS (
40
+ SELECT
41
+ ps1.stepid,
42
+ COALESCE(prev.completed_at, p.started_at) AS new_started_at
43
+ FROM process_steps ps1
44
+ JOIN processes p ON ps1.pid = p.pid
45
+ LEFT JOIN LATERAL (
46
+ SELECT ps2.completed_at
47
+ FROM process_steps ps2
48
+ WHERE ps2.pid = ps1.pid AND ps2.completed_at < ps1.completed_at
49
+ ORDER BY ps2.completed_at DESC
50
+ LIMIT 1
51
+ ) prev ON true
52
+ )
53
+ UPDATE process_steps
54
+ SET started_at = b.new_started_at
55
+ FROM backfill_started_at b
56
+ WHERE process_steps.stepid = b.stepid;
57
+ """
58
+ )
59
+
60
+
61
+ def downgrade() -> None:
62
+ # ### commands auto generated by Alembic - please adjust! ###
63
+ op.drop_column("process_steps", "started_at")
64
+ op.alter_column("process_steps", "completed_at", new_column_name="executed_at")
65
+ # ### end Alembic commands ###
@@ -0,0 +1,30 @@
1
+ """Deprecating workflow target in ProcessSubscriptionTable.
2
+
3
+ Revision ID: 4b58e336d1bf
4
+ Revises: 161918133bec
5
+ Create Date: 2025-07-04 15:27:23.814954
6
+
7
+ """
8
+
9
+ import sqlalchemy as sa
10
+ from alembic import op
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = "4b58e336d1bf"
14
+ down_revision = "161918133bec"
15
+ branch_labels = None
16
+ depends_on = None
17
+
18
+
19
+ def upgrade() -> None:
20
+ op.alter_column("processes_subscriptions", "workflow_target", existing_type=sa.VARCHAR(length=255), nullable=True)
21
+
22
+
23
+ def downgrade() -> None:
24
+ op.alter_column(
25
+ "processes_subscriptions",
26
+ "workflow_target",
27
+ existing_type=sa.VARCHAR(length=255),
28
+ nullable=False,
29
+ existing_server_default=sa.text("'CREATE'::character varying"),
30
+ )
@@ -49,7 +49,11 @@ class ProcessStepSchema(OrchestratorBaseModel):
49
49
  name: str
50
50
  status: str
51
51
  created_by: str | None = None
52
- executed: datetime | None = None
52
+ executed: datetime | None = Field(
53
+ None, deprecated="Deprecated, use 'started' and 'completed' for step start and completion times"
54
+ )
55
+ started: datetime | None = None
56
+ completed: datetime | None = None
53
57
  commit_hash: str | None = None
54
58
  state: dict[str, Any] | None = None
55
59
  state_delta: dict[str, Any] | None = None
@@ -12,6 +12,7 @@
12
12
  # limitations under the License.
13
13
  from collections.abc import Callable, Sequence
14
14
  from concurrent.futures.thread import ThreadPoolExecutor
15
+ from datetime import datetime
15
16
  from functools import partial
16
17
  from http import HTTPStatus
17
18
  from typing import Any
@@ -19,6 +20,7 @@ from uuid import UUID, uuid4
19
20
 
20
21
  import structlog
21
22
  from deepmerge.merger import Merger
23
+ from pytz import utc
22
24
  from sqlalchemy import delete, select
23
25
  from sqlalchemy.exc import SQLAlchemyError
24
26
  from sqlalchemy.orm import joinedload
@@ -206,6 +208,10 @@ def _get_current_step_to_update(
206
208
  finally:
207
209
  step_state.pop("__remove_keys", None)
208
210
 
211
+ # We don't have __last_step_started in __remove_keys because the way __remove_keys is populated appears like it would overwrite
212
+ # what's put there in the step decorator in certain cases (step groups and callback steps)
213
+ step_start_time = step_state.pop("__last_step_started_at", None)
214
+
209
215
  if process_state.isfailed() or process_state.iswaiting():
210
216
  if (
211
217
  last_db_step is not None
@@ -216,7 +222,7 @@ def _get_current_step_to_update(
216
222
  ):
217
223
  state_ex_info = {
218
224
  "retries": last_db_step.state.get("retries", 0) + 1,
219
- "executed_at": last_db_step.state.get("executed_at", []) + [str(last_db_step.executed_at)],
225
+ "completed_at": last_db_step.state.get("completed_at", []) + [str(last_db_step.completed_at)],
220
226
  }
221
227
 
222
228
  # write new state info and execution date
@@ -236,10 +242,13 @@ def _get_current_step_to_update(
236
242
  state=step_state,
237
243
  created_by=stat.current_user,
238
244
  )
245
+ # Since the Start step does not have a __last_step_started_at in it's state, we effectively assume it is instantaneous.
246
+ now = nowtz()
247
+ current_step.started_at = datetime.fromtimestamp(step_start_time or now.timestamp(), tz=utc)
239
248
 
240
249
  # Always explicitly set this instead of leaving it to the database to prevent failing tests
241
250
  # Test will fail if multiple steps have the same timestamp
242
- current_step.executed_at = nowtz()
251
+ current_step.completed_at = now
243
252
  return current_step
244
253
 
245
254
 
@@ -14,7 +14,7 @@
14
14
  from typing import Any, Dict, Type
15
15
 
16
16
  from pydantic import SecretStr as PydanticSecretStr
17
- from pydantic_core import MultiHostUrl
17
+ from pydantic_core import MultiHostUrl, Url
18
18
  from pydantic_settings import BaseSettings
19
19
 
20
20
  from orchestrator.utils.expose_settings import SecretStr as OrchSecretStr
@@ -32,21 +32,9 @@ def expose_settings(settings_name: str, base_settings: Type[BaseSettings]) -> Ty
32
32
 
33
33
  def mask_value(key: str, value: Any) -> Any:
34
34
  key_lower = key.lower()
35
+ is_sensitive_key = "secret" in key_lower or "password" in key_lower
35
36
 
36
- if "secret" in key_lower or "password" in key_lower:
37
- # Mask sensitive information
38
- return MASK
39
-
40
- if isinstance(value, PydanticSecretStr):
41
- # Need to convert SecretStr to str for serialization
42
- return str(value)
43
-
44
- if isinstance(value, OrchSecretStr):
45
- return MASK
46
-
47
- # PostgresDsn is just MultiHostUrl with extra metadata (annotations)
48
- if isinstance(value, MultiHostUrl):
49
- # Convert PostgresDsn to str for serialization
37
+ if is_sensitive_key or isinstance(value, (OrchSecretStr, PydanticSecretStr, MultiHostUrl, Url)):
50
38
  return MASK
51
39
 
52
40
  return value
orchestrator/settings.py CHANGED
@@ -72,7 +72,7 @@ class AppSettings(BaseSettings):
72
72
  TRACING_ENABLED: bool = False
73
73
  TRACE_HOST: str = "http://localhost:4317"
74
74
  TRANSLATIONS_DIR: Path | None = None
75
- WEBSOCKET_BROADCASTER_URL: str = "memory://"
75
+ WEBSOCKET_BROADCASTER_URL: OrchSecretStr = "memory://" # type: ignore
76
76
  ENABLE_WEBSOCKETS: bool = True
77
77
  DISABLE_INSYNC_CHECK: bool = False
78
78
  DEFAULT_PRODUCT_WORKFLOWS: list[str] = ["modify_note"]
@@ -57,7 +57,9 @@ def enrich_step_details(step: ProcessStepTable, previous_step: ProcessStepTable
57
57
 
58
58
  return {
59
59
  "name": step.name,
60
- "executed": step.executed_at.timestamp(),
60
+ "executed": step.completed_at.timestamp(),
61
+ "started": step.started_at.timestamp(),
62
+ "completed": step.completed_at.timestamp(),
61
63
  "status": step.status,
62
64
  "state": step.state,
63
65
  "created_by": step.created_by,
@@ -103,7 +105,7 @@ def enrich_process(process: ProcessTable, p_stat: ProcessStat | None = None) ->
103
105
  "is_task": process.is_task,
104
106
  "workflow_id": process.workflow_id,
105
107
  "workflow_name": process.workflow.name,
106
- "workflow_target": process.process_subscriptions[0].workflow_target if process.process_subscriptions else None,
108
+ "workflow_target": process.workflow.target,
107
109
  "failed_reason": process.failed_reason,
108
110
  "created_by": process.created_by,
109
111
  "started_at": process.started_at,
@@ -128,12 +128,13 @@ def _(err: Exception) -> ErrorDict:
128
128
  # We can't dispatch on ApiException, see is_api_exception docstring
129
129
  if is_api_exception(err):
130
130
  err = cast(ApiException, err)
131
+ headers = err.headers or {}
131
132
  return {
132
133
  "class": type(err).__name__,
133
134
  "error": err.reason,
134
135
  "status_code": err.status,
135
136
  "body": err.body,
136
- "headers": "\n".join(f"{k}: {v}" for k, v in err.headers.items()),
137
+ "headers": "\n".join(f"{k}: {v}" for k, v in headers.items()),
137
138
  "traceback": show_ex(err),
138
139
  }
139
140
 
orchestrator/workflow.py CHANGED
@@ -46,6 +46,7 @@ from orchestrator.services.settings import get_engine_settings
46
46
  from orchestrator.targets import Target
47
47
  from orchestrator.types import ErrorDict, StepFunc
48
48
  from orchestrator.utils.auth import Authorizer
49
+ from orchestrator.utils.datetime import nowtz
49
50
  from orchestrator.utils.docs import make_workflow_doc
50
51
  from orchestrator.utils.errors import error_state_to_dict
51
52
  from orchestrator.utils.state import form_inject_args, inject_args
@@ -381,11 +382,13 @@ def step_group(name: str, steps: StepList, extract_form: bool = True) -> Step:
381
382
  p = p.map(lambda s: s | {"__replace_last_state": True})
382
383
  return step_log_fn(step_, p)
383
384
 
385
+ step_group_start_time = nowtz().timestamp()
384
386
  process: Process = Success(initial_state)
385
387
  process = _exec_steps(step_list, process, dblogstep)
386
-
387
388
  # Add instruction to replace state of last sub step before returning process _exec_steps higher in the call tree
388
- return process.map(lambda s: s | {"__replace_last_state": True})
389
+ return process.map(
390
+ lambda s: s | {"__replace_last_state": True, "__last_step_started_at": step_group_start_time}
391
+ )
389
392
 
390
393
  # Make sure we return a form is a sub step has a form
391
394
  form = next((sub_step.form for sub_step in steps if sub_step.form), None) if extract_form else None
@@ -1454,6 +1457,8 @@ def _exec_steps(steps: StepList, starting_process: Process, dblogstep: StepLogFu
1454
1457
  "Not executing Step as the workflow engine is Paused. Process will remain in state 'running'"
1455
1458
  )
1456
1459
  return process
1460
+
1461
+ process = process.map(lambda s: s | {"__last_step_started_at": nowtz().timestamp()})
1457
1462
  step_result_process = process.execute_step(step)
1458
1463
  except Exception as e:
1459
1464
  consolelogger.error("An exception occurred while executing the workflow step.")
@@ -53,4 +53,4 @@ def store_subscription_note(subscription_id: UUIDstr, note: str) -> State:
53
53
 
54
54
  @workflow("Modify Note", initial_input_form=wrap_modify_initial_input_form(initial_input_form), target=Target.MODIFY)
55
55
  def modify_note() -> StepList:
56
- return init >> store_process_subscription(Target.MODIFY) >> store_subscription_note >> done
56
+ return init >> store_process_subscription() >> store_subscription_note >> done
@@ -23,6 +23,7 @@ from orchestrator.services.subscriptions import get_subscription
23
23
  from orchestrator.targets import Target
24
24
  from orchestrator.types import SubscriptionLifecycle
25
25
  from orchestrator.utils.json import to_serializable
26
+ from orchestrator.websocket import sync_invalidate_subscription_cache
26
27
  from orchestrator.workflow import Step, step
27
28
  from pydantic_forms.types import State, UUIDstr
28
29
 
@@ -33,6 +34,7 @@ logger = structlog.get_logger(__name__)
33
34
  def resync(subscription: SubscriptionModel) -> State:
34
35
  """Transition a subscription to in sync."""
35
36
  subscription.insync = True
37
+ sync_invalidate_subscription_cache(subscription.subscription_id)
36
38
  return {"subscription": subscription}
37
39
 
38
40
 
@@ -93,6 +95,7 @@ def unsync(subscription_id: UUIDstr, __old_subscriptions__: dict | None = None)
93
95
  if not subscription.insync:
94
96
  raise ValueError("Subscription is already out of sync, cannot continue!")
95
97
  subscription.insync = False
98
+ sync_invalidate_subscription_cache(subscription.subscription_id)
96
99
 
97
100
  return {"subscription": subscription, "__old_subscriptions__": subscription_backup}
98
101
 
@@ -105,20 +108,23 @@ def unsync_unchecked(subscription_id: UUIDstr) -> State:
105
108
  return {"subscription": subscription}
106
109
 
107
110
 
108
- def store_process_subscription_relationship(
109
- process_id: UUIDstr, subscription_id: UUIDstr, workflow_target: str
110
- ) -> ProcessSubscriptionTable:
111
- process_subscription = ProcessSubscriptionTable(
112
- process_id=process_id, subscription_id=subscription_id, workflow_target=workflow_target
113
- )
111
+ def store_process_subscription_relationship(process_id: UUIDstr, subscription_id: UUIDstr) -> ProcessSubscriptionTable:
112
+ process_subscription = ProcessSubscriptionTable(process_id=process_id, subscription_id=subscription_id)
114
113
  db.session.add(process_subscription)
115
114
  return process_subscription
116
115
 
117
116
 
118
- def store_process_subscription(workflow_target: Target) -> Step:
117
+ def store_process_subscription(workflow_target: Target | None = None) -> Step:
118
+ if workflow_target:
119
+ deprecation_warning = (
120
+ "Providing a workflow target to function store_process_subscription() is deprecated. "
121
+ "This information is already stored in the workflow table."
122
+ )
123
+ logger.warning(deprecation_warning)
124
+
119
125
  @step("Create Process Subscription relation")
120
126
  def _store_process_subscription(process_id: UUIDstr, subscription_id: UUIDstr) -> None:
121
- store_process_subscription_relationship(process_id, subscription_id, workflow_target)
127
+ store_process_subscription_relationship(process_id, subscription_id)
122
128
 
123
129
  return _store_process_subscription
124
130
 
@@ -265,7 +265,7 @@ def modify_workflow(
265
265
  def _modify_workflow(f: Callable[[], StepList]) -> Workflow:
266
266
  steplist = (
267
267
  init
268
- >> store_process_subscription(Target.MODIFY)
268
+ >> store_process_subscription()
269
269
  >> unsync
270
270
  >> f()
271
271
  >> (additional_steps or StepList())
@@ -311,7 +311,7 @@ def terminate_workflow(
311
311
  def _terminate_workflow(f: Callable[[], StepList]) -> Workflow:
312
312
  steplist = (
313
313
  init
314
- >> store_process_subscription(Target.TERMINATE)
314
+ >> store_process_subscription()
315
315
  >> unsync
316
316
  >> f()
317
317
  >> (additional_steps or StepList())
@@ -348,7 +348,7 @@ def validate_workflow(description: str) -> Callable[[Callable[[], StepList]], Wo
348
348
  """
349
349
 
350
350
  def _validate_workflow(f: Callable[[], StepList]) -> Workflow:
351
- steplist = init >> store_process_subscription(Target.SYSTEM) >> unsync_unchecked >> f() >> resync >> done
351
+ steplist = init >> store_process_subscription() >> unsync_unchecked >> f() >> resync >> done
352
352
 
353
353
  return make_workflow(f, description, validate_initial_input_form_generator, Target.VALIDATE, steplist)
354
354
 
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.4
2
+ Name: orchestrator-core
3
+ Version: 4.2.0
4
+ Summary: This is the orchestrator workflow engine.
5
+ Author-email: SURF <automation-beheer@surf.nl>
6
+ Requires-Python: >=3.11,<3.14
7
+ Description-Content-Type: text/markdown
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Web Environment
11
+ Classifier: Framework :: AsyncIO
12
+ Classifier: Framework :: FastAPI
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Information Technology
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Intended Audience :: Telecommunications Industry
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python
24
+ Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
25
+ Classifier: Topic :: Internet :: WWW/HTTP
26
+ Classifier: Topic :: Internet
27
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
28
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
29
+ Classifier: Topic :: Software Development :: Libraries
30
+ Classifier: Topic :: Software Development
31
+ Classifier: Typing :: Typed
32
+ License-File: LICENSE
33
+ Requires-Dist: alembic==1.16.1
34
+ Requires-Dist: anyio>=3.7.0
35
+ Requires-Dist: click==8.*
36
+ Requires-Dist: deepmerge==2.0
37
+ Requires-Dist: deprecated>=1.2.18
38
+ Requires-Dist: fastapi~=0.115.2
39
+ Requires-Dist: fastapi-etag==0.4.0
40
+ Requires-Dist: itsdangerous>=2.2.0
41
+ Requires-Dist: jinja2==3.1.6
42
+ Requires-Dist: more-itertools~=10.7.0
43
+ Requires-Dist: nwa-stdlib~=1.9.0
44
+ Requires-Dist: oauth2-lib~=2.4.0
45
+ Requires-Dist: orjson==3.10.18
46
+ Requires-Dist: prometheus-client==0.22.1
47
+ Requires-Dist: psycopg2-binary==2.9.10
48
+ Requires-Dist: pydantic-forms>=1.4.0,<=2.1.0
49
+ Requires-Dist: pydantic-settings~=2.9.1
50
+ Requires-Dist: pydantic[email]~=2.8.2
51
+ Requires-Dist: python-dateutil==2.8.2
52
+ Requires-Dist: python-rapidjson>=1.18,<1.21
53
+ Requires-Dist: pytz==2025.2
54
+ Requires-Dist: redis==5.1.1
55
+ Requires-Dist: schedule==1.1.0
56
+ Requires-Dist: semver==3.0.4
57
+ Requires-Dist: sentry-sdk[fastapi]~=2.29.1
58
+ Requires-Dist: sqlalchemy==2.0.41
59
+ Requires-Dist: sqlalchemy-utils==0.41.2
60
+ Requires-Dist: strawberry-graphql>=0.246.2
61
+ Requires-Dist: structlog>=25.4.0
62
+ Requires-Dist: tabulate==0.9.0
63
+ Requires-Dist: typer==0.15.4
64
+ Requires-Dist: uvicorn[standard]~=0.34.0
65
+ Requires-Dist: celery~=5.5.1 ; extra == "celery"
66
+ Project-URL: Documentation, https://workfloworchestrator.org/orchestrator-core
67
+ Project-URL: Homepage, https://workfloworchestrator.org/orchestrator-core
68
+ Project-URL: Source, https://github.com/workfloworchestrator/orchestrator-core
69
+ Provides-Extra: celery
70
+
71
+ # Orchestrator-Core
72
+
73
+ [![Downloads](https://pepy.tech/badge/orchestrator-core/month)](https://pepy.tech/project/orchestrator-core)
74
+ [![codecov](https://codecov.io/gh/workfloworchestrator/orchestrator-core/branch/main/graph/badge.svg?token=5ANQFI2DHS)](https://codecov.io/gh/workfloworchestrator/orchestrator-core)
75
+ [![pypi_version](https://img.shields.io/pypi/v/orchestrator-core?color=%2334D058&label=pypi%20package)](https://pypi.org/project/orchestrator-core)
76
+ [![Supported python versions](https://img.shields.io/pypi/pyversions/orchestrator-core.svg?color=%2334D058)](https://pypi.org/project/orchestrator-core)
77
+ ![Discord](https://img.shields.io/discord/1295834294270558280?style=flat&logo=discord&label=discord&link=https%3A%2F%2Fdiscord.gg%2FKNgF6gE8)
78
+
79
+ <p style="text-align: center"><em>Production ready Orchestration Framework to manage product lifecycle and workflows. Easy to use, built on top of FastAPI and Pydantic</em></p>
80
+
81
+ ## Documentation
82
+
83
+ The documentation can be found at [workfloworchestrator.org](https://workfloworchestrator.org/orchestrator-core/).
84
+
85
+ ## Installation (quick start)
86
+
87
+ Simplified steps to install and use the orchestrator-core.
88
+ For more details, read the [Getting started](https://workfloworchestrator.org/orchestrator-core/getting-started/base/) documentation.
89
+
90
+ ### Step 1 - Install the package
91
+
92
+ Create a virtualenv and install the orchestrator-core.
93
+
94
+ ```shell
95
+ python -m venv .venv
96
+ source .venv/bin/activate
97
+ pip install orchestrator-core
98
+ ```
99
+
100
+ ### Step 2 - Setup the database
101
+
102
+ Create a postgres database:
103
+
104
+ ```shell
105
+ createuser -sP nwa
106
+ createdb orchestrator-core -O nwa # set password to 'nwa'
107
+ ```
108
+
109
+ Configure the database URI in your local environment:
110
+
111
+ ```
112
+ export DATABASE_URI=postgresql://nwa:nwa@localhost:5432/orchestrator-core
113
+ ```
114
+
115
+ ### Step 3 - Create main.py
116
+
117
+ Create a `main.py` file.
118
+
119
+ ```python
120
+ from orchestrator import OrchestratorCore
121
+ from orchestrator.cli.main import app as core_cli
122
+ from orchestrator.settings import AppSettings
123
+
124
+ app = OrchestratorCore(base_settings=AppSettings())
125
+
126
+ if __name__ == "__main__":
127
+ core_cli()
128
+ ```
129
+
130
+ ### Step 4 - Run the database migrations
131
+
132
+ Initialize the migration environment and database tables.
133
+
134
+ ```shell
135
+ python main.py db init
136
+ python main.py db upgrade heads
137
+ ```
138
+
139
+ ### Step 5 - Run the app
140
+
141
+ ```shell
142
+ export OAUTH2_ACTIVE=False
143
+ uvicorn --reload --host 127.0.0.1 --port 8080 main:app
144
+ ```
145
+
146
+ Visit the [ReDoc](http://127.0.0.1:8080/api/redoc) or [OpenAPI](http://127.0.0.1:8080/api/docs) page to view and interact with the API.
147
+
148
+ ## Contributing
149
+
150
+ We use [uv](https://docs.astral.sh/uv/getting-started/installation/) to manage dependencies.
151
+
152
+ To get started, follow these steps:
153
+
154
+ ```shell
155
+ # in your postgres database
156
+ createdb orchestrator-core-test -O nwa # set password to 'nwa'
157
+
158
+ # on your local machine
159
+ git clone https://github.com/workfloworchestrator/orchestrator-core
160
+ cd orchestrator-core
161
+ export DATABASE_URI=postgresql://nwa:nwa@localhost:5432/orchestrator-core-test
162
+ uv sync --all-extras --all-groups
163
+ uv run pytest
164
+ ```
165
+
166
+ For more details please read the [development docs](https://workfloworchestrator.org/orchestrator-core/contributing/development/).
167
+