oagi-core 0.10.1__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 (68) hide show
  1. oagi/__init__.py +148 -0
  2. oagi/agent/__init__.py +33 -0
  3. oagi/agent/default.py +124 -0
  4. oagi/agent/factories.py +74 -0
  5. oagi/agent/observer/__init__.py +38 -0
  6. oagi/agent/observer/agent_observer.py +99 -0
  7. oagi/agent/observer/events.py +28 -0
  8. oagi/agent/observer/exporters.py +445 -0
  9. oagi/agent/observer/protocol.py +12 -0
  10. oagi/agent/protocol.py +55 -0
  11. oagi/agent/registry.py +155 -0
  12. oagi/agent/tasker/__init__.py +33 -0
  13. oagi/agent/tasker/memory.py +160 -0
  14. oagi/agent/tasker/models.py +77 -0
  15. oagi/agent/tasker/planner.py +408 -0
  16. oagi/agent/tasker/taskee_agent.py +512 -0
  17. oagi/agent/tasker/tasker_agent.py +324 -0
  18. oagi/cli/__init__.py +11 -0
  19. oagi/cli/agent.py +281 -0
  20. oagi/cli/display.py +56 -0
  21. oagi/cli/main.py +77 -0
  22. oagi/cli/server.py +94 -0
  23. oagi/cli/tracking.py +55 -0
  24. oagi/cli/utils.py +89 -0
  25. oagi/client/__init__.py +12 -0
  26. oagi/client/async_.py +290 -0
  27. oagi/client/base.py +457 -0
  28. oagi/client/sync.py +293 -0
  29. oagi/exceptions.py +118 -0
  30. oagi/handler/__init__.py +24 -0
  31. oagi/handler/_macos.py +55 -0
  32. oagi/handler/async_pyautogui_action_handler.py +44 -0
  33. oagi/handler/async_screenshot_maker.py +47 -0
  34. oagi/handler/pil_image.py +102 -0
  35. oagi/handler/pyautogui_action_handler.py +291 -0
  36. oagi/handler/screenshot_maker.py +41 -0
  37. oagi/logging.py +55 -0
  38. oagi/server/__init__.py +13 -0
  39. oagi/server/agent_wrappers.py +98 -0
  40. oagi/server/config.py +46 -0
  41. oagi/server/main.py +157 -0
  42. oagi/server/models.py +98 -0
  43. oagi/server/session_store.py +116 -0
  44. oagi/server/socketio_server.py +405 -0
  45. oagi/task/__init__.py +21 -0
  46. oagi/task/async_.py +101 -0
  47. oagi/task/async_short.py +76 -0
  48. oagi/task/base.py +157 -0
  49. oagi/task/short.py +76 -0
  50. oagi/task/sync.py +99 -0
  51. oagi/types/__init__.py +50 -0
  52. oagi/types/action_handler.py +30 -0
  53. oagi/types/async_action_handler.py +30 -0
  54. oagi/types/async_image_provider.py +38 -0
  55. oagi/types/image.py +17 -0
  56. oagi/types/image_provider.py +35 -0
  57. oagi/types/models/__init__.py +32 -0
  58. oagi/types/models/action.py +33 -0
  59. oagi/types/models/client.py +68 -0
  60. oagi/types/models/image_config.py +47 -0
  61. oagi/types/models/step.py +17 -0
  62. oagi/types/step_observer.py +93 -0
  63. oagi/types/url.py +3 -0
  64. oagi_core-0.10.1.dist-info/METADATA +245 -0
  65. oagi_core-0.10.1.dist-info/RECORD +68 -0
  66. oagi_core-0.10.1.dist-info/WHEEL +4 -0
  67. oagi_core-0.10.1.dist-info/entry_points.txt +2 -0
  68. oagi_core-0.10.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,291 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ import re
