@factorialco/fcode-cli 1.1.0
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.
- package/README.md +1021 -0
- package/bin/dev +17 -0
- package/bin/dev.cmd +3 -0
- package/bin/run +7 -0
- package/bin/run.cmd +3 -0
- package/dist/commands/add.d.ts +6 -0
- package/dist/commands/add.js +22 -0
- package/dist/commands/clone.d.ts +26 -0
- package/dist/commands/clone.js +177 -0
- package/dist/commands/dependencies/add.d.ts +9 -0
- package/dist/commands/dependencies/add.js +13 -0
- package/dist/commands/dependencies/install.d.ts +14 -0
- package/dist/commands/dependencies/install.js +44 -0
- package/dist/commands/dependencies/pull.d.ts +12 -0
- package/dist/commands/dependencies/pull.js +14 -0
- package/dist/commands/dependencies/push.d.ts +12 -0
- package/dist/commands/dependencies/push.js +14 -0
- package/dist/commands/dependencies/reset.d.ts +9 -0
- package/dist/commands/dependencies/reset.js +13 -0
- package/dist/commands/dependencies/status.d.ts +9 -0
- package/dist/commands/dependencies/status.js +13 -0
- package/dist/commands/http.d.ts +22 -0
- package/dist/commands/http.js +272 -0
- package/dist/commands/login.d.ts +18 -0
- package/dist/commands/login.js +62 -0
- package/dist/commands/logout.d.ts +10 -0
- package/dist/commands/logout.js +34 -0
- package/dist/commands/modules/add.d.ts +9 -0
- package/dist/commands/modules/add.js +13 -0
- package/dist/commands/modules/pull.d.ts +13 -0
- package/dist/commands/modules/pull.js +43 -0
- package/dist/commands/modules/push.d.ts +13 -0
- package/dist/commands/modules/push.js +35 -0
- package/dist/commands/modules/remove.d.ts +9 -0
- package/dist/commands/modules/remove.js +19 -0
- package/dist/commands/modules/reset.d.ts +9 -0
- package/dist/commands/modules/reset.js +13 -0
- package/dist/commands/modules/status.d.ts +11 -0
- package/dist/commands/modules/status.js +40 -0
- package/dist/commands/pre-debug.d.ts +20 -0
- package/dist/commands/pre-debug.js +120 -0
- package/dist/commands/process-metadata.d.ts +18 -0
- package/dist/commands/process-metadata.js +63 -0
- package/dist/commands/processes/add.d.ts +9 -0
- package/dist/commands/processes/add.js +13 -0
- package/dist/commands/processes/install-dependencies.d.ts +14 -0
- package/dist/commands/processes/install-dependencies.js +30 -0
- package/dist/commands/processes/pull.d.ts +13 -0
- package/dist/commands/processes/pull.js +47 -0
- package/dist/commands/processes/push.d.ts +13 -0
- package/dist/commands/processes/push.js +39 -0
- package/dist/commands/processes/remove.d.ts +9 -0
- package/dist/commands/processes/remove.js +19 -0
- package/dist/commands/processes/reset.d.ts +9 -0
- package/dist/commands/processes/reset.js +13 -0
- package/dist/commands/processes/status.d.ts +11 -0
- package/dist/commands/processes/status.js +58 -0
- package/dist/commands/pull.d.ts +9 -0
- package/dist/commands/pull.js +32 -0
- package/dist/commands/push.d.ts +9 -0
- package/dist/commands/push.js +29 -0
- package/dist/commands/remote/add.d.ts +10 -0
- package/dist/commands/remote/add.js +32 -0
- package/dist/commands/remote/list.d.ts +7 -0
- package/dist/commands/remote/list.js +33 -0
- package/dist/commands/remote/set.d.ts +10 -0
- package/dist/commands/remote/set.js +28 -0
- package/dist/commands/reset.d.ts +6 -0
- package/dist/commands/reset.js +19 -0
- package/dist/commands/run.d.ts +16 -0
- package/dist/commands/run.js +56 -0
- package/dist/commands/runtime-env.d.ts +12 -0
- package/dist/commands/runtime-env.js +42 -0
- package/dist/commands/settings/get.d.ts +12 -0
- package/dist/commands/settings/get.js +67 -0
- package/dist/commands/settings/remove.d.ts +9 -0
- package/dist/commands/settings/remove.js +35 -0
- package/dist/commands/settings/set.d.ts +10 -0
- package/dist/commands/settings/set.js +39 -0
- package/dist/commands/setup-debug.d.ts +12 -0
- package/dist/commands/setup-debug.js +118 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.js +22 -0
- package/dist/commands/team/pull.d.ts +8 -0
- package/dist/commands/team/pull.js +23 -0
- package/dist/commands/team/push.d.ts +11 -0
- package/dist/commands/team/push.js +41 -0
- package/dist/commands/team/status.d.ts +8 -0
- package/dist/commands/team/status.js +40 -0
- package/dist/commands/test/scaffold.d.ts +9 -0
- package/dist/commands/test/scaffold.js +61 -0
- package/dist/commands/test.d.ts +13 -0
- package/dist/commands/test.js +72 -0
- package/dist/commands/variables/add.d.ts +12 -0
- package/dist/commands/variables/add.js +21 -0
- package/dist/commands/variables/pull.d.ts +12 -0
- package/dist/commands/variables/pull.js +14 -0
- package/dist/commands/variables/push.d.ts +12 -0
- package/dist/commands/variables/push.js +14 -0
- package/dist/commands/variables/remove.d.ts +9 -0
- package/dist/commands/variables/remove.js +19 -0
- package/dist/commands/variables/reset.d.ts +9 -0
- package/dist/commands/variables/reset.js +13 -0
- package/dist/commands/variables/status.d.ts +12 -0
- package/dist/commands/variables/status.js +58 -0
- package/dist/config/global.d.ts +15 -0
- package/dist/config/global.js +77 -0
- package/dist/config/index.d.ts +14 -0
- package/dist/config/index.js +44 -0
- package/dist/config/remote.d.ts +15 -0
- package/dist/config/remote.js +60 -0
- package/dist/config/settings.d.ts +16 -0
- package/dist/config/settings.js +54 -0
- package/dist/config/workspace.d.ts +28 -0
- package/dist/config/workspace.js +158 -0
- package/dist/core/commands/api-client-command.d.ts +6 -0
- package/dist/core/commands/api-client-command.js +30 -0
- package/dist/core/commands/base-command.d.ts +11 -0
- package/dist/core/commands/base-command.js +61 -0
- package/dist/core/commands/dependency-command.d.ts +5 -0
- package/dist/core/commands/dependency-command.js +16 -0
- package/dist/core/commands/modules-command.d.ts +7 -0
- package/dist/core/commands/modules-command.js +18 -0
- package/dist/core/commands/processes-command.d.ts +8 -0
- package/dist/core/commands/processes-command.js +27 -0
- package/dist/core/commands/resource-command.d.ts +24 -0
- package/dist/core/commands/resource-command.js +92 -0
- package/dist/core/commands/variables-command.d.ts +5 -0
- package/dist/core/commands/variables-command.js +16 -0
- package/dist/core/dependencies/env-dependencies-command.d.ts +19 -0
- package/dist/core/dependencies/env-dependencies-command.js +70 -0
- package/dist/core/dependencies/env-process-dependencies-command.d.ts +15 -0
- package/dist/core/dependencies/env-process-dependencies-command.js +78 -0
- package/dist/core/dependencies/javascript-env-dependencies.d.ts +8 -0
- package/dist/core/dependencies/javascript-env-dependencies.js +77 -0
- package/dist/core/dependencies/python-env-dependencies.d.ts +9 -0
- package/dist/core/dependencies/python-env-dependencies.js +91 -0
- package/dist/core/errors.d.ts +55 -0
- package/dist/core/errors.js +144 -0
- package/dist/core/executors/execution-task.d.ts +29 -0
- package/dist/core/executors/execution-task.js +139 -0
- package/dist/core/executors/javascript-execution-task.d.ts +13 -0
- package/dist/core/executors/javascript-execution-task.js +618 -0
- package/dist/core/executors/python/context.d.ts +2 -0
- package/dist/core/executors/python/context.js +3 -0
- package/dist/core/executors/python/datastore.d.ts +2 -0
- package/dist/core/executors/python/datastore.js +4 -0
- package/dist/core/executors/python-execution-task.d.ts +11 -0
- package/dist/core/executors/python-execution-task.js +173 -0
- package/dist/core/executors/source-code-sanitizer.d.ts +4 -0
- package/dist/core/executors/source-code-sanitizer.js +45 -0
- package/dist/core/resource-merger.d.ts +14 -0
- package/dist/core/resource-merger.js +155 -0
- package/dist/core/resource-pusher.d.ts +12 -0
- package/dist/core/resource-pusher.js +133 -0
- package/dist/core/resource-repository.d.ts +8 -0
- package/dist/core/resource-repository.js +2 -0
- package/dist/core/types.d.ts +290 -0
- package/dist/core/types.js +238 -0
- package/dist/core/utils/dependencies.d.ts +5 -0
- package/dist/core/utils/dependencies.js +82 -0
- package/dist/core/utils/variables.d.ts +4 -0
- package/dist/core/utils/variables.js +10 -0
- package/dist/hooks/init/auth.d.ts +3 -0
- package/dist/hooks/init/auth.js +28 -0
- package/dist/hooks/init/update.d.ts +3 -0
- package/dist/hooks/init/update.js +42 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -0
- package/dist/repositories/process.d.ts +36 -0
- package/dist/repositories/process.js +339 -0
- package/dist/repositories/programming-language-manifest.d.ts +17 -0
- package/dist/repositories/programming-language-manifest.js +95 -0
- package/dist/repositories/resource.d.ts +29 -0
- package/dist/repositories/resource.js +79 -0
- package/dist/repositories/script-library.d.ts +28 -0
- package/dist/repositories/script-library.js +164 -0
- package/dist/repositories/team.d.ts +23 -0
- package/dist/repositories/team.js +74 -0
- package/dist/repositories/variable.d.ts +26 -0
- package/dist/repositories/variable.js +174 -0
- package/dist/runtime/executor.js +87 -0
- package/dist/runtime/executor.py +81 -0
- package/dist/runtime/libs/node/fcode/index.js +1 -0
- package/dist/runtime/libs/python/fcode/__init__.py +75 -0
- package/dist/runtime/libs/python/fcode/context.py +98 -0
- package/dist/runtime/libs/python/fcode/datastore.py +35 -0
- package/dist/runtime/libs/python/fcode/schedule.py +45 -0
- package/dist/runtime/libs/python/fcode/storage.py +271 -0
- package/dist/runtime/libs/python/fcode/variables.py +102 -0
- package/dist/services/auth-service.d.ts +12 -0
- package/dist/services/auth-service.js +114 -0
- package/dist/services/credential-prompter.d.ts +6 -0
- package/dist/services/credential-prompter.js +60 -0
- package/dist/services/fcode-graphql-client.d.ts +45 -0
- package/dist/services/fcode-graphql-client.js +721 -0
- package/dist/services/http-server.d.ts +3 -0
- package/dist/services/http-server.js +17 -0
- package/dist/services/oauth-browser-service.d.ts +9 -0
- package/dist/services/oauth-browser-service.js +164 -0
- package/dist/services/process-parameters-sampler.d.ts +2 -0
- package/dist/services/process-parameters-sampler.js +41 -0
- package/dist/services/process-runner.d.ts +22 -0
- package/dist/services/process-runner.js +97 -0
- package/dist/services/resource-checker.d.ts +8 -0
- package/dist/services/resource-checker.js +82 -0
- package/dist/services/test-runner.d.ts +17 -0
- package/dist/services/test-runner.js +228 -0
- package/dist/utils/canonical-hash.d.ts +9 -0
- package/dist/utils/canonical-hash.js +30 -0
- package/dist/utils/cli/index.d.ts +14 -0
- package/dist/utils/cli/index.js +50 -0
- package/dist/utils/cli/screen.d.ts +2 -0
- package/dist/utils/cli/screen.js +25 -0
- package/dist/utils/cli/styled/table.d.ts +23 -0
- package/dist/utils/cli/styled/table.js +272 -0
- package/dist/utils/colorize-resource.d.ts +1 -0
- package/dist/utils/colorize-resource.js +7 -0
- package/dist/utils/env-utils.d.ts +2 -0
- package/dist/utils/env-utils.js +16 -0
- package/dist/utils/file-utils.d.ts +11 -0
- package/dist/utils/file-utils.js +33 -0
- package/dist/utils/filenamify.d.ts +4 -0
- package/dist/utils/filenamify.js +73 -0
- package/dist/utils/gitignore.d.ts +17 -0
- package/dist/utils/gitignore.js +60 -0
- package/dist/utils/promise.d.ts +4 -0
- package/dist/utils/promise.js +38 -0
- package/dist/utils/url-utils.d.ts +2 -0
- package/dist/utils/url-utils.js +10 -0
- package/dist/utils/variable-warnings.d.ts +8 -0
- package/dist/utils/variable-warnings.js +38 -0
- package/oclif.manifest.json +1783 -0
- package/package.json +129 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
YC_COMMAND = os.environ.get("YC_COMMAND", "fcode")
|
|
7
|
+
|
|
8
|
+
metadata = {
|
|
9
|
+
"execution": {"process": {"name": "", "id": ""}},
|
|
10
|
+
"parameters": {},
|
|
11
|
+
"variables": [],
|
|
12
|
+
}
|
|
13
|
+
runtime_metadata_path = os.environ.get("FCODE_RUNTIME_METADATA_PATH", "")
|
|
14
|
+
process_slug = os.environ.get("FCODE_PROCESS_SLUG", "")
|
|
15
|
+
if runtime_metadata_path:
|
|
16
|
+
os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
|
|
17
|
+
"[FCODE LIBRARY] Reading metadata from file: " + runtime_metadata_path
|
|
18
|
+
)
|
|
19
|
+
with open(runtime_metadata_path, "r") as f:
|
|
20
|
+
metadata = json.load(f)
|
|
21
|
+
else:
|
|
22
|
+
process_params = os.environ.get("FCODE_PROCESS_PARAMS", "")
|
|
23
|
+
process_version = os.environ.get("FCODE_PROCESS_VERSION", "")
|
|
24
|
+
os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
|
|
25
|
+
"[FCODE LIBRARY] Reading metadata from process-metadata command"
|
|
26
|
+
)
|
|
27
|
+
command = [YC_COMMAND, "process-metadata"]
|
|
28
|
+
if process_slug:
|
|
29
|
+
command.append(process_slug)
|
|
30
|
+
if process_slug and process_version:
|
|
31
|
+
command.append("-v")
|
|
32
|
+
command.append(process_version)
|
|
33
|
+
if process_params:
|
|
34
|
+
command.append("-p")
|
|
35
|
+
command.append(process_params)
|
|
36
|
+
os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
|
|
37
|
+
"[FCODE LIBRARY] Running command: " + " ".join(command)
|
|
38
|
+
)
|
|
39
|
+
result = subprocess.run(command, encoding="utf-8", stdout=subprocess.PIPE)
|
|
40
|
+
if result.returncode != 0:
|
|
41
|
+
os.environ.get("FCODE_LOG_LEVEL", "") == "DEBUG" and print(
|
|
42
|
+
"[FCODE LIBRARY] Command finished with exit code: "
|
|
43
|
+
+ str(result.returncode)
|
|
44
|
+
)
|
|
45
|
+
exit(1)
|
|
46
|
+
metadata = json.loads(result.stdout)
|
|
47
|
+
|
|
48
|
+
from fcode.context import FcodeGlobal, Context, ExecutionInfo, Environment
|
|
49
|
+
from fcode.datastore import Datastore
|
|
50
|
+
from fcode.storage import Storage
|
|
51
|
+
from fcode.schedule import Schedule
|
|
52
|
+
from fcode.variables import Variables
|
|
53
|
+
|
|
54
|
+
context = Context(metadata.get("parameters", {}))
|
|
55
|
+
execution = ExecutionInfo(metadata.get("execution", {}))
|
|
56
|
+
env_variables = Environment(metadata.get("variables", []))
|
|
57
|
+
import builtins
|
|
58
|
+
|
|
59
|
+
fcode = FcodeGlobal(
|
|
60
|
+
context,
|
|
61
|
+
execution,
|
|
62
|
+
env_variables,
|
|
63
|
+
Datastore(),
|
|
64
|
+
Storage(),
|
|
65
|
+
Schedule(),
|
|
66
|
+
Variables(env_variables),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
logging.basicConfig(level=os.environ.get("FCODE_LOG_LEVEL", logging.INFO))
|
|
70
|
+
builtins.logging = logging
|
|
71
|
+
logger = logging.getLogger("fcode_logger")
|
|
72
|
+
builtins.fcode = fcode
|
|
73
|
+
builtins.logger = logger
|
|
74
|
+
|
|
75
|
+
__all__ = ["logging", "logger", "fcode"]
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Context(dict):
|
|
6
|
+
def __init__(self, parameters):
|
|
7
|
+
self["parameters"] = parameters
|
|
8
|
+
self["request"] = "No request, you are using the CLI"
|
|
9
|
+
|
|
10
|
+
def __getattr__(self, attr):
|
|
11
|
+
return self.get(attr)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AttrsInfo(dict):
|
|
15
|
+
def __init__(self, attrs):
|
|
16
|
+
if attrs:
|
|
17
|
+
for key, value in attrs.items():
|
|
18
|
+
self[key] = value
|
|
19
|
+
|
|
20
|
+
def __getattr__(self, attr):
|
|
21
|
+
return self.get(attr)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExecutionInfo(dict):
|
|
25
|
+
def __init__(self, parameters):
|
|
26
|
+
self["id"] = "00000000-0000-0000-0000-000000000000"
|
|
27
|
+
process = parameters.get("process", {})
|
|
28
|
+
self["process"] = AttrsInfo(
|
|
29
|
+
{
|
|
30
|
+
"id": process.get("id", "00000000-0000-0000-0000-000000000000"),
|
|
31
|
+
"name": process.get("name", "Process not yet pushed to cloud"),
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
self["timeout"] = 86400000
|
|
35
|
+
|
|
36
|
+
def __getattr__(self, attr):
|
|
37
|
+
return self.get(attr)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Environment(dict):
|
|
41
|
+
def __init__(self, team_variables):
|
|
42
|
+
if team_variables:
|
|
43
|
+
for key, value in team_variables.items():
|
|
44
|
+
self[key] = value
|
|
45
|
+
|
|
46
|
+
def __getattr__(self, attr):
|
|
47
|
+
return self.get(attr)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Processes(dict):
|
|
51
|
+
def run(self, process_identifier, options=None):
|
|
52
|
+
import subprocess
|
|
53
|
+
import json
|
|
54
|
+
import sys
|
|
55
|
+
|
|
56
|
+
if not process_identifier:
|
|
57
|
+
raise ValueError("Process identifier is required")
|
|
58
|
+
|
|
59
|
+
tag = f" -v {options['tag']}" if options and "tag" in options else ""
|
|
60
|
+
parameters = (
|
|
61
|
+
f" -p '{json.dumps(options.get('parameters', {}))}'" if options else ""
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
command = f"fcode run {process_identifier}{tag}{parameters}"
|
|
65
|
+
print(f"Executing command: {command}") # Debug line
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
process = subprocess.Popen(
|
|
69
|
+
command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
70
|
+
)
|
|
71
|
+
stdout, stderr = process.communicate()
|
|
72
|
+
|
|
73
|
+
if process.returncode != 0:
|
|
74
|
+
print(stderr.decode(), file=sys.stderr)
|
|
75
|
+
print(stdout.decode())
|
|
76
|
+
print(stderr.decode(), file=sys.stderr)
|
|
77
|
+
except Exception as error:
|
|
78
|
+
print(error, file=sys.stderr)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class FcodeGlobal:
|
|
82
|
+
def __init__(self, context, execution, env, datastore, storage, schedule, variables):
|
|
83
|
+
self.context = context
|
|
84
|
+
self.execution = execution
|
|
85
|
+
self.team = AttrsInfo({"timezone": "UTC"})
|
|
86
|
+
self.env = env
|
|
87
|
+
self.datastore = datastore
|
|
88
|
+
self.storage = storage
|
|
89
|
+
self.schedule = schedule
|
|
90
|
+
self.variables = variables
|
|
91
|
+
self.processes = Processes()
|
|
92
|
+
|
|
93
|
+
def import_module(self, module_name, version=None):
|
|
94
|
+
module_path = module_name
|
|
95
|
+
if version:
|
|
96
|
+
module_path = os.path.join(module_path, "versions", version)
|
|
97
|
+
sys.path.append(os.path.join(os.getcwd(), "modules", module_path))
|
|
98
|
+
return __import__(module_name)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Datastore:
|
|
6
|
+
def save(self, datastore):
|
|
7
|
+
path = os.path.join(os.getcwd(), "datastore.json")
|
|
8
|
+
with open(path, "w") as file:
|
|
9
|
+
json.dump(datastore, file, indent=2)
|
|
10
|
+
|
|
11
|
+
def getAll(self):
|
|
12
|
+
path = os.path.join(os.getcwd(), "datastore.json")
|
|
13
|
+
try:
|
|
14
|
+
with open(path, "r") as file:
|
|
15
|
+
return json.load(file)
|
|
16
|
+
except FileNotFoundError:
|
|
17
|
+
return {}
|
|
18
|
+
|
|
19
|
+
def get(self, key):
|
|
20
|
+
if isinstance(key, (str, int)):
|
|
21
|
+
return self.getAll().get(key, None)
|
|
22
|
+
raise TypeError("Invalid argument type")
|
|
23
|
+
|
|
24
|
+
def set(self, key, value):
|
|
25
|
+
if isinstance(key, (str, int)) and isinstance(value, (str, int)):
|
|
26
|
+
datastore = self.getAll()
|
|
27
|
+
datastore[key] = value
|
|
28
|
+
self.save(datastore)
|
|
29
|
+
else:
|
|
30
|
+
raise TypeError("Invalid argument type")
|
|
31
|
+
|
|
32
|
+
def delete(self, key):
|
|
33
|
+
datastore = self.getAll()
|
|
34
|
+
datastore.pop(key, None)
|
|
35
|
+
self.save(datastore)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
logger = logging.getLogger("fcode_logger")
|
|
4
|
+
|
|
5
|
+
_CLOUD_ONLY = (
|
|
6
|
+
"[fcode] schedule.%s only runs in the cloud; it is a no-op during local "
|
|
7
|
+
"`fcode run`."
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Schedule:
|
|
12
|
+
"""Local `fcode run` shim for `fcode.schedule`.
|
|
13
|
+
|
|
14
|
+
Scheduling is a cloud-only capability, so every method is a logged no-op
|
|
15
|
+
locally. Push the process and schedule it in the cloud to exercise it.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def create(self, process_id_or_slug, **kwargs):
|
|
19
|
+
logger.warning(_CLOUD_ONLY, "create")
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
def list(self, process_id=None, keywords=None):
|
|
23
|
+
logger.warning(_CLOUD_ONLY, "list")
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
def get(self, id):
|
|
27
|
+
logger.warning(_CLOUD_ONLY, "get")
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def update(self, id, **kwargs):
|
|
31
|
+
logger.warning(_CLOUD_ONLY, "update")
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def delete(self, id):
|
|
35
|
+
logger.warning(_CLOUD_ONLY, "delete")
|
|
36
|
+
|
|
37
|
+
def delete_for_process(self, process_id_or_slug):
|
|
38
|
+
logger.warning(_CLOUD_ONLY, "delete_for_process")
|
|
39
|
+
return 0
|
|
40
|
+
|
|
41
|
+
def pause(self, id):
|
|
42
|
+
logger.warning(_CLOUD_ONLY, "pause")
|
|
43
|
+
|
|
44
|
+
def resume(self, id):
|
|
45
|
+
logger.warning(_CLOUD_ONLY, "resume")
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import hashlib
|
|
3
|
+
import mimetypes
|
|
4
|
+
from datetime import datetime, timedelta, timezone
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SignedUrl:
|
|
9
|
+
def __init__(self, url: str, path: str, expires_at: str):
|
|
10
|
+
self.url = url
|
|
11
|
+
self.path = path
|
|
12
|
+
self.expires_at = expires_at
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StorageObject:
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
name: str,
|
|
19
|
+
size: int,
|
|
20
|
+
link: str,
|
|
21
|
+
md5_hash: str,
|
|
22
|
+
content_type: str,
|
|
23
|
+
created_at: str,
|
|
24
|
+
updated_at: str,
|
|
25
|
+
):
|
|
26
|
+
self.name = name
|
|
27
|
+
self.size = size
|
|
28
|
+
self.md5_hash = md5_hash
|
|
29
|
+
self.content_type = content_type
|
|
30
|
+
self.created_at = created_at
|
|
31
|
+
self.updated_at = updated_at
|
|
32
|
+
self.link = link
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Storage:
|
|
36
|
+
def _create_storage_object(self, filename, file_path, md5_hash):
|
|
37
|
+
try:
|
|
38
|
+
stats = os.stat(file_path)
|
|
39
|
+
current_time = datetime.utcnow().isoformat() + "Z"
|
|
40
|
+
|
|
41
|
+
content_type, _ = mimetypes.guess_type(filename)
|
|
42
|
+
if not content_type:
|
|
43
|
+
content_type = "application/octet-stream"
|
|
44
|
+
|
|
45
|
+
return StorageObject(
|
|
46
|
+
name=filename,
|
|
47
|
+
size=stats.st_size,
|
|
48
|
+
link=file_path,
|
|
49
|
+
md5_hash=md5_hash.hexdigest(),
|
|
50
|
+
content_type=content_type,
|
|
51
|
+
created_at=current_time,
|
|
52
|
+
updated_at=current_time,
|
|
53
|
+
)
|
|
54
|
+
except Exception as error:
|
|
55
|
+
raise RuntimeError(
|
|
56
|
+
f"Storage upload failed: unable to process file - {error}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def upload(self, filename, file_obj):
|
|
60
|
+
if not isinstance(filename, str):
|
|
61
|
+
raise TypeError("Storage upload failed: filename must be a string")
|
|
62
|
+
|
|
63
|
+
if not filename.strip():
|
|
64
|
+
raise ValueError("Storage upload failed: filename cannot be empty")
|
|
65
|
+
|
|
66
|
+
if file_obj is None:
|
|
67
|
+
raise ValueError("Storage upload failed: file parameter is required")
|
|
68
|
+
|
|
69
|
+
is_file_like = hasattr(file_obj, "read")
|
|
70
|
+
is_bytes = isinstance(file_obj, bytes)
|
|
71
|
+
is_string = isinstance(file_obj, str)
|
|
72
|
+
|
|
73
|
+
if not is_file_like and not is_bytes and not is_string:
|
|
74
|
+
raise TypeError(
|
|
75
|
+
"Storage upload failed: file must be file-like object, bytes, or string"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
storage_dir = os.path.join(os.getcwd(), "storage")
|
|
80
|
+
if not os.path.exists(storage_dir):
|
|
81
|
+
os.makedirs(storage_dir, exist_ok=True)
|
|
82
|
+
|
|
83
|
+
file_path = os.path.join(storage_dir, filename)
|
|
84
|
+
file_dir = os.path.dirname(file_path)
|
|
85
|
+
|
|
86
|
+
if not os.path.exists(file_dir):
|
|
87
|
+
os.makedirs(file_dir, exist_ok=True)
|
|
88
|
+
|
|
89
|
+
md5_hash = hashlib.md5()
|
|
90
|
+
|
|
91
|
+
with open(file_path, "wb") as f:
|
|
92
|
+
if is_file_like:
|
|
93
|
+
while chunk := file_obj.read(8192):
|
|
94
|
+
md5_hash.update(chunk)
|
|
95
|
+
f.write(chunk)
|
|
96
|
+
elif is_string:
|
|
97
|
+
content = file_obj.encode("utf-8")
|
|
98
|
+
md5_hash.update(content)
|
|
99
|
+
f.write(content)
|
|
100
|
+
else:
|
|
101
|
+
md5_hash.update(file_obj)
|
|
102
|
+
f.write(file_obj)
|
|
103
|
+
|
|
104
|
+
return self._create_storage_object(filename, file_path, md5_hash)
|
|
105
|
+
|
|
106
|
+
except (OSError, IOError) as error:
|
|
107
|
+
raise RuntimeError(f"Storage upload failed: {error}")
|
|
108
|
+
except Exception as error:
|
|
109
|
+
raise RuntimeError(f"Storage upload failed: {error}")
|
|
110
|
+
|
|
111
|
+
def list(self, prefix=""):
|
|
112
|
+
if not isinstance(prefix, str):
|
|
113
|
+
raise TypeError("Storage list failed: prefix must be a string")
|
|
114
|
+
|
|
115
|
+
storage_dir = os.path.join(os.getcwd(), "storage")
|
|
116
|
+
|
|
117
|
+
if not os.path.exists(storage_dir):
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
files = []
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
for root, dirs, file_names in os.walk(storage_dir):
|
|
124
|
+
for file_name in file_names:
|
|
125
|
+
full_path = os.path.join(root, file_name)
|
|
126
|
+
relative_path = os.path.relpath(full_path, storage_dir)
|
|
127
|
+
|
|
128
|
+
normalized_path = relative_path.replace(os.sep, "/")
|
|
129
|
+
|
|
130
|
+
if not prefix or normalized_path.startswith(prefix):
|
|
131
|
+
try:
|
|
132
|
+
stats = os.stat(full_path)
|
|
133
|
+
|
|
134
|
+
md5_hash = hashlib.md5()
|
|
135
|
+
with open(full_path, "rb") as f:
|
|
136
|
+
while chunk := f.read(8192):
|
|
137
|
+
md5_hash.update(chunk)
|
|
138
|
+
|
|
139
|
+
content_type, _ = mimetypes.guess_type(normalized_path)
|
|
140
|
+
if not content_type:
|
|
141
|
+
content_type = "application/octet-stream"
|
|
142
|
+
|
|
143
|
+
files.append(
|
|
144
|
+
StorageObject(
|
|
145
|
+
name=normalized_path,
|
|
146
|
+
size=stats.st_size,
|
|
147
|
+
link=full_path,
|
|
148
|
+
md5_hash=md5_hash.hexdigest(),
|
|
149
|
+
content_type=content_type,
|
|
150
|
+
created_at=datetime.fromtimestamp(
|
|
151
|
+
stats.st_birthtime
|
|
152
|
+
).isoformat()
|
|
153
|
+
+ "Z",
|
|
154
|
+
updated_at=datetime.fromtimestamp(
|
|
155
|
+
stats.st_mtime
|
|
156
|
+
).isoformat()
|
|
157
|
+
+ "Z",
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
except (OSError, IOError):
|
|
161
|
+
print(
|
|
162
|
+
f"Storage list warning: unable to process file {normalized_path}"
|
|
163
|
+
)
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
except Exception as error:
|
|
167
|
+
raise RuntimeError(f"Storage list failed: {error}")
|
|
168
|
+
|
|
169
|
+
return files
|
|
170
|
+
|
|
171
|
+
def download(self, filename):
|
|
172
|
+
if not isinstance(filename, str):
|
|
173
|
+
raise TypeError("Storage download failed: filename must be a string")
|
|
174
|
+
|
|
175
|
+
if not filename.strip():
|
|
176
|
+
raise ValueError("Storage download failed: filename cannot be empty")
|
|
177
|
+
|
|
178
|
+
file_path = os.path.join(os.getcwd(), "storage", filename)
|
|
179
|
+
|
|
180
|
+
if not os.path.exists(file_path):
|
|
181
|
+
raise FileNotFoundError(
|
|
182
|
+
f"Storage download failed: file not found - {filename}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if not os.path.isfile(file_path):
|
|
186
|
+
raise ValueError(f"Storage download failed: {filename} is not a file")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
with open(file_path, "rb") as f:
|
|
190
|
+
return f.read()
|
|
191
|
+
except (OSError, IOError) as error:
|
|
192
|
+
raise RuntimeError(
|
|
193
|
+
f"Storage download failed: unable to read file - {error}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
def delete(self, filename):
|
|
197
|
+
if not isinstance(filename, str):
|
|
198
|
+
raise TypeError("Storage delete failed: filename must be a string")
|
|
199
|
+
|
|
200
|
+
if not filename.strip():
|
|
201
|
+
raise ValueError("Storage delete failed: filename cannot be empty")
|
|
202
|
+
|
|
203
|
+
file_path = os.path.join(os.getcwd(), "storage", filename)
|
|
204
|
+
|
|
205
|
+
if not os.path.exists(file_path):
|
|
206
|
+
raise FileNotFoundError(
|
|
207
|
+
f"Storage delete failed: file not found - {filename}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
if not os.path.isfile(file_path):
|
|
211
|
+
raise ValueError(f"Storage delete failed: {filename} is not a file")
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
os.remove(file_path)
|
|
215
|
+
|
|
216
|
+
dir_path = os.path.dirname(file_path)
|
|
217
|
+
storage_dir = os.path.join(os.getcwd(), "storage")
|
|
218
|
+
|
|
219
|
+
while dir_path != storage_dir and dir_path != os.path.dirname(dir_path):
|
|
220
|
+
try:
|
|
221
|
+
if not os.listdir(dir_path):
|
|
222
|
+
os.rmdir(dir_path)
|
|
223
|
+
dir_path = os.path.dirname(dir_path)
|
|
224
|
+
else:
|
|
225
|
+
break
|
|
226
|
+
except OSError as error:
|
|
227
|
+
print(
|
|
228
|
+
f"Storage delete warning: unable to clean up directory {dir_path}: {error}"
|
|
229
|
+
)
|
|
230
|
+
break
|
|
231
|
+
|
|
232
|
+
except (OSError, IOError) as error:
|
|
233
|
+
raise RuntimeError(f"Storage delete failed: {error}")
|
|
234
|
+
|
|
235
|
+
def create_signed_url(self, name, expires_in_seconds=None):
|
|
236
|
+
if not isinstance(name, str):
|
|
237
|
+
raise TypeError("Storage create_signed_url failed: name must be a string")
|
|
238
|
+
|
|
239
|
+
if not name.strip():
|
|
240
|
+
raise ValueError("Storage create_signed_url failed: name cannot be empty")
|
|
241
|
+
|
|
242
|
+
if expires_in_seconds is None:
|
|
243
|
+
expires_in_seconds = 3600
|
|
244
|
+
if not isinstance(expires_in_seconds, int) or not (
|
|
245
|
+
1 <= expires_in_seconds <= 604800
|
|
246
|
+
):
|
|
247
|
+
raise ValueError(
|
|
248
|
+
"Storage create_signed_url failed: expires_in_seconds must be an "
|
|
249
|
+
"integer between 1 and 604800"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
file_path = os.path.join(os.getcwd(), "storage", name)
|
|
253
|
+
|
|
254
|
+
if not os.path.exists(file_path):
|
|
255
|
+
raise FileNotFoundError(
|
|
256
|
+
f"Storage create_signed_url failed: file not found - {name}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# Local runtime has no object store to sign against, so return a file://
|
|
260
|
+
# URL to the file on disk. Mirrors the cloud { url, path, expires_at }
|
|
261
|
+
# contract so process code needs no local-vs-cloud guard.
|
|
262
|
+
print(
|
|
263
|
+
"Storage create_signed_url: no object store in local runtime; "
|
|
264
|
+
"returning a file:// URL to the local file (mock)."
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
expires_at = (
|
|
268
|
+
datetime.now(timezone.utc) + timedelta(seconds=expires_in_seconds)
|
|
269
|
+
).isoformat()
|
|
270
|
+
|
|
271
|
+
return SignedUrl(url=Path(file_path).as_uri(), path=name, expires_at=expires_at)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
logger = logging.getLogger("fcode_logger")
|
|
5
|
+
|
|
6
|
+
LOCAL_VARIABLES_FILE = "variables.local.env"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Variables:
|
|
10
|
+
"""Local `fcode run` shim for `fcode.variables`.
|
|
11
|
+
|
|
12
|
+
Reads return the team + local variables loaded for this run, overlaid with the
|
|
13
|
+
current `variables.local.env`. Writes (set/delete) affect only
|
|
14
|
+
`variables.local.env` in the workspace root, never the shared `variables.env`.
|
|
15
|
+
The `sensitive` flag has no representation in the `.env` file and is ignored
|
|
16
|
+
locally.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, env):
|
|
20
|
+
self._base = dict(env or {})
|
|
21
|
+
|
|
22
|
+
def _local_path(self):
|
|
23
|
+
return os.path.join(os.getcwd(), LOCAL_VARIABLES_FILE)
|
|
24
|
+
|
|
25
|
+
def _read_local(self):
|
|
26
|
+
try:
|
|
27
|
+
with open(self._local_path(), "r") as file:
|
|
28
|
+
return file.read()
|
|
29
|
+
except FileNotFoundError:
|
|
30
|
+
return ""
|
|
31
|
+
|
|
32
|
+
def _parse(self, text):
|
|
33
|
+
result = {}
|
|
34
|
+
for line in text.splitlines():
|
|
35
|
+
stripped = line.strip()
|
|
36
|
+
if not stripped or stripped.startswith("#") or "=" not in stripped:
|
|
37
|
+
continue
|
|
38
|
+
key, value = stripped.split("=", 1)
|
|
39
|
+
result[key.strip()] = value
|
|
40
|
+
return result
|
|
41
|
+
|
|
42
|
+
def _merged(self):
|
|
43
|
+
merged = dict(self._base)
|
|
44
|
+
merged.update(self._parse(self._read_local()))
|
|
45
|
+
return merged
|
|
46
|
+
|
|
47
|
+
def list(self):
|
|
48
|
+
return [{"key": key, "value": value} for key, value in self._merged().items()]
|
|
49
|
+
|
|
50
|
+
def get(self, key):
|
|
51
|
+
return self._merged().get(key)
|
|
52
|
+
|
|
53
|
+
def set(self, key, value, sensitive=True):
|
|
54
|
+
out = []
|
|
55
|
+
replaced = False
|
|
56
|
+
for line in self._read_local().splitlines():
|
|
57
|
+
stripped = line.strip()
|
|
58
|
+
if (
|
|
59
|
+
stripped
|
|
60
|
+
and not stripped.startswith("#")
|
|
61
|
+
and "=" in stripped
|
|
62
|
+
and stripped.split("=", 1)[0].strip() == key
|
|
63
|
+
):
|
|
64
|
+
out.append(f"{key}={value}")
|
|
65
|
+
replaced = True
|
|
66
|
+
else:
|
|
67
|
+
out.append(line)
|
|
68
|
+
while out and out[-1].strip() == "":
|
|
69
|
+
out.pop()
|
|
70
|
+
if not replaced:
|
|
71
|
+
out.append(f"{key}={value}")
|
|
72
|
+
with open(self._local_path(), "w") as file:
|
|
73
|
+
file.write("\n".join(out) + "\n")
|
|
74
|
+
logger.info(
|
|
75
|
+
"[fcode] variables.set wrote %s to %s (local run).", key, LOCAL_VARIABLES_FILE
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def delete(self, key):
|
|
79
|
+
text = self._read_local()
|
|
80
|
+
if not text:
|
|
81
|
+
return
|
|
82
|
+
out = []
|
|
83
|
+
for line in text.splitlines():
|
|
84
|
+
stripped = line.strip()
|
|
85
|
+
if (
|
|
86
|
+
stripped
|
|
87
|
+
and not stripped.startswith("#")
|
|
88
|
+
and "=" in stripped
|
|
89
|
+
and stripped.split("=", 1)[0].strip() == key
|
|
90
|
+
):
|
|
91
|
+
continue
|
|
92
|
+
out.append(line)
|
|
93
|
+
while out and out[-1].strip() == "":
|
|
94
|
+
out.pop()
|
|
95
|
+
content = "\n".join(out) + "\n" if out else ""
|
|
96
|
+
with open(self._local_path(), "w") as file:
|
|
97
|
+
file.write(content)
|
|
98
|
+
logger.info(
|
|
99
|
+
"[fcode] variables.delete removed %s from %s (local run).",
|
|
100
|
+
key,
|
|
101
|
+
LOCAL_VARIABLES_FILE,
|
|
102
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AuthInfo, Credentials, Token, UserInfo } from '../core/types';
|
|
2
|
+
import GlobalConfig from '../config/global';
|
|
3
|
+
declare const AuthService: {
|
|
4
|
+
clientId: string;
|
|
5
|
+
getToken({ authUrl, email, password, clientId, clientSecret }: Credentials): Promise<Token>;
|
|
6
|
+
getUserInfo(authUrl: string, token: Token): Promise<UserInfo>;
|
|
7
|
+
globalConfig: GlobalConfig;
|
|
8
|
+
isAuthenticated(url: string): Promise<boolean>;
|
|
9
|
+
login({ url, authUrl, email, password, clientId, clientSecret, accessToken }: Credentials): Promise<AuthInfo>;
|
|
10
|
+
logout(url: string): Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
export default AuthService;
|