dotflow 0.15.0.dev2__py3-none-any.whl → 0.15.0.dev4__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 (69) hide show
  1. dotflow/__init__.py +1 -1
  2. dotflow/abc/flow.py +19 -0
  3. dotflow/abc/server.py +30 -0
  4. dotflow/cli/commands/cloud.py +9 -1
  5. dotflow/cli/commands/deploy.py +128 -67
  6. dotflow/cli/commands/init.py +1 -3
  7. dotflow/cli/commands/log.py +2 -1
  8. dotflow/cli/commands/storage.py +55 -0
  9. dotflow/cli/setup.py +5 -2
  10. dotflow/cloud/alibaba/__init__.py +11 -0
  11. dotflow/cloud/alibaba/constants.py +22 -0
  12. dotflow/cloud/alibaba/deployers/__init__.py +13 -0
  13. dotflow/cloud/alibaba/deployers/fc.py +134 -0
  14. dotflow/cloud/alibaba/deployers/fc_scheduled.py +78 -0
  15. dotflow/cloud/alibaba/services/__init__.py +1 -0
  16. dotflow/cloud/alibaba/services/cr.py +99 -0
  17. dotflow/cloud/aws/__init__.py +11 -7
  18. dotflow/cloud/aws/constants.py +15 -1
  19. dotflow/cloud/aws/deployers/__init__.py +19 -0
  20. dotflow/cloud/aws/{base_lambda_deployer.py → deployers/base_lambda.py} +6 -9
  21. dotflow/cloud/aws/{ecs_deployer.py → deployers/ecs.py} +11 -3
  22. dotflow/cloud/aws/deployers/ecs_scheduled.py +154 -0
  23. dotflow/cloud/aws/{lambda_deployer.py → deployers/lambda_.py} +9 -3
  24. dotflow/cloud/aws/{lambda_api_deployer.py → deployers/lambda_api.py} +6 -3
  25. dotflow/cloud/aws/{lambda_s3_deployer.py → deployers/lambda_s3.py} +10 -4
  26. dotflow/cloud/aws/{lambda_sqs_deployer.py → deployers/lambda_sqs.py} +19 -4
  27. dotflow/cloud/aws/schedule.py +66 -0
  28. dotflow/cloud/aws/services/cloudwatch.py +4 -1
  29. dotflow/cloud/aws/services/ecr.py +8 -5
  30. dotflow/cloud/aws/services/iam.py +4 -1
  31. dotflow/cloud/core.py +8 -2
  32. dotflow/cloud/gcp/__init__.py +1 -1
  33. dotflow/cloud/gcp/constants.py +9 -0
  34. dotflow/cloud/gcp/deployers/__init__.py +5 -0
  35. dotflow/cloud/gcp/{cloudrun_deployer.py → deployers/cloudrun.py} +12 -10
  36. dotflow/cloud/gcp/services/apis.py +11 -3
  37. dotflow/cloud/gcp/services/artifact_registry.py +16 -5
  38. dotflow/cloud/github/__init__.py +1 -1
  39. dotflow/cloud/github/deployers/__init__.py +5 -0
  40. dotflow/cloud/github/{actions_deployer.py → deployers/actions.py} +24 -26
  41. dotflow/core/action.py +40 -79
  42. dotflow/core/config.py +5 -5
  43. dotflow/core/context.py +17 -4
  44. dotflow/core/dotflow.py +1 -1
  45. dotflow/core/engine.py +280 -0
  46. dotflow/core/module.py +11 -17
  47. dotflow/core/serializers/task.py +51 -35
  48. dotflow/core/serializers/transport.py +2 -2
  49. dotflow/core/serializers/workflow.py +1 -10
  50. dotflow/core/task.py +10 -2
  51. dotflow/core/types/workflow.py +1 -0
  52. dotflow/core/workflow.py +69 -67
  53. dotflow/providers/__init__.py +2 -0
  54. dotflow/providers/server_default.py +112 -0
  55. dotflow/providers/storage_default.py +8 -6
  56. dotflow/providers/storage_file.py +11 -8
  57. dotflow/settings.py +3 -1
  58. dotflow/utils/error_handler.py +3 -12
  59. dotflow/utils/tools.py +0 -10
  60. {dotflow-0.15.0.dev2.dist-info → dotflow-0.15.0.dev4.dist-info}/METADATA +243 -211
  61. dotflow-0.15.0.dev4.dist-info/RECORD +117 -0
  62. dotflow/abc/api.py +0 -24
  63. dotflow/core/execution.py +0 -176
  64. dotflow/providers/api_default.py +0 -158
  65. dotflow/storage.py +0 -22
  66. dotflow-0.15.0.dev2.dist-info/RECORD +0 -105
  67. {dotflow-0.15.0.dev2.dist-info → dotflow-0.15.0.dev4.dist-info}/WHEEL +0 -0
  68. {dotflow-0.15.0.dev2.dist-info → dotflow-0.15.0.dev4.dist-info}/entry_points.txt +0 -0
  69. {dotflow-0.15.0.dev2.dist-info → dotflow-0.15.0.dev4.dist-info}/licenses/LICENSE +0 -0
