sylriekit 0.15.6__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kasterfly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.4
2
+ Name: sylriekit
3
+ Version: 0.15.6
4
+ Summary: A personal Python toolbox of utilities.
5
+ Author: Kasterfly
6
+ License: MIT License
7
+
8
+ Copyright (c) 2025 Kasterfly
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.12
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Dynamic: license-file
32
+
33
+ ## Sylriekit
34
+ A personal toolset I made for myself to speed up the creation of some random personal projects.
35
+
36
+ **Warning**: This is a personal project and may contain bugs or incomplete features. It is mainly coded for convenience over optimization. Use at your own risk.
37
+
38
+ ---
39
+ ## Tools
40
+ + ### PHP-like tools:
41
+ #### Constants
42
+ - Immutable constants manager. Define values that cannot be modified or deleted after creation.
43
+ #### InlinePython:
44
+ - Python renderer for inline code blocks in files. (similar to php with `<?py ... ?>`)
45
+
46
+ + ### AI-related tools:
47
+ #### LLM
48
+ - LLM API client supports OpenAI, Anthropic, xAI, and Gemini. Handles chat sessions, tool calling, and MCP server integration.
49
+ #### MCP
50
+ - A simple FastAPI extension/wrapper. Adds some logging and error handling built-in tools for convenience.
51
+
52
+ + ### Execution and Automation:
53
+ #### LimitedScript
54
+ - Executes Python scripts in a partially sandboxed environment with restricted builtins and controlled access to tools/classes.
55
+ #### Schedule
56
+ - Scheduler for interval, daily, and file-watch tasks with registry management.
57
+ #### Process
58
+ - Subprocess wrapper for running commands with configurable timeouts, environment, shell usage, and output trimming while returning structured results.
59
+
60
+ + ### Development Support:
61
+ #### Log
62
+ - Simple logging tool with Log.log(...) or automatic logging with decorators for functions.
63
+ #### Cache
64
+ - A simple function-based cache using decorators.
65
+ #### Profile
66
+ - Timing and profiling helper with context managers, decorators, and direct measurements plus optional memory tracking and stored records.
67
+
68
+
69
+ + ### Other Tools:
70
+ #### Files
71
+ - File system utilities with searching, reading, editing, directory operations, grep, etc.
72
+ #### Website
73
+ - Simple flask web server management tool using Gunicorn. Supports URL processing and template rendering.
74
+ #### API
75
+ - HTTP request helper with configurable presets for headers, endpoints, API keys, and quick call execution returning structured responses.
76
+ #### Git
77
+ - Local git wrapper for managing repositories, branches, commits, and remotes.
78
+ #### Security
79
+ - Cryptography helper for RSA/Symmetric encryption, hashing, and JWT management.
80
+ #### Database
81
+ - Unified interface for interacting with SQLite, PostgreSQL, MySQL, and DynamoDB.
82
+
83
+ ---
84
+
85
+ ## Config
86
+ Each tool has class-level defaults that can be overridden via `load_config()`. Pass a dictionary/JSON where keys are tool names and values are dictionaries of settings (Settings/Variables not provided will use the tool's built-in defaults.)
87
+
88
+ Example config structure:
89
+ ```
90
+ {
91
+ "Files": {
92
+ "MAX_DEPTH_DEFAULT": 42,
93
+ "PATH_SEP": "/"
94
+ }
95
+ }
96
+ ```
97
+ using `sylriekit.load_config(config:dict)` will load the config for all tools, to centeralize the configuration of multiple tools.
98
+
99
+ and using `sylriekit.generate_default_config(file_name: str, include_tools="*")` will generate a file that will contain all (or just selected ones with `include_tools=["ToolName1", ...]`) of the configurable values and their defaults.
100
+
101
+
102
+ ---
103
+
104
+ #### Other Stuff
105
+ - `sylriekit.tool_functions(tool_name: str) -> list[str]`: Returns all public functions available in a tool.
106
+ - `sylriekit.get_code(tool_name: str, function_name: str) -> str`: Returns the source code of a specific tool's function. Use `"*"` as the function name to get the entire source code of the tool.
107
+ - `sylriekit.help()`: Prints a short message for how to use the sylriekit's help-related functions.
108
+
109
+ ---
110
+
111
+ ### Change Log:
112
+ **0.15.6**:
113
+ - Fixed the description to remove an inaccurate statement about GitHub Copilot support with LLM.
114
+ + LLM Tool:
115
+ - Added support for having multiple MCP servers active simultaneously.
116
+ - Tools now prefer their original short names. Namespacing (`server__tool`) is automatically applied only when tool names collide between servers.
117
+ - Namespaced tools automatically revert to short names if the conflicting server is removed.
118
+ - Fixed tool execution to ensure the correct original tool name is sent to the MCP server, regardless of how it is named in the client.
119
+ + MCP tool:
120
+ - Added `check_previous_logs` tool to inspect logs from previous sessions.
121
+ - Added `add_debug_tools()` and `remove_debug_tools()` methods to dynamically toggle built-in debug tools (`health`, `recent_logs`, `recent_errors`, `check_previous_logs`).
122
+ - Added `DEBUG_TOOLS_ENABLED` configuration option to control default debug tool state.
123
+ - Fixed the MCP tool's naming to correctly work as MCP instead of Mcp
124
+
125
+
126
+ **0.15.2**:
127
+ - Added Change log to README.md
128
+ - Fixed the issue with the Files tool: `Files.read` incorrectly identifying file paths as directories.
129
+ - Fixed `Files.create`, `Files.write`, and `Files.append` to properly respect `Files.CWD` for relative paths.
130
+ - Updated `Files.read` to properly handle relative paths using `Files.CWD`.
131
+
132
+ **0.15.1**:
133
+ - Uploaded to PyPI in the current state.
@@ -0,0 +1,101 @@
1
+ ## Sylriekit
2
+ A personal toolset I made for myself to speed up the creation of some random personal projects.
3
+
4
+ **Warning**: This is a personal project and may contain bugs or incomplete features. It is mainly coded for convenience over optimization. Use at your own risk.
5
+
6
+ ---
7
+ ## Tools
8
+ + ### PHP-like tools:
9
+ #### Constants
10
+ - Immutable constants manager. Define values that cannot be modified or deleted after creation.
11
+ #### InlinePython:
12
+ - Python renderer for inline code blocks in files. (similar to php with `<?py ... ?>`)
13
+
14
+ + ### AI-related tools:
15
+ #### LLM
16
+ - LLM API client supports OpenAI, Anthropic, xAI, and Gemini. Handles chat sessions, tool calling, and MCP server integration.
17
+ #### MCP
18
+ - A simple FastAPI extension/wrapper. Adds some logging and error handling built-in tools for convenience.
19
+
20
+ + ### Execution and Automation:
21
+ #### LimitedScript
22
+ - Executes Python scripts in a partially sandboxed environment with restricted builtins and controlled access to tools/classes.
23
+ #### Schedule
24
+ - Scheduler for interval, daily, and file-watch tasks with registry management.
25
+ #### Process
26
+ - Subprocess wrapper for running commands with configurable timeouts, environment, shell usage, and output trimming while returning structured results.
27
+
28
+ + ### Development Support:
29
+ #### Log
30
+ - Simple logging tool with Log.log(...) or automatic logging with decorators for functions.
31
+ #### Cache
32
+ - A simple function-based cache using decorators.
33
+ #### Profile
34
+ - Timing and profiling helper with context managers, decorators, and direct measurements plus optional memory tracking and stored records.
35
+
36
+
37
+ + ### Other Tools:
38
+ #### Files
39
+ - File system utilities with searching, reading, editing, directory operations, grep, etc.
40
+ #### Website
41
+ - Simple flask web server management tool using Gunicorn. Supports URL processing and template rendering.
42
+ #### API
43
+ - HTTP request helper with configurable presets for headers, endpoints, API keys, and quick call execution returning structured responses.
44
+ #### Git
45
+ - Local git wrapper for managing repositories, branches, commits, and remotes.
46
+ #### Security
47
+ - Cryptography helper for RSA/Symmetric encryption, hashing, and JWT management.
48
+ #### Database
49
+ - Unified interface for interacting with SQLite, PostgreSQL, MySQL, and DynamoDB.
50
+
51
+ ---
52
+
53
+ ## Config
54
+ Each tool has class-level defaults that can be overridden via `load_config()`. Pass a dictionary/JSON where keys are tool names and values are dictionaries of settings (Settings/Variables not provided will use the tool's built-in defaults.)
55
+
56
+ Example config structure:
57
+ ```
58
+ {
59
+ "Files": {
60
+ "MAX_DEPTH_DEFAULT": 42,
61
+ "PATH_SEP": "/"
62
+ }
63
+ }
64
+ ```
65
+ using `sylriekit.load_config(config:dict)` will load the config for all tools, to centeralize the configuration of multiple tools.
66
+
67
+ and using `sylriekit.generate_default_config(file_name: str, include_tools="*")` will generate a file that will contain all (or just selected ones with `include_tools=["ToolName1", ...]`) of the configurable values and their defaults.
68
+
69
+
70
+ ---
71
+
72
+ #### Other Stuff
73
+ - `sylriekit.tool_functions(tool_name: str) -> list[str]`: Returns all public functions available in a tool.
74
+ - `sylriekit.get_code(tool_name: str, function_name: str) -> str`: Returns the source code of a specific tool's function. Use `"*"` as the function name to get the entire source code of the tool.
75
+ - `sylriekit.help()`: Prints a short message for how to use the sylriekit's help-related functions.
76
+
77
+ ---
78
+
79
+ ### Change Log:
80
+ **0.15.6**:
81
+ - Fixed the description to remove an inaccurate statement about GitHub Copilot support with LLM.
82
+ + LLM Tool:
83
+ - Added support for having multiple MCP servers active simultaneously.
84
+ - Tools now prefer their original short names. Namespacing (`server__tool`) is automatically applied only when tool names collide between servers.
85
+ - Namespaced tools automatically revert to short names if the conflicting server is removed.
86
+ - Fixed tool execution to ensure the correct original tool name is sent to the MCP server, regardless of how it is named in the client.
87
+ + MCP tool:
88
+ - Added `check_previous_logs` tool to inspect logs from previous sessions.
89
+ - Added `add_debug_tools()` and `remove_debug_tools()` methods to dynamically toggle built-in debug tools (`health`, `recent_logs`, `recent_errors`, `check_previous_logs`).
90
+ - Added `DEBUG_TOOLS_ENABLED` configuration option to control default debug tool state.
91
+ - Fixed the MCP tool's naming to correctly work as MCP instead of Mcp
92
+
93
+
94
+ **0.15.2**:
95
+ - Added Change log to README.md
96
+ - Fixed the issue with the Files tool: `Files.read` incorrectly identifying file paths as directories.
97
+ - Fixed `Files.create`, `Files.write`, and `Files.append` to properly respect `Files.CWD` for relative paths.
98
+ - Updated `Files.read` to properly handle relative paths using `Files.CWD`.
99
+
100
+ **0.15.1**:
101
+ - Uploaded to PyPI in the current state.
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sylriekit"
7
+ version = "0.15.6"
8
+ description = "A personal Python toolbox of utilities."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = { file = "LICENSE" }
12
+ authors = [{ name = "Kasterfly" }]
13
+ dependencies = []
14
+
15
+ [tool.setuptools]
16
+ package-dir = { "" = "src" }
17
+ include-package-data = true
18
+
19
+ [tool.setuptools.packages.find]
20
+ where = ["src"]
21
+ include = ["sylriekit*"]
22
+
23
+ [tool.setuptools.package-data]
24
+ sylriekit = ["resources/**/*", "templates/**/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,362 @@
1
+ import json
2
+ import time
3
+
4
+ import requests
5
+
6
+
7
+ class API:
8
+ BYTES_IN_KB = 1024
9
+ MILLISECONDS_IN_SECOND = 1000
10
+ MIN_TIMEOUT_S = 1
11
+ MIN_RESPONSE_KB = 1
12
+
13
+ DEFAULT_TIMEOUT_S = 30
14
+ DEFAULT_MAX_RESPONSE_KB = 512
15
+ DEFAULT_VERIFY_SSL = True
16
+ DEFAULT_METHOD = "GET"
17
+ DEFAULT_BASE_URL = None
18
+ DEFAULT_HEADERS = {}
19
+ DEFAULT_PARAMS = {}
20
+ DEFAULT_API_KEY_HEADER = "Authorization"
21
+ DEFAULT_API_KEY_PREFIX = "Bearer "
22
+ DEFAULT_API_KEY_QUERY = None
23
+
24
+ API_KEYS = {}
25
+ PRESETS = {}
26
+
27
+ @classmethod
28
+ def load_config(cls, config: dict):
29
+ api_keys = config.get("api_keys") or config.get("api_key", {})
30
+ env_variables = config.get("env", {})
31
+ api_config = None
32
+ if "API" in config.keys():
33
+ api_config = config["API"]
34
+ elif "Api" in config.keys():
35
+ api_config = config["Api"]
36
+ cls.API_KEYS = api_keys if isinstance(api_keys, dict) else {}
37
+ if api_config is None:
38
+ return
39
+ cls.DEFAULT_TIMEOUT_S = cls._resolve_timeout(api_config.get("DEFAULT_TIMEOUT_S", cls.DEFAULT_TIMEOUT_S))
40
+ cls.DEFAULT_MAX_RESPONSE_KB = cls._resolve_max_kb(api_config.get("DEFAULT_MAX_RESPONSE_KB", cls.DEFAULT_MAX_RESPONSE_KB))
41
+ cls.DEFAULT_VERIFY_SSL = cls._resolve_bool(api_config.get("DEFAULT_VERIFY_SSL", cls.DEFAULT_VERIFY_SSL), cls.DEFAULT_VERIFY_SSL)
42
+ cls.DEFAULT_METHOD = cls._resolve_method(api_config.get("DEFAULT_METHOD", cls.DEFAULT_METHOD))
43
+ cls.DEFAULT_BASE_URL = api_config.get("DEFAULT_BASE_URL", cls.DEFAULT_BASE_URL)
44
+ cls.DEFAULT_HEADERS = cls._clone_map(api_config.get("DEFAULT_HEADERS", cls.DEFAULT_HEADERS))
45
+ cls.DEFAULT_PARAMS = cls._clone_map(api_config.get("DEFAULT_PARAMS", cls.DEFAULT_PARAMS))
46
+ cls.DEFAULT_API_KEY_HEADER = api_config.get("DEFAULT_API_KEY_HEADER", cls.DEFAULT_API_KEY_HEADER)
47
+ cls.DEFAULT_API_KEY_PREFIX = api_config.get("DEFAULT_API_KEY_PREFIX", cls.DEFAULT_API_KEY_PREFIX)
48
+ cls.DEFAULT_API_KEY_QUERY = api_config.get("DEFAULT_API_KEY_QUERY", cls.DEFAULT_API_KEY_QUERY)
49
+ cls.PRESETS = {}
50
+ presets_config = api_config.get("PRESETS", {})
51
+ if isinstance(presets_config, dict):
52
+ for name, preset in presets_config.items():
53
+ cls._load_preset_from_config(name, preset)
54
+
55
+ @classmethod
56
+ def configure(cls, default_timeout_s: int = None, default_max_response_kb: int = None, default_verify_ssl: bool = None, default_method: str = None, default_base_url: str = None, default_headers: dict = None, default_params: dict = None, default_api_key_header: str = None, default_api_key_prefix: str = None, default_api_key_query: str = None, api_keys: dict = None):
57
+ if default_timeout_s is not None:
58
+ cls.DEFAULT_TIMEOUT_S = cls._resolve_timeout(default_timeout_s)
59
+ if default_max_response_kb is not None:
60
+ cls.DEFAULT_MAX_RESPONSE_KB = cls._resolve_max_kb(default_max_response_kb)
61
+ if default_verify_ssl is not None:
62
+ cls.DEFAULT_VERIFY_SSL = cls._resolve_bool(default_verify_ssl, cls.DEFAULT_VERIFY_SSL)
63
+ if default_method is not None:
64
+ cls.DEFAULT_METHOD = cls._resolve_method(default_method)
65
+ if default_base_url is not None:
66
+ cls.DEFAULT_BASE_URL = default_base_url
67
+ if default_headers is not None:
68
+ cls.DEFAULT_HEADERS = cls._clone_map(default_headers)
69
+ if default_params is not None:
70
+ cls.DEFAULT_PARAMS = cls._clone_map(default_params)
71
+ if default_api_key_header is not None:
72
+ cls.DEFAULT_API_KEY_HEADER = default_api_key_header
73
+ if default_api_key_prefix is not None:
74
+ cls.DEFAULT_API_KEY_PREFIX = default_api_key_prefix
75
+ if default_api_key_query is not None:
76
+ cls.DEFAULT_API_KEY_QUERY = default_api_key_query
77
+ if api_keys is not None and isinstance(api_keys, dict):
78
+ cls.API_KEYS = cls._clone_map(api_keys)
79
+
80
+ @classmethod
81
+ def add(cls, name: str, base_url: str = None, endpoints: dict = None, headers: dict = None, params: dict = None, api_key: str = None, api_key_name: str = None, api_key_header: str = None, api_key_prefix: str = None, api_key_query: str = None, method: str = None, timeout_s: int = None, verify_ssl: bool = None, max_response_kb: int = None):
82
+ preset_name = cls._normalize_name(name)
83
+ if not preset_name:
84
+ raise ValueError("Preset name is required")
85
+ preset = {
86
+ "base_url": base_url if base_url is not None else cls.DEFAULT_BASE_URL,
87
+ "endpoints": cls._clone_map(endpoints) if endpoints is not None else {},
88
+ "headers": cls._clone_map(headers) if headers is not None else {},
89
+ "params": cls._clone_map(params) if params is not None else {},
90
+ "api_key": cls._resolve_api_key(api_key, api_key_name),
91
+ "api_key_header": api_key_header if api_key_header is not None else cls.DEFAULT_API_KEY_HEADER,
92
+ "api_key_prefix": api_key_prefix if api_key_prefix is not None else cls.DEFAULT_API_KEY_PREFIX,
93
+ "api_key_query": api_key_query if api_key_query is not None else cls.DEFAULT_API_KEY_QUERY,
94
+ "method": cls._resolve_method(method if method is not None else cls.DEFAULT_METHOD),
95
+ "timeout_s": cls._resolve_timeout(timeout_s if timeout_s is not None else cls.DEFAULT_TIMEOUT_S),
96
+ "verify_ssl": cls._resolve_bool(verify_ssl, cls.DEFAULT_VERIFY_SSL),
97
+ "max_response_kb": cls._resolve_max_kb(max_response_kb if max_response_kb is not None else cls.DEFAULT_MAX_RESPONSE_KB),
98
+ }
99
+ cls.PRESETS[preset_name] = preset
100
+ return preset_name
101
+
102
+ @classmethod
103
+ def remove(cls, name: str):
104
+ preset_name = cls._normalize_name(name)
105
+ if preset_name in cls.PRESETS:
106
+ del cls.PRESETS[preset_name]
107
+ return True
108
+ return False
109
+
110
+ @classmethod
111
+ def list_presets(cls) -> list:
112
+ return sorted(list(cls.PRESETS.keys()))
113
+
114
+ @classmethod
115
+ def call(cls, name: str, tool: str = None, path: str = None, method: str = None, params: dict = None, headers: dict = None, data=None, json_body=None, timeout_s: int = None, verify_ssl: bool = None, max_response_kb: int = None, url: str = None) -> dict:
116
+ preset_name = cls._normalize_name(name)
117
+ if preset_name not in cls.PRESETS:
118
+ raise ValueError(f"Preset '{name}' not found")
119
+ preset = cls.PRESETS[preset_name]
120
+ resolved_method = cls._resolve_method(method if method is not None else preset["method"])
121
+ resolved_timeout = cls._resolve_timeout(timeout_s if timeout_s is not None else preset["timeout_s"])
122
+ resolved_verify = cls._resolve_bool(verify_ssl, preset["verify_ssl"])
123
+ resolved_max_kb = cls._resolve_max_kb(max_response_kb if max_response_kb is not None else preset["max_response_kb"])
124
+ max_bytes = cls._resolve_max_bytes(resolved_max_kb)
125
+
126
+ target_url = url if url is not None else cls._build_url(preset, tool, path)
127
+ if not target_url:
128
+ raise ValueError("A target URL could not be resolved for this call")
129
+
130
+ merged_headers = cls._merge_maps(cls.DEFAULT_HEADERS, preset["headers"])
131
+ merged_params = cls._merge_maps(cls.DEFAULT_PARAMS, preset["params"])
132
+ if headers is not None:
133
+ merged_headers.update(cls._clone_map(headers))
134
+ if params is not None:
135
+ merged_params.update(cls._clone_map(params))
136
+ cls._apply_api_key(preset, merged_headers, merged_params)
137
+
138
+ start = cls._now()
139
+ try:
140
+ response = requests.request(
141
+ method=resolved_method,
142
+ url=target_url,
143
+ params=merged_params if merged_params else None,
144
+ headers=merged_headers if merged_headers else None,
145
+ timeout=resolved_timeout,
146
+ verify=resolved_verify,
147
+ json=json_body,
148
+ data=data
149
+ )
150
+ duration_ms = cls._duration_ms(start)
151
+ content_bytes = response.content if response.content is not None else b""
152
+ trimmed_bytes = cls._trim_body(content_bytes, max_bytes)
153
+ encoding = response.encoding or "utf-8"
154
+ body_text = cls._decode_body(trimmed_bytes, encoding)
155
+ parsed_json = None
156
+ content_type = response.headers.get("Content-Type", "") if response.headers else ""
157
+ if "json" in content_type.lower():
158
+ parsed_json = cls._parse_json(trimmed_bytes, encoding)
159
+ return {
160
+ "url": target_url,
161
+ "method": resolved_method,
162
+ "status_code": response.status_code,
163
+ "ok": response.ok,
164
+ "duration_ms": duration_ms,
165
+ "headers": dict(response.headers),
166
+ "body": body_text,
167
+ "json": parsed_json,
168
+ }
169
+ except requests.exceptions.Timeout as exc:
170
+ duration_ms = cls._duration_ms(start)
171
+ return {
172
+ "url": target_url,
173
+ "method": resolved_method,
174
+ "status_code": None,
175
+ "ok": False,
176
+ "duration_ms": duration_ms,
177
+ "error": str(exc),
178
+ "timeout": True
179
+ }
180
+ except requests.exceptions.RequestException as exc:
181
+ duration_ms = cls._duration_ms(start)
182
+ return {
183
+ "url": target_url,
184
+ "method": resolved_method,
185
+ "status_code": getattr(exc.response, "status_code", None),
186
+ "ok": False,
187
+ "duration_ms": duration_ms,
188
+ "error": str(exc),
189
+ "timeout": False
190
+ }
191
+
192
+ ### PRIVATE UTILITIES START
193
+ @classmethod
194
+ def _load_preset_from_config(cls, name: str, preset_config: dict):
195
+ api_key = None
196
+ api_key_name = None
197
+ if isinstance(preset_config, dict):
198
+ api_key = preset_config.get("API_KEY")
199
+ api_key_name = preset_config.get("API_KEY_NAME")
200
+ cls.add(
201
+ name=name,
202
+ base_url=preset_config.get("BASE_URL") if isinstance(preset_config, dict) else None,
203
+ endpoints=preset_config.get("ENDPOINTS") if isinstance(preset_config, dict) else None,
204
+ headers=preset_config.get("HEADERS") if isinstance(preset_config, dict) else None,
205
+ params=preset_config.get("PARAMS") if isinstance(preset_config, dict) else None,
206
+ api_key=api_key,
207
+ api_key_name=api_key_name,
208
+ api_key_header=preset_config.get("API_KEY_HEADER") if isinstance(preset_config, dict) else None,
209
+ api_key_prefix=preset_config.get("API_KEY_PREFIX") if isinstance(preset_config, dict) else None,
210
+ api_key_query=preset_config.get("API_KEY_QUERY") if isinstance(preset_config, dict) else None,
211
+ method=preset_config.get("METHOD") if isinstance(preset_config, dict) else None,
212
+ timeout_s=preset_config.get("TIMEOUT_S") if isinstance(preset_config, dict) else None,
213
+ verify_ssl=preset_config.get("VERIFY_SSL") if isinstance(preset_config, dict) else None,
214
+ max_response_kb=preset_config.get("MAX_RESPONSE_KB") if isinstance(preset_config, dict) else None,
215
+ )
216
+
217
+ @classmethod
218
+ def _normalize_name(cls, name: str) -> str:
219
+ if name is None:
220
+ return None
221
+ try:
222
+ return str(name).strip()
223
+ except Exception:
224
+ return None
225
+
226
+ @classmethod
227
+ def _resolve_api_key(cls, api_key: str, api_key_name: str):
228
+ if api_key is not None:
229
+ return api_key
230
+ if api_key_name and isinstance(cls.API_KEYS, dict):
231
+ return cls.API_KEYS.get(api_key_name)
232
+ return None
233
+
234
+ @classmethod
235
+ def _build_url(cls, preset: dict, tool: str, path: str):
236
+ base = preset.get("base_url") or cls.DEFAULT_BASE_URL
237
+ if path:
238
+ return cls._join_url(base, path)
239
+ if tool:
240
+ endpoints = preset.get("endpoints", {})
241
+ endpoint = endpoints.get(tool)
242
+ if endpoint:
243
+ return cls._join_url(base, endpoint)
244
+ return base
245
+
246
+ @classmethod
247
+ def _join_url(cls, base: str, endpoint: str):
248
+ if not base:
249
+ return endpoint
250
+ if endpoint is None:
251
+ return base
252
+ base_trimmed = base[:-1] if base.endswith("/") else base
253
+ endpoint_trimmed = endpoint[1:] if endpoint.startswith("/") else endpoint
254
+ return base_trimmed + "/" + endpoint_trimmed
255
+
256
+ @classmethod
257
+ def _merge_maps(cls, first: dict, second: dict) -> dict:
258
+ merged = cls._clone_map(first)
259
+ merged.update(cls._clone_map(second))
260
+ return merged
261
+
262
+ @classmethod
263
+ def _clone_map(cls, value):
264
+ if isinstance(value, dict):
265
+ return dict(value)
266
+ return {}
267
+
268
+ @classmethod
269
+ def _apply_api_key(cls, preset: dict, headers: dict, params: dict):
270
+ key = preset.get("api_key")
271
+ if not key:
272
+ return
273
+ header_name = preset.get("api_key_header")
274
+ query_param = preset.get("api_key_query")
275
+ prefix = preset.get("api_key_prefix") or ""
276
+ if query_param:
277
+ params[query_param] = key
278
+ return
279
+ if header_name:
280
+ headers[header_name] = f"{prefix}{key}" if prefix else key
281
+
282
+ @classmethod
283
+ def _trim_body(cls, content: bytes, max_bytes: int):
284
+ if max_bytes is None or content is None:
285
+ return content
286
+ if len(content) <= max_bytes:
287
+ return content
288
+ return content[:max_bytes]
289
+
290
+ @classmethod
291
+ def _decode_body(cls, content: bytes, encoding: str) -> str:
292
+ if content is None:
293
+ return ""
294
+ try:
295
+ return content.decode(encoding or "utf-8", errors="replace")
296
+ except Exception:
297
+ return ""
298
+
299
+ @classmethod
300
+ def _parse_json(cls, content: bytes, encoding: str):
301
+ try:
302
+ text = cls._decode_body(content, encoding)
303
+ return json.loads(text)
304
+ except Exception:
305
+ return None
306
+
307
+ @classmethod
308
+ def _resolve_timeout(cls, value):
309
+ seconds = cls._safe_int(value, cls.DEFAULT_TIMEOUT_S)
310
+ if seconds is None:
311
+ return None
312
+ if seconds < cls.MIN_TIMEOUT_S:
313
+ seconds = cls.MIN_TIMEOUT_S
314
+ return seconds
315
+
316
+ @classmethod
317
+ def _resolve_max_kb(cls, value):
318
+ kilobytes = cls._safe_int(value, cls.DEFAULT_MAX_RESPONSE_KB)
319
+ if kilobytes is None:
320
+ return None
321
+ if kilobytes < cls.MIN_RESPONSE_KB:
322
+ kilobytes = cls.MIN_RESPONSE_KB
323
+ return kilobytes
324
+
325
+ @classmethod
326
+ def _resolve_max_bytes(cls, kilobytes):
327
+ if kilobytes is None:
328
+ return None
329
+ return kilobytes * cls.BYTES_IN_KB
330
+
331
+ @classmethod
332
+ def _resolve_method(cls, value: str):
333
+ if not value:
334
+ return cls.DEFAULT_METHOD
335
+ try:
336
+ return str(value).upper()
337
+ except Exception:
338
+ return cls.DEFAULT_METHOD
339
+
340
+ @classmethod
341
+ def _resolve_bool(cls, value, default_value: bool):
342
+ if isinstance(value, bool):
343
+ return value
344
+ if value is None:
345
+ return default_value
346
+ return bool(value)
347
+
348
+ @classmethod
349
+ def _safe_int(cls, value, default_value):
350
+ try:
351
+ return int(value)
352
+ except (TypeError, ValueError):
353
+ return default_value
354
+
355
+ @classmethod
356
+ def _duration_ms(cls, start_ts: float) -> float:
357
+ return (cls._now() - start_ts) * cls.MILLISECONDS_IN_SECOND
358
+
359
+ @classmethod
360
+ def _now(cls) -> float:
361
+ return time.time()
362
+ ### PRIVATE UTILITIES END