dotflow 0.14.1.dev2__tar.gz → 0.15.0.dev2__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.
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/PKG-INFO +17 -1
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/__init__.py +1 -1
- dotflow-0.15.0.dev2/dotflow/abc/log.py +122 -0
- dotflow-0.15.0.dev2/dotflow/abc/metrics.py +32 -0
- dotflow-0.15.0.dev2/dotflow/abc/tracer.py +24 -0
- dotflow-0.15.0.dev2/dotflow/cli/commands/__init__.py +18 -0
- dotflow-0.15.0.dev2/dotflow/cli/commands/cloud.py +194 -0
- dotflow-0.15.0.dev2/dotflow/cli/commands/deploy.py +110 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/setup.py +65 -0
- dotflow-0.15.0.dev2/dotflow/cloud/__init__.py +1 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/__init__.py +15 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/base_lambda_deployer.py +96 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/constants.py +9 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/ecs_deployer.py +108 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/lambda_api_deployer.py +67 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/lambda_deployer.py +49 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/lambda_s3_deployer.py +56 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/lambda_sqs_deployer.py +43 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/services/__init__.py +0 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/services/cloudwatch.py +22 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/services/ecr.py +72 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/services/iam.py +79 -0
- dotflow-0.15.0.dev2/dotflow/cloud/aws/services/s3.py +47 -0
- dotflow-0.15.0.dev2/dotflow/cloud/core.py +89 -0
- dotflow-0.15.0.dev2/dotflow/cloud/gcp/__init__.py +5 -0
- dotflow-0.15.0.dev2/dotflow/cloud/gcp/cloudrun_deployer.py +111 -0
- dotflow-0.15.0.dev2/dotflow/cloud/gcp/constants.py +11 -0
- dotflow-0.15.0.dev2/dotflow/cloud/gcp/services/__init__.py +1 -0
- dotflow-0.15.0.dev2/dotflow/cloud/gcp/services/apis.py +37 -0
- dotflow-0.15.0.dev2/dotflow/cloud/gcp/services/artifact_registry.py +127 -0
- dotflow-0.15.0.dev2/dotflow/cloud/gcp/services/gcs.py +45 -0
- dotflow-0.15.0.dev2/dotflow/cloud/github/__init__.py +5 -0
- dotflow-0.15.0.dev2/dotflow/cloud/github/actions_deployer.py +113 -0
- dotflow-0.15.0.dev2/dotflow/cloud/github/constants.py +8 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/config.py +14 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/dotflow.py +4 -1
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/execution.py +3 -1
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/task.py +12 -3
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/workflow.py +33 -1
- dotflow-0.15.0.dev2/dotflow/logging.py +14 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/__init__.py +36 -0
- dotflow-0.15.0.dev2/dotflow/providers/log_default.py +62 -0
- dotflow-0.15.0.dev2/dotflow/providers/log_opentelemetry.py +91 -0
- dotflow-0.15.0.dev2/dotflow/providers/log_sentry.py +96 -0
- dotflow-0.15.0.dev2/dotflow/providers/metrics_default.py +25 -0
- dotflow-0.15.0.dev2/dotflow/providers/metrics_opentelemetry.py +95 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/storage_gcs.py +4 -32
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/storage_s3.py +4 -34
- dotflow-0.15.0.dev2/dotflow/providers/tracer_default.py +19 -0
- dotflow-0.15.0.dev2/dotflow/providers/tracer_opentelemetry.py +126 -0
- dotflow-0.15.0.dev2/dotflow/providers/tracer_sentry.py +126 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/settings.py +9 -2
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/pyproject.toml +7 -2
- dotflow-0.14.1.dev2/dotflow/abc/log.py +0 -16
- dotflow-0.14.1.dev2/dotflow/cli/commands/__init__.py +0 -8
- dotflow-0.14.1.dev2/dotflow/logging.py +0 -27
- dotflow-0.14.1.dev2/dotflow/providers/log_default.py +0 -25
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/LICENSE +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/README.md +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/api.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/file.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/flow.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/http.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/notify.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/scheduler.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/storage.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/abc/tcp.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/command.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/commands/init.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/commands/log.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/commands/schedule.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/commands/start.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/validators/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/cli/validators/start.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/action.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/context.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/decorators/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/decorators/time.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/exception.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/module.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/serializers/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/serializers/task.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/serializers/transport.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/serializers/workflow.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/types/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/types/execution.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/types/overlap.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/types/status.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/types/storage.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/core/types/workflow.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/main.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/api_default.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/notify_default.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/notify_discord.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/notify_telegram.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/scheduler_cron.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/scheduler_default.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/storage_default.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/providers/storage_file.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/storage.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/types.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/utils/__init__.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/utils/basic_functions.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/utils/error_handler.py +0 -0
- {dotflow-0.14.1.dev2 → dotflow-0.15.0.dev2}/dotflow/utils/tools.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dotflow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.15.0.dev2
|
|
4
4
|
Summary: 🎲 Dotflow turns an idea into flow! Lightweight Python library for execution pipelines with retry, parallel, cron and async support.
|
|
5
5
|
License: MIT License
|
|
6
6
|
|
|
@@ -42,15 +42,31 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
42
42
|
Classifier: Programming Language :: Python :: 3.12
|
|
43
43
|
Classifier: Programming Language :: Python :: 3.13
|
|
44
44
|
Provides-Extra: aws
|
|
45
|
+
Provides-Extra: deploy-aws
|
|
46
|
+
Provides-Extra: deploy-gcp
|
|
47
|
+
Provides-Extra: deploy-github
|
|
45
48
|
Provides-Extra: gcp
|
|
49
|
+
Provides-Extra: otel
|
|
46
50
|
Provides-Extra: scheduler
|
|
51
|
+
Provides-Extra: sentry
|
|
52
|
+
Requires-Dist: PyGithub ; extra == "deploy-github"
|
|
47
53
|
Requires-Dist: boto3 ; extra == "aws"
|
|
54
|
+
Requires-Dist: boto3 ; extra == "deploy-aws"
|
|
48
55
|
Requires-Dist: cookiecutter (>=2.0)
|
|
49
56
|
Requires-Dist: croniter ; extra == "scheduler"
|
|
57
|
+
Requires-Dist: google-auth ; extra == "deploy-gcp"
|
|
58
|
+
Requires-Dist: google-cloud-artifact-registry ; extra == "deploy-gcp"
|
|
59
|
+
Requires-Dist: google-cloud-build ; extra == "deploy-gcp"
|
|
60
|
+
Requires-Dist: google-cloud-run ; extra == "deploy-gcp"
|
|
61
|
+
Requires-Dist: google-cloud-service-usage ; extra == "deploy-gcp"
|
|
62
|
+
Requires-Dist: google-cloud-storage ; extra == "deploy-gcp"
|
|
50
63
|
Requires-Dist: google-cloud-storage ; extra == "gcp"
|
|
64
|
+
Requires-Dist: opentelemetry-api ; extra == "otel"
|
|
65
|
+
Requires-Dist: opentelemetry-sdk ; extra == "otel"
|
|
51
66
|
Requires-Dist: pydantic
|
|
52
67
|
Requires-Dist: requests
|
|
53
68
|
Requires-Dist: rich
|
|
69
|
+
Requires-Dist: sentry-sdk ; extra == "sentry"
|
|
54
70
|
Requires-Dist: typing-extensions
|
|
55
71
|
Project-URL: Changelog, https://dotflow-io.github.io/dotflow/nav/development/release-notes/
|
|
56
72
|
Project-URL: Documentation, https://dotflow-io.github.io/dotflow/
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Log ABC"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
LEVELS = {
|
|
13
|
+
"DEBUG": logging.DEBUG,
|
|
14
|
+
"INFO": logging.INFO,
|
|
15
|
+
"WARNING": logging.WARNING,
|
|
16
|
+
"ERROR": logging.ERROR,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LogRecordError(BaseModel):
|
|
21
|
+
exception: str = Field(default="")
|
|
22
|
+
message: str = Field(default="")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LogRecord(BaseModel):
|
|
26
|
+
timestamp: str = Field(default="")
|
|
27
|
+
level: str = Field(default="")
|
|
28
|
+
workflow_id: str = Field(default="")
|
|
29
|
+
task_id: str = Field(default="")
|
|
30
|
+
status: str = Field(default="")
|
|
31
|
+
duration: Optional[float] = Field(default=None)
|
|
32
|
+
retry_count: Optional[int] = Field(default=None)
|
|
33
|
+
error: Optional[LogRecordError] = Field(default=None)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Log(ABC):
|
|
37
|
+
"""Log
|
|
38
|
+
|
|
39
|
+
Base class for all log providers. Subclasses must set
|
|
40
|
+
``self._logger``, ``self._level`` and ``self._format``
|
|
41
|
+
in their ``__init__``.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
_logger: logging.Logger
|
|
45
|
+
_level: int = logging.INFO
|
|
46
|
+
_format: str = "simple"
|
|
47
|
+
|
|
48
|
+
def info(self, **kwargs) -> None:
|
|
49
|
+
"""Info"""
|
|
50
|
+
self._log(logging.INFO, **kwargs)
|
|
51
|
+
|
|
52
|
+
def error(self, **kwargs) -> None:
|
|
53
|
+
"""Error"""
|
|
54
|
+
self._log(logging.ERROR, **kwargs)
|
|
55
|
+
|
|
56
|
+
def warning(self, **kwargs) -> None:
|
|
57
|
+
"""Warning"""
|
|
58
|
+
self._log(logging.WARNING, **kwargs)
|
|
59
|
+
|
|
60
|
+
def debug(self, **kwargs) -> None:
|
|
61
|
+
"""Debug"""
|
|
62
|
+
self._log(logging.DEBUG, **kwargs)
|
|
63
|
+
|
|
64
|
+
_FORMATTERS = {
|
|
65
|
+
"json": "_format_json",
|
|
66
|
+
"simple": "_format_text",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def _log(self, level: int, **kwargs) -> None:
|
|
70
|
+
if level < self._level:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
formatter = getattr(
|
|
74
|
+
self, self._FORMATTERS.get(self._format, "_format_text")
|
|
75
|
+
)
|
|
76
|
+
task = kwargs.get("task")
|
|
77
|
+
|
|
78
|
+
if task:
|
|
79
|
+
message = formatter(level, task=task)
|
|
80
|
+
else:
|
|
81
|
+
message = formatter(level, **kwargs)
|
|
82
|
+
|
|
83
|
+
self._logger.log(level, message)
|
|
84
|
+
|
|
85
|
+
def _format_json(self, level: int, **kwargs) -> str:
|
|
86
|
+
task = kwargs.get("task")
|
|
87
|
+
if task:
|
|
88
|
+
record = LogRecord(
|
|
89
|
+
timestamp=datetime.now(timezone.utc).isoformat(),
|
|
90
|
+
level=logging.getLevelName(level),
|
|
91
|
+
workflow_id=str(task.workflow_id),
|
|
92
|
+
task_id=str(task.task_id),
|
|
93
|
+
status=str(task.status),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if task.duration is not None:
|
|
97
|
+
record.duration = task.duration
|
|
98
|
+
|
|
99
|
+
if task.retry_count:
|
|
100
|
+
record.retry_count = task.retry_count
|
|
101
|
+
|
|
102
|
+
if task.errors and level >= logging.ERROR:
|
|
103
|
+
last_error = task.errors[-1]
|
|
104
|
+
record.error = LogRecordError(
|
|
105
|
+
exception=str(last_error.exception),
|
|
106
|
+
message=str(last_error.message),
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return record.model_dump_json(exclude_none=True)
|
|
110
|
+
|
|
111
|
+
record = LogRecord(**{k: str(v) for k, v in kwargs.items()})
|
|
112
|
+
return record.model_dump_json(exclude_none=True)
|
|
113
|
+
|
|
114
|
+
def _format_text(self, level: int, **kwargs) -> str:
|
|
115
|
+
task = kwargs.get("task")
|
|
116
|
+
if task:
|
|
117
|
+
message = f"ID {task.workflow_id} - {task.task_id} - {task.status}"
|
|
118
|
+
if task.errors and level >= logging.ERROR:
|
|
119
|
+
message += f" \n {task.errors[-1].traceback}"
|
|
120
|
+
return message
|
|
121
|
+
|
|
122
|
+
return str(kwargs)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Metrics ABC"""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Metrics(ABC):
|
|
8
|
+
"""Metrics"""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def workflow_started(self, workflow_id: Any, **kwargs) -> None:
|
|
12
|
+
"""Called when a workflow starts."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def workflow_completed(self, workflow_id: Any, duration: float) -> None:
|
|
16
|
+
"""Called when a workflow completes successfully."""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def workflow_failed(self, workflow_id: Any, duration: float) -> None:
|
|
20
|
+
"""Called when a workflow fails."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def task_completed(self, task: Any) -> None:
|
|
24
|
+
"""Called when a task completes successfully."""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def task_failed(self, task: Any) -> None:
|
|
28
|
+
"""Called when a task fails."""
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def task_retried(self, task: Any) -> None:
|
|
32
|
+
"""Called when a task is retried."""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Tracer ABC"""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Tracer(ABC):
|
|
8
|
+
"""Tracer"""
|
|
9
|
+
|
|
10
|
+
@abstractmethod
|
|
11
|
+
def start_workflow(self, workflow_id: Any, **kwargs) -> None:
|
|
12
|
+
"""Called when a workflow starts execution."""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def end_workflow(self, workflow_id: Any, **kwargs) -> None:
|
|
16
|
+
"""Called when a workflow finishes execution."""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def start_task(self, task: Any) -> None:
|
|
20
|
+
"""Called when a task starts execution."""
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def end_task(self, task: Any) -> None:
|
|
24
|
+
"""Called when a task finishes execution."""
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Commands __init__ module."""
|
|
2
|
+
|
|
3
|
+
from dotflow.cli.commands.cloud import CloudGenerateCommand, CloudListCommand
|
|
4
|
+
from dotflow.cli.commands.deploy import DeployCommand
|
|
5
|
+
from dotflow.cli.commands.init import InitCommand
|
|
6
|
+
from dotflow.cli.commands.log import LogCommand
|
|
7
|
+
from dotflow.cli.commands.schedule import ScheduleCommand
|
|
8
|
+
from dotflow.cli.commands.start import StartCommand
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"CloudGenerateCommand",
|
|
12
|
+
"CloudListCommand",
|
|
13
|
+
"DeployCommand",
|
|
14
|
+
"InitCommand",
|
|
15
|
+
"LogCommand",
|
|
16
|
+
"ScheduleCommand",
|
|
17
|
+
"StartCommand",
|
|
18
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Command cloud module"""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from subprocess import run
|
|
7
|
+
|
|
8
|
+
from rich import print # type: ignore
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from dotflow.cli.command import Command
|
|
13
|
+
from dotflow.settings import Settings as settings
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _get_template_dir() -> Path | None:
|
|
17
|
+
import tempfile
|
|
18
|
+
|
|
19
|
+
target = Path(tempfile.gettempdir()) / "dotflow-template"
|
|
20
|
+
cloud_dir = target / settings.TEMPLATE_CLOUD_DIR
|
|
21
|
+
|
|
22
|
+
if cloud_dir.exists():
|
|
23
|
+
return cloud_dir
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
result = run(
|
|
27
|
+
[
|
|
28
|
+
"git",
|
|
29
|
+
"clone",
|
|
30
|
+
"--depth",
|
|
31
|
+
"1",
|
|
32
|
+
"--branch",
|
|
33
|
+
settings.TEMPLATE_BRANCH,
|
|
34
|
+
settings.TEMPLATE_REPO,
|
|
35
|
+
str(target),
|
|
36
|
+
],
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
)
|
|
40
|
+
if result.returncode == 0:
|
|
41
|
+
return cloud_dir
|
|
42
|
+
except Exception as err:
|
|
43
|
+
print(
|
|
44
|
+
settings.ERROR_ALERT,
|
|
45
|
+
f"Failed to clone template repository: {err}",
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _load_registry(cloud_dir: Path) -> dict | None:
|
|
52
|
+
registry_path = cloud_dir / "registry.json"
|
|
53
|
+
if not registry_path.exists():
|
|
54
|
+
print(settings.ERROR_ALERT, "Registry not found in template.")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
return json.loads(registry_path.read_text())
|
|
59
|
+
except Exception as err:
|
|
60
|
+
print(settings.ERROR_ALERT, f"Failed to read registry: {err}")
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_platforms(registry: dict) -> dict | None:
|
|
65
|
+
platforms = registry.get("platforms")
|
|
66
|
+
if not isinstance(platforms, dict):
|
|
67
|
+
print(settings.ERROR_ALERT, "Unexpected registry format.")
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
return platforms
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _read_project_name(pyproject: Path) -> str | None:
|
|
74
|
+
try:
|
|
75
|
+
in_project = False
|
|
76
|
+
for line in pyproject.read_text().splitlines():
|
|
77
|
+
stripped = line.strip()
|
|
78
|
+
if stripped.startswith("["):
|
|
79
|
+
in_project = stripped in ("[project]", "[tool.poetry]")
|
|
80
|
+
if in_project and re.match(r"^name\s*=", stripped):
|
|
81
|
+
return stripped.split("=", 1)[1].strip().strip('"').strip("'")
|
|
82
|
+
except Exception:
|
|
83
|
+
return None
|
|
84
|
+
return None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class CloudGenerateCommand(Command):
|
|
88
|
+
def setup(self):
|
|
89
|
+
platform = self.params.platform
|
|
90
|
+
|
|
91
|
+
cloud_dir = _get_template_dir()
|
|
92
|
+
if cloud_dir is None:
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
registry = _load_registry(cloud_dir)
|
|
96
|
+
if registry is None:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
platforms = _get_platforms(registry)
|
|
100
|
+
if platforms is None:
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
if platform not in platforms:
|
|
104
|
+
available = ", ".join(sorted(platforms.keys()))
|
|
105
|
+
print(
|
|
106
|
+
settings.ERROR_ALERT,
|
|
107
|
+
f"Unknown platform '{platform}'. Available: {available}",
|
|
108
|
+
)
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
platform_info = platforms[platform]
|
|
112
|
+
files = platform_info.get("files", [])
|
|
113
|
+
output = Path(self.params.output).resolve()
|
|
114
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
project_name = self.params.project
|
|
117
|
+
if not project_name:
|
|
118
|
+
pyproject = Path.cwd() / "pyproject.toml"
|
|
119
|
+
if pyproject.exists():
|
|
120
|
+
project_name = _read_project_name(pyproject)
|
|
121
|
+
if not project_name:
|
|
122
|
+
project_name = Path.cwd().name
|
|
123
|
+
|
|
124
|
+
module_name = project_name.replace("-", "_")
|
|
125
|
+
|
|
126
|
+
print(
|
|
127
|
+
settings.INFO_ALERT,
|
|
128
|
+
f"Generating {platform_info.get('name', platform)} files"
|
|
129
|
+
f" for '{project_name}'...",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
source_dir = cloud_dir / platform
|
|
133
|
+
|
|
134
|
+
for filename in files:
|
|
135
|
+
filepath = (output / filename).resolve()
|
|
136
|
+
if not str(filepath).startswith(str(output)):
|
|
137
|
+
print(
|
|
138
|
+
settings.ERROR_ALERT,
|
|
139
|
+
f" Skipped (unsafe filename): {filename}",
|
|
140
|
+
)
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
if filepath.exists():
|
|
144
|
+
print(
|
|
145
|
+
settings.WARNING_ALERT,
|
|
146
|
+
f" Skipped (already exists): {filepath}",
|
|
147
|
+
)
|
|
148
|
+
continue
|
|
149
|
+
|
|
150
|
+
source = source_dir / filename
|
|
151
|
+
if not source.exists():
|
|
152
|
+
print(
|
|
153
|
+
settings.ERROR_ALERT,
|
|
154
|
+
f" Skipped (template not found): {filename}",
|
|
155
|
+
)
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
content = source.read_text()
|
|
159
|
+
content = content.replace("{{PROJECT_NAME}}", project_name)
|
|
160
|
+
content = content.replace("{{MODULE_NAME}}", module_name)
|
|
161
|
+
|
|
162
|
+
filepath.write_text(content)
|
|
163
|
+
print(f" Created: {filepath}")
|
|
164
|
+
|
|
165
|
+
print(settings.INFO_ALERT, "Done.")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class CloudListCommand(Command):
|
|
169
|
+
def setup(self):
|
|
170
|
+
cloud_dir = _get_template_dir()
|
|
171
|
+
if cloud_dir is None:
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
registry = _load_registry(cloud_dir)
|
|
175
|
+
if registry is None:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
platforms = _get_platforms(registry)
|
|
179
|
+
if platforms is None:
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
table = Table(title="Available Platforms")
|
|
183
|
+
table.add_column("Platform", style="bold cyan")
|
|
184
|
+
table.add_column("Name", style="bold")
|
|
185
|
+
table.add_column("Description")
|
|
186
|
+
|
|
187
|
+
for key, info in platforms.items():
|
|
188
|
+
table.add_row(
|
|
189
|
+
key,
|
|
190
|
+
info.get("name", ""),
|
|
191
|
+
info.get("description", ""),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
Console().print(table)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Command deploy module."""
|
|
2
|
+
|
|
3
|
+
from rich import print # type: ignore
|
|
4
|
+
|
|
5
|
+
from dotflow.cli.command import Command
|
|
6
|
+
from dotflow.settings import Settings as settings
|
|
7
|
+
|
|
8
|
+
SAM_PLATFORMS = {
|
|
9
|
+
"ecs-scheduled",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
DEFAULT_REGIONS = {
|
|
13
|
+
"lambda": "us-east-1",
|
|
14
|
+
"lambda-scheduled": "us-east-1",
|
|
15
|
+
"lambda-s3-trigger": "us-east-1",
|
|
16
|
+
"lambda-sqs-trigger": "us-east-1",
|
|
17
|
+
"lambda-api-trigger": "us-east-1",
|
|
18
|
+
"ecs": "us-east-1",
|
|
19
|
+
"cloud-run": "us-central1",
|
|
20
|
+
"cloud-run-scheduled": "us-central1",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class DeployCommand(Command):
|
|
25
|
+
def setup(self):
|
|
26
|
+
platform = self.params.platform
|
|
27
|
+
name = self.params.project
|
|
28
|
+
region = self.params.region or DEFAULT_REGIONS.get(
|
|
29
|
+
platform, "us-east-1"
|
|
30
|
+
)
|
|
31
|
+
schedule = getattr(self.params, "schedule", None)
|
|
32
|
+
|
|
33
|
+
if platform in SAM_PLATFORMS:
|
|
34
|
+
print(
|
|
35
|
+
settings.INFO_ALERT,
|
|
36
|
+
f"'{platform}' requires SAM for full trigger setup.",
|
|
37
|
+
)
|
|
38
|
+
print(" Run: sam build && sam deploy")
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
deployers = {
|
|
42
|
+
"lambda": self._deploy_lambda,
|
|
43
|
+
"lambda-scheduled": self._deploy_lambda,
|
|
44
|
+
"lambda-s3-trigger": self._deploy_lambda_s3,
|
|
45
|
+
"lambda-sqs-trigger": self._deploy_lambda_sqs,
|
|
46
|
+
"lambda-api-trigger": self._deploy_lambda_api,
|
|
47
|
+
"ecs": self._deploy_ecs,
|
|
48
|
+
"cloud-run": self._deploy_cloud_run,
|
|
49
|
+
"cloud-run-scheduled": self._deploy_cloud_run,
|
|
50
|
+
"github-actions": self._deploy_github_actions,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
handler = deployers.get(platform)
|
|
54
|
+
if not handler:
|
|
55
|
+
print(
|
|
56
|
+
settings.ERROR_ALERT,
|
|
57
|
+
f"Deploy not supported for '{platform}'.",
|
|
58
|
+
)
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
handler(name=name, region=region, schedule=schedule)
|
|
62
|
+
|
|
63
|
+
def _deploy_lambda(self, name: str, region: str, schedule: str = None):
|
|
64
|
+
"""Deploy to AWS Lambda."""
|
|
65
|
+
from dotflow.cloud.aws import LambdaDeployer
|
|
66
|
+
|
|
67
|
+
deployer = LambdaDeployer(region=region)
|
|
68
|
+
deployer.deploy(name, schedule=schedule)
|
|
69
|
+
|
|
70
|
+
def _deploy_lambda_api(self, name: str, region: str, **kwargs):
|
|
71
|
+
"""Deploy to AWS Lambda + API Gateway."""
|
|
72
|
+
from dotflow.cloud.aws import LambdaApiDeployer
|
|
73
|
+
|
|
74
|
+
deployer = LambdaApiDeployer(region=region)
|
|
75
|
+
deployer.deploy(name)
|
|
76
|
+
|
|
77
|
+
def _deploy_lambda_s3(self, name: str, region: str, **kwargs):
|
|
78
|
+
"""Deploy to AWS Lambda + S3 Trigger."""
|
|
79
|
+
from dotflow.cloud.aws import LambdaS3Deployer
|
|
80
|
+
|
|
81
|
+
deployer = LambdaS3Deployer(region=region)
|
|
82
|
+
deployer.deploy(name)
|
|
83
|
+
|
|
84
|
+
def _deploy_lambda_sqs(self, name: str, region: str, **kwargs):
|
|
85
|
+
"""Deploy to AWS Lambda + SQS Trigger."""
|
|
86
|
+
from dotflow.cloud.aws import LambdaSQSDeployer
|
|
87
|
+
|
|
88
|
+
deployer = LambdaSQSDeployer(region=region)
|
|
89
|
+
deployer.deploy(name)
|
|
90
|
+
|
|
91
|
+
def _deploy_ecs(self, name: str, region: str, **kwargs):
|
|
92
|
+
"""Deploy to AWS ECS Fargate."""
|
|
93
|
+
from dotflow.cloud.aws import ECSDeployer
|
|
94
|
+
|
|
95
|
+
deployer = ECSDeployer(region=region)
|
|
96
|
+
deployer.deploy(name)
|
|
97
|
+
|
|
98
|
+
def _deploy_cloud_run(self, name: str, region: str, **kwargs):
|
|
99
|
+
"""Deploy to Google Cloud Run."""
|
|
100
|
+
from dotflow.cloud.gcp import CloudRunDeployer
|
|
101
|
+
|
|
102
|
+
deployer = CloudRunDeployer(region=region)
|
|
103
|
+
deployer.deploy(name)
|
|
104
|
+
|
|
105
|
+
def _deploy_github_actions(self, name: str, **kwargs):
|
|
106
|
+
"""Deploy to GitHub Actions."""
|
|
107
|
+
from dotflow.cloud.github import ActionsDeployer
|
|
108
|
+
|
|
109
|
+
deployer = ActionsDeployer()
|
|
110
|
+
deployer.deploy(name)
|
|
@@ -4,6 +4,9 @@ from rich import print # type: ignore
|
|
|
4
4
|
|
|
5
5
|
from dotflow import __description__, __version__
|
|
6
6
|
from dotflow.cli.commands import (
|
|
7
|
+
CloudGenerateCommand,
|
|
8
|
+
CloudListCommand,
|
|
9
|
+
DeployCommand,
|
|
7
10
|
InitCommand,
|
|
8
11
|
LogCommand,
|
|
9
12
|
ScheduleCommand,
|
|
@@ -39,6 +42,8 @@ class Command:
|
|
|
39
42
|
self.setup_logs()
|
|
40
43
|
self.setup_start()
|
|
41
44
|
self.setup_schedule()
|
|
45
|
+
self.setup_cloud()
|
|
46
|
+
self.setup_deploy()
|
|
42
47
|
self.command()
|
|
43
48
|
|
|
44
49
|
def setup_init(self):
|
|
@@ -145,6 +150,66 @@ class Command:
|
|
|
145
150
|
|
|
146
151
|
self.cmd_schedule.set_defaults(exec=ScheduleCommand)
|
|
147
152
|
|
|
153
|
+
def setup_cloud(self):
|
|
154
|
+
self.cmd_cloud = self.subparsers.add_parser(
|
|
155
|
+
"cloud",
|
|
156
|
+
help="Generate cloud infrastructure files for a target platform",
|
|
157
|
+
)
|
|
158
|
+
cloud_subparsers = self.cmd_cloud.add_subparsers()
|
|
159
|
+
|
|
160
|
+
cmd_generate = cloud_subparsers.add_parser(
|
|
161
|
+
"generate", help="Generate infrastructure files"
|
|
162
|
+
)
|
|
163
|
+
cmd_generate.add_argument(
|
|
164
|
+
"--platform",
|
|
165
|
+
required=True,
|
|
166
|
+
help="Target platform (e.g. docker, lambda, cloud-run, ecs, kubernetes)",
|
|
167
|
+
)
|
|
168
|
+
cmd_generate.add_argument(
|
|
169
|
+
"--project",
|
|
170
|
+
default=None,
|
|
171
|
+
help="Project name (defaults to current directory name)",
|
|
172
|
+
)
|
|
173
|
+
cmd_generate.add_argument(
|
|
174
|
+
"--output",
|
|
175
|
+
default=".",
|
|
176
|
+
help="Output directory (defaults to current directory)",
|
|
177
|
+
)
|
|
178
|
+
cmd_generate.set_defaults(exec=CloudGenerateCommand)
|
|
179
|
+
|
|
180
|
+
cmd_list = cloud_subparsers.add_parser(
|
|
181
|
+
"list", help="List available cloud platforms"
|
|
182
|
+
)
|
|
183
|
+
cmd_list.set_defaults(exec=CloudListCommand)
|
|
184
|
+
|
|
185
|
+
def setup_deploy(self):
|
|
186
|
+
self.cmd_deploy = self.subparsers.add_parser(
|
|
187
|
+
"deploy",
|
|
188
|
+
help="Deploy pipeline to a cloud platform",
|
|
189
|
+
)
|
|
190
|
+
self.cmd_deploy.add_argument(
|
|
191
|
+
"--platform",
|
|
192
|
+
required=True,
|
|
193
|
+
help="Target platform (e.g. lambda, ecs)",
|
|
194
|
+
)
|
|
195
|
+
self.cmd_deploy.add_argument(
|
|
196
|
+
"--project",
|
|
197
|
+
default=None,
|
|
198
|
+
required=True,
|
|
199
|
+
help="Project name",
|
|
200
|
+
)
|
|
201
|
+
self.cmd_deploy.add_argument(
|
|
202
|
+
"--region",
|
|
203
|
+
default=None,
|
|
204
|
+
help="Cloud region (e.g. us-east-1 for AWS, us-central1 for GCP)",
|
|
205
|
+
)
|
|
206
|
+
self.cmd_deploy.add_argument(
|
|
207
|
+
"--schedule",
|
|
208
|
+
default=None,
|
|
209
|
+
help="Schedule expression (e.g. 'rate(6 hours)')",
|
|
210
|
+
)
|
|
211
|
+
self.cmd_deploy.set_defaults(exec=DeployCommand)
|
|
212
|
+
|
|
148
213
|
def setup_logs(self):
|
|
149
214
|
self.cmd_logs = self.subparsers.add_parser("logs", help="Logs")
|
|
150
215
|
self.cmd_logs = self.cmd_logs.add_argument_group(
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Deploy module."""
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""AWS deploy module."""
|
|
2
|
+
|
|
3
|
+
from dotflow.cloud.aws.ecs_deployer import ECSDeployer
|
|
4
|
+
from dotflow.cloud.aws.lambda_api_deployer import LambdaApiDeployer
|
|
5
|
+
from dotflow.cloud.aws.lambda_deployer import LambdaDeployer
|
|
6
|
+
from dotflow.cloud.aws.lambda_s3_deployer import LambdaS3Deployer
|
|
7
|
+
from dotflow.cloud.aws.lambda_sqs_deployer import LambdaSQSDeployer
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"LambdaDeployer",
|
|
11
|
+
"LambdaApiDeployer",
|
|
12
|
+
"LambdaS3Deployer",
|
|
13
|
+
"LambdaSQSDeployer",
|
|
14
|
+
"ECSDeployer",
|
|
15
|
+
]
|