dotflow 1.0.0.dev3__tar.gz → 1.0.0.dev4__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.
Files changed (119) hide show
  1. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/PKG-INFO +1 -1
  2. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/__init__.py +1 -1
  3. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/__init__.py +4 -0
  4. dotflow-1.0.0.dev4/dotflow/cli/commands/login.py +123 -0
  5. dotflow-1.0.0.dev4/dotflow/cli/commands/logout.py +15 -0
  6. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/setup.py +27 -0
  7. dotflow-1.0.0.dev4/dotflow/core/config_file.py +95 -0
  8. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/server_default.py +10 -17
  9. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/pyproject.toml +2 -2
  10. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/LICENSE +0 -0
  11. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/README.md +0 -0
  12. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/__init__.py +0 -0
  13. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/file.py +0 -0
  14. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/flow.py +0 -0
  15. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/http.py +0 -0
  16. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/log.py +0 -0
  17. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/metrics.py +0 -0
  18. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/notify.py +0 -0
  19. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/scheduler.py +0 -0
  20. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/server.py +0 -0
  21. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/storage.py +0 -0
  22. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/tcp.py +0 -0
  23. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/abc/tracer.py +0 -0
  24. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/__init__.py +0 -0
  25. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/command.py +0 -0
  26. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/cloud.py +0 -0
  27. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/deploy.py +0 -0
  28. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/init.py +0 -0
  29. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/log.py +0 -0
  30. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/schedule.py +0 -0
  31. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/start.py +0 -0
  32. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/commands/storage.py +0 -0
  33. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/validators/__init__.py +0 -0
  34. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cli/validators/start.py +0 -0
  35. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/__init__.py +0 -0
  36. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/alibaba/__init__.py +0 -0
  37. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/alibaba/constants.py +0 -0
  38. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/alibaba/deployers/__init__.py +0 -0
  39. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/alibaba/deployers/fc.py +0 -0
  40. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/alibaba/deployers/fc_scheduled.py +0 -0
  41. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/alibaba/services/__init__.py +0 -0
  42. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/alibaba/services/cr.py +0 -0
  43. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/__init__.py +0 -0
  44. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/constants.py +0 -0
  45. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/__init__.py +0 -0
  46. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/base_lambda.py +0 -0
  47. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/ecs.py +0 -0
  48. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/ecs_scheduled.py +0 -0
  49. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/lambda_.py +0 -0
  50. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/lambda_api.py +0 -0
  51. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/lambda_s3.py +0 -0
  52. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/deployers/lambda_sqs.py +0 -0
  53. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/schedule.py +0 -0
  54. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/services/__init__.py +0 -0
  55. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/services/cloudwatch.py +0 -0
  56. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/services/ecr.py +0 -0
  57. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/services/iam.py +0 -0
  58. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/aws/services/s3.py +0 -0
  59. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/core.py +0 -0
  60. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/__init__.py +0 -0
  61. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/constants.py +0 -0
  62. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/deployers/__init__.py +0 -0
  63. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/deployers/cloudrun.py +0 -0
  64. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/services/__init__.py +0 -0
  65. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/services/apis.py +0 -0
  66. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/services/artifact_registry.py +0 -0
  67. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/gcp/services/gcs.py +0 -0
  68. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/github/__init__.py +0 -0
  69. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/github/constants.py +0 -0
  70. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/github/deployers/__init__.py +0 -0
  71. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/cloud/github/deployers/actions.py +0 -0
  72. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/__init__.py +0 -0
  73. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/action.py +0 -0
  74. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/config.py +0 -0
  75. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/context.py +0 -0
  76. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/decorators/__init__.py +0 -0
  77. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/decorators/time.py +0 -0
  78. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/dotflow.py +0 -0
  79. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/engine.py +0 -0
  80. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/exception.py +0 -0
  81. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/module.py +0 -0
  82. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/serializers/__init__.py +0 -0
  83. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/serializers/task.py +0 -0
  84. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/serializers/transport.py +0 -0
  85. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/serializers/workflow.py +0 -0
  86. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/task.py +0 -0
  87. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/types/__init__.py +0 -0
  88. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/types/execution.py +0 -0
  89. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/types/overlap.py +0 -0
  90. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/types/status.py +0 -0
  91. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/types/storage.py +0 -0
  92. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/types/workflow.py +0 -0
  93. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/core/workflow.py +0 -0
  94. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/logging.py +0 -0
  95. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/main.py +0 -0
  96. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/__init__.py +0 -0
  97. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/log_default.py +0 -0
  98. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/log_opentelemetry.py +0 -0
  99. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/log_sentry.py +0 -0
  100. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/metrics_default.py +0 -0
  101. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/metrics_opentelemetry.py +0 -0
  102. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/notify_default.py +0 -0
  103. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/notify_discord.py +0 -0
  104. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/notify_telegram.py +0 -0
  105. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/scheduler_cron.py +0 -0
  106. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/scheduler_default.py +0 -0
  107. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/storage_default.py +0 -0
  108. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/storage_file.py +0 -0
  109. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/storage_gcs.py +0 -0
  110. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/storage_s3.py +0 -0
  111. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/tracer_default.py +0 -0
  112. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/tracer_opentelemetry.py +0 -0
  113. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/providers/tracer_sentry.py +0 -0
  114. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/settings.py +0 -0
  115. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/types.py +0 -0
  116. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/utils/__init__.py +0 -0
  117. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/utils/basic_functions.py +0 -0
  118. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/utils/error_handler.py +0 -0
  119. {dotflow-1.0.0.dev3 → dotflow-1.0.0.dev4}/dotflow/utils/tools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dotflow
