robyn 0.73.0__cp311-cp311-macosx_10_12_x86_64.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.
Potentially problematic release.
This version of robyn might be problematic. Click here for more details.
- robyn/__init__.py +757 -0
- robyn/__main__.py +4 -0
- robyn/ai.py +308 -0
- robyn/argument_parser.py +129 -0
- robyn/authentication.py +96 -0
- robyn/cli.py +136 -0
- robyn/dependency_injection.py +71 -0
- robyn/env_populator.py +35 -0
- robyn/events.py +6 -0
- robyn/exceptions.py +32 -0
- robyn/jsonify.py +13 -0
- robyn/logger.py +80 -0
- robyn/mcp.py +461 -0
- robyn/openapi.py +448 -0
- robyn/processpool.py +226 -0
- robyn/py.typed +0 -0
- robyn/reloader.py +164 -0
- robyn/responses.py +208 -0
- robyn/robyn.cpython-311-darwin.so +0 -0
- robyn/robyn.pyi +421 -0
- robyn/router.py +410 -0
- robyn/scaffold/mongo/Dockerfile +12 -0
- robyn/scaffold/mongo/app.py +43 -0
- robyn/scaffold/mongo/requirements.txt +2 -0
- robyn/scaffold/no-db/Dockerfile +12 -0
- robyn/scaffold/no-db/app.py +12 -0
- robyn/scaffold/no-db/requirements.txt +1 -0
- robyn/scaffold/postgres/Dockerfile +32 -0
- robyn/scaffold/postgres/app.py +31 -0
- robyn/scaffold/postgres/requirements.txt +3 -0
- robyn/scaffold/postgres/supervisord.conf +14 -0
- robyn/scaffold/prisma/Dockerfile +15 -0
- robyn/scaffold/prisma/app.py +32 -0
- robyn/scaffold/prisma/requirements.txt +2 -0
- robyn/scaffold/prisma/schema.prisma +13 -0
- robyn/scaffold/sqlalchemy/Dockerfile +12 -0
- robyn/scaffold/sqlalchemy/__init__.py +0 -0
- robyn/scaffold/sqlalchemy/app.py +13 -0
- robyn/scaffold/sqlalchemy/models.py +21 -0
- robyn/scaffold/sqlalchemy/requirements.txt +2 -0
- robyn/scaffold/sqlite/Dockerfile +12 -0
- robyn/scaffold/sqlite/app.py +22 -0
- robyn/scaffold/sqlite/requirements.txt +1 -0
- robyn/scaffold/sqlmodel/Dockerfile +11 -0
- robyn/scaffold/sqlmodel/app.py +46 -0
- robyn/scaffold/sqlmodel/models.py +10 -0
- robyn/scaffold/sqlmodel/requirements.txt +2 -0
- robyn/status_codes.py +137 -0
- robyn/swagger.html +32 -0
- robyn/templating.py +30 -0
- robyn/types.py +44 -0
- robyn/ws.py +67 -0
- robyn-0.73.0.dist-info/METADATA +32 -0
- robyn-0.73.0.dist-info/RECORD +57 -0
- robyn-0.73.0.dist-info/WHEEL +4 -0
- robyn-0.73.0.dist-info/entry_points.txt +3 -0
- robyn-0.73.0.dist-info/licenses/LICENSE +25 -0
robyn/__main__.py
ADDED
robyn/ai.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This is an experimental AI integration module for Robyn framework.
|
|
3
|
+
|
|
4
|
+
A poc for the blog at https://sanskar.wtf/posts/the-future-of-robyn
|
|
5
|
+
|
|
6
|
+
Provides agent and memory functionality for building AI-powered applications for the demonstration of the vision mentioned in
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from typing import Any, Dict, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AIConfig:
|
|
18
|
+
"""Configuration class for AI providers and settings"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, **kwargs):
|
|
21
|
+
self.config = kwargs
|
|
22
|
+
self._load_from_env()
|
|
23
|
+
|
|
24
|
+
def _load_from_env(self):
|
|
25
|
+
"""Load configuration from environment variables"""
|
|
26
|
+
env_vars = {
|
|
27
|
+
"OPENAI_API_KEY": "openai_api_key",
|
|
28
|
+
"ANTHROPIC_API_KEY": "anthropic_api_key",
|
|
29
|
+
"GOOGLE_API_KEY": "google_api_key",
|
|
30
|
+
"AI_MODEL": "model",
|
|
31
|
+
"AI_TEMPERATURE": "temperature",
|
|
32
|
+
"AI_MAX_TOKENS": "max_tokens",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for env_var, config_key in env_vars.items():
|
|
36
|
+
if env_var in os.environ and config_key not in self.config:
|
|
37
|
+
value = os.environ[env_var]
|
|
38
|
+
# Convert numeric values
|
|
39
|
+
if config_key in ["temperature", "max_tokens"]:
|
|
40
|
+
try:
|
|
41
|
+
value = float(value) if config_key == "temperature" else int(value)
|
|
42
|
+
except ValueError:
|
|
43
|
+
pass
|
|
44
|
+
self.config[config_key] = value
|
|
45
|
+
|
|
46
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
47
|
+
"""Get configuration value"""
|
|
48
|
+
return self.config.get(key, default)
|
|
49
|
+
|
|
50
|
+
def set(self, key: str, value: Any) -> None:
|
|
51
|
+
"""Set configuration value"""
|
|
52
|
+
self.config[key] = value
|
|
53
|
+
|
|
54
|
+
def update(self, **kwargs) -> None:
|
|
55
|
+
"""Update configuration with new values"""
|
|
56
|
+
self.config.update(kwargs)
|
|
57
|
+
|
|
58
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
59
|
+
"""Get configuration as dictionary"""
|
|
60
|
+
return self.config.copy()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MemoryProvider(ABC):
|
|
64
|
+
"""Abstract base class for memory providers"""
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
async def store(self, user_id: str, data: Dict[str, Any]) -> None:
|
|
68
|
+
"""Store data in memory"""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
async def retrieve(self, user_id: str, query: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
73
|
+
"""Retrieve data from memory"""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def clear(self, user_id: str) -> None:
|
|
78
|
+
"""Clear memory for a user"""
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InMemoryProvider(MemoryProvider):
|
|
83
|
+
"""Simple in-memory storage provider"""
|
|
84
|
+
|
|
85
|
+
def __init__(self):
|
|
86
|
+
self._storage: Dict[str, List[Dict[str, Any]]] = {}
|
|
87
|
+
|
|
88
|
+
async def store(self, user_id: str, data: Dict[str, Any]) -> None:
|
|
89
|
+
if user_id not in self._storage:
|
|
90
|
+
self._storage[user_id] = []
|
|
91
|
+
self._storage[user_id].append(data)
|
|
92
|
+
|
|
93
|
+
async def retrieve(self, user_id: str, query: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
94
|
+
return self._storage.get(user_id, [])
|
|
95
|
+
|
|
96
|
+
async def clear(self, user_id: str) -> None:
|
|
97
|
+
if user_id in self._storage:
|
|
98
|
+
del self._storage[user_id]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Memory:
|
|
102
|
+
"""Memory interface for storing and retrieving conversation history and context"""
|
|
103
|
+
|
|
104
|
+
def __init__(self, provider: Union[str, MemoryProvider], user_id: str, **kwargs):
|
|
105
|
+
self.user_id = user_id
|
|
106
|
+
|
|
107
|
+
if isinstance(provider, str):
|
|
108
|
+
if provider == "inmemory":
|
|
109
|
+
self.provider = InMemoryProvider()
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError(f"Unknown memory provider: {provider}")
|
|
112
|
+
else:
|
|
113
|
+
self.provider = provider
|
|
114
|
+
|
|
115
|
+
async def add(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
|
116
|
+
"""Add a message to memory"""
|
|
117
|
+
data = {"message": message, "metadata": metadata or {}}
|
|
118
|
+
await self.provider.store(self.user_id, data)
|
|
119
|
+
|
|
120
|
+
async def get(self, query: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
121
|
+
"""Get messages from memory"""
|
|
122
|
+
return await self.provider.retrieve(self.user_id, query)
|
|
123
|
+
|
|
124
|
+
async def clear(self) -> None:
|
|
125
|
+
"""Clear all memory for this user"""
|
|
126
|
+
await self.provider.clear(self.user_id)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class AgentRunner(ABC):
|
|
130
|
+
"""Abstract base class for agent runners"""
|
|
131
|
+
|
|
132
|
+
@abstractmethod
|
|
133
|
+
async def run(self, query: str, **kwargs) -> Dict[str, Any]:
|
|
134
|
+
"""Execute the agent with the given query"""
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SimpleRunner(AgentRunner):
|
|
139
|
+
"""Simple runner with OpenAI integration and fallback responses"""
|
|
140
|
+
|
|
141
|
+
def __init__(self, **config):
|
|
142
|
+
self.config = AIConfig(**config)
|
|
143
|
+
self._client = None
|
|
144
|
+
|
|
145
|
+
def _get_client(self):
|
|
146
|
+
"""Get OpenAI client if API key is available"""
|
|
147
|
+
if self._client is None and self.config.get("openai_api_key"):
|
|
148
|
+
try:
|
|
149
|
+
import openai
|
|
150
|
+
|
|
151
|
+
self._client = openai.OpenAI(api_key=self.config.get("openai_api_key"))
|
|
152
|
+
except ImportError:
|
|
153
|
+
raise ImportError("openai package not installed. Install with: pip install openai")
|
|
154
|
+
return self._client
|
|
155
|
+
|
|
156
|
+
async def run(self, query: str, **kwargs) -> Dict[str, Any]:
|
|
157
|
+
"""Execute with OpenAI (requires API key)"""
|
|
158
|
+
client = self._get_client()
|
|
159
|
+
|
|
160
|
+
if not client:
|
|
161
|
+
raise ValueError("OpenAI API key is required. Set 'openai_api_key' in configuration.")
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
# Use OpenAI
|
|
165
|
+
messages = [{"role": "user", "content": query}]
|
|
166
|
+
|
|
167
|
+
# Add conversation history
|
|
168
|
+
context = kwargs.get("context", {})
|
|
169
|
+
history = context.get("history", [])
|
|
170
|
+
|
|
171
|
+
if history:
|
|
172
|
+
for item in history[-10:]:
|
|
173
|
+
msg = item.get("message", str(item))
|
|
174
|
+
if msg.startswith("Query: "):
|
|
175
|
+
messages.insert(-1, {"role": "user", "content": msg[7:]})
|
|
176
|
+
elif msg.startswith("Response: "):
|
|
177
|
+
messages.insert(-1, {"role": "assistant", "content": msg[10:]})
|
|
178
|
+
|
|
179
|
+
response = client.chat.completions.create(
|
|
180
|
+
model=self.config.get("model", "gpt-4o"),
|
|
181
|
+
messages=messages,
|
|
182
|
+
temperature=self.config.get("temperature", 0.7),
|
|
183
|
+
max_tokens=self.config.get("max_tokens", 1000),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
content = response.choices[0].message.content
|
|
187
|
+
metadata = {
|
|
188
|
+
"runner_type": "simple_openai",
|
|
189
|
+
"model": self.config.get("model", "gpt-4o"),
|
|
190
|
+
"usage": {
|
|
191
|
+
"prompt_tokens": response.usage.prompt_tokens,
|
|
192
|
+
"completion_tokens": response.usage.completion_tokens,
|
|
193
|
+
"total_tokens": response.usage.total_tokens,
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if self.config.get("debug", False):
|
|
198
|
+
metadata["debug_info"] = {"config": self.config.to_dict()}
|
|
199
|
+
|
|
200
|
+
return {"response": content, "query": query, "metadata": metadata}
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.error(f"Error in SimpleRunner: {e}")
|
|
204
|
+
raise e
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class Agent:
|
|
208
|
+
"""AI Agent interface for handling queries with memory and execution"""
|
|
209
|
+
|
|
210
|
+
def __init__(self, runner: Union[str, AgentRunner], memory: Optional[Memory] = None, **kwargs):
|
|
211
|
+
self.memory = memory
|
|
212
|
+
|
|
213
|
+
if isinstance(runner, str):
|
|
214
|
+
if runner == "simple":
|
|
215
|
+
self.runner = SimpleRunner(**kwargs)
|
|
216
|
+
else:
|
|
217
|
+
raise ValueError(f"Unknown runner type: {runner}")
|
|
218
|
+
else:
|
|
219
|
+
self.runner = runner
|
|
220
|
+
|
|
221
|
+
async def run(self, query: str, history: bool = False, **kwargs) -> Dict[str, Any]:
|
|
222
|
+
"""Run the agent with the given query"""
|
|
223
|
+
context = {}
|
|
224
|
+
|
|
225
|
+
if self.memory and history:
|
|
226
|
+
context["history"] = await self.memory.get()
|
|
227
|
+
|
|
228
|
+
# Add context to kwargs
|
|
229
|
+
if context:
|
|
230
|
+
kwargs["context"] = context
|
|
231
|
+
|
|
232
|
+
# Execute the agent
|
|
233
|
+
result = await self.runner.run(query, **kwargs)
|
|
234
|
+
|
|
235
|
+
# Store the interaction in memory if available
|
|
236
|
+
if self.memory:
|
|
237
|
+
await self.memory.add(f"Query: {query}")
|
|
238
|
+
if "response" in result:
|
|
239
|
+
await self.memory.add(f"Response: {result['response']}")
|
|
240
|
+
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def memory(provider: str = "inmemory", user_id: str = "default", **kwargs) -> Memory:
|
|
245
|
+
"""
|
|
246
|
+
Create a memory instance with the specified provider
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
provider: Memory provider type ("inmemory")
|
|
250
|
+
user_id: User identifier for memory isolation
|
|
251
|
+
**kwargs: Additional configuration for the provider
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Memory instance
|
|
255
|
+
"""
|
|
256
|
+
return Memory(provider=provider, user_id=user_id, **kwargs)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def configure(**kwargs) -> AIConfig:
|
|
260
|
+
"""
|
|
261
|
+
Create and configure AI settings
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
**kwargs: Configuration options including API keys, model settings, etc.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
AIConfig instance
|
|
268
|
+
|
|
269
|
+
Example:
|
|
270
|
+
config = configure(
|
|
271
|
+
openai_api_key="your-key",
|
|
272
|
+
model="gpt-4",
|
|
273
|
+
temperature=0.7
|
|
274
|
+
)
|
|
275
|
+
"""
|
|
276
|
+
return AIConfig(**kwargs)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def agent(runner: str = "simple", memory: Optional[Memory] = None, config: Optional[AIConfig] = None, **kwargs) -> Agent:
|
|
280
|
+
"""
|
|
281
|
+
Create an agent instance with the specified runner
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
runner: Agent runner type ("simple")
|
|
285
|
+
memory: Optional memory instance for context
|
|
286
|
+
config: Optional AIConfig instance for configuration
|
|
287
|
+
**kwargs: Additional configuration for the runner
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Agent instance
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
# Simple usage
|
|
294
|
+
chat = agent()
|
|
295
|
+
|
|
296
|
+
# With configuration
|
|
297
|
+
config = configure(openai_api_key="your-key")
|
|
298
|
+
chat = agent(runner="simple", config=config)
|
|
299
|
+
|
|
300
|
+
# With memory
|
|
301
|
+
mem = memory(provider="inmemory", user_id="user123")
|
|
302
|
+
chat = agent(runner="simple", memory=mem)
|
|
303
|
+
"""
|
|
304
|
+
# Merge config if provided
|
|
305
|
+
if config:
|
|
306
|
+
kwargs.update(config.to_dict())
|
|
307
|
+
|
|
308
|
+
return Agent(runner=runner, memory=memory, **kwargs)
|
robyn/argument_parser.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Config:
|
|
6
|
+
def __init__(self) -> None:
|
|
7
|
+
parser = argparse.ArgumentParser(description="Robyn, a fast async web framework with a rust runtime.")
|
|
8
|
+
self.parser = parser
|
|
9
|
+
parser.add_argument(
|
|
10
|
+
"--processes",
|
|
11
|
+
type=int,
|
|
12
|
+
default=None,
|
|
13
|
+
required=False,
|
|
14
|
+
help="Choose the number of processes. [Default: 1]",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--workers",
|
|
18
|
+
type=int,
|
|
19
|
+
default=None,
|
|
20
|
+
required=False,
|
|
21
|
+
help="Choose the number of workers. [Default: 1]",
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument(
|
|
24
|
+
"--dev",
|
|
25
|
+
dest="dev",
|
|
26
|
+
action="store_true",
|
|
27
|
+
default=None,
|
|
28
|
+
help="Development mode. It restarts the server based on file changes.",
|
|
29
|
+
)
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--log-level",
|
|
32
|
+
dest="log_level",
|
|
33
|
+
default=None,
|
|
34
|
+
help="Set the log level name",
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"--create",
|
|
38
|
+
action="store_true",
|
|
39
|
+
default=False,
|
|
40
|
+
help="Create a new project template.",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--docs",
|
|
44
|
+
action="store_true",
|
|
45
|
+
default=False,
|
|
46
|
+
help="Open the Robyn documentation.",
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--open-browser",
|
|
50
|
+
action="store_true",
|
|
51
|
+
default=False,
|
|
52
|
+
help="Open the browser on successful start.",
|
|
53
|
+
)
|
|
54
|
+
parser.add_argument(
|
|
55
|
+
"--version",
|
|
56
|
+
action="store_true",
|
|
57
|
+
default=False,
|
|
58
|
+
help="Show the Robyn version.",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--compile-rust-path",
|
|
62
|
+
dest="compile_rust_path",
|
|
63
|
+
default=None,
|
|
64
|
+
help="Compile rust files in the given path.",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"--create-rust-file",
|
|
69
|
+
dest="create_rust_file",
|
|
70
|
+
default=None,
|
|
71
|
+
help="Create a rust file with the given name.",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--disable-openapi",
|
|
75
|
+
dest="disable_openapi",
|
|
76
|
+
action="store_true",
|
|
77
|
+
default=False,
|
|
78
|
+
help="Disable the OpenAPI documentation.",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--fast",
|
|
82
|
+
dest="fast",
|
|
83
|
+
action="store_true",
|
|
84
|
+
default=False,
|
|
85
|
+
help="Fast mode. It sets the optimal values for processes, workers and log level. However, you can override them.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
args, unknown_args = parser.parse_known_args()
|
|
89
|
+
self.fast = args.fast
|
|
90
|
+
self.dev = args.dev
|
|
91
|
+
self.processes = args.processes
|
|
92
|
+
self.workers = args.workers
|
|
93
|
+
self.create = args.create
|
|
94
|
+
self.docs = args.docs
|
|
95
|
+
self.open_browser = args.open_browser
|
|
96
|
+
self.version = args.version
|
|
97
|
+
self.compile_rust_path = args.compile_rust_path
|
|
98
|
+
self.create_rust_file = args.create_rust_file
|
|
99
|
+
self.file_path = None
|
|
100
|
+
self.disable_openapi = args.disable_openapi
|
|
101
|
+
self.log_level = args.log_level
|
|
102
|
+
|
|
103
|
+
if self.fast:
|
|
104
|
+
# doing this here before every other check
|
|
105
|
+
# so that processes, workers and log_level can be overridden
|
|
106
|
+
cpu_count: int = os.cpu_count() or 1
|
|
107
|
+
self.processes = self.processes or ((cpu_count * 2) + 1) or 1
|
|
108
|
+
self.workers = self.workers or 2
|
|
109
|
+
self.log_level = self.log_level or "WARNING"
|
|
110
|
+
|
|
111
|
+
self.processes = self.processes or 1
|
|
112
|
+
self.workers = self.workers or 1
|
|
113
|
+
|
|
114
|
+
# find something that ends with .py in unknown_args
|
|
115
|
+
for arg in unknown_args:
|
|
116
|
+
if arg.endswith(".py"):
|
|
117
|
+
self.file_path = arg
|
|
118
|
+
break
|
|
119
|
+
|
|
120
|
+
if self.fast and self.dev:
|
|
121
|
+
raise Exception("--fast and --dev shouldn't be used together")
|
|
122
|
+
|
|
123
|
+
if self.dev and (self.processes != 1 or self.workers != 1):
|
|
124
|
+
raise Exception("--processes and --workers shouldn't be used with --dev")
|
|
125
|
+
|
|
126
|
+
if self.dev and self.log_level is None:
|
|
127
|
+
self.log_level = "DEBUG"
|
|
128
|
+
elif self.log_level is None:
|
|
129
|
+
self.log_level = "INFO"
|
robyn/authentication.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from robyn.robyn import Headers, Identity, Request, Response
|
|
5
|
+
from robyn.status_codes import HTTP_401_UNAUTHORIZED
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuthenticationNotConfiguredError(Exception):
|
|
9
|
+
"""
|
|
10
|
+
This exception is raised when the authentication is not configured.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __str__(self):
|
|
14
|
+
return "Authentication is not configured. Use app.configure_authentication() to configure it."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TokenGetter(ABC):
|
|
18
|
+
@property
|
|
19
|
+
def scheme(self) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Gets the scheme of the token.
|
|
22
|
+
:return: The scheme of the token.
|
|
23
|
+
"""
|
|
24
|
+
return self.__class__.__name__
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def get_token(cls, request: Request) -> Optional[str]:
|
|
29
|
+
"""
|
|
30
|
+
Gets the token from the request.
|
|
31
|
+
This method should not decode the token. Decoding is the role of the authentication handler.
|
|
32
|
+
:param request: The request object.
|
|
33
|
+
:return: The encoded token.
|
|
34
|
+
"""
|
|
35
|
+
raise NotImplementedError()
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def set_token(cls, request: Request, token: str):
|
|
40
|
+
"""
|
|
41
|
+
Sets the token in the request.
|
|
42
|
+
This method should not encode the token. Encoding is the role of the authentication handler.
|
|
43
|
+
:param request: The request object.
|
|
44
|
+
:param token: The encoded token.
|
|
45
|
+
"""
|
|
46
|
+
raise NotImplementedError()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class AuthenticationHandler(ABC):
|
|
50
|
+
def __init__(self, token_getter: TokenGetter):
|
|
51
|
+
"""
|
|
52
|
+
Creates a new instance of the AuthenticationHandler class.
|
|
53
|
+
This class is an abstract class used to authenticate a user.
|
|
54
|
+
:param token_getter: The token getter used to get the token from the request.
|
|
55
|
+
"""
|
|
56
|
+
self.token_getter = token_getter
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def unauthorized_response(self) -> Response:
|
|
60
|
+
return Response(
|
|
61
|
+
headers=Headers({"WWW-Authenticate": self.token_getter.scheme}),
|
|
62
|
+
description="Unauthorized",
|
|
63
|
+
status_code=HTTP_401_UNAUTHORIZED,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def authenticate(self, request: Request) -> Optional[Identity]:
|
|
68
|
+
"""
|
|
69
|
+
Authenticates the user.
|
|
70
|
+
:param request: The request object.
|
|
71
|
+
:return: The identity of the user.
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class BearerGetter(TokenGetter):
|
|
77
|
+
"""
|
|
78
|
+
This class is used to get the token from the Authorization header.
|
|
79
|
+
The scheme of the header must be Bearer.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def get_token(cls, request: Request) -> Optional[str]:
|
|
84
|
+
if request.headers.contains("authorization"):
|
|
85
|
+
authorization_header = request.headers.get("authorization")
|
|
86
|
+
else:
|
|
87
|
+
authorization_header = None
|
|
88
|
+
|
|
89
|
+
if not authorization_header or not authorization_header.startswith("Bearer "):
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
return authorization_header[7:] # Remove the "Bearer " prefix
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def set_token(cls, request: Request, token: str):
|
|
96
|
+
request.headers["Authorization"] = f"Bearer {token}"
|
robyn/cli.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
import webbrowser
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from InquirerPy.base.control import Choice
|
|
10
|
+
from InquirerPy.resolver import prompt
|
|
11
|
+
|
|
12
|
+
from robyn.env_populator import load_vars
|
|
13
|
+
from robyn.robyn import get_version
|
|
14
|
+
|
|
15
|
+
from .argument_parser import Config
|
|
16
|
+
from .reloader import create_rust_file, setup_reloader
|
|
17
|
+
|
|
18
|
+
SCAFFOLD_DIR = Path(__file__).parent / "scaffold"
|
|
19
|
+
CURRENT_WORKING_DIR = Path.cwd()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def create_robyn_app():
|
|
23
|
+
questions = [
|
|
24
|
+
{
|
|
25
|
+
"type": "input",
|
|
26
|
+
"message": "Directory Path:",
|
|
27
|
+
"name": "directory",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"type": "list",
|
|
31
|
+
"message": "Need Docker? (Y/N)",
|
|
32
|
+
"choices": [
|
|
33
|
+
Choice("Y", name="Y"),
|
|
34
|
+
Choice("N", name="N"),
|
|
35
|
+
],
|
|
36
|
+
"default": Choice("N", name="N"),
|
|
37
|
+
"name": "docker",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "list",
|
|
41
|
+
"message": "Please select project type (Mongo/Postgres/Sqlalchemy/Prisma): ",
|
|
42
|
+
"choices": [
|
|
43
|
+
Choice("no-db", name="No DB"),
|
|
44
|
+
Choice("sqlite", name="Sqlite"),
|
|
45
|
+
Choice("postgres", name="Postgres"),
|
|
46
|
+
Choice("mongo", name="MongoDB"),
|
|
47
|
+
Choice("sqlalchemy", name="SqlAlchemy"),
|
|
48
|
+
Choice("prisma", name="Prisma"),
|
|
49
|
+
Choice("sqlmodel", name="SQLModel"),
|
|
50
|
+
],
|
|
51
|
+
"default": Choice("no-db", name="No DB"),
|
|
52
|
+
"name": "project_type",
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
result = prompt(questions=questions)
|
|
56
|
+
project_dir_path = Path(str(result["directory"])).resolve()
|
|
57
|
+
docker = result["docker"]
|
|
58
|
+
project_type = str(result["project_type"])
|
|
59
|
+
|
|
60
|
+
final_project_dir_path = (CURRENT_WORKING_DIR / project_dir_path).resolve()
|
|
61
|
+
|
|
62
|
+
print(f"Creating a new Robyn project '{final_project_dir_path}'...")
|
|
63
|
+
|
|
64
|
+
# Create a new directory for the project
|
|
65
|
+
os.makedirs(final_project_dir_path, exist_ok=True)
|
|
66
|
+
|
|
67
|
+
selected_project_template = (SCAFFOLD_DIR / Path(project_type)).resolve()
|
|
68
|
+
shutil.copytree(str(selected_project_template), str(final_project_dir_path), dirs_exist_ok=True)
|
|
69
|
+
|
|
70
|
+
# If docker is not needed, delete the docker file
|
|
71
|
+
if docker == "N":
|
|
72
|
+
os.remove(f"{final_project_dir_path}/Dockerfile")
|
|
73
|
+
|
|
74
|
+
print(f"New Robyn project created in '{final_project_dir_path}' ")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def docs():
|
|
78
|
+
print("Opening Robyn documentation... | Offline docs coming soon!")
|
|
79
|
+
webbrowser.open("https://robyn.tech")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def start_dev_server(config: Config, file_path: Optional[str] = None):
|
|
83
|
+
if file_path is None:
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
absolute_file_path = (Path.cwd() / file_path).resolve()
|
|
87
|
+
directory_path = absolute_file_path.parent
|
|
88
|
+
|
|
89
|
+
if config.dev and not os.environ.get("IS_RELOADER_RUNNING", False):
|
|
90
|
+
setup_reloader(str(directory_path), str(absolute_file_path))
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def start_app_normally(config: Config):
|
|
95
|
+
command = [sys.executable]
|
|
96
|
+
|
|
97
|
+
for arg in sys.argv[1:]:
|
|
98
|
+
command.append(arg)
|
|
99
|
+
|
|
100
|
+
# Run the subprocess
|
|
101
|
+
subprocess.run(command, start_new_session=False)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def run():
|
|
105
|
+
config = Config()
|
|
106
|
+
|
|
107
|
+
if not config.file_path:
|
|
108
|
+
config.file_path = f"{os.getcwd()}/{__name__}"
|
|
109
|
+
|
|
110
|
+
load_vars(project_root=os.path.dirname(os.path.abspath(config.file_path)))
|
|
111
|
+
os.environ["ROBYN_CLI"] = "True"
|
|
112
|
+
|
|
113
|
+
if config.dev is None:
|
|
114
|
+
config.dev = os.getenv("ROBYN_DEV_MODE", False) == "True"
|
|
115
|
+
|
|
116
|
+
if config.create:
|
|
117
|
+
create_robyn_app()
|
|
118
|
+
|
|
119
|
+
elif file_name := config.create_rust_file:
|
|
120
|
+
create_rust_file(file_name)
|
|
121
|
+
|
|
122
|
+
elif config.version:
|
|
123
|
+
print(get_version())
|
|
124
|
+
|
|
125
|
+
elif config.docs:
|
|
126
|
+
docs()
|
|
127
|
+
|
|
128
|
+
elif config.dev:
|
|
129
|
+
print("Starting dev server...")
|
|
130
|
+
start_dev_server(config, config.file_path)
|
|
131
|
+
else:
|
|
132
|
+
try:
|
|
133
|
+
start_app_normally(config)
|
|
134
|
+
except KeyboardInterrupt:
|
|
135
|
+
# for the crash happening upon pressing Ctrl + C
|
|
136
|
+
pass
|