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.
Files changed (94) hide show
  1. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/PKG-INFO +1 -1
  2. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/pyproject.toml +6 -3
  3. clue_api-1.0.1.dev67/clue/plugin/files/gunicorn_config.py +0 -29
  4. clue_api-1.0.1.dev67/clue/plugin/files/patched.py +0 -5
  5. clue_api-1.0.1.dev67/clue/plugin/interactive.py +0 -265
  6. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/LICENSE +0 -0
  7. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/README.md +0 -0
  8. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/.gitignore +0 -0
  9. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/__init__.py +0 -0
  10. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/__init__.py +0 -0
  11. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/base.py +0 -0
  12. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/__init__.py +0 -0
  13. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/actions.py +0 -0
  14. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/auth.py +0 -0
  15. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/configs.py +0 -0
  16. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/fetchers.py +0 -0
  17. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/lookup.py +0 -0
  18. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/registration.py +0 -0
  19. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/api/v1/static.py +0 -0
  20. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/app.py +0 -0
  21. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/cache/__init__.py +0 -0
  22. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/__init__.py +0 -0
  23. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/classification.py +0 -0
  24. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/classification.yml +0 -0
  25. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/dict_utils.py +0 -0
  26. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/exceptions.py +0 -0
  27. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/forge.py +0 -0
  28. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/json_utils.py +0 -0
  29. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/list_utils.py +0 -0
  30. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/logging/__init__.py +0 -0
  31. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/logging/audit.py +0 -0
  32. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/logging/format.py +0 -0
  33. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/regex.py +0 -0
  34. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/str_utils.py +0 -0
  35. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/swagger.py +0 -0
  36. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/common/uid.py +0 -0
  37. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/config.py +0 -0
  38. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/constants/__init__.py +0 -0
  39. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/constants/supported_types.py +0 -0
  40. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/cronjobs/__init__.py +0 -0
  41. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/cronjobs/plugins.py +0 -0
  42. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/error.py +0 -0
  43. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/extensions/__init__.py +0 -0
  44. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/extensions/config.py +0 -0
  45. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/gunicorn_config.py +0 -0
  46. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/healthz.py +0 -0
  47. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/helper/discover.py +0 -0
  48. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/helper/headers.py +0 -0
  49. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/helper/oauth.py +0 -0
  50. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/__init__.py +0 -0
  51. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/actions.py +0 -0
  52. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/config.py +0 -0
  53. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/fetchers.py +0 -0
  54. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/graph.py +0 -0
  55. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/model_list.py +0 -0
  56. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/network.py +0 -0
  57. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/__init__.py +0 -0
  58. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/base.py +0 -0
  59. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/graph.py +0 -0
  60. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/image.py +0 -0
  61. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/status.py +0 -0
  62. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/results/validation.py +0 -0
  63. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/selector.py +0 -0
  64. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/models/validators.py +0 -0
  65. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/patched.py +0 -0
  66. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/__init__.py +0 -0
  67. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/__init__.py +0 -0
  68. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/central_server.py +0 -0
  69. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/email_render.py +0 -0
  70. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/token.py +0 -0
  71. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/helpers/trino.py +0 -0
  72. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/models.py +0 -0
  73. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/plugin/utils.py +0 -0
  74. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/py.typed +0 -0
  75. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/__init__.py +0 -0
  76. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/__init__.py +0 -0
  77. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/cache.py +0 -0
  78. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/events.py +0 -0
  79. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/hash.py +0 -0
  80. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/queues/__init__.py +0 -0
  81. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/queues/comms.py +0 -0
  82. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/set.py +0 -0
  83. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/remote/datatypes/user_quota_tracker.py +0 -0
  84. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/security/__init__.py +0 -0
  85. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/security/obo.py +0 -0
  86. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/security/utils.py +0 -0
  87. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/action_service.py +0 -0
  88. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/auth_service.py +0 -0
  89. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/config_service.py +0 -0
  90. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/fetcher_service.py +0 -0
  91. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/jwt_service.py +0 -0
  92. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/lookup_service.py +0 -0
  93. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/type_service.py +0 -0
  94. {clue_api-1.0.1.dev67 → clue_api-1.0.1.dev69}/clue/services/user_service.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clue-api
3
- Version: 1.0.1.dev67
3
+ Version: 1.0.1.dev69
4
4
  Summary: Clue distributed enrichment service
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -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.dev67"
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 = "clue.plugin.interactive:main"
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,5 +0,0 @@
1
- from gevent.monkey import patch_all
2
-
3
- patch_all()
4
-
5
- from app import app # noqa: F401, E402
@@ -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