modulith 0.1.0__py3-none-any.whl
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.
- modulith/__init__.py +1 -0
- modulith/cli/login.py +16 -0
- modulith/cli/logout.py +10 -0
- modulith/cli/main.py +18 -0
- modulith/cli/module/resolve.py +16 -0
- modulith/cli/module/workload.py +20 -0
- modulith/cli/serve.py +179 -0
- modulith/cli/signup.py +20 -0
- modulith/cli/whoami.py +19 -0
- modulith/dataclasses/__init__.py +1 -0
- modulith/dataclasses/capability.py +176 -0
- modulith/global_variables.py +1 -0
- modulith/mns/__init__.py +0 -0
- modulith/mns/guest_signup.py +13 -0
- modulith/mns/login.py +11 -0
- modulith/mns/module/__init__.py +2 -0
- modulith/mns/module/register.py +17 -0
- modulith/mns/module/resolve.py +13 -0
- modulith/mns/module/workload.py +16 -0
- modulith/mns/signup.py +22 -0
- modulith/mns/whoami.py +10 -0
- modulith/modulith.py +93 -0
- modulith/server.py +400 -0
- modulith/utils/__init__.py +3 -0
- modulith/utils/cache_manager.py +24 -0
- modulith/utils/connections/__init__.py +1 -0
- modulith/utils/connections/wait_for_connection.py +25 -0
- modulith/utils/hashing.py +3 -0
- modulith/utils/rate_limiters/__init__.py +1 -0
- modulith/utils/rate_limiters/token_bucket_limiter.py +94 -0
- modulith/utils/safe_min.py +9 -0
- modulith/utils/token_managers/__init__.py +1 -0
- modulith/utils/token_managers/secret_token_manager.py +29 -0
- modulith/utils/tunnels/__init__.py +33 -0
- modulith/utils/tunnels/cloudflare.py +24 -0
- modulith/utils/tunnels/instatunnel.py +51 -0
- modulith/utils/tunnels/ngrok.py +19 -0
- modulith-0.1.0.dist-info/METADATA +166 -0
- modulith-0.1.0.dist-info/RECORD +105 -0
- modulith-0.1.0.dist-info/WHEEL +5 -0
- modulith-0.1.0.dist-info/entry_points.txt +2 -0
- modulith-0.1.0.dist-info/licenses/LICENSE +201 -0
- modulith-0.1.0.dist-info/top_level.txt +3 -0
- modulith-integrations/embeddings/openai/modulith/modules/embeddings/openai/__init__.py +1 -0
- modulith-integrations/embeddings/openai/modulith/modules/embeddings/openai/module.py +20 -0
- modulith-integrations/embeddings/openai/test/import.py +1 -0
- modulith-integrations/embeddings/openai-compatible/modulith/modules/embeddings/openai_compatible/__init__.py +1 -0
- modulith-integrations/embeddings/openai-compatible/modulith/modules/embeddings/openai_compatible/module.py +6 -0
- modulith-integrations/embeddings/openai-compatible/test/import.py +1 -0
- modulith-integrations/embeddings/openrouter/modulith/modules/embeddings/openrouter/__init__.py +1 -0
- modulith-integrations/embeddings/openrouter/modulith/modules/embeddings/openrouter/module.py +12 -0
- modulith-integrations/embeddings/openrouter/test/import.py +1 -0
- modulith-integrations/image_generation/xai/modulith/modules/image_generation/xai/__init__.py +1 -0
- modulith-integrations/image_generation/xai/modulith/modules/image_generation/xai/module.py +32 -0
- modulith-integrations/image_generation/xai/test/test.py +24 -0
- modulith-integrations/index/bm25/modulith/modules/index/bm25/__init__.py +1 -0
- modulith-integrations/index/bm25/modulith/modules/index/bm25/module.py +58 -0
- modulith-integrations/index/chroma/modulith/modules/index/chroma/__init__.py +1 -0
- modulith-integrations/index/chroma/modulith/modules/index/chroma/module.py +52 -0
- modulith-integrations/index/faiss/modulith/modules/indices/faiss/__init__.py +1 -0
- modulith-integrations/index/faiss/modulith/modules/indices/faiss/module.py +161 -0
- modulith-integrations/llm/openai/modulith/modules/llm/openai/__init__.py +3 -0
- modulith-integrations/llm/openai/modulith/modules/llm/openai/chat_module.py +23 -0
- modulith-integrations/llm/openai/modulith/modules/llm/openai/completion_module.py +21 -0
- modulith-integrations/llm/openai/modulith/modules/llm/openai/response_module.py +21 -0
- modulith-integrations/llm/openai/test/import.py +1 -0
- modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/__init__.py +3 -0
- modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/chat_module.py +6 -0
- modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/completion_module.py +6 -0
- modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/response_module.py +6 -0
- modulith-integrations/llm/openai-compatible/test/import.py +1 -0
- modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/__init__.py +3 -0
- modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/chat_module.py +12 -0
- modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/completion_module.py +12 -0
- modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/response_module.py +12 -0
- modulith-integrations/llm/openrouter/test/import.py +1 -0
- modulith-integrations/llm/xai/modulith/modules/llm/xai/__init__.py +1 -0
- modulith-integrations/llm/xai/modulith/modules/llm/xai/module.py +38 -0
- modulith-integrations/llm/xai/test/test.py +16 -0
- modulith-integrations/tts/xai/modulith/modules/tts/xai/__init__.py +1 -0
- modulith-integrations/tts/xai/modulith/modules/tts/xai/module.py +40 -0
- modulith-integrations/tts/xai/test/test.py +16 -0
- modulith-integrations/video_generation/xai/modulith/modules/video_generation/xai/__init__.py +1 -0
- modulith-integrations/video_generation/xai/modulith/modules/video_generation/xai/module.py +33 -0
- modulith-integrations/video_generation/xai/test/test.py +25 -0
- modulith-modules/modulith/modules/agent/__init__.py +1 -0
- modulith-modules/modulith/modules/agent/generative_agent_module.py +70 -0
- modulith-modules/modulith/modules/embeddings/auto/__init__.py +1 -0
- modulith-modules/modulith/modules/embeddings/auto/auto_module.py +37 -0
- modulith-modules/modulith/modules/embeddings/auto/const.py +4 -0
- modulith-modules/modulith/modules/embeddings/base_classes/__init__.py +1 -0
- modulith-modules/modulith/modules/embeddings/base_classes/base_module.py +7 -0
- modulith-modules/modulith/modules/embeddings/datatypes.py +4 -0
- modulith-modules/modulith/modules/index/base_classes/__init__.py +1 -0
- modulith-modules/modulith/modules/index/base_classes/base_module.py +59 -0
- modulith-modules/modulith/modules/llm/auto/__init__.py +1 -0
- modulith-modules/modulith/modules/llm/auto/auto_module.py +65 -0
- modulith-modules/modulith/modules/llm/auto/const.py +6 -0
- modulith-modules/modulith/modules/llm/base_classes/__init__.py +1 -0
- modulith-modules/modulith/modules/llm/base_classes/base_module.py +18 -0
- modulith-modules/modulith/modules/llm/datatypes.py +4 -0
- modulith-modules/modulith/modules/reranking/reciprocal_rank_fusion/__init__.py +1 -0
- modulith-modules/modulith/modules/reranking/reciprocal_rank_fusion/module.py +36 -0
- modulith-modules/modulith/modules/tool/__init__.py +1 -0
- modulith-modules/modulith/modules/tool/document_search_module.py +52 -0
modulith/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .modulith import Modulith
|
modulith/cli/login.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from modulith.mns.login import login
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
login_app = typer.Typer()
|
|
6
|
+
|
|
7
|
+
@login_app.command("login")
|
|
8
|
+
def login_command():
|
|
9
|
+
auth_token = typer.prompt("Authentication Token")
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
username = login(auth_token)
|
|
13
|
+
typer.echo("✅ Login successful")
|
|
14
|
+
typer.echo(f"Username: {username}")
|
|
15
|
+
except ValueError as e:
|
|
16
|
+
typer.echo(e)
|
modulith/cli/logout.py
ADDED
modulith/cli/main.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from .signup import singup_app
|
|
3
|
+
from .login import login_app
|
|
4
|
+
from .logout import logout_app
|
|
5
|
+
from .whoami import whoami_app
|
|
6
|
+
from .serve import serve_app
|
|
7
|
+
from .module.resolve import resolve_module_app
|
|
8
|
+
from .module.workload import workload_module_app
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
app.add_typer(login_app)
|
|
13
|
+
app.add_typer(logout_app)
|
|
14
|
+
app.add_typer(singup_app)
|
|
15
|
+
app.add_typer(whoami_app)
|
|
16
|
+
app.add_typer(serve_app)
|
|
17
|
+
app.add_typer(resolve_module_app, name="module")
|
|
18
|
+
app.add_typer(workload_module_app, name="module")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from modulith.mns.module.resolve import resolve_module
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
resolve_module_app = typer.Typer()
|
|
6
|
+
|
|
7
|
+
@resolve_module_app.command("resolve")
|
|
8
|
+
def resolve_command():
|
|
9
|
+
repository = typer.prompt("Repository")
|
|
10
|
+
token = typer.prompt("Authentication Token", hide_input=True)
|
|
11
|
+
validate_url = typer.confirm("Validate URL?", default=True)
|
|
12
|
+
try:
|
|
13
|
+
urls = resolve_module(repository, token, validate_url)
|
|
14
|
+
typer.echo(f"Module URLs: {urls}")
|
|
15
|
+
except ValueError as e:
|
|
16
|
+
typer.echo(f"Failed to resolve module: {str(e)}")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from modulith.mns.module.workload import workload_module
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
workload_module_app = typer.Typer()
|
|
6
|
+
|
|
7
|
+
@workload_module_app.command("workload")
|
|
8
|
+
def workload_command(
|
|
9
|
+
repo: str = typer.Argument(..., help="Repository"),
|
|
10
|
+
token: str = typer.Option(None, help="Authentication Token", prompt=True, hide_input=True),
|
|
11
|
+
validate_url: bool = typer.Option(True, help="Validate URL?"),
|
|
12
|
+
):
|
|
13
|
+
repo = typer.prompt("Repository") if not repo else repo
|
|
14
|
+
token = typer.prompt("Authentication Token", hide_input=True) if not token else token
|
|
15
|
+
validate_url = typer.confirm("Validate URL?", default=True) if validate_url is None else validate_url
|
|
16
|
+
try:
|
|
17
|
+
results = workload_module(repo, token, validate_url)
|
|
18
|
+
typer.echo(f"Module Workload: {results}")
|
|
19
|
+
except ValueError as e:
|
|
20
|
+
typer.echo(f"Failed to retrieve workload: {str(e)}")
|
modulith/cli/serve.py
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import typer
|
|
4
|
+
import keyring
|
|
5
|
+
import importlib
|
|
6
|
+
from typing import Literal
|
|
7
|
+
from modulith import Modulith
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
|
+
from modulith.utils.tunnels import start_tunnel
|
|
10
|
+
from modulith.server import create_fastapi_server
|
|
11
|
+
from modulith.mns.guest_signup import guest_signup
|
|
12
|
+
from modulith.mns.module.register import register_module
|
|
13
|
+
from modulith.utils.connections import wait_for_connection
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_module(module_ref: str):
|
|
17
|
+
variable_name = None
|
|
18
|
+
if ":" in module_ref:
|
|
19
|
+
module_ref, variable_name = module_ref.split(":")
|
|
20
|
+
|
|
21
|
+
# Add the current working directory to sys.path if it's not already there
|
|
22
|
+
if os.getcwd() not in sys.path:
|
|
23
|
+
sys.path.insert(0, os.getcwd())
|
|
24
|
+
|
|
25
|
+
mod = importlib.import_module(module_ref)
|
|
26
|
+
if variable_name is None:
|
|
27
|
+
for name in ("module", "app", "service", "tool"):
|
|
28
|
+
if hasattr(mod, name):
|
|
29
|
+
variable_name = name
|
|
30
|
+
break
|
|
31
|
+
|
|
32
|
+
if variable_name is None:
|
|
33
|
+
raise ValueError(f"Module reference '{module_ref}' does not specify a variable name and no default variable ('module', 'app', 'service', or 'tool') was found. Please specify the variable name explicitly, e.g. '{module_ref}:variable_name'.")
|
|
34
|
+
|
|
35
|
+
module = getattr(mod, variable_name)
|
|
36
|
+
if isinstance(module, Modulith):
|
|
37
|
+
return module
|
|
38
|
+
|
|
39
|
+
raise ValueError(f"Module '{module_ref}' is not an instance of Modulith.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
serve_app = typer.Typer()
|
|
43
|
+
@serve_app.command("serve")
|
|
44
|
+
def serve_command(
|
|
45
|
+
module_path: Annotated[
|
|
46
|
+
str,
|
|
47
|
+
typer.Argument(
|
|
48
|
+
...,
|
|
49
|
+
help="Python module path, e.g. 'calculator.module'",
|
|
50
|
+
),
|
|
51
|
+
],
|
|
52
|
+
port: Annotated[
|
|
53
|
+
int,
|
|
54
|
+
typer.Option("--port", help="Port to bind the server to."),
|
|
55
|
+
] = 8000,
|
|
56
|
+
host: Annotated[
|
|
57
|
+
str,
|
|
58
|
+
typer.Option("--host", help="Host to bind the server to. Use 'localhost' for tunnel providers to work properly."),
|
|
59
|
+
] = "localhost",
|
|
60
|
+
tunnel: Annotated[
|
|
61
|
+
Optional[Literal["instatunnel", "cloudflare", "ngrok", "any"]],
|
|
62
|
+
typer.Option("--tunnel", help="Tunnel provider to use for public exposure. Options: 'instatunnel', 'cloudflare', 'ngrok', 'any'. If not specified, the server will not be exposed publicly."),
|
|
63
|
+
] = "any",
|
|
64
|
+
public: Annotated[
|
|
65
|
+
bool,
|
|
66
|
+
typer.Option("--public", help="Expose the module publicly."),
|
|
67
|
+
] = False,
|
|
68
|
+
burst_limit: Annotated[
|
|
69
|
+
Optional[int],
|
|
70
|
+
typer.Option("--burst-limit", help="Maximum number of concurrent requests allowed for public access."),
|
|
71
|
+
] = None,
|
|
72
|
+
rate_limit: Annotated[
|
|
73
|
+
Optional[int],
|
|
74
|
+
typer.Option("--rate-limit", help="Maximum number of requests per minute allowed for public access."),
|
|
75
|
+
] = None,
|
|
76
|
+
max_queue_size: Annotated[
|
|
77
|
+
int,
|
|
78
|
+
typer.Option("--max-queue-size", help="Maximum number of requests to queue when the server is under heavy load. Requests beyond this limit will be rejected with a 503 error."),
|
|
79
|
+
] = 10000,
|
|
80
|
+
num_workers: Annotated[
|
|
81
|
+
Optional[int],
|
|
82
|
+
typer.Option("--num-workers", help="Number of worker processes to handle requests. If not specified, it will default to the number of CPU cores."),
|
|
83
|
+
] = None,
|
|
84
|
+
timeout: Annotated[
|
|
85
|
+
Optional[float],
|
|
86
|
+
typer.Option("--timeout", help="Maximum time in seconds to allow a request to be processed before timing out. If not specified, there will be no timeout."),
|
|
87
|
+
] = None,
|
|
88
|
+
logging_path: Annotated[
|
|
89
|
+
str,
|
|
90
|
+
typer.Option("--logging-path", help="Path to the log file for the server. Logs will be rotated daily."),
|
|
91
|
+
] = "logs/server.log",
|
|
92
|
+
auth_token: Annotated[
|
|
93
|
+
Optional[str],
|
|
94
|
+
typer.Option("--auth-token", help="Your authentication token. If not provided, a token will be generated and displayed in the console."),
|
|
95
|
+
] = None,
|
|
96
|
+
no_tunnel: Annotated[
|
|
97
|
+
bool,
|
|
98
|
+
typer.Option("--no-tunnel", help="Do not establish a tunnel even if the --tunnel option is specified. This is useful for testing the server without exposing it publicly."),
|
|
99
|
+
] = False,
|
|
100
|
+
skip_registration: Annotated[
|
|
101
|
+
bool,
|
|
102
|
+
typer.Option("--skip-registration", help="Skip registering the module to the Modulith Network Service."),
|
|
103
|
+
] = False,
|
|
104
|
+
):
|
|
105
|
+
module: Modulith = load_module(module_path)
|
|
106
|
+
|
|
107
|
+
local_url = f"http://{host}:{port}"
|
|
108
|
+
thread = create_fastapi_server(
|
|
109
|
+
module=module,
|
|
110
|
+
host=host,
|
|
111
|
+
port=port,
|
|
112
|
+
auth_token=auth_token,
|
|
113
|
+
public=public,
|
|
114
|
+
burst_limit=burst_limit,
|
|
115
|
+
rate_limit=rate_limit,
|
|
116
|
+
max_queue_size=max_queue_size,
|
|
117
|
+
num_workers=num_workers,
|
|
118
|
+
timeout=timeout,
|
|
119
|
+
logging_path=logging_path,
|
|
120
|
+
)
|
|
121
|
+
# Wait for server to be up
|
|
122
|
+
if not wait_for_connection(local_url):
|
|
123
|
+
raise RuntimeError(f"❌ Failed to start server at port {port}")
|
|
124
|
+
|
|
125
|
+
public_url = None
|
|
126
|
+
repository = None
|
|
127
|
+
if not no_tunnel:
|
|
128
|
+
typer.echo("🚀 Establishing tunnel...")
|
|
129
|
+
tunnel_instance = start_tunnel(local_port=port, provider=tunnel)
|
|
130
|
+
public_url = tunnel_instance.public_url
|
|
131
|
+
|
|
132
|
+
guest_token = None
|
|
133
|
+
if not skip_registration and public_url:
|
|
134
|
+
typer.echo("🚀 Authenticating...")
|
|
135
|
+
auth_token = auth_token or os.environ.get("MODULITH_API_TOKEN") or keyring.get_password("modulith", "api_key")
|
|
136
|
+
if auth_token is None:
|
|
137
|
+
auth_token = guest_signup()
|
|
138
|
+
guest_token = auth_token
|
|
139
|
+
|
|
140
|
+
typer.echo("🚀 Registering module...")
|
|
141
|
+
repository = register_module(modulename=module.name, public_url=public_url, public=public, token=auth_token)
|
|
142
|
+
|
|
143
|
+
if guest_token:
|
|
144
|
+
typer.echo()
|
|
145
|
+
typer.echo(f"🔑 Your authentication token: {guest_token}")
|
|
146
|
+
|
|
147
|
+
typer.echo()
|
|
148
|
+
typer.echo(f"🌐 Module '{module.name}' is live:")
|
|
149
|
+
if repository:
|
|
150
|
+
typer.echo(f"- Repository: {repository}")
|
|
151
|
+
if public_url:
|
|
152
|
+
typer.echo(f"- Public URL: {public_url}")
|
|
153
|
+
typer.echo(f"- Local URL: {local_url}")
|
|
154
|
+
typer.echo()
|
|
155
|
+
|
|
156
|
+
typer.echo("⚙️ Capabilities:")
|
|
157
|
+
for name, capability in module.capabilities.items():
|
|
158
|
+
typer.echo(f"- {name}{capability.signature}")
|
|
159
|
+
typer.echo()
|
|
160
|
+
|
|
161
|
+
if public and (burst_limit or rate_limit):
|
|
162
|
+
typer.echo("⚡ Public access limits:")
|
|
163
|
+
if rate_limit:
|
|
164
|
+
typer.echo(f"- Rate limit: {rate_limit} requests per minute")
|
|
165
|
+
if burst_limit:
|
|
166
|
+
typer.echo(f"- Burst limit: {burst_limit} concurrent requests")
|
|
167
|
+
typer.echo()
|
|
168
|
+
|
|
169
|
+
typer.echo("🛑 Press Ctrl+C to stop")
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
while thread.is_alive():
|
|
173
|
+
thread.join(timeout=1)
|
|
174
|
+
except KeyboardInterrupt:
|
|
175
|
+
print("🛑 Shutting down...")
|
|
176
|
+
if tunnel_instance:
|
|
177
|
+
tunnel_instance.stop()
|
|
178
|
+
# TODO: deregister model from MNS
|
|
179
|
+
# print("✅ Shutdown complete")
|
modulith/cli/signup.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from modulith.mns.signup import sign_up
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
singup_app = typer.Typer()
|
|
6
|
+
|
|
7
|
+
@singup_app.command("signup")
|
|
8
|
+
def signup_command():
|
|
9
|
+
email = typer.prompt("Email")
|
|
10
|
+
username = typer.prompt("Username")
|
|
11
|
+
password = typer.prompt("Password", hide_input=True)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
token = sign_up(email, username, password)
|
|
15
|
+
typer.echo("✅ Signup successful")
|
|
16
|
+
typer.echo(f"Email: {email}")
|
|
17
|
+
typer.echo(f"Username: {username}")
|
|
18
|
+
typer.echo(f"Authentication key: {token}")
|
|
19
|
+
except ValueError as e:
|
|
20
|
+
typer.echo(f"Failed to signup: {str(e)}")
|
modulith/cli/whoami.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import keyring
|
|
3
|
+
from modulith.mns.whoami import whoami
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
whoami_app = typer.Typer()
|
|
7
|
+
|
|
8
|
+
@whoami_app.command("whoami")
|
|
9
|
+
def whoami_command():
|
|
10
|
+
api_key = keyring.get_password("modulith", "api_key")
|
|
11
|
+
if not api_key:
|
|
12
|
+
typer.echo("No authentication token found. Please login first.")
|
|
13
|
+
raise typer.Exit(code=1)
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
username = whoami(api_key)
|
|
17
|
+
typer.echo(f"Authenticated as: {username}")
|
|
18
|
+
except ValueError as e:
|
|
19
|
+
typer.echo(e)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .capability import Capability, LocalCapability, RemoteCapability
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import atexit
|
|
3
|
+
import inspect
|
|
4
|
+
import requests
|
|
5
|
+
import threading
|
|
6
|
+
from typing import Callable, Any
|
|
7
|
+
from modulith.mns.module import resolve_module
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Capability:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
name: str,
|
|
14
|
+
signature: str,
|
|
15
|
+
docstring: str,
|
|
16
|
+
allow_cache: bool = True,
|
|
17
|
+
max_concurrency: int | None = None,
|
|
18
|
+
):
|
|
19
|
+
self.name = name
|
|
20
|
+
self.signature = signature
|
|
21
|
+
self.docstring = docstring
|
|
22
|
+
self.allow_cache = allow_cache
|
|
23
|
+
self.max_concurrency = max_concurrency
|
|
24
|
+
|
|
25
|
+
def execute(self, kwargs: dict) -> Any:
|
|
26
|
+
raise NotImplementedError("Subclasses must implement the execute method.")
|
|
27
|
+
|
|
28
|
+
class LocalCapability(Capability):
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
name: str,
|
|
32
|
+
func: Callable[[dict], Any],
|
|
33
|
+
allow_cache: bool = True,
|
|
34
|
+
max_concurrency: int | None = None,
|
|
35
|
+
):
|
|
36
|
+
super().__init__(
|
|
37
|
+
name=name,
|
|
38
|
+
signature=str(inspect.signature(func)),
|
|
39
|
+
docstring=inspect.getdoc(func),
|
|
40
|
+
allow_cache=allow_cache,
|
|
41
|
+
max_concurrency=max_concurrency,
|
|
42
|
+
)
|
|
43
|
+
self.func = func
|
|
44
|
+
|
|
45
|
+
def execute(self, kwargs: dict | None = None) -> Any:
|
|
46
|
+
kwargs = kwargs or {}
|
|
47
|
+
return self.func(**kwargs)
|
|
48
|
+
|
|
49
|
+
class RemoteCapability(Capability):
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
name: str,
|
|
53
|
+
repository: str,
|
|
54
|
+
signature: str,
|
|
55
|
+
docstring: str,
|
|
56
|
+
urls: list[str],
|
|
57
|
+
allow_cache: bool,
|
|
58
|
+
access_token: str,
|
|
59
|
+
):
|
|
60
|
+
super().__init__(
|
|
61
|
+
name=name,
|
|
62
|
+
signature=signature,
|
|
63
|
+
docstring=docstring,
|
|
64
|
+
allow_cache=allow_cache,
|
|
65
|
+
max_concurrency=1,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.repository = repository
|
|
69
|
+
self.access_token = access_token
|
|
70
|
+
self.urls = urls
|
|
71
|
+
|
|
72
|
+
self.running_index = 0 # Index for round-robin load balancing
|
|
73
|
+
self.last_check_time = time.monotonic() # Timestamp of the last URLs and quota check
|
|
74
|
+
|
|
75
|
+
self.shutdown_event = threading.Event()
|
|
76
|
+
self.wake_event = threading.Event()
|
|
77
|
+
|
|
78
|
+
self.routine_worker = threading.Thread(target=self._routine_loop, daemon=True)
|
|
79
|
+
self.routine_worker.start()
|
|
80
|
+
self.wake_event.set() # Trigger the routine loop to perform the initial URLs and quota check immediately
|
|
81
|
+
|
|
82
|
+
atexit.register(self._shutdown)
|
|
83
|
+
|
|
84
|
+
def _shutdown(self):
|
|
85
|
+
# Signal all threads to shut down
|
|
86
|
+
self.shutdown_event.set()
|
|
87
|
+
# Wait for the routine worker to finish
|
|
88
|
+
self.routine_worker.join()
|
|
89
|
+
|
|
90
|
+
def _routine_loop(self):
|
|
91
|
+
while not self.shutdown_event.is_set():
|
|
92
|
+
self.wake_event.wait(timeout=60) # Wait for either timeout or wake signal
|
|
93
|
+
# Update list of accessible URLs
|
|
94
|
+
remote_urls = resolve_module(self.repository, self.access_token)
|
|
95
|
+
accessible_urls = []
|
|
96
|
+
for url in remote_urls:
|
|
97
|
+
response = requests.get(f"{url}/{self.name}", headers={"Authorization": f"Bearer {self.access_token}"} if self.access_token else {})
|
|
98
|
+
if response.status_code == 200:
|
|
99
|
+
accessible_urls.append(url)
|
|
100
|
+
with threading.Lock():
|
|
101
|
+
self.urls = accessible_urls
|
|
102
|
+
# Ensure that running_index is within the bounds of the updated URLs list
|
|
103
|
+
if self.running_index >= len(self.urls):
|
|
104
|
+
self.running_index = 0
|
|
105
|
+
self.wake_event.clear() # Clear the wake event after processing
|
|
106
|
+
|
|
107
|
+
def execute(self, kwargs: dict | None = None, timeout: float | None = 10) -> Any:
|
|
108
|
+
kwargs = kwargs or {}
|
|
109
|
+
# If URLs are being updated, wait until the update is done
|
|
110
|
+
while self.wake_event.is_set():
|
|
111
|
+
time.sleep(0.1)
|
|
112
|
+
if len(self.urls) == 0:
|
|
113
|
+
raise Exception(f"No accessible module URLs. Please check the '{self.repository}' repository and your access token.")
|
|
114
|
+
|
|
115
|
+
for attempt in range(2):
|
|
116
|
+
# Get url for remote call with simple round-robin load balancing
|
|
117
|
+
with threading.Lock():
|
|
118
|
+
url = self.urls[self.running_index]
|
|
119
|
+
self.running_index = (self.running_index + 1) % len(self.urls)
|
|
120
|
+
try:
|
|
121
|
+
result = self.remote_call(
|
|
122
|
+
url=url,
|
|
123
|
+
endpoint=self.name,
|
|
124
|
+
access_token=self.access_token,
|
|
125
|
+
kwargs=kwargs,
|
|
126
|
+
timeout=timeout,
|
|
127
|
+
)
|
|
128
|
+
break # Break after successful execution
|
|
129
|
+
except Exception as e:
|
|
130
|
+
if attempt == 0:
|
|
131
|
+
self.wake_event.set() # Wake up the routine loop to refresh URLs and quotas on the first failure
|
|
132
|
+
while self.wake_event.is_set():
|
|
133
|
+
time.sleep(0.1) # Wait for the routine loop to update the URLs before retrying
|
|
134
|
+
if len(self.urls) == 0:
|
|
135
|
+
raise Exception(f"No accessible module URLs. Please check the '{self.repository}' repository and your access token.")
|
|
136
|
+
continue # Retry on the first attempt
|
|
137
|
+
raise e
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def remote_call(
|
|
142
|
+
url: str,
|
|
143
|
+
endpoint: str,
|
|
144
|
+
access_token: str,
|
|
145
|
+
kwargs: dict,
|
|
146
|
+
timeout: float | None = 10,
|
|
147
|
+
) -> Any:
|
|
148
|
+
"""
|
|
149
|
+
Perform a remote call to the specified URL and endpoint with the given access token and arguments.
|
|
150
|
+
Retry the call if it fails due to rate limiting (HTTP 429) or server queue full (HTTP 503), until it succeeds or the optional timeout is reached.
|
|
151
|
+
"""
|
|
152
|
+
start_time = time.monotonic()
|
|
153
|
+
while True:
|
|
154
|
+
# Timeout check
|
|
155
|
+
if timeout:
|
|
156
|
+
elapsed_time = time.monotonic() - start_time
|
|
157
|
+
if elapsed_time > timeout:
|
|
158
|
+
raise TimeoutError(f"Remote call to {url}/{endpoint} timed out after {timeout} seconds.")
|
|
159
|
+
|
|
160
|
+
# Make the remote call
|
|
161
|
+
headers = {"Authorization": f"Bearer {access_token}"} if access_token else {}
|
|
162
|
+
response = requests.post(f"{url}/{endpoint}", json=kwargs, headers=headers, timeout=timeout)
|
|
163
|
+
|
|
164
|
+
if response.status_code == 200:
|
|
165
|
+
return response.json()["output"]
|
|
166
|
+
elif response.status_code == 429:
|
|
167
|
+
# Rate limit exceeded, wait and keep retry until timeout or success
|
|
168
|
+
retry_after = response.json().get("retry_after", 1)
|
|
169
|
+
time.sleep(retry_after)
|
|
170
|
+
continue
|
|
171
|
+
elif response.status_code == 503:
|
|
172
|
+
# Server queue full, wait and keep retry until timeout or success
|
|
173
|
+
time.sleep(1)
|
|
174
|
+
continue
|
|
175
|
+
else:
|
|
176
|
+
raise Exception(f"Remote call failed with status code {response.status_code}: {response.text}")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
MNS_URL = "https://0q4dl5ht80.execute-api.ap-southeast-2.amazonaws.com/prod"
|
modulith/mns/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from modulith.global_variables import MNS_URL
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def guest_signup() -> str:
|
|
6
|
+
from .login import login
|
|
7
|
+
response = requests.post(f"{MNS_URL}/guest_sign_up", json={})
|
|
8
|
+
if response.status_code == 200:
|
|
9
|
+
token = response.json().get("token")
|
|
10
|
+
# Automatically log in the user after successful sign-up
|
|
11
|
+
login(token)
|
|
12
|
+
return token
|
|
13
|
+
raise ValueError("Failed to sign up as guest")
|
modulith/mns/login.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import keyring
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def login(api_key: str) -> str:
|
|
5
|
+
from .whoami import whoami
|
|
6
|
+
try:
|
|
7
|
+
username = whoami(api_key)
|
|
8
|
+
keyring.set_password("modulith", "api_key", api_key)
|
|
9
|
+
return username
|
|
10
|
+
except ValueError as e:
|
|
11
|
+
raise ValueError(f"Login failed: {e}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from modulith.global_variables import MNS_URL
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def register_module(modulename: str, public_url: str, public: bool, token: str = None) -> str:
|
|
6
|
+
response = requests.post(
|
|
7
|
+
f"{MNS_URL}/register_module",
|
|
8
|
+
json={
|
|
9
|
+
"modulename": modulename,
|
|
10
|
+
"url": public_url,
|
|
11
|
+
"public": public,
|
|
12
|
+
"token": token,
|
|
13
|
+
},
|
|
14
|
+
)
|
|
15
|
+
if response.status_code == 200:
|
|
16
|
+
return response.json()["repo"]
|
|
17
|
+
raise ValueError(f"Failed to register module: {response.text}")
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from modulith.global_variables import MNS_URL
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def resolve_module(repository: str, token: str = None, validate_url: bool = True) -> list[str]:
|
|
6
|
+
response = requests.get(f"{MNS_URL}/resolve_module", json={
|
|
7
|
+
"repository": repository,
|
|
8
|
+
"token": token,
|
|
9
|
+
"validate_url": validate_url,
|
|
10
|
+
})
|
|
11
|
+
if response.status_code == 200:
|
|
12
|
+
return response.json().get("urls", [])
|
|
13
|
+
raise ValueError(f"Failed to resolve module: {response.text}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .resolve import resolve_module
|
|
3
|
+
from modulith.global_variables import MNS_URL
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def workload_module(repository: str, token: str = None, validate_url: bool = True) -> list[str]:
|
|
7
|
+
urls = resolve_module(repository, token, validate_url)
|
|
8
|
+
|
|
9
|
+
results = {}
|
|
10
|
+
for url in urls:
|
|
11
|
+
response = requests.get(f"{url}/workload")
|
|
12
|
+
if response.status_code == 200:
|
|
13
|
+
results[url] = response.json()
|
|
14
|
+
else:
|
|
15
|
+
results[url] = {"error": f"Failed to retrieve workload: {response.text}"}
|
|
16
|
+
return results
|
modulith/mns/signup.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from modulith.global_variables import MNS_URL
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def sign_up(
|
|
6
|
+
email: str,
|
|
7
|
+
username: str,
|
|
8
|
+
password: str,
|
|
9
|
+
) -> str:
|
|
10
|
+
from .login import login
|
|
11
|
+
payload = {
|
|
12
|
+
"email": email,
|
|
13
|
+
"password": password,
|
|
14
|
+
"username": username,
|
|
15
|
+
}
|
|
16
|
+
response = requests.post(f"{MNS_URL}/sign_up", json=payload)
|
|
17
|
+
if response.status_code == 200:
|
|
18
|
+
token = response.json().get("token")
|
|
19
|
+
# Automatically log in the user after successful sign-up
|
|
20
|
+
login(token)
|
|
21
|
+
return token
|
|
22
|
+
raise ValueError(f"Failed to sign up: {response.text}")
|
modulith/mns/whoami.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from modulith.global_variables import MNS_URL
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def whoami(api_key: str) -> str:
|
|
6
|
+
response = requests.get(f"{MNS_URL}/whoami", json={"token": api_key})
|
|
7
|
+
if response.status_code == 200:
|
|
8
|
+
username = response.json()["username"]
|
|
9
|
+
return username
|
|
10
|
+
raise ValueError(f"Failed to retrieve user information: {response.text.replace('Internal Server Error: ', '')}")
|