3
- Version: 1.0.0.dev3
3
+ Version: 1.0.0.dev4
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: Apache License
6
6
  Version 2.0, January 2004
@@ -1,6 +1,6 @@
1
1
  """Dotflow __init__ module."""
2
2
 
3
- __version__ = "1.0.0.dev3"
3
+ __version__ = "1.0.0.dev4"
4
4
  __description__ = "🎲 Dotflow turns an idea into flow!"
5
5
 
6
6
  from .core.action import Action as action
@@ -4,6 +4,8 @@ from dotflow.cli.commands.cloud import CloudGenerateCommand, CloudListCommand
4
4
  from dotflow.cli.commands.deploy import DeployCommand
5
5
  from dotflow.cli.commands.init import InitCommand
6
6
  from dotflow.cli.commands.log import LogCommand
7
+ from dotflow.cli.commands.login import LoginCommand
8
+ from dotflow.cli.commands.logout import LogoutCommand
7
9
  from dotflow.cli.commands.schedule import ScheduleCommand
8
10
  from dotflow.cli.commands.start import StartCommand
9
11
 
@@ -13,6 +15,8 @@ __all__ = [
13
15
  "DeployCommand",
14
16
  "InitCommand",
15
17
  "LogCommand",
18
+ "LoginCommand",
19
+ "LogoutCommand",
16
20
  "ScheduleCommand",
17
21
  "StartCommand",
18
22
  ]
