clue-api 1.0.1.dev67__tar.gz → 1.0.1.dev69__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.
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/PKG-INFO +1 -1
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/pyproject.toml +6 -3
- clue_api-1.0.1.dev67/clue/plugin/files/gunicorn_config.py +0 -29
- clue_api-1.0.1.dev67/clue/plugin/files/patched.py +0 -5
- clue_api-1.0.1.dev67/clue/plugin/interactive.py +0 -265
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/LICENSE +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/README.md +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/.gitignore +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/base.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/actions.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/auth.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/configs.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/fetchers.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/lookup.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/registration.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/static.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/app.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/cache/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/classification.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/classification.yml +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/dict_utils.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/exceptions.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/forge.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/json_utils.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/list_utils.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/logging/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/logging/audit.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/logging/format.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/regex.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/str_utils.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/swagger.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/uid.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/config.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/constants/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/constants/supported_types.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/cronjobs/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/cronjobs/plugins.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/error.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/extensions/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/extensions/config.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/gunicorn_config.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/healthz.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/helper/discover.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/helper/headers.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/helper/oauth.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/actions.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/config.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/fetchers.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/graph.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/model_list.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/network.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/base.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/graph.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/image.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/status.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/validation.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/selector.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/validators.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/patched.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/central_server.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/email_render.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/token.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/trino.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/models.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/utils.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/py.typed +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/cache.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/events.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/hash.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/queues/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/queues/comms.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/set.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/user_quota_tracker.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/security/__init__.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/security/obo.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/security/utils.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/action_service.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/auth_service.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/config_service.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/fetcher_service.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/jwt_service.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/lookup_service.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/type_service.py +0 -0
- {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/user_service.py +0 -0
|
@@ -129,9 +129,11 @@ suppress-none-returning = true
|
|
|
129
129
|
"clue/app.py" = ["E402"]
|
|
130
130
|
"clue/api/v1/auth.py" = ["TRY301"]
|
|
131
131
|
"clue/common/classification.py" = ["D", "ANN", "C901", "TRY301", "T203"]
|
|
132
|
-
"clue/plugin/interactive.py" = ["T201"]
|
|
133
132
|
"clue/remote/datatypes/*" = ["D", "ANN", "C901"]
|
|
134
133
|
"clue/security/__init__.py" = ["TRY301"]
|
|
134
|
+
"plugin/interactive.py" = ["T201"]
|
|
135
|
+
"plugin/create.py" = ["T201", "D103"]
|
|
136
|
+
"plugin/commands.py" = ["T201", "D103"]
|
|
135
137
|
"test/conftest.py" = ["E402"]
|
|
136
138
|
|
|
137
139
|
###################
|
|
@@ -147,7 +149,7 @@ log_cli_level = "WARN"
|
|
|
147
149
|
[tool.poetry]
|
|
148
150
|
package-mode = true
|
|
149
151
|
name = "clue-api"
|
|
150
|
-
version = "1.0.1.
|
|
152
|
+
version = "1.0.1.dev69"
|
|
151
153
|
description = "Clue distributed enrichment service"
|
|
152
154
|
authors = ["Canadian Centre for Cyber Security <contact@cyber.gc.ca>"]
|
|
153
155
|
license = "MIT"
|
|
@@ -239,7 +241,8 @@ last_success = "build_scripts.last_success:main"
|
|
|
239
241
|
check_changes = "build_scripts.check_changes:main"
|
|
240
242
|
type_check = "build_scripts.type_check:main"
|
|
241
243
|
coverage_report = "build_scripts.coverage_reports:main"
|
|
242
|
-
plugin = "
|
|
244
|
+
plugin = "plugin.interactive:main"
|
|
245
|
+
create = "plugin.create:main"
|
|
243
246
|
|
|
244
247
|
[tool.poetry.group.test.dependencies]
|
|
245
248
|
pytest = "^8.1.1"
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import multiprocessing
|
|
2
|
-
from os import environ as env
|
|
3
|
-
|
|
4
|
-
# Port to bind to
|
|
5
|
-
bind = f"{env.get('HOST')}:{int(env.get('PORT', 8000))}"
|
|
6
|
-
|
|
7
|
-
# Number of processes to launch
|
|
8
|
-
workers = int(env.get("WORKERS", multiprocessing.cpu_count()))
|
|
9
|
-
|
|
10
|
-
# Number of concurrent handled connections
|
|
11
|
-
threads = int(env.get("THREADS", 4))
|
|
12
|
-
worker_connections = int(env.get("WORKER_CONNECTIONS", "1000"))
|
|
13
|
-
|
|
14
|
-
# Recycle the process after X request randomized by the jitter
|
|
15
|
-
max_requests = int(env.get("MAX_REQUESTS", "1000"))
|
|
16
|
-
max_requests_jitter = int(env.get("MAX_REQUESTS_JITTER", "100"))
|
|
17
|
-
|
|
18
|
-
# Connection timeouts
|
|
19
|
-
graceful_timeout = int(env.get("GRACEFUL_TIMEOUT", "30"))
|
|
20
|
-
timeout = int(env.get("TIMEOUT", "30"))
|
|
21
|
-
|
|
22
|
-
# TLS/SSL Configuration
|
|
23
|
-
certfile = env.get("CERTFILE")
|
|
24
|
-
keyfile = env.get("KEYFILE")
|
|
25
|
-
|
|
26
|
-
# Request Max Size Configuration
|
|
27
|
-
limit_request_line = int(env.get("LIMIT_REQUEST_LINE", "4094"))
|
|
28
|
-
limit_request_fields = int(env.get("LIMIT_REQUEST_FIELDS", "100"))
|
|
29
|
-
limit_request_field_size = int(env.get("LIMIT_REQUEST_FIELD_SIZE", "8190"))
|
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
import inspect
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
import sys
|
|
6
|
-
import textwrap
|
|
7
|
-
from pathlib import Path
|
|
8
|
-
from typing import Callable
|
|
9
|
-
from urllib.parse import quote_plus
|
|
10
|
-
|
|
11
|
-
from flask.testing import FlaskClient
|
|
12
|
-
from termcolor import colored
|
|
13
|
-
|
|
14
|
-
from clue.plugin import CluePlugin
|
|
15
|
-
|
|
16
|
-
PLUGINS_PATH = Path(__file__).parent.parent.parent.parent / "plugins"
|
|
17
|
-
sys.path.insert(0, str(PLUGINS_PATH))
|
|
18
|
-
|
|
19
|
-
TESTABLE_FUNCTIONS = [
|
|
20
|
-
("get_actions", None),
|
|
21
|
-
("execute_action", "run_action"),
|
|
22
|
-
("get_fetchers", None),
|
|
23
|
-
("execute_fetcher", "run_fetcher"),
|
|
24
|
-
("get_type_names", None),
|
|
25
|
-
("lookup", "enrich"),
|
|
26
|
-
("bulk_lookup", "enrich"),
|
|
27
|
-
("liveness", None),
|
|
28
|
-
("readyness", None),
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def success(*messages: str):
|
|
33
|
-
"Print success message"
|
|
34
|
-
print(f"[{colored("success", "green")}]", *messages)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def warn(*messages: str):
|
|
38
|
-
"Print error message"
|
|
39
|
-
print(f"[{colored("warn", "yellow")}]", *messages)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def error(*messages: str):
|
|
43
|
-
"Print error message"
|
|
44
|
-
print(f"[{colored("error", "red")}]", *messages)
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def info(*messages: str):
|
|
48
|
-
"Print info message"
|
|
49
|
-
print(f"[{colored("info", "cyan")}]", *messages)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class CustomTestClient(FlaskClient):
|
|
53
|
-
"Custom test client to inject authorization headers"
|
|
54
|
-
|
|
55
|
-
def open(self, *args, buffered=False, follow_redirects=False, **kwargs):
|
|
56
|
-
"Overriden open function to inject auth header"
|
|
57
|
-
headers = kwargs.setdefault("headers", {})
|
|
58
|
-
|
|
59
|
-
if "CLUE_ACCESS_TOKEN" in os.environ:
|
|
60
|
-
info("Clue access token in env, setting Authorization header")
|
|
61
|
-
headers["Authorization"] = f"Bearer {os.environ["CLUE_ACCESS_TOKEN"]}"
|
|
62
|
-
else:
|
|
63
|
-
warn("Missing access token, skipping authorization header.")
|
|
64
|
-
|
|
65
|
-
return super().open(*args, buffered=buffered, follow_redirects=follow_redirects, **kwargs)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def filter_members(member, current_module):
|
|
69
|
-
"Get a filtered list of members exported by a given application"
|
|
70
|
-
member_module = inspect.getmodule(member)
|
|
71
|
-
|
|
72
|
-
if member_module is None:
|
|
73
|
-
return False
|
|
74
|
-
|
|
75
|
-
if member_module == current_module:
|
|
76
|
-
return True
|
|
77
|
-
|
|
78
|
-
if not member_module.__name__.startswith("clue"):
|
|
79
|
-
return False
|
|
80
|
-
|
|
81
|
-
return True
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
def test_function(plugin: CluePlugin, fn_id: str, fn: Callable): # noqa: C901
|
|
85
|
-
"test a function"
|
|
86
|
-
info(f"Executing test functionality for {fn_id}")
|
|
87
|
-
|
|
88
|
-
plugin.app.test_client_class = CustomTestClient
|
|
89
|
-
|
|
90
|
-
for rule in plugin.app.url_map.iter_rules():
|
|
91
|
-
if rule.endpoint != fn_id:
|
|
92
|
-
continue
|
|
93
|
-
|
|
94
|
-
if "GET" in (rule.methods or {}) and "<" not in rule.rule:
|
|
95
|
-
info("Simple endpoint detected. Running GET")
|
|
96
|
-
response = plugin.app.test_client().get(rule.rule)
|
|
97
|
-
info("Response:", json.dumps(response.json, indent=2) if response.json else response.data.decode())
|
|
98
|
-
elif "GET" in (rule.methods or {}):
|
|
99
|
-
kwargs: dict[str, str] = {}
|
|
100
|
-
info(f"{len(rule.arguments)} arguments are necessary. Supply them now:")
|
|
101
|
-
for argument in sorted(list(rule.arguments)):
|
|
102
|
-
kwargs[argument] = quote_plus(quote_plus(input(f"{argument}: ")))
|
|
103
|
-
|
|
104
|
-
with plugin.app.test_request_context():
|
|
105
|
-
path = plugin.app.url_for(fn_id, **kwargs) # type: ignore[arg-type]
|
|
106
|
-
info(f"Making request to path {path}")
|
|
107
|
-
|
|
108
|
-
response = plugin.app.test_client().get(path)
|
|
109
|
-
|
|
110
|
-
if response.status_code > 299:
|
|
111
|
-
error(
|
|
112
|
-
(response.json or {}).get(
|
|
113
|
-
"api_error_message", f"An unknown error occurred. Full response:\n{response.text}"
|
|
114
|
-
)
|
|
115
|
-
)
|
|
116
|
-
else:
|
|
117
|
-
info("Response:", json.dumps(response.json, indent=2) if response.json else response.data.decode())
|
|
118
|
-
elif "POST" in (rule.methods or {}):
|
|
119
|
-
kwargs: dict[str, str] = {}
|
|
120
|
-
if "<" in rule.rule:
|
|
121
|
-
info(f"{len(rule.arguments)} arguments are necessary. Supply them now:")
|
|
122
|
-
for argument in sorted(list(rule.arguments)):
|
|
123
|
-
kwargs[argument] = quote_plus(quote_plus(input(f"{argument}: ")))
|
|
124
|
-
|
|
125
|
-
info(
|
|
126
|
-
"Endpoint requires POST data. You can probide a JSON file for this data. "
|
|
127
|
-
f"Provide a path relative to {os.getcwd()} or an absolute path."
|
|
128
|
-
)
|
|
129
|
-
json_path = Path(os.getcwd()) / input("Path to JSON: ").strip()
|
|
130
|
-
|
|
131
|
-
if not json_path.exists() or json_path.is_dir():
|
|
132
|
-
error(f"Provided path {json_path} is invalid or is a directory.")
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
with json_path.open("r") as _data, plugin.app.test_request_context():
|
|
136
|
-
try:
|
|
137
|
-
post_data = json.load(_data)
|
|
138
|
-
except json.JSONDecodeError:
|
|
139
|
-
error(f"The file data in {json_path} is not valid JSON.")
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
api_path = rule.rule if "<" not in rule.rule else plugin.app.url_for(fn_id, **kwargs) # type: ignore[arg-type]
|
|
143
|
-
|
|
144
|
-
info(f"Submitting POST request to {api_path}:\n{post_data}")
|
|
145
|
-
|
|
146
|
-
response = plugin.app.test_client().post(
|
|
147
|
-
api_path,
|
|
148
|
-
data=json.dumps(post_data),
|
|
149
|
-
headers={"Content-Type": "application/json"},
|
|
150
|
-
)
|
|
151
|
-
|
|
152
|
-
if response.status_code > 299:
|
|
153
|
-
error(
|
|
154
|
-
(response.json or {}).get(
|
|
155
|
-
"api_error_message", f"An unknown error occurred. Full response:\n{response.text}"
|
|
156
|
-
)
|
|
157
|
-
)
|
|
158
|
-
else:
|
|
159
|
-
info("Response:", json.dumps(response.json, indent=2) if response.json else response.data.decode())
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def main(): # noqa: C901
|
|
163
|
-
"main interactive loop"
|
|
164
|
-
os.environ["ENABLE_CACHE"] = "false"
|
|
165
|
-
|
|
166
|
-
plugin_name = None
|
|
167
|
-
if len(sys.argv) > 1:
|
|
168
|
-
plugin_name = sys.argv[1]
|
|
169
|
-
|
|
170
|
-
if not (PLUGINS_PATH / plugin_name).exists():
|
|
171
|
-
error(f"Plugin {plugin_name} does not exist.")
|
|
172
|
-
plugin_name = None
|
|
173
|
-
|
|
174
|
-
while plugin_name is None:
|
|
175
|
-
plugin_name = input("What plugin do you want to interact with?\n> ")
|
|
176
|
-
if not (Path(__file__).parent.parent.parent / "plugins" / plugin_name).exists():
|
|
177
|
-
error(f"Plugin {plugin_name} does not exist.")
|
|
178
|
-
plugin_name = None
|
|
179
|
-
|
|
180
|
-
try:
|
|
181
|
-
_module = importlib.import_module(f"{plugin_name}.app")
|
|
182
|
-
success(f"Initializing plugin {plugin_name} for interactivity")
|
|
183
|
-
except Exception:
|
|
184
|
-
error(f"Initializing plugin {plugin_name} for interactivity")
|
|
185
|
-
raise
|
|
186
|
-
|
|
187
|
-
plugin: CluePlugin | None = None
|
|
188
|
-
for key, member in inspect.getmembers(_module, predicate=lambda _m: filter_members(_m, _module)):
|
|
189
|
-
if isinstance(member, CluePlugin):
|
|
190
|
-
success(f"Plugin found exported as member {key}")
|
|
191
|
-
plugin = member
|
|
192
|
-
break
|
|
193
|
-
|
|
194
|
-
if plugin is None:
|
|
195
|
-
error("CluePlugin object is not exported from this module!")
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
plugin.cache = None
|
|
199
|
-
|
|
200
|
-
functions: list[tuple[str, Callable]] = []
|
|
201
|
-
|
|
202
|
-
for attribute in dir(plugin):
|
|
203
|
-
test_entry = next((entry for entry in TESTABLE_FUNCTIONS if entry[0] == attribute), None)
|
|
204
|
-
if test_entry is None:
|
|
205
|
-
continue
|
|
206
|
-
|
|
207
|
-
fn = plugin.__getattribute__(attribute)
|
|
208
|
-
if fn is None:
|
|
209
|
-
continue
|
|
210
|
-
|
|
211
|
-
if test_entry[1] is not None:
|
|
212
|
-
helper_fn = plugin.__getattribute__(test_entry[1])
|
|
213
|
-
|
|
214
|
-
if helper_fn is None:
|
|
215
|
-
continue
|
|
216
|
-
|
|
217
|
-
functions.append((attribute, fn))
|
|
218
|
-
|
|
219
|
-
choice: int | None = None
|
|
220
|
-
|
|
221
|
-
print(
|
|
222
|
-
textwrap.dedent("""
|
|
223
|
-
Clue Plugin Development Script
|
|
224
|
-
|
|
225
|
-
This script will help you test various aspects of your plugin interactively.
|
|
226
|
-
"""),
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
if "CLUE_ACCESS_TOKEN" not in os.environ:
|
|
230
|
-
warn(
|
|
231
|
-
textwrap.dedent("""
|
|
232
|
-
Environment variable CLUE_ACCESS_TOKEN not set!
|
|
233
|
-
|
|
234
|
-
It is highly likely your plugin will not work if it connects to an external service.
|
|
235
|
-
""").strip() # noqa: E501
|
|
236
|
-
)
|
|
237
|
-
|
|
238
|
-
while choice is None:
|
|
239
|
-
print("\nAvailable functions:")
|
|
240
|
-
|
|
241
|
-
for i in range(len(functions)):
|
|
242
|
-
print(f"{i + 1}) {' '.join(word.capitalize() for word in functions[i][0].split("_"))}")
|
|
243
|
-
print(f"{len(functions) + 1}) Quit")
|
|
244
|
-
|
|
245
|
-
action = input("\nEnter a selection: ")
|
|
246
|
-
|
|
247
|
-
try:
|
|
248
|
-
choice = int(action)
|
|
249
|
-
|
|
250
|
-
if choice > len(functions) + 1:
|
|
251
|
-
error(f"Invalid choice, choose option between 1 - {len(functions)}.")
|
|
252
|
-
choice = None
|
|
253
|
-
except ValueError:
|
|
254
|
-
error(f"Invalid integer, choose option between 1 - {len(functions)}.")
|
|
255
|
-
|
|
256
|
-
if choice is not None and choice <= len(functions):
|
|
257
|
-
test_function(plugin, *functions[choice - 1])
|
|
258
|
-
choice = None
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if __name__ == "__main__":
|
|
262
|
-
try:
|
|
263
|
-
main()
|
|
264
|
-
except KeyboardInterrupt:
|
|
265
|
-
print("\rExiting!" + " " * 80)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|