dotflow/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Dotflow __init__ module."""
2
2
 
3
- __version__ = "0.15.0.dev2"
3
+ __version__ = "0.15.0.dev4"
4
4
  __description__ = "🎲 Dotflow turns an idea into flow!"
5
5
 
6
6
  from .core.action import Action as action
dotflow/abc/flow.py CHANGED
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
4
4
  from uuid import UUID
5
5
 
6
6
  from dotflow.core.task import Task
7
+ from dotflow.core.types import TypeStatus
7
8
 
8
9
 
9
10
  class Flow(ABC):
@@ -40,3 +41,21 @@ class Flow(ABC):
40
41
  @abstractmethod
41
42
  def run(self) -> None:
42
43
  return None
44
+
45
+ def _try_restore_checkpoint(self, task: Task):
46
+ """Attempts to restore a checkpoint. Returns the Context if found, None otherwise."""
47
+ if not self.resume:
48
+ return None
49
+
50
+ context = task.config.storage.get(
51
+ key=task.config.storage.key(task=task)
52
+ )
53
+
54
+ if context.storage is None:
55
+ return None
56
+
57
+ task.status = TypeStatus.COMPLETED
58
+ task.current_context = context
59
+ self._flow_callback(task=task)
60
+
61
+ return context
dotflow/abc/server.py ADDED
@@ -0,0 +1,30 @@
1
+ """Abstract base for remote server communication."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any
5
+
6
+
7
+ class Server(ABC):
8
+ """Server ABC provider for sending workflow and task
9
+ data to a remote API.
10
+
11
+ Implementations should handle HTTP communication with
12
+ a server like dotflow-api, sending execution data
13
+ (status, duration, errors, context) in real time.
14
+ """
15
+
16
+ @abstractmethod
17
+ def create_workflow(self, workflow: Any) -> None:
18
+ """Register a new workflow on the remote server."""
19
+
20
+ @abstractmethod
21
+ def update_workflow(self, workflow: Any, status: str = "") -> None:
22
+ """Update workflow status on the remote server."""
23
+
24
+ @abstractmethod
25
+ def create_task(self, task: Any) -> None:
26
+ """Register a new task on the remote server."""
27
+
28
+ @abstractmethod
29
+ def update_task(self, task: Any) -> None:
30
+ """Update task data on the remote server."""
@@ -20,6 +20,11 @@ def _get_template_dir() -> Path | None:
20
20
  cloud_dir = target / settings.TEMPLATE_CLOUD_DIR
21
21
 
22
22
  if cloud_dir.exists():
23
+ run(
24
+ ["git", "-C", str(target), "pull", "--ff-only"],
25
+ capture_output=True,
26
+ text=True,
27
+ )
23
28
  return cloud_dir
24
29
 
25
30
  try:
@@ -121,6 +126,7 @@ class CloudGenerateCommand(Command):
121
126
  if not project_name:
122
127
  project_name = Path.cwd().name
123
128
 
129
+ project_name = project_name.replace("_", "-").lower()
124
130
  module_name = project_name.replace("-", "_")
125
131
 