@@ -0,0 +1,123 @@
1
+ """Command login module."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ import webbrowser
8
+
9
+ from requests import RequestException, post
10
+ from rich import print # type: ignore
11
+
12
+ from dotflow.cli.command import Command
13
+ from dotflow.core.config_file import save_cloud_config
14
+ from dotflow.settings import Settings as settings
15
+
16
+ DEFAULT_BASE_URL = "https://www.host.dotflow.io/api/v1"
17
+ DEVICE_ENDPOINT = "/cli/device"
18
+ TOKEN_ENDPOINT = "/cli/token"
19
+ TIMEOUT = 15
20
+ DEFAULT_INTERVAL = 5
21
+ MAX_POLL_INTERVAL = 60
22
+
23
+
24
+ class LoginCommand(Command):
25
+ def setup(self):
26
+ token = getattr(self.params, "token", None)
27
+ base_url = self._resolve_base_url()
28
+
29
+ if token:
30
+ save_cloud_config(token=token, base_url=base_url)
31
+ print(settings.INFO_ALERT, "Token saved.")
32
+ return
33
+
34
+ handshake = self._start_device(base_url)
35
+ if handshake is None:
36
+ return
37
+
38
+ print(settings.INFO_ALERT, f"Opening {handshake['verification_uri']}")
39
+ print(settings.INFO_ALERT, f"Code: {handshake['user_code']}")
40
+
41
+ webbrowser.open(handshake["verification_uri"])
42
+
43
+ token = self._poll_token(base_url, handshake)
44
+
45
+ if not token:
46
+ return
47
+
48
+ save_cloud_config(token=token, base_url=base_url)
49
+ print(settings.INFO_ALERT, "Authenticated.")
50
+
51
+ def _resolve_base_url(self) -> str:
52
+ """Flag or env var wins over the default."""
53
+ explicit = getattr(self.params, "base_url", None) or os.environ.get(
54
+ "SERVER_BASE_URL"
55
+ )
56
+ return explicit.rstrip("/") if explicit else DEFAULT_BASE_URL
57
+
58
+ def _start_device(self, base_url: str) -> dict | None:
59
+ try:
60
+ response = post(
61
+ f"{base_url}{DEVICE_ENDPOINT}",
62
+ timeout=TIMEOUT,
63
+ )
64
+ response.raise_for_status()
65
+ return response.json()
66
+ except RequestException as error:
67
+ print(
68
+ settings.ERROR_ALERT,
69
+ f"Could not reach Dotflow Cloud: {error}",
70
+ )
71
+ return None
72
+
73
+ def _poll_token(self, base_url: str, handshake: dict) -> str | None:
74
+ interval = handshake.get("interval") or DEFAULT_INTERVAL
75
+ deadline = time.monotonic() + (handshake.get("expires_in") or 300)
76
+
77
+ while time.monotonic() < deadline:
78
+ try:
79
+ response = post(
80
+ f"{base_url}{TOKEN_ENDPOINT}",
81
+ json={"device_code": handshake["device_code"]},
82
+ timeout=TIMEOUT,
83
+ )
84
+ except RequestException as error:
85
+ print(settings.ERROR_ALERT, f"Network error: {error}")
86
+ return None
87
+
88
+ if response.status_code == 200:
89
+ return response.json().get("api_token")
90
+
91
+ detail = self._detail(response)
92
+
93
+ if (
94
+ response.status_code == 400
95
+ and detail == "authorization_pending"
96
+ ):
97
+ time.sleep(interval)
98
+ continue
99
+
100
+ if response.status_code == 400 and detail in (
101
+ "slow_down",
102
+ "slow down",
103
+ ):
104
+ interval = min(interval + 5, MAX_POLL_INTERVAL)
105
+ time.sleep(interval)
106
+ continue
107
+
108
+ if response.status_code == 410:
109
+ print(settings.ERROR_ALERT, "Authorization expired.")
110
+ return None
111
+
112
+ print(settings.ERROR_ALERT, f"{response.status_code}: {detail}")
113
+ return None
114
+
115
+ print(settings.ERROR_ALERT, "Timed out waiting for authorization.")
116
+ return None
117
+
118
+ @staticmethod
119
+ def _detail(response) -> str:
120
+ try:
121
+ return str(response.json().get("detail", response.text))
122
+ except ValueError:
123
+ return response.text
@@ -0,0 +1,15 @@
1
+ """Command logout module."""
2
+
3
+ from rich import print # type: ignore
4
+
5
+ from dotflow.cli.command import Command
6
+ from dotflow.core.config_file import clear_cloud_config
7
+ from dotflow.settings import Settings as settings
8
+
9
+
10
+ class LogoutCommand(Command):
11
+ def setup(self):
12
+ if clear_cloud_config():
13
+ print(settings.INFO_ALERT, "Signed out.")
14
+ else:
15
+ print(settings.WARNING_ALERT, "No saved credentials.")
@@ -9,6 +9,8 @@ from dotflow.cli.commands import (
9
9
  DeployCommand,
10
10
  InitCommand,
11
11
  LogCommand,
12
+ LoginCommand,
13
+ LogoutCommand,
12
14
  ScheduleCommand,
13
15
  StartCommand,
14
16
  )
@@ -42,12 +44,37 @@ class Command:
42
44
 
43
45
  self.setup_init()
44
46
  self.setup_logs()
47
+ self.setup_login()
48
+ self.setup_logout()
45
49
  self.setup_start()
46
50
  self.setup_schedule()
47
51
  self.setup_cloud()
48
52
  self.setup_deploy()
49
53
  self.command()
50
54
 
55
+ def setup_login(self):
56
+ cmd = self.subparsers.add_parser(
57
+ "login", help="Authenticate the CLI via browser"
58
+ )
59
+ cmd_group = cmd.add_argument_group("Usage: dotflow login [OPTIONS]")
60
+ cmd_group.add_argument(
61
+ "--base-url",
62
+ default=None,
63
+ help="Override the Dotflow Cloud API base URL",
64
+ )
65
+ cmd_group.add_argument(
66
+ "--token",
67
+ default=None,
68
+ help="Skip the browser flow and save a pre-obtained API token",
69
+ )
70
+ cmd.set_defaults(exec=LoginCommand)
71
+
72
+ def setup_logout(self):
73
+ cmd = self.subparsers.add_parser(
74
+ "logout", help="Remove saved CLI credentials"
75
+ )
76
+ cmd.set_defaults(exec=LogoutCommand)
77
+
51
78
  def setup_init(self):
52
79
  self.cmd_init = self.subparsers.add_parser(
53
80
  "init", help="Scaffold a new dotflow project"
@@ -0,0 +1,95 @@
1
+ """Persistent CLI config stored at ``~/.dotflow/config.json``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+
10
+ CONFIG_DIR_NAME = ".dotflow"
11
+ CONFIG_FILE_NAME = "config.json"
12
+ CLOUD_SECTION = "cloud"
13
+
14
+
15
+ def config_path() -> Path:
16
+ """Return the absolute path of the CLI config file."""
17
+ return Path.home() / CONFIG_DIR_NAME / CONFIG_FILE_NAME
18
+
19
+
20
+ def load_cloud_config() -> dict[str, str]:
21
+ """Read the ``cloud`` section from the config file. ``{}`` when absent."""
22
+ path = config_path()
23
+
24
+ if not path.exists():
25
+ return {}
26
+
27
+ try:
28
+ raw = path.read_text(encoding="utf-8")
29
+ except OSError:
30
+ return {}
31
+
32
+ try:
33
+ data = json.loads(raw)
34
+ except json.JSONDecodeError:
35
+ return {}
36
+
37
+ section = data.get(CLOUD_SECTION) if isinstance(data, dict) else None
38
+ if not isinstance(section, dict):
39
+ return {}
40
+
41
+ return {
42
+ key: str(value)
43
+ for key, value in section.items()
44
+ if isinstance(value, str)
45
+ }
46
+
47
+
48
+ def save_cloud_config(token: str, base_url: str) -> Path:
49
+ """Persist ``cloud`` with the given token/base_url. Returns the file path."""
50
+ path = config_path()
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+
53
+ payload = {
54
+ CLOUD_SECTION: {
55
+ "base_url": base_url,
56
+ "token": token,
57
+ }
58
+ }
59
+ content = json.dumps(payload, indent=2) + "\n"
60
+
61
+ fd = os.open(
62
+ str(path),
63
+ os.O_WRONLY | os.O_CREAT | os.O_TRUNC,
64
+ 0o600,
65
+ )
66
+ with contextlib.suppress(OSError):
67
+ os.fchmod(fd, 0o600)
68
+ with os.fdopen(fd, "w", encoding="utf-8") as fh:
69
+ fh.write(content)
70
+
71
+ return path
72
+
73
+
74
+ def clear_cloud_config() -> bool:
75
+ """Remove the config file. Returns True when something was deleted."""
76
+ path = config_path()
77
+
78
+ if not path.exists():
79
+ return False
80
+
81
+ try:
82
+ path.unlink()
83
+ return True
84
+ except OSError:
85
+ return False
86
+
87
+
88
+ def resolve(key: str, env_var: str) -> str | None:
89
+ """Resolve a setting with env > file precedence. ``None`` when absent."""
90
+ env_value = os.environ.get(env_var)
91
+
92
+ if env_value:
93
+ return env_value
94
+
95
+ return load_cloud_config().get(key) or None
@@ -1,4 +1,4 @@
1
- """ServerDefault — no-op or managed HTTP server provider."""
1
+ """ServerDefault - managed HTTP server provider."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -24,24 +24,19 @@ def managed(method):
24
24
 
25
25
 
26
26
  class ServerDefault(Server):
27
- """Default Server provider with auto-detected managed mode.
28
-
29
- When ``SERVER_BASE_URL`` and ``SERVER_USER_TOKEN`` env vars are set,
30
- sends workflow and task events to the remote API. Otherwise, all
31
- methods are no-ops.
32
- """
27
+ """Default Server provider with auto-detected managed mode."""
33
28
 
34
29
  MAX_RESULT_SIZE = 5_000_000
35
30
  TIMEOUT = 15.0
36
31
 
37
- ENDPOINT_WORKFLOWS = "/workflows"
38
- ENDPOINT_WORKFLOW = "/workflows/{workflow_id}"
39
- ENDPOINT_TASKS = "/workflows/{workflow_id}/tasks"
40
- ENDPOINT_TASK = "/workflows/{workflow_id}/tasks/{task_id}"
32
+ ENDPOINT_WORKFLOWS = "/cli/workflows"
33
+ ENDPOINT_WORKFLOW = "/cli/workflows/{workflow_id}"
34
+ ENDPOINT_TASKS = "/cli/workflows/{workflow_id}/tasks"
35
+ ENDPOINT_TASK = "/cli/workflows/{workflow_id}/tasks/{task_id}"
41
36
 
42
37
  def __init__(self) -> None:
43
- base_url = os.getenv("SERVER_BASE_URL")
44
- user_token = os.getenv("SERVER_USER_TOKEN")
38
+ base_url = os.environ.get("SERVER_BASE_URL") or None
39
+ user_token = os.environ.get("SERVER_USER_TOKEN") or None
45
40
  self._managed = bool(base_url and user_token)
46
41
 
47
42
  self._base_url = base_url.rstrip("/") if base_url else None
@@ -56,25 +51,23 @@ class ServerDefault(Server):
56
51
 
57
52
  def _post(self, url: str, json: dict) -> None:
58
53
  try:
59
- response = post(
54
+ post(
60
55
  url,
61
56
  json=json,
62
57
  headers=self._headers,
63
58
  timeout=self.TIMEOUT,
64
59
  )
65
- logger.info("POST %s [%s]", url, response.status_code)
66
60
  except RequestException as error:
67
61
  logger.error("POST %s failed: %s", url, error)
68
62
 
69
63
  def _patch(self, url: str, json: dict) -> None:
70
64
  try:
71
- response = patch(
65
+ patch(
72
66
  url,
73
67
  json=json,
74
68
  headers=self._headers,
75
69
  timeout=self.TIMEOUT,
76
70
  )
77
- logger.info("PATCH %s [%s]", url, response.status_code)
78
71
  except RequestException as error:
79
72
  logger.error("PATCH %s failed: %s", url, error)
80
73
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dotflow"
3
- version = "1.0.0.dev3"
3
+ version = "1.0.0.dev4"
4
4
  authors = [
5
5
  { name="Fernando Celmer", email="email@fernandocelmer.com" },
6
6
  ]
@@ -67,7 +67,7 @@ sentry = ["sentry-sdk"]
67
67
 
68
68
  [tool.poetry]
69
69
  name = "dotflow"
70
- version = "1.0.0.dev3"
70
+ version = "1.0.0.dev4"
71
71
  description = "🎲 Dotflow turns an idea into flow! Lightweight Python library for execution pipelines with retry, parallel, cron and async support."
72
72
  authors = ["Fernando Celmer <email@fernandocelmer.com>"]
73
73
  readme = "README.md"
File without changes
File without changes