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.
- sylriekit-0.15.6/LICENSE +21 -0
- sylriekit-0.15.6/PKG-INFO +133 -0
- sylriekit-0.15.6/README.md +101 -0
- sylriekit-0.15.6/pyproject.toml +24 -0
- sylriekit-0.15.6/setup.cfg +4 -0
- sylriekit-0.15.6/src/sylriekit/API.py +362 -0
- sylriekit-0.15.6/src/sylriekit/Cache.py +131 -0
- sylriekit-0.15.6/src/sylriekit/Constants.py +79 -0
- sylriekit-0.15.6/src/sylriekit/Database.py +138 -0
- sylriekit-0.15.6/src/sylriekit/Files.py +934 -0
- sylriekit-0.15.6/src/sylriekit/Git.py +103 -0
- sylriekit-0.15.6/src/sylriekit/InlinePython.py +222 -0
- sylriekit-0.15.6/src/sylriekit/LLM.py +859 -0
- sylriekit-0.15.6/src/sylriekit/LimitedScript.py +105 -0
- sylriekit-0.15.6/src/sylriekit/Log.py +260 -0
- sylriekit-0.15.6/src/sylriekit/Mcp.py +608 -0
- sylriekit-0.15.6/src/sylriekit/Process.py +230 -0
- sylriekit-0.15.6/src/sylriekit/Profiler.py +226 -0
- sylriekit-0.15.6/src/sylriekit/Schedule.py +545 -0
- sylriekit-0.15.6/src/sylriekit/Security.py +155 -0
- sylriekit-0.15.6/src/sylriekit/Website.py +397 -0
- sylriekit-0.15.6/src/sylriekit/__init__.py +118 -0
- sylriekit-0.15.6/src/sylriekit.egg-info/PKG-INFO +133 -0
- sylriekit-0.15.6/src/sylriekit.egg-info/SOURCES.txt +25 -0
- sylriekit-0.15.6/src/sylriekit.egg-info/dependency_links.txt +1 -0
- sylriekit-0.15.6/src/sylriekit.egg-info/top_level.txt +1 -0
sylriekit-0.15.6/LICENSE
ADDED
|
@@ -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,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
|