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.
Files changed (105) hide show
  1. modulith/__init__.py +1 -0
  2. modulith/cli/login.py +16 -0
  3. modulith/cli/logout.py +10 -0
  4. modulith/cli/main.py +18 -0
  5. modulith/cli/module/resolve.py +16 -0
  6. modulith/cli/module/workload.py +20 -0
  7. modulith/cli/serve.py +179 -0
  8. modulith/cli/signup.py +20 -0
  9. modulith/cli/whoami.py +19 -0
  10. modulith/dataclasses/__init__.py +1 -0
  11. modulith/dataclasses/capability.py +176 -0
  12. modulith/global_variables.py +1 -0
  13. modulith/mns/__init__.py +0 -0
  14. modulith/mns/guest_signup.py +13 -0
  15. modulith/mns/login.py +11 -0
  16. modulith/mns/module/__init__.py +2 -0
  17. modulith/mns/module/register.py +17 -0
  18. modulith/mns/module/resolve.py +13 -0
  19. modulith/mns/module/workload.py +16 -0
  20. modulith/mns/signup.py +22 -0
  21. modulith/mns/whoami.py +10 -0
  22. modulith/modulith.py +93 -0
  23. modulith/server.py +400 -0
  24. modulith/utils/__init__.py +3 -0
  25. modulith/utils/cache_manager.py +24 -0
  26. modulith/utils/connections/__init__.py +1 -0
  27. modulith/utils/connections/wait_for_connection.py +25 -0
  28. modulith/utils/hashing.py +3 -0
  29. modulith/utils/rate_limiters/__init__.py +1 -0
  30. modulith/utils/rate_limiters/token_bucket_limiter.py +94 -0
  31. modulith/utils/safe_min.py +9 -0
  32. modulith/utils/token_managers/__init__.py +1 -0
  33. modulith/utils/token_managers/secret_token_manager.py +29 -0
  34. modulith/utils/tunnels/__init__.py +33 -0
  35. modulith/utils/tunnels/cloudflare.py +24 -0
  36. modulith/utils/tunnels/instatunnel.py +51 -0
  37. modulith/utils/tunnels/ngrok.py +19 -0
  38. modulith-0.1.0.dist-info/METADATA +166 -0
  39. modulith-0.1.0.dist-info/RECORD +105 -0
  40. modulith-0.1.0.dist-info/WHEEL +5 -0
  41. modulith-0.1.0.dist-info/entry_points.txt +2 -0
  42. modulith-0.1.0.dist-info/licenses/LICENSE +201 -0
  43. modulith-0.1.0.dist-info/top_level.txt +3 -0
  44. modulith-integrations/embeddings/openai/modulith/modules/embeddings/openai/__init__.py +1 -0
  45. modulith-integrations/embeddings/openai/modulith/modules/embeddings/openai/module.py +20 -0
  46. modulith-integrations/embeddings/openai/test/import.py +1 -0
  47. modulith-integrations/embeddings/openai-compatible/modulith/modules/embeddings/openai_compatible/__init__.py +1 -0
  48. modulith-integrations/embeddings/openai-compatible/modulith/modules/embeddings/openai_compatible/module.py +6 -0
  49. modulith-integrations/embeddings/openai-compatible/test/import.py +1 -0
  50. modulith-integrations/embeddings/openrouter/modulith/modules/embeddings/openrouter/__init__.py +1 -0
  51. modulith-integrations/embeddings/openrouter/modulith/modules/embeddings/openrouter/module.py +12 -0
  52. modulith-integrations/embeddings/openrouter/test/import.py +1 -0
  53. modulith-integrations/image_generation/xai/modulith/modules/image_generation/xai/__init__.py +1 -0
  54. modulith-integrations/image_generation/xai/modulith/modules/image_generation/xai/module.py +32 -0
  55. modulith-integrations/image_generation/xai/test/test.py +24 -0
  56. modulith-integrations/index/bm25/modulith/modules/index/bm25/__init__.py +1 -0
  57. modulith-integrations/index/bm25/modulith/modules/index/bm25/module.py +58 -0
  58. modulith-integrations/index/chroma/modulith/modules/index/chroma/__init__.py +1 -0
  59. modulith-integrations/index/chroma/modulith/modules/index/chroma/module.py +52 -0
  60. modulith-integrations/index/faiss/modulith/modules/indices/faiss/__init__.py +1 -0
  61. modulith-integrations/index/faiss/modulith/modules/indices/faiss/module.py +161 -0
  62. modulith-integrations/llm/openai/modulith/modules/llm/openai/__init__.py +3 -0
  63. modulith-integrations/llm/openai/modulith/modules/llm/openai/chat_module.py +23 -0
  64. modulith-integrations/llm/openai/modulith/modules/llm/openai/completion_module.py +21 -0
  65. modulith-integrations/llm/openai/modulith/modules/llm/openai/response_module.py +21 -0
  66. modulith-integrations/llm/openai/test/import.py +1 -0
  67. modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/__init__.py +3 -0
  68. modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/chat_module.py +6 -0
  69. modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/completion_module.py +6 -0
  70. modulith-integrations/llm/openai-compatible/modulith/modules/llm/openai_compatible/response_module.py +6 -0
  71. modulith-integrations/llm/openai-compatible/test/import.py +1 -0
  72. modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/__init__.py +3 -0
  73. modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/chat_module.py +12 -0
  74. modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/completion_module.py +12 -0
  75. modulith-integrations/llm/openrouter/modulith/modules/llm/openrouter/response_module.py +12 -0
  76. modulith-integrations/llm/openrouter/test/import.py +1 -0
  77. modulith-integrations/llm/xai/modulith/modules/llm/xai/__init__.py +1 -0
  78. modulith-integrations/llm/xai/modulith/modules/llm/xai/module.py +38 -0
  79. modulith-integrations/llm/xai/test/test.py +16 -0
  80. modulith-integrations/tts/xai/modulith/modules/tts/xai/__init__.py +1 -0
  81. modulith-integrations/tts/xai/modulith/modules/tts/xai/module.py +40 -0
  82. modulith-integrations/tts/xai/test/test.py +16 -0
  83. modulith-integrations/video_generation/xai/modulith/modules/video_generation/xai/__init__.py +1 -0
  84. modulith-integrations/video_generation/xai/modulith/modules/video_generation/xai/module.py +33 -0
  85. modulith-integrations/video_generation/xai/test/test.py +25 -0
  86. modulith-modules/modulith/modules/agent/__init__.py +1 -0
  87. modulith-modules/modulith/modules/agent/generative_agent_module.py +70 -0
  88. modulith-modules/modulith/modules/embeddings/auto/__init__.py +1 -0
  89. modulith-modules/modulith/modules/embeddings/auto/auto_module.py +37 -0
  90. modulith-modules/modulith/modules/embeddings/auto/const.py +4 -0
  91. modulith-modules/modulith/modules/embeddings/base_classes/__init__.py +1 -0
  92. modulith-modules/modulith/modules/embeddings/base_classes/base_module.py +7 -0
  93. modulith-modules/modulith/modules/embeddings/datatypes.py +4 -0
  94. modulith-modules/modulith/modules/index/base_classes/__init__.py +1 -0
  95. modulith-modules/modulith/modules/index/base_classes/base_module.py +59 -0
  96. modulith-modules/modulith/modules/llm/auto/__init__.py +1 -0
  97. modulith-modules/modulith/modules/llm/auto/auto_module.py +65 -0
  98. modulith-modules/modulith/modules/llm/auto/const.py +6 -0
  99. modulith-modules/modulith/modules/llm/base_classes/__init__.py +1 -0
  100. modulith-modules/modulith/modules/llm/base_classes/base_module.py +18 -0
  101. modulith-modules/modulith/modules/llm/datatypes.py +4 -0
  102. modulith-modules/modulith/modules/reranking/reciprocal_rank_fusion/__init__.py +1 -0
  103. modulith-modules/modulith/modules/reranking/reciprocal_rank_fusion/module.py +36 -0
  104. modulith-modules/modulith/modules/tool/__init__.py +1 -0
  105. 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
@@ -0,0 +1,10 @@
1
+ import typer
2
+ import keyring
3
+
4
+
5
+ logout_app = typer.Typer()
6
+
7
+ @logout_app.command("logout")
8
+ def logout_command():
9
+ keyring.delete_password("modulith", "api_key")
10
+ typer.echo("✅ Logout successful. Authentication token removed.")
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"
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,2 @@
1
+ from .resolve import resolve_module
2
+ from .workload import workload_module
@@ -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: ', '')}")