10
+ import sys
11
+ import time
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+ from ..exceptions import check_optional_dependency
16
+ from ..types import Action, ActionType
17
+
18
+ check_optional_dependency("pyautogui", "PyautoguiActionHandler", "desktop")
19
+ import pyautogui # noqa: E402
20
+
21
+ if sys.platform == "darwin":
22
+ from . import _macos
23
+
24
+
25
+ class CapsLockManager:
26
+ """Manages caps lock state for text transformation."""
27
+
28
+ def __init__(self, mode: str = "session"):
29
+ """Initialize caps lock manager.
30
+
31
+ Args:
32
+ mode: Either "session" (internal state) or "system" (OS-level)
33
+ """
34
+ self.mode = mode
35
+ self.caps_enabled = False
36
+
37
+ def toggle(self):
38
+ """Toggle caps lock state in session mode."""
39
+ if self.mode == "session":
40
+ self.caps_enabled = not self.caps_enabled
41
+
42
+ def transform_text(self, text: str) -> str:
43
+ """Transform text based on caps lock state.
44
+
45
+ Args:
46
+ text: Input text to transform
47
+
48
+ Returns:
49
+ Transformed text (uppercase if caps enabled in session mode)
50
+ """
51
+ if self.mode == "session" and self.caps_enabled:
52
+ # Transform letters to uppercase, preserve special characters
53
+ return "".join(c.upper() if c.isalpha() else c for c in text)
54
+ return text
55
+
56
+ def should_use_system_capslock(self) -> bool:
57
+ """Check if system-level caps lock should be used."""
58
+ return self.mode == "system"
59
+
60
+
61
+ class PyautoguiConfig(BaseModel):
62
+ """Configuration for PyautoguiActionHandler."""
63
+
64
+ drag_duration: float = Field(
65
+ default=0.5, description="Duration for drag operations in seconds"
66
+ )
67
+ scroll_amount: int = Field(
68
+ default=30, description="Amount to scroll (positive for up, negative for down)"
69
+ )
70
+ wait_duration: float = Field(
71
+ default=1.0, description="Duration for wait actions in seconds"
72
+ )
73
+ action_pause: float = Field(
74
+ default=0.1, description="Pause between PyAutoGUI actions in seconds"
75
+ )
76
+ hotkey_interval: float = Field(
77
+ default=0.1, description="Interval between key presses in hotkey combinations"
78
+ )
79
+ capslock_mode: str = Field(
80
+ default="session",
81
+ description="Caps lock handling mode: 'session' (internal state) or 'system' (OS-level)",
82
+ )
83
+ macos_ctrl_to_cmd: bool = Field(
84
+ default=True,
85
+ description="Replace 'ctrl' with 'command' in hotkey combinations on macOS",
86
+ )
87
+
88
+
89
+ class PyautoguiActionHandler:
90
+ """
91
+ Handles actions to be executed using PyAutoGUI.
92
+
93
+ This class provides functionality for handling and executing a sequence of
94
+ actions using the PyAutoGUI library. It processes a list of actions and executes
95
+ them as per the implementation.
96
+
97
+ Methods:
98
+ __call__: Executes the provided list of actions.
99
+
100
+ Args:
101
+ actions (list[Action]): List of actions to be processed and executed.
102
+ """
103
+
104
+ def __init__(self, config: PyautoguiConfig | None = None):
105
+ # Use default config if none provided
106
+ self.config = config or PyautoguiConfig()
107
+ # Get screen dimensions for coordinate denormalization
108
+ self.screen_width, self.screen_height = pyautogui.size()
109
+ # Set default delay between actions
110
+ pyautogui.PAUSE = self.config.action_pause
111
+ # Initialize caps lock manager
112
+ self.caps_manager = CapsLockManager(mode=self.config.capslock_mode)
113
+
114
+ def _denormalize_coords(self, x: float, y: float) -> tuple[int, int]:
115
+ """Convert coordinates from 0-1000 range to actual screen coordinates.
116
+
117
+ Also handles corner coordinates to prevent PyAutoGUI fail-safe trigger.
118
+ Corner coordinates (0,0), (0,max), (max,0), (max,max) are offset by 1 pixel.
119
+ """
120
+ screen_x = int(x * self.screen_width / 1000)
121
+ screen_y = int(y * self.screen_height / 1000)
122
+
123
+ # Prevent fail-safe by adjusting corner coordinates
124
+ # Check if coordinates are at screen corners (with small tolerance)
125
+ if screen_x < 1:
126
+ screen_x = 1
127
+ elif screen_x > self.screen_width - 1:
128
+ screen_x = self.screen_width - 1
129
+
130
+ if screen_y < 1:
131
+ screen_y = 1
132
+ elif screen_y > self.screen_height - 1:
133
+ screen_y = self.screen_height - 1
134
+
135
+ return screen_x, screen_y
136
+
137
+ def _parse_coords(self, args_str: str) -> tuple[int, int]:
138
+ """Extract x, y coordinates from argument string."""
139
+ match = re.match(r"(\d+),\s*(\d+)", args_str)
140
+ if not match:
141
+ raise ValueError(f"Invalid coordinates format: {args_str}")
142
+ x, y = int(match.group(1)), int(match.group(2))
143
+ return self._denormalize_coords(x, y)
144
+
145
+ def _parse_drag_coords(self, args_str: str) -> tuple[int, int, int, int]:
146
+ """Extract x1, y1, x2, y2 coordinates from drag argument string."""
147
+ match = re.match(r"(\d+),\s*(\d+),\s*(\d+),\s*(\d+)", args_str)
148
+ if not match:
149
+ raise ValueError(f"Invalid drag coordinates format: {args_str}")
150
+ x1, y1, x2, y2 = (
151
+ int(match.group(1)),
152
+ int(match.group(2)),
153
+ int(match.group(3)),
154
+ int(match.group(4)),
155
+ )
156
+ x1, y1 = self._denormalize_coords(x1, y1)
157
+ x2, y2 = self._denormalize_coords(x2, y2)
158
+ return x1, y1, x2, y2
159
+
160
+ def _parse_scroll(self, args_str: str) -> tuple[int, int, str]:
161
+ """Extract x, y, direction from scroll argument string."""
162
+ match = re.match(r"(\d+),\s*(\d+),\s*(\w+)", args_str)
163
+ if not match:
164
+ raise ValueError(f"Invalid scroll format: {args_str}")
165
+ x, y = int(match.group(1)), int(match.group(2))
166
+ x, y = self._denormalize_coords(x, y)
167
+ direction = match.group(3).lower()
168
+ return x, y, direction
169
+
170
+ def _normalize_key(self, key: str) -> str:
171
+ """Normalize key names for consistency."""
172
+ key = key.strip().lower()
173
+ # Normalize caps lock variations
174
+ hotkey_variations_mapping = {
175
+ "capslock": ["caps_lock", "caps", "capslock"],
176
+ "pgup": ["page_up", "pageup"],
177
+ "pgdn": ["page_down", "pagedown"],
178
+ }
179
+ for normalized, variations in hotkey_variations_mapping.items():
180
+ if key in variations:
181
+ return normalized
182
+ # Remap ctrl to command on macOS if enabled
183
+ if self.config.macos_ctrl_to_cmd and sys.platform == "darwin" and key == "ctrl":
184
+ return "command"
185
+ return key
186
+
187
+ def _parse_hotkey(self, args_str: str) -> list[str]:
188
+ """Parse hotkey string into list of keys."""
189
+ # Remove parentheses if present
190
+ args_str = args_str.strip("()")
191
+ # Split by '+' to get individual keys
192
+ keys = [self._normalize_key(key) for key in args_str.split("+")]
193
+ return keys
194
+
195
+ def _execute_single_action(self, action: Action) -> None:
196
+ """Execute a single action once."""
197
+ arg = action.argument.strip("()") # Remove outer parentheses if present
198
+
199
+ match action.type:
200
+ case ActionType.CLICK:
201
+ x, y = self._parse_coords(arg)
202
+ pyautogui.click(x, y)
203
+
204
+ case ActionType.LEFT_DOUBLE:
205
+ x, y = self._parse_coords(arg)
206
+ if sys.platform == "darwin":
207
+ _macos.macos_click(x, y, clicks=2)
208
+ else:
209
+ pyautogui.doubleClick(x, y)
210
+
211
+ case ActionType.LEFT_TRIPLE:
212
+ x, y = self._parse_coords(arg)
213
+ if sys.platform == "darwin":
214
+ _macos.macos_click(x, y, clicks=3)
215
+ else:
216
+ pyautogui.tripleClick(x, y)
217
+
218
+ case ActionType.RIGHT_SINGLE:
219
+ x, y = self._parse_coords(arg)
220
+ pyautogui.rightClick(x, y)
221
+
222
+ case ActionType.DRAG:
223
+ x1, y1, x2, y2 = self._parse_drag_coords(arg)
224
+ pyautogui.moveTo(x1, y1)
225
+ pyautogui.dragTo(
226
+ x2, y2, duration=self.config.drag_duration, button="left"
227
+ )
228
+
229
+ case ActionType.HOTKEY:
230
+ keys = self._parse_hotkey(arg)
231
+ # Check if this is a caps lock key press
232
+ if len(keys) == 1 and keys[0] == "capslock":
233
+ if self.caps_manager.should_use_system_capslock():
234
+ # System mode: use OS-level caps lock
235
+ pyautogui.hotkey(
236
+ "capslock", interval=self.config.hotkey_interval
237
+ )
238
+ else:
239
+ # Session mode: toggle internal state
240
+ self.caps_manager.toggle()
241
+ else:
242
+ # Regular hotkey combination
243
+ pyautogui.hotkey(*keys, interval=self.config.hotkey_interval)
244
+
245
+ case ActionType.TYPE:
246
+ # Remove quotes if present
247
+ text = arg.strip("\"'")
248
+ # Apply caps lock transformation if needed
249
+ text = self.caps_manager.transform_text(text)
250
+ pyautogui.typewrite(text)
251
+
252
+ case ActionType.SCROLL:
253
+ x, y, direction = self._parse_scroll(arg)
254
+ pyautogui.moveTo(x, y)
255
+ scroll_amount = (
256
+ self.config.scroll_amount
257
+ if direction == "up"
258
+ else -self.config.scroll_amount
259
+ )
260
+ pyautogui.scroll(scroll_amount)
261
+
262
+ case ActionType.FINISH:
263
+ # Task completion - no action needed
264
+ pass
265
+
266
+ case ActionType.WAIT:
267
+ # Wait for a short period
268
+ time.sleep(self.config.wait_duration)
269
+
270
+ case ActionType.CALL_USER:
271
+ # Call user - implementation depends on requirements
272
+ print("User intervention requested")
273
+
274
+ case _:
275
+ print(f"Unknown action type: {action.type}")
276
+
277
+ def _execute_action(self, action: Action) -> None:
278
+ """Execute an action, potentially multiple times."""
279
+ count = action.count or 1
280
+
281
+ for _ in range(count):
282
+ self._execute_single_action(action)
283
+
284
+ def __call__(self, actions: list[Action]) -> None:
285
+ """Execute the provided list of actions."""
286
+ for action in actions:
287
+ try:
288
+ self._execute_action(action)
289
+ except Exception as e:
290
+ print(f"Error executing action {action.type}: {e}")
291
+ raise
@@ -0,0 +1,41 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ from typing import Optional
10
+
11
+ from ..types import Image
12
+ from ..types.models.image_config import ImageConfig
13
+ from .pil_image import PILImage
14
+
15
+
16
+ class ScreenshotMaker:
17
+ """Takes screenshots using pyautogui."""
18
+
19
+ def __init__(self, config: ImageConfig | None = None):
20
+ self.config = config or ImageConfig()
21
+ self._last_image: Optional[PILImage] = None
22
+
23
+ def __call__(self) -> Image:
24
+ """Take and process a screenshot."""
25
+ # Create PILImage from screenshot
26
+ pil_image = PILImage.from_screenshot()
27
+
28
+ # Apply transformation if config is set
29
+ if self.config:
30
+ pil_image = pil_image.transform(self.config)
31
+
32
+ # Store as the last image
33
+ self._last_image = pil_image
34
+
35
+ return pil_image
36
+
37
+ def last_image(self) -> Image:
38
+ """Return the last screenshot taken, or take a new one if none exists."""
39
+ if self._last_image is None:
40
+ return self()
41
+ return self._last_image
oagi/logging.py ADDED
@@ -0,0 +1,55 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ import logging
10
+ import os
11
+
12
+
13
+ def get_logger(name: str) -> logging.Logger:
14
+ """
15
+ Get a logger with the specified name under the 'oagi' namespace.
16
+
17
+ Log level is controlled by OAGI_LOG environment variable.
18
+ Valid values: DEBUG, INFO, WARNING, ERROR, CRITICAL
19
+ Default: INFO
20
+ """
21
+ logger = logging.getLogger(f"oagi.{name}")
22
+ oagi_root = logging.getLogger("oagi")
23
+
24
+ # Get log level from environment
25
+ log_level = os.getenv("OAGI_LOG", "INFO").upper()
26
+
27
+ # Convert string to logging level
28
+ try:
29
+ level = getattr(logging, log_level)
30
+ except AttributeError:
31
+ level = logging.INFO
32
+
33
+ # Configure root oagi logger once
34
+ if not oagi_root.handlers:
35
+ handler = logging.StreamHandler()
36
+ formatter = logging.Formatter(
37
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
38
+ )
39
+ handler.setFormatter(formatter)
40
+ oagi_root.addHandler(handler)
41
+ # Prevent propagation to root logger to avoid duplicate logs
42
+ oagi_root.propagate = False
43
+
44
+ # Always update level in case environment variable changed
45
+ oagi_root.setLevel(level)
46
+
47
+ # Suppress verbose httpx logs unless DEBUG level is enabled
48
+ # httpx logs every HTTP request at INFO level by default
49
+ httpx_logger = logging.getLogger("httpx")
50
+ if level == logging.DEBUG:
51
+ httpx_logger.setLevel(logging.DEBUG)
52
+ else:
53
+ httpx_logger.setLevel(logging.WARNING)
54
+
55
+ return logger
@@ -0,0 +1,13 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ from .config import ServerConfig
10
+ from .main import create_app
11
+ from .socketio_server import sio
12
+
13
+ __all__ = ["create_app", "sio", "ServerConfig"]
@@ -0,0 +1,98 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ import logging
10
+ from typing import TYPE_CHECKING
11
+
12
+ from ..types import URL
13
+ from ..types.models.action import Action
14
+ from .models import ScreenshotRequestData, ScreenshotResponseData
15
+
16
+ if TYPE_CHECKING:
17
+ from .session_store import Session
18
+ from .socketio_server import SessionNamespace
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class SocketIOActionHandler:
24
+ """Wraps Socket.IO connection as an AsyncActionHandler.
25
+
26
+ This handler emits actions through the Socket.IO connection to the client.
27
+ """
28
+
29
+ def __init__(self, namespace: "SessionNamespace", session: "Session"):
30
+ self.namespace = namespace
31
+ self.session = session
32
+
33
+ async def __call__(self, actions: list[Action]) -> None:
34
+ if not actions:
35
+ logger.debug("No actions to execute")
36
+ return
37
+
38
+ logger.debug(f"Executing {len(actions)} actions via Socket.IO")
39
+ await self.namespace._emit_actions(self.session, actions)
40
+
41
+
42
+ class SocketIOImageProvider:
43
+ """Wraps Socket.IO connection as an AsyncImageProvider.
44
+
45
+ This provider requests screenshots from the client through Socket.IO.
46
+ """
47
+
48
+ def __init__(
49
+ self,
50
+ namespace: "SessionNamespace",
51
+ session: "Session",
52
+ oagi_client,
53
+ ):
54
+ self.namespace = namespace
55
+ self.session = session
56
+ self.oagi_client = oagi_client
57
+ self._last_url: str | None = None
58
+
59
+ async def __call__(self) -> URL:
60
+ logger.debug("Requesting screenshot via Socket.IO")
61
+
62
+ # Get S3 presigned URL from OAGI
63
+ upload_response = await self.oagi_client.get_s3_presigned_url()
64
+
65
+ # Request screenshot from client with the presigned URL
66
+ screenshot_data = await self.namespace.call(
67
+ "request_screenshot",
68
+ ScreenshotRequestData(
69
+ presigned_url=upload_response.url,
70
+ uuid=upload_response.uuid,
71
+ expires_at=str(upload_response.expires_at), # Convert int to string
72
+ ).model_dump(),
73
+ to=self.session.socket_id,
74
+ timeout=self.namespace.config.socketio_timeout,
75
+ )
76
+
77
+ if not screenshot_data:
78
+ raise Exception("No response from screenshot request")
79
+
80
+ # Validate response
81
+ ack = ScreenshotResponseData(**screenshot_data)
82
+ if not ack.success:
83
+ raise Exception(f"Screenshot upload failed: {ack.error}")
84
+
85
+ # Store the URL for last_image()
86
+ self._last_url = upload_response.download_url
87
+ self.session.current_screenshot_url = upload_response.download_url
88
+
89
+ logger.debug(f"Screenshot captured successfully: {upload_response.uuid}")
90
+ return URL(upload_response.download_url)
91
+
92
+ async def last_image(self) -> URL:
93
+ if self._last_url:
94
+ logger.debug("Returning last captured screenshot")
95
+ return URL(self._last_url)
96
+
97
+ logger.debug("No previous screenshot, capturing new one")
98
+ return await self()
oagi/server/config.py ADDED
@@ -0,0 +1,46 @@
1
+ # -----------------------------------------------------------------------------
2
+ # Copyright (c) OpenAGI Foundation
3
+ # All rights reserved.
4
+ #
5
+ # This file is part of the official API project.
6
+ # Licensed under the MIT License.
7
+ # -----------------------------------------------------------------------------
8
+
9
+ from pydantic import Field
10
+
11
+ from ..exceptions import check_optional_dependency
12
+
13
+ check_optional_dependency("pydantic_settings", "Server features", "server")
14
+ from pydantic_settings import BaseSettings # noqa: E402
15
+
16
+
17
+ class ServerConfig(BaseSettings):
18
+ # OAGI API settings
19
+ oagi_api_key: str = Field(..., alias="OAGI_API_KEY")
20
+ oagi_base_url: str = Field(default="https://api.agiopen.org", alias="OAGI_BASE_URL")
21
+
22
+ # Server settings
23
+ server_host: str = Field(default="0.0.0.0", alias="OAGI_SERVER_HOST")
24
+ server_port: int = Field(default=8000, alias="OAGI_SERVER_PORT")
25
+ cors_allowed_origins: str = Field(default="*", alias="OAGI_CORS_ORIGINS")
26
+
27
+ # Session settings
28
+ session_timeout_seconds: float = Field(default=10.0)
29
+
30
+ # Model settings
31
+ default_model: str = Field(default="lux-actor-1", alias="OAGI_DEFAULT_MODEL")
32
+ default_temperature: float = Field(default=0.5, ge=0.0, le=2.0)
33
+
34
+ # Agent settings
35
+ max_steps: int = Field(default=20, alias="OAGI_MAX_STEPS", ge=1, le=100)
36
+
37
+ # Socket.IO settings
38
+ socketio_path: str = Field(default="/socket.io")
39
+ socketio_timeout: float = Field(default=30.0)
40
+
41
+ model_config = {
42
+ "env_file": ".env",
43
+ "env_file_encoding": "utf-8",
44
+ "populate_by_name": True,
45
+ "extra": "ignore",
46
+ }