126
132
  print(
@@ -133,7 +139,9 @@ class CloudGenerateCommand(Command):
133
139
 
134
140
  for filename in files:
135
141
  filepath = (output / filename).resolve()
136
- if not str(filepath).startswith(str(output)):
142
+ try:
143
+ filepath.relative_to(output)
144
+ except ValueError:
137
145
  print(
138
146
  settings.ERROR_ALERT,
139
147
  f" Skipped (unsafe filename): {filename}",
@@ -1,110 +1,171 @@
1
1
  """Command deploy module."""
2
2
 
3
3
  from rich import print # type: ignore
4
+ from rich.prompt import Prompt
4
5
 
5
6
  from dotflow.cli.command import Command
7
+ from dotflow.cloud.alibaba.constants import (
8
+ DEFAULT_REGION as ALI_DEFAULT_REGION,
9
+ )
10
+ from dotflow.cloud.alibaba.constants import PLATFORMS as ALI_PLATFORMS
11
+ from dotflow.cloud.alibaba.constants import (
12
+ SCHEDULED_PLATFORMS as ALI_SCHEDULED,
13
+ )
14
+ from dotflow.cloud.aws.constants import DEFAULT_REGION as AWS_DEFAULT_REGION
15
+ from dotflow.cloud.aws.constants import PLATFORMS as AWS_PLATFORMS
16
+ from dotflow.cloud.aws.constants import SCHEDULED_PLATFORMS as AWS_SCHEDULED
17
+ from dotflow.cloud.gcp.constants import DEFAULT_REGION as GCP_DEFAULT_REGION
18
+ from dotflow.cloud.gcp.constants import PLATFORMS as GCP_PLATFORMS
19
+ from dotflow.cloud.gcp.constants import SCHEDULED_PLATFORMS as GCP_SCHEDULED
6
20
  from dotflow.settings import Settings as settings
7
21
 
8
- SAM_PLATFORMS = {
9
- "ecs-scheduled",
10
- }
11
-
12
22
  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",
23
+ **dict.fromkeys(AWS_PLATFORMS, AWS_DEFAULT_REGION),
24
+ **dict.fromkeys(GCP_PLATFORMS, GCP_DEFAULT_REGION),
25
+ **dict.fromkeys(ALI_PLATFORMS, ALI_DEFAULT_REGION),
21
26
  }
22
27
 
28
+ SCHEDULED_PLATFORMS = AWS_SCHEDULED | GCP_SCHEDULED | ALI_SCHEDULED
29
+
30
+
31
+ class ScheduleResolver:
32
+ """Resolves a schedule expression.
33
+
34
+ From CLI args, template, or user input.
35
+ """
36
+
37
+ @classmethod
38
+ def _get_provider(cls, platform: str) -> type | None:
39
+ if platform in AWS_PLATFORMS:
40
+ from dotflow.cloud.aws.schedule import AWSSchedule
41
+
42
+ return AWSSchedule
43
+
44
+ return None
45
+
46
+ @classmethod
47
+ def resolve(cls, schedule: str | None, platform: str) -> str | None:
48
+ if platform not in SCHEDULED_PLATFORMS:
49
+ return schedule
50
+
51
+ provider = cls._get_provider(platform)
52
+
53
+ if schedule:
54
+ return provider.convert(schedule) if provider else schedule
55
+
56
+ raw = cls._ask_schedule(provider)
57
+
58
+ if not raw:
59
+ raise SystemExit(
60
+ f"{settings.ERROR_ALERT} --schedule (cron) is required for {platform}"
61
+ )
62
+
63
+ return provider.convert(raw) if provider else raw
64
+
65
+ @classmethod
66
+ def _ask_schedule(cls, provider: type | None) -> str | None:
67
+ template_schedule = None
68
+ if provider:
69
+ template_schedule = provider.read_from_template()
70
+
71
+ choices = {}
72
+ if template_schedule:
73
+ choices["1"] = f"Use from template.yaml: {template_schedule}"
74
+ choices["2"] = "Enter cron expression"
75
+
76
+ print(settings.QUESTION_ALERT, "Cron schedule is required:")
77
+ for key, label in choices.items():
78
+ print(f" [bold cyan]{key}[/bold cyan] - {label}")
79
+
80
+ choice = Prompt.ask(" Select", choices=list(choices.keys()))
81
+
82
+ if choice == "1" and template_schedule:
83
+ print(settings.INFO_ALERT, f"Using cron: {template_schedule}")
84
+ return template_schedule
85
+
86
+ print(" Format: [bold]min hour day month weekday[/bold]")
87
+ return Prompt.ask(" Cron (e.g. */5 * * * *)") or None
88
+
23
89
 
24
90
  class DeployCommand(Command):
25
91
  def setup(self):
26
92
  platform = self.params.platform
27
93
  name = self.params.project
28
- region = self.params.region or DEFAULT_REGIONS.get(
29
- platform, "us-east-1"
94
+ region = self.params.region or DEFAULT_REGIONS.get(platform)
95
+ schedule = ScheduleResolver.resolve(
96
+ getattr(self.params, "schedule", None), platform
30
97
  )
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
98
 
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)
99
+ method_name = f"_deploy_{platform.replace('-', '_')}"
100
+ handler = getattr(self, method_name, None)
54
101
  if not handler:
55
102
  print(
56
- settings.ERROR_ALERT,
57
- f"Deploy not supported for '{platform}'.",
103
+ settings.ERROR_ALERT, f"Deploy not supported for '{platform}'."
58
104
  )
59
105
  return
60
106
 
61
107
  handler(name=name, region=region, schedule=schedule)
62
108
 
63
- def _deploy_lambda(self, name: str, region: str, schedule: str = None):
64
- """Deploy to AWS Lambda."""
109
+ def _deploy_lambda(self, name, region, schedule=None):
65
110
  from dotflow.cloud.aws import LambdaDeployer
66
111
 
67
- deployer = LambdaDeployer(region=region)
68
- deployer.deploy(name, schedule=schedule)
112
+ LambdaDeployer(region=region).deploy(name, schedule=schedule)
69
113
 
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
114
+ def _deploy_lambda_scheduled(self, name, region, schedule=None):
115
+ from dotflow.cloud.aws import LambdaDeployer
73
116
 
74
- deployer = LambdaApiDeployer(region=region)
75
- deployer.deploy(name)
117
+ LambdaDeployer(region=region).deploy(name, schedule=schedule)
76
118
 
77
- def _deploy_lambda_s3(self, name: str, region: str, **kwargs):
78
- """Deploy to AWS Lambda + S3 Trigger."""
119
+ def _deploy_lambda_s3_trigger(self, name, region, **kwargs):
79
120
  from dotflow.cloud.aws import LambdaS3Deployer
80
121
 
81
- deployer = LambdaS3Deployer(region=region)
82
- deployer.deploy(name)
122
+ LambdaS3Deployer(region=region).deploy(name)
83
123
 
84
- def _deploy_lambda_sqs(self, name: str, region: str, **kwargs):
85
- """Deploy to AWS Lambda + SQS Trigger."""
124
+ def _deploy_lambda_sqs_trigger(self, name, region, **kwargs):
86
125
  from dotflow.cloud.aws import LambdaSQSDeployer
87
126
 
88
- deployer = LambdaSQSDeployer(region=region)
89
- deployer.deploy(name)
127
+ LambdaSQSDeployer(region=region).deploy(name)
128
+
129
+ def _deploy_lambda_api_trigger(self, name, region, **kwargs):
130
+ from dotflow.cloud.aws import LambdaApiDeployer
90
131
 
91
- def _deploy_ecs(self, name: str, region: str, **kwargs):
92
- """Deploy to AWS ECS Fargate."""
132
+ LambdaApiDeployer(region=region).deploy(name)
133
+
134
+ def _deploy_ecs(self, name, region, **kwargs):
93
135
  from dotflow.cloud.aws import ECSDeployer
94
136
 
95
- deployer = ECSDeployer(region=region)
96
- deployer.deploy(name)
137
+ ECSDeployer(region=region).deploy(name)
138
+
139
+ def _deploy_ecs_scheduled(self, name, region, schedule=None):
140
+ from dotflow.cloud.aws import ECSScheduledDeployer
97
141
 
98
- def _deploy_cloud_run(self, name: str, region: str, **kwargs):
99
- """Deploy to Google Cloud Run."""
142
+ ECSScheduledDeployer(region=region).deploy(name, schedule=schedule)
143
+
144
+ def _deploy_cloud_run(self, name, region, **kwargs):
100
145
  from dotflow.cloud.gcp import CloudRunDeployer
101
146
 
102
- deployer = CloudRunDeployer(region=region)
103
- deployer.deploy(name)
147
+ CloudRunDeployer(region=region).deploy(name)
148
+
149
+ def _deploy_cloud_run_scheduled(self, name, region, **kwargs):
150
+ from dotflow.cloud.gcp import CloudRunDeployer
104
151
 
105
- def _deploy_github_actions(self, name: str, **kwargs):
106
- """Deploy to GitHub Actions."""
152
+ CloudRunDeployer(region=region).deploy(name)
153
+
154
+ def _deploy_github_actions(self, name, **kwargs):
107
155
  from dotflow.cloud.github import ActionsDeployer
108
156
 
109
- deployer = ActionsDeployer()
110
- deployer.deploy(name)
157
+ ActionsDeployer().deploy(name)
158
+
159
+ def _deploy_alibaba_fc(self, name, region, **kwargs):
160
+ from dotflow.cloud.alibaba import AliyunFCDeployer
161
+
162
+ AliyunFCDeployer(region=region).deploy(name)
163
+
164
+ def _deploy_alibaba_fc_scheduled(self, name, region, schedule=None):
165
+ from dotflow.cloud.alibaba import (
166
+ AliyunFCScheduledDeployer,
167
+ )
168
+
169
+ AliyunFCScheduledDeployer(region=region).deploy(
170
+ name, schedule=schedule
171
+ )
@@ -6,12 +6,10 @@ from rich import print # type: ignore
6
6
  from dotflow.cli.command import Command
7
7
  from dotflow.settings import Settings as settings
8
8
 
9
- TEMPLATE_REPO = "https://github.com/dotflow-io/template"
10
-
11
9
 
12
10
  class InitCommand(Command):
13
11
  def setup(self):
14
- cookiecutter(TEMPLATE_REPO)
12
+ cookiecutter(settings.TEMPLATE_REPO)
15
13
 
16
14
  print(
17
15
  settings.INFO_ALERT,
@@ -14,7 +14,8 @@ class LogCommand(Command):
14
14
 
15
15
  print(
16
16
  settings.INFO_ALERT,
17
- f"To access all logs, open the file ({settings.LOG_PATH.resolve()}).",
17
+ "To access all logs, open the file"
18
+ f" ({settings.LOG_PATH.resolve()}).",
18
19
  )
19
20
  else:
20
21
  print(settings.WARNING_ALERT, "Log file not found.")
@@ -0,0 +1,55 @@
1
+ """Storage resolver for CLI commands"""
2
+
3
+ from dotflow.providers import (
4
+ StorageDefault,
5
+ StorageFile,
6
+ StorageGCS,
7
+ StorageS3,
8
+ )
9
+ from dotflow.settings import Settings as settings
10
+
11
+
12
+ class StorageResolver:
13
+ """Resolves a storage provider instance from CLI params."""
14
+
15
+ def __init__(self, params):
16
+ self.params = params
17
+
18
+ def resolve(self):
19
+ """Returns a storage instance or None if no --storage was specified."""
20
+ if not self.params.storage:
21
+ return None
22
+
23
+ resolvers = {
24
+ "default": self._resolve_local,
25
+ "file": self._resolve_local,
26
+ "s3": self._resolve_s3,
27
+ "gcs": self._resolve_gcs,
28
+ }
29
+
30
+ resolver = resolvers.get(self.params.storage)
31
+ if resolver is None:
32
+ return None
33
+
34
+ return resolver()
35
+
36
+ def _resolve_local(self):
37
+ storage_cls = {"default": StorageDefault, "file": StorageFile}
38
+ return storage_cls[self.params.storage](path=self.params.path)
39
+
40
+ def _resolve_s3(self):
41
+ self._require("bucket")
42
+ return StorageS3(bucket=self.params.bucket, region=self.params.region)
43
+
44
+ def _resolve_gcs(self):
45
+ self._require("bucket")
46
+ return StorageGCS(
47
+ bucket=self.params.bucket, project=self.params.gcp_project
48
+ )
49
+
50
+ def _require(self, arg):
51
+ if not getattr(self.params, arg, None):
52
+ raise SystemExit(
53
+ f"{settings.ERROR_ALERT} --storage {self.params.storage}"
54
+ f" requires --{arg}"
55
+ )
dotflow/cli/setup.py CHANGED
@@ -163,7 +163,7 @@ class Command:
163
163
  cmd_generate.add_argument(
164
164
  "--platform",
165
165
  required=True,
166
- help="Target platform (e.g. docker, lambda, cloud-run, ecs, kubernetes)",
166
+ help=("Target platform (e.g. docker, lambda, cloud-run)"),
167
167
  )
168
168
  cmd_generate.add_argument(
169
169
  "--project",
@@ -206,7 +206,7 @@ class Command:
206
206
  self.cmd_deploy.add_argument(
207
207
  "--schedule",
208
208
  default=None,
209
- help="Schedule expression (e.g. 'rate(6 hours)')",
209
+ help="Cron expression (e.g. '*/5 * * * *')",
210
210
  )
211
211
  self.cmd_deploy.set_defaults(exec=DeployCommand)
212
212
 
@@ -233,6 +233,9 @@ class Command:
233
233
  except ImportModuleError as err:
234
234
  print(settings.WARNING_ALERT, err)
235
235
 
236
+ except KeyboardInterrupt:
237
+ print("\n", settings.INFO_ALERT, "Aborted.")
238
+
236
239
  except Exception as err:
237
240
  logger.error(f"Internal problem: {str(err)}")
238
241
  print(settings.ERROR_ALERT, MESSAGE_UNKNOWN_ERROR)
@@ -0,0 +1,11 @@
1
+ """Alibaba Cloud module."""
2
+
3
+ from dotflow.cloud.alibaba.deployers import (
4
+ AliyunFCDeployer,
5
+ AliyunFCScheduledDeployer,
6
+ )
7
+
8
+ __all__ = [
9
+ "AliyunFCDeployer",
10
+ "AliyunFCScheduledDeployer",
11
+ ]
@@ -0,0 +1,22 @@
1
+ """Alibaba Cloud constants."""
2
+
3
+ SDK_NOT_FOUND = (
4
+ "alibabacloud-fc-open20210406 is required: "
5
+ "pip install dotflow[deploy-alibaba]"
6
+ )
7
+
8
+ CREDENTIALS_NOT_FOUND = (
9
+ "Alibaba Cloud credentials not found. "
10
+ "Run 'aliyun configure' or set "
11
+ "ALIBABA_CLOUD_ACCESS_KEY_ID and "
12
+ "ALIBABA_CLOUD_ACCESS_KEY_SECRET."
13
+ )
14
+
15
+ PLATFORMS = {
16
+ "alibaba-fc",
17
+ "alibaba-fc-scheduled",
18
+ }
19
+
20
+ SCHEDULED_PLATFORMS = {"alibaba-fc-scheduled"}
21
+
22
+ DEFAULT_REGION = "cn-hangzhou"
@@ -0,0 +1,13 @@
1
+ """Alibaba Cloud deployers."""
2
+
3
+ from dotflow.cloud.alibaba.deployers.fc import (
4
+ AliyunFCDeployer,
5
+ )
6
+ from dotflow.cloud.alibaba.deployers.fc_scheduled import (
7
+ AliyunFCScheduledDeployer,
8
+ )
9
+
10
+ __all__ = [
11
+ "AliyunFCDeployer",
12
+ "AliyunFCScheduledDeployer",
13
+ ]
@@ -0,0 +1,134 @@
1
+ """Alibaba Function Compute deployment."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from rich import print # type: ignore
8
+
9
+ from dotflow.cloud.alibaba.constants import (
10
+ CREDENTIALS_NOT_FOUND,
11
+ SDK_NOT_FOUND,
12
+ )
13
+ from dotflow.cloud.alibaba.services.cr import ContainerRegistry
14
+ from dotflow.cloud.core import Deployer
15
+ from dotflow.settings import Settings as settings
16
+
17
+
18
+ class AliyunFCDeployer(Deployer):
19
+ """Deploy dotflow pipeline to Alibaba Cloud FC."""
20
+
21
+ def __init__(
22
+ self,
23
+ region: str = "cn-hangzhou",
24
+ namespace: str = "dotflow",
25
+ ):
26
+ try:
27
+ from alibabacloud_fc_open20210406.client import (
28
+ Client,
29
+ )
30
+ from alibabacloud_tea_openapi.models import (
31
+ Config,
32
+ )
33
+ except ImportError as err:
34
+ raise SystemExit(SDK_NOT_FOUND) from err
35
+
36
+ key_id = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID")
37
+ key_secret = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
38
+
39
+ if not key_id or not key_secret:
40
+ try:
41
+ import json
42
+ from pathlib import Path
43
+
44
+ config_path = Path.home() / ".aliyun" / "config.json"
45
+ data = json.loads(config_path.read_text())
46
+ for p in data.get("profiles", []):
47
+ if p.get("name") == data.get("current"):
48
+ key_id = p.get("access_key_id")
49
+ key_secret = p.get("access_key_secret")
50
+ if not region or region == "cn-hangzhou":
51
+ region = p.get("region_id", region)
52
+ break
53
+ except Exception:
54
+ pass
55
+
56
+ if not key_id or not key_secret:
57
+ raise SystemExit(CREDENTIALS_NOT_FOUND)
58
+
59
+ self._region = region
60
+ self._namespace = namespace
61
+
62
+ config = Config(
63
+ access_key_id=key_id,
64
+ access_key_secret=key_secret,
65
+ region_id=region,
66
+ )
67
+ config.endpoint = f"{self._region}.fc.aliyuncs.com"
68
+ self._fc = Client(config)
69
+
70
+ self._cr = ContainerRegistry(
71
+ region=region,
72
+ namespace=namespace,
73
+ )
74
+
75
+ @staticmethod
76
+ def _sanitize_name(name: str) -> str:
77
+ return name.replace("_", "-").lower()
78
+
79
+ def deploy(self, name: str, **kwargs) -> None:
80
+ """Deploy to Alibaba Function Compute."""
81
+ name = self._sanitize_name(name)
82
+ print(
83
+ settings.INFO_ALERT,
84
+ f"Deploying to Alibaba FC '{name}'...",
85
+ )
86
+
87
+ image_uri = self._cr.push(name)
88
+ self._create_or_update_function(name, image_uri)
89
+
90
+ print(settings.INFO_ALERT, "Done.")
91
+
92
+ def _create_or_update_function(self, name: str, image_uri: str) -> None:
93
+ """Create or update FC function."""
94
+ from alibabacloud_fc_open20210406 import models
95
+
96
+ try:
97
+ self._fc.get_function(name)
98
+ print(f" {settings.STEP_ICON} Updating function '{name}'...")
99
+ self._fc.update_function(
100
+ name,
101
+ models.UpdateFunctionRequest(
102
+ runtime="custom-container",
103
+ handler="handler.handler",
104
+ timeout=900,
105
+ memory_size=512,
106
+ custom_container_config=(
107
+ models.CustomContainerConfig(
108
+ image=image_uri,
109
+ port=9000,
110
+ )
111
+ ),
112
+ ),
113
+ )
114
+ except Exception as err:
115
+ if not (hasattr(err, "status_code") and err.status_code == 404):
116
+ raise
117
+ print(f" {settings.STEP_ICON} Creating function '{name}'...")
118
+ self._fc.create_function(
119
+ models.CreateFunctionRequest(
120
+ function_name=name,
121
+ runtime="custom-container",
122
+ handler="handler.handler",
123
+ timeout=900,
124
+ cpu=1,
125
+ memory_size=512,
126
+ disk_size=512,
127
+ custom_container_config=(
128
+ models.CustomContainerConfig(
129
+ image=image_uri,
130
+ port=9000,
131
+ )
132
+ ),
133
+ )
134
+ )