Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ dependencies = [
"pydantic>=2.10,<3",
]

[project.scripts]
ofw = "ofw.cli:main"

[project.optional-dependencies]
dev = [
"mypy>=1.10,<2",
Expand Down
70 changes: 70 additions & 0 deletions src/ofw/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Public OpenFlyWheel harness API."""

from datetime import datetime
from pathlib import Path
from threading import Event

Expand Down Expand Up @@ -125,6 +126,36 @@
read_trace_observations,
search_observation_content,
)
from ofw.promotion import (
ApprovalDecision,
ApprovalRecord,
ApproverId,
DeploymentAdapter,
DeploymentReference,
DeploymentRequest,
GitHubCliPublisher,
GitRemote,
PromotionBranch,
PromotionError,
PromotionErrorCode,
PromotionEvent,
PromotionEventKind,
PromotionJobHandler,
PromotionMarker,
PromotionMode,
PromotionPolicy,
PromotionRequest,
PromotionRequestResolver,
PromotionResult,
PullRequestDraft,
PullRequestId,
PullRequestPublisher,
PullRequestReference,
RollbackPlan,
)
from ofw.promotion import (
PromotionService as GitPromotionService,
)
from ofw.runtime import (
CanaryCase,
CaseId,
Expand Down Expand Up @@ -192,6 +223,7 @@

AutomationPolicy = SchedulerAutomationPolicy
LocalScheduler = SQLiteScheduler
PromotionService = GitPromotionService


class _OfwNamespace:
Expand Down Expand Up @@ -226,6 +258,8 @@ class _OfwNamespace:
Money = Money
QuietHours = QuietHours
StageBudgets = StageBudgets
PromotionPolicy = PromotionPolicy
PromotionService = GitPromotionService

def editable(self, path: Path) -> EditableFile:
return editable(path)
Expand Down Expand Up @@ -261,6 +295,16 @@ def serve(
finally:
scheduler.close()

def promote(
self,
request: PromotionRequest,
*,
now: datetime,
pull_requests: PullRequestPublisher | None = None,
deployments: DeploymentAdapter | None = None,
) -> PromotionResult:
return GitPromotionService(pull_requests, deployments).run(request, now)

def search_observation_content(
self,
collection: CollectionResult,
Expand Down Expand Up @@ -295,6 +339,9 @@ def read_snapshot_content(

__all__ = [
"AssetAccess",
"ApprovalDecision",
"ApprovalRecord",
"ApproverId",
"AutomationPolicy",
"Baseline",
"BenchmarkError",
Expand Down Expand Up @@ -337,6 +384,9 @@ def read_snapshot_content(
"DiagnosisErrorCode",
"Dependency",
"DependencyMode",
"DeploymentAdapter",
"DeploymentReference",
"DeploymentRequest",
"EditableFile",
"EvidenceAnchor",
"EvidenceAnchorKind",
Expand All @@ -354,6 +404,8 @@ def read_snapshot_content(
"FitPolicy",
"FitResult",
"GitCommit",
"GitHubCliPublisher",
"GitRemote",
"Harness",
"HarnessAsset",
"HarnessComponent",
Expand Down Expand Up @@ -397,13 +449,31 @@ def read_snapshot_content(
"ProcessCommand",
"ProcessLimits",
"PrivacyTransform",
"PromotionBranch",
"PromotionError",
"PromotionErrorCode",
"PromotionEvent",
"PromotionEventKind",
"PromotionJobHandler",
"PromotionMarker",
"PromotionMode",
"PromotionPolicy",
"PromotionRequest",
"PromotionRequestResolver",
"PromotionResult",
"PromotionService",
"PullRequestDraft",
"PullRequestId",
"PullRequestPublisher",
"PullRequestReference",
"PythonEntrypoint",
"PythonLoop",
"PythonDiagnoser",
"PythonVerifier",
"QuietHours",
"ReconcileReport",
"ResultId",
"RollbackPlan",
"RunErrorCode",
"RunResult",
"RunStatus",
Expand Down
62 changes: 62 additions & 0 deletions src/ofw/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Dependency-free operator CLI over the scheduler application service."""

from __future__ import annotations

import sys
from datetime import UTC, datetime
from enum import StrEnum
from pathlib import Path

from ofw.scheduler import (
JobId,
LocalScheduler,
ScheduledJob,
SchedulerError,
SchedulerErrorCode,
read_automation_policy,
)


class CampaignCommand(StrEnum):
STATUS = "status"
CANCEL = "cancel"
RESUME = "resume"


def run_campaign_command(arguments: tuple[str, ...], now: datetime) -> ScheduledJob:
if len(arguments) != 5 or arguments[0] != "campaign":
raise SchedulerError(
SchedulerErrorCode.INVALID_TRANSITION,
"usage: ofw campaign status|cancel|resume STORE POLICY JOB_ID",
)
try:
command = CampaignCommand(arguments[1])
except ValueError as error:
raise SchedulerError(
SchedulerErrorCode.INVALID_TRANSITION,
arguments[1],
) from error
store_path = Path(arguments[2])
policy = read_automation_policy(Path(arguments[3]))
job_id = JobId(arguments[4])
scheduler = LocalScheduler(store_path, policy)
try:
match command:
case CampaignCommand.STATUS:
return scheduler.job(job_id)
case CampaignCommand.CANCEL:
return scheduler.cancel(job_id, now)
case CampaignCommand.RESUME:
return scheduler.resume(job_id, now)
finally:
scheduler.close()


def main() -> int:
try:
result = run_campaign_command(tuple(sys.argv[1:]), datetime.now(UTC))
except SchedulerError as error:
print(str(error), file=sys.stderr)
return 1
print(result.to_json())
return 0
Loading