orionis 0.481.0__py3-none-any.whl → 0.483.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.
@@ -0,0 +1,139 @@
1
+ import importlib
2
+ import os
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from rich.panel import Panel
6
+ from rich.table import Table
7
+ from orionis.console.base.command import BaseCommand
8
+ from orionis.console.contracts.schedule import ISchedule
9
+ from orionis.console.exceptions import CLIOrionisRuntimeError
10
+ from orionis.foundation.contracts.application import IApplication
11
+
12
+ class ScheduleListCommand(BaseCommand):
13
+ """
14
+ Command class to display usage information for the Orionis CLI.
15
+
16
+ Methods
17
+ -------
18
+ handle(orionis: IApplication, console: Console) -> bool
19
+ Displays a table of scheduled tasks defined in the application.
20
+ Imports the scheduler module, retrieves scheduled jobs, and prints them
21
+ in a formatted table using the rich library.
22
+
23
+ Returns
24
+ -------
25
+ bool
26
+ Returns True if the scheduled jobs are listed successfully or if no jobs are found.
27
+ Raises CLIOrionisRuntimeError if an error occurs during execution.
28
+ """
29
+
30
+ # Indicates whether timestamps will be shown in the command output
31
+ timestamps: bool = False
32
+
33
+ # Command signature and description
34
+ signature: str = "schedule:list"
35
+
36
+ # Command description
37
+ description: str = "Executes the scheduled tasks defined in the application."
38
+
39
+ def handle(self, orionis: IApplication, console: Console) -> bool:
40
+ """
41
+ Displays a table of scheduled jobs defined in the application.
42
+
43
+ This method dynamically imports the scheduler module, retrieves the list of
44
+ scheduled jobs using the ISchedule service, and prints the jobs in a formatted
45
+ table. If no jobs are found, a message is displayed. Handles and reports errors
46
+ encountered during the process.
47
+
48
+ Parameters
49
+ ----------
50
+ orionis : IApplication
51
+ The application instance providing configuration and service resolution.
52
+ console : Console
53
+ The rich Console instance used for output.
54
+
55
+ Returns
56
+ -------
57
+ bool
58
+ Returns True if the scheduled jobs are listed successfully or if no jobs are found.
59
+ Raises CLIOrionisRuntimeError if an error occurs during execution.
60
+ """
61
+
62
+ try:
63
+
64
+ # Get the absolute path of the scheduler from the application configuration
65
+ scheduler_path = orionis.path('console_scheduler')
66
+
67
+ # Get the base path from the current working directory
68
+ base_path = Path(os.getcwd()).resolve()
69
+ scheduler_path = Path(scheduler_path).resolve()
70
+ rel_path = scheduler_path.relative_to(base_path)
71
+
72
+ # Convert the path to a module name (replace separators with dots, remove .py)
73
+ module_name = ".".join(rel_path.with_suffix('').parts)
74
+
75
+ # Dynamically import the scheduler module
76
+ scheduler_module = importlib.import_module(module_name)
77
+
78
+ # Retrieve the Scheduler class from the imported module
79
+ Scheduler = getattr(scheduler_module, "Scheduler", None)
80
+
81
+ # Raise an error if the Scheduler class is not found
82
+ if Scheduler is None:
83
+ raise CLIOrionisRuntimeError(f"Scheduler class not found in module {module_name}")
84
+
85
+ # Retrieve the 'tasks' method from the Scheduler class
86
+ task_method = getattr(Scheduler, "tasks", None)
87
+
88
+ # Raise an error if the 'tasks' method is not found
89
+ if task_method is None:
90
+ raise CLIOrionisRuntimeError(f"Method 'tasks' not found in Scheduler class in module {module_name}")
91
+
92
+ # Create an instance of ISchedule using the application container
93
+ schedule_serice: ISchedule = orionis.make(ISchedule)
94
+
95
+ # Initialize the scheduled tasks by calling the 'tasks' method
96
+ task_method(schedule_serice)
97
+
98
+ # Retrieve the list of scheduled jobs/events
99
+ list_tasks = schedule_serice.events()
100
+
101
+ # Display a message if no scheduled jobs are found
102
+ if not list_tasks:
103
+ console.line()
104
+ console.print(Panel("No scheduled jobs found.", border_style="green"))
105
+ console.line()
106
+ return True
107
+
108
+ # Create and configure a table to display scheduled jobs
109
+ table = Table(title="Scheduled Jobs", show_lines=True)
110
+ table.add_column("Signature", style="cyan", no_wrap=True)
111
+ table.add_column("Arguments", style="magenta")
112
+ table.add_column("Purpose", style="green")
113
+ table.add_column("Random Delay", style="yellow")
114
+ table.add_column("Start Date", style="white")
115
+ table.add_column("End Date", style="white")
116
+ table.add_column("Details", style="dim")
117
+
118
+ # Populate the table with job details
119
+ for job in list_tasks:
120
+ signature = str(job.get("signature", ""))
121
+ args = ", ".join(map(str, job.get("args", [])))
122
+ purpose = str(job.get("purpose", ""))
123
+ random_delay = str(job.get("random_delay", ""))
124
+ start_date = str(job.get("start_date", "")) if job.get("start_date") else "-"
125
+ end_date = str(job.get("end_date", "")) if job.get("end_date") else "-"
126
+ details = str(job.get("details", ""))
127
+
128
+ table.add_row(signature, args, purpose, random_delay, start_date, end_date, details)
129
+
130
+ # Print the table to the console
131
+ console.line()
132
+ console.print(table)
133
+ console.line()
134
+ return True
135
+
136
+ except Exception as exc:
137
+
138
+ # Catch any unexpected exceptions and raise as a CLIOrionisRuntimeError
139
+ raise CLIOrionisRuntimeError(f"An unexpected error occurred while clearing the cache: {exc}")
@@ -1,18 +1,41 @@
1
1
  import importlib
2
2
  import os
3
+ from datetime import datetime
3
4
  from pathlib import Path
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+ from rich.text import Text
4
8
  from orionis.console.base.command import BaseCommand
5
9
  from orionis.console.contracts.schedule import ISchedule
6
10
  from orionis.console.exceptions import CLIOrionisRuntimeError
7
11
  from orionis.foundation.contracts.application import IApplication
8
- from rich.console import Console
9
- from rich.panel import Panel
10
- from rich.text import Text
11
- from datetime import datetime
12
12
 
13
13
  class ScheduleWorkCommand(BaseCommand):
14
14
  """
15
- Command class to display usage information for the Orionis CLI.
15
+ Executes the scheduled tasks defined in the application's scheduler.
16
+
17
+ This command dynamically loads the scheduler module specified in the application's configuration,
18
+ retrieves the `Scheduler` class and its `tasks` method, registers the scheduled tasks with the
19
+ ISchedule service, and starts the scheduler worker. It provides user feedback via the console and
20
+ handles errors by raising CLIOrionisRuntimeError exceptions.
21
+
22
+ Parameters
23
+ ----------
24
+ orionis : IApplication
25
+ The application instance providing configuration and service resolution.
26
+ console : Console
27
+ The Rich console instance used for displaying output to the user.
28
+
29
+ Returns
30
+ -------
31
+ bool
32
+ Returns True if the scheduler worker starts successfully. If an error occurs during the process,
33
+ a CLIOrionisRuntimeError is raised.
34
+
35
+ Raises
36
+ ------
37
+ CLIOrionisRuntimeError
38
+ If the scheduler module, class, or tasks method cannot be found, or if any unexpected error occurs.
16
39
  """
17
40
 
18
41
  # Indicates whether timestamps will be shown in the command output
@@ -25,44 +48,66 @@ class ScheduleWorkCommand(BaseCommand):
25
48
  description: str = "Executes the scheduled tasks defined in the application."
26
49
 
27
50
  async def handle(self, orionis: IApplication, console: Console) -> bool:
28
-
51
+ """
52
+ Executes the scheduled tasks defined in the application's scheduler.
53
+
54
+ This method dynamically loads the scheduler module specified in the application's configuration,
55
+ retrieves the `Scheduler` class and its `tasks` method, registers the scheduled tasks with the
56
+ ISchedule service, and starts the scheduler worker. It provides user feedback via the console and
57
+ handles errors by raising CLIOrionisRuntimeError exceptions.
58
+
59
+ Parameters
60
+ ----------
61
+ orionis : IApplication
62
+ The application instance providing configuration and service resolution.
63
+ console : Console
64
+ The Rich console instance used for displaying output to the user.
65
+
66
+ Returns
67
+ -------
68
+ bool
69
+ Returns True if the scheduler worker starts successfully. If an error occurs during the process,
70
+ a CLIOrionisRuntimeError is raised.
71
+
72
+ Raises
73
+ ------
74
+ CLIOrionisRuntimeError
75
+ If the scheduler module, class, or tasks method cannot be found, or if any unexpected error occurs.
76
+ """
29
77
  try:
30
-
31
- # Obtener la ruta absoluta del scheduler desde la configuración de la aplicación
78
+ # Get the absolute path to the scheduler module from the application configuration
32
79
  scheduler_path = orionis.path('console_scheduler')
33
80
 
34
- # Obtener la base path desde la variable de entorno o desde la configuración local
81
+ # Resolve the base path (current working directory)
35
82
  base_path = Path(os.getcwd()).resolve()
36
83
  scheduler_path = Path(scheduler_path).resolve()
84
+
85
+ # Compute the relative path from the base path to the scheduler module
37
86
  rel_path = scheduler_path.relative_to(base_path)
38
87
 
39
- # Reemplazar los separadores por puntos y quitar la extensión .py
88
+ # Convert the relative path to a Python module name (dot notation, no .py extension)
40
89
  module_name = ".".join(rel_path.with_suffix('').parts)
41
90
 
42
- # Importar el módulo del scheduler
91
+ # Dynamically import the scheduler module
43
92
  scheduler_module = importlib.import_module(module_name)
44
93
 
45
- # Obtener la clase Scheduler del módulo importado
94
+ # Retrieve the Scheduler class from the imported module
46
95
  Scheduler = getattr(scheduler_module, "Scheduler", None)
47
-
48
- # Check if the Scheduler class was found
49
96
  if Scheduler is None:
50
97
  raise CLIOrionisRuntimeError(f"Scheduler class not found in module {module_name}")
51
98
 
52
- # Obtener el método tasks de la clase Scheduler
99
+ # Retrieve the 'tasks' method from the Scheduler class
53
100
  task_method = getattr(Scheduler, "tasks", None)
54
-
55
- # Check if the method exists
56
101
  if task_method is None:
57
102
  raise CLIOrionisRuntimeError(f"Method 'tasks' not found in Scheduler class in module {module_name}")
58
103
 
59
- # Crear una instancia de ISchedule
104
+ # Create an instance of the ISchedule service
60
105
  schedule_serice: ISchedule = orionis.make(ISchedule)
61
106
 
62
- # Inicializar el metodo
107
+ # Register scheduled tasks using the Scheduler's tasks method
63
108
  task_method(schedule_serice)
64
109
 
65
- # Display a professional start message for the scheduler worker
110
+ # Display a start message for the scheduler worker
66
111
  console.line()
67
112
  start_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
68
113
  panel_content = Text.assemble(
@@ -79,10 +124,10 @@ class ScheduleWorkCommand(BaseCommand):
79
124
  )
80
125
  console.line()
81
126
 
82
- # Iniciar el scheduler
127
+ # Start the scheduler worker asynchronously
83
128
  await schedule_serice.start()
129
+ return True
84
130
 
85
131
  except Exception as exc:
86
-
87
- # Catch any unexpected exceptions and raise as a CLIOrionisRuntimeError
132
+ # Raise any unexpected exceptions as CLIOrionisRuntimeError
88
133
  raise CLIOrionisRuntimeError(f"An unexpected error occurred while clearing the cache: {exc}")
@@ -112,6 +112,7 @@ class Reactor(IReactor):
112
112
  from orionis.console.commands.workflow import WorkFlowGithubCommand
113
113
  from orionis.console.commands.cache import CacheClearCommand
114
114
  from orionis.console.commands.scheduler_work import ScheduleWorkCommand
115
+ from orionis.console.commands.scheduler_list import ScheduleListCommand
115
116
 
116
117
  # List of core command classes to load (extend this list as more core commands are added)
117
118
  core_commands = [
@@ -121,7 +122,8 @@ class Reactor(IReactor):
121
122
  PublisherCommand,
122
123
  WorkFlowGithubCommand,
123
124
  CacheClearCommand,
124
- ScheduleWorkCommand
125
+ ScheduleWorkCommand,
126
+ ScheduleListCommand
125
127
  ]
126
128
 
127
129
  # Iterate through the core command classes and register them
@@ -5,7 +5,7 @@
5
5
  NAME = "orionis"
6
6
 
7
7
  # Current version of the framework
8
- VERSION = "0.481.0"
8
+ VERSION = "0.483.0"
9
9
 
10
10
  # Full name of the author or maintainer of the project
11
11
  AUTHOR = "Raul Mauricio Uñate Castro"
@@ -1,6 +1,5 @@
1
1
  import os
2
- from datetime import datetime
3
- from pathlib import Path
2
+ from datetime import datetime, timedelta
4
3
 
5
4
  class FileNameLogger:
6
5
 
@@ -18,7 +17,6 @@ class FileNameLogger:
18
17
  ValueError
19
18
  If the provided path is not a non-empty string.
20
19
  """
21
-
22
20
  # Validate that the path is a non-empty string
23
21
  if not isinstance(path, str) or not path:
24
22
  raise ValueError("The 'path' parameter must be a non-empty string.")
@@ -26,26 +24,31 @@ class FileNameLogger:
26
24
  # Store the stripped path as a private instance variable
27
25
  self.__path = path.strip()
28
26
 
29
- def generate(self) -> str:
27
+ def __splitDirectory(
28
+ self
29
+ ) -> tuple[str, str, str]:
30
30
  """
31
- Generate a new log file path with a timestamp prefix.
31
+ Split the original file path into directory, file name, and extension.
32
32
 
33
- The method constructs a new file path by prefixing the original file name
34
- with a timestamp in the format 'YYYYMMDD_HHMMSS'. It also ensures that the
35
- directory for the log file exists, creating it if necessary.
33
+ This private method processes the stored file path, separating it into its
34
+ directory path, base file name (without extension), and file extension. It
35
+ also ensures that the directory exists, creating it if necessary.
36
36
 
37
37
  Returns
38
38
  -------
39
- str
40
- The full path to the log file with a timestamped file name.
39
+ tuple of str
40
+ A tuple containing:
41
+ - The directory path as a string.
42
+ - The base file name (without extension) as a string.
43
+ - The file extension (including the dot) as a string.
41
44
 
42
45
  Notes
43
46
  -----
44
- - The timestamp is generated using the current date and time.
45
- - The directory for the log file is created if it does not exist.
47
+ - The method handles both forward slash ('/') and backslash ('\\') as path separators.
48
+ - If the directory does not exist, it will be created.
46
49
  """
47
50
 
48
- # Split the original path into components based on the separator
51
+ # Determine the path separator and split the path into components
49
52
  if '/' in self.__path:
50
53
  parts = self.__path.split('/')
51
54
  elif '\\' in self.__path:
@@ -53,22 +56,471 @@ class FileNameLogger:
53
56
  else:
54
57
  parts = self.__path.split(os.sep)
55
58
 
56
- # Extract the base file name and its extension
59
+ # Extract the base file name and its extension from the last component
57
60
  filename, ext = os.path.splitext(parts[-1])
58
61
 
59
62
  # Reconstruct the directory path (excluding the file name)
60
63
  path = os.path.join(*parts[:-1]) if len(parts) > 1 else ''
61
64
 
62
- # Generate a timestamp prefix for the file name
63
- prefix = datetime.now().strftime("%Y%m%d_%H%M%S")
65
+ # Ensure the directory exists; create it if it does not
66
+ if not os.path.isdir(path):
67
+ os.makedirs(path, exist_ok=True)
68
+
69
+ # Return the directory path, file name, and extension as a tuple
70
+ return path, filename, ext
71
+
72
+ def __listFilesInDirectory(
73
+ self,
74
+ directory: str
75
+ ) -> list[str]:
76
+ """
77
+ List all files in the specified directory.
78
+
79
+ This private method retrieves all files in the given directory, returning
80
+ their names as a list. It does not include subdirectories or hidden files.
81
+
82
+ Parameters
83
+ ----------
84
+ directory : str
85
+ The path to the directory from which to list files.
86
+
87
+ Returns
88
+ -------
89
+ list of dict
90
+ A list of dictionaries, each containing:
91
+
92
+ Notes
93
+ -----
94
+ - The method does not check for file types; it returns all files regardless of extension.
95
+ - If the directory does not exist, an empty list is returned.
96
+ """
97
+
98
+ # Check if the directory exists; if not, return an empty list
99
+ if not os.path.isdir(directory):
100
+ return []
101
+
102
+ # List all files in the directory and return their names
103
+ files = []
104
+ for f in os.listdir(directory):
105
+ if os.path.isfile(os.path.join(directory, f)):
106
+ files.append({
107
+ 'name': f,
108
+ 'path': os.path.join(directory, f),
109
+ 'size': os.path.getsize(os.path.join(directory, f)),
110
+ 'modified': datetime.fromtimestamp(os.path.getmtime(os.path.join(directory, f)))
111
+ })
112
+
113
+ # Return the list of file names
114
+ return files
115
+
116
+ def __stack(
117
+ self,
118
+ path: str,
119
+ filename: str,
120
+ ext: str
121
+ ) -> str:
122
+ """
123
+ Construct the log file path for the 'stack' channel.
124
+
125
+ This private method generates the full file path for a log file when the
126
+ 'stack' channel is specified. It combines the provided directory path,
127
+ base file name, and file extension to create the complete file path.
128
+
129
+ Parameters
130
+ ----------
131
+ path : str
132
+ The directory path where the log file should be stored.
133
+ filename : str
134
+ The base name of the log file (without extension).
135
+ ext : str
136
+ The file extension, including the dot (e.g., '.log').
137
+
138
+ Returns
139
+ -------
140
+ str
141
+ The full file path for the log file in the specified directory.
142
+
143
+ Notes
144
+ -----
145
+ - This method does not modify or create any directories; it only constructs the path.
146
+ - The resulting path is platform-independent.
147
+ """
148
+
149
+ # Join the directory path, file name, and extension to form the full file path
150
+ return os.path.join(path, f"{filename}{ext}")
151
+
152
+ def __hourly(
153
+ self,
154
+ directory: str,
155
+ filename: str,
156
+ ext: str
157
+ ) -> str:
158
+ """
159
+ Construct the log file path for the 'hourly' channel.
160
+
161
+ This private method generates the full file path for a log file when the
162
+ 'hourly' channel is specified. If a log file has already been created within
163
+ the last hour, it returns its path; otherwise, it creates a new file name
164
+ with a timestamp.
64
165
 
65
- # Build the full file path with the timestamped file name
66
- full_path = os.path.join(path, f"{prefix}_{filename}{ext}")
166
+ Parameters
167
+ ----------
168
+ directory : str
169
+ The directory path where the log file should be stored.
170
+ filename : str
171
+ The base name of the log file (without extension).
172
+ ext : str
173
+ The file extension, including the dot (e.g., '.log').
174
+
175
+ Returns
176
+ -------
177
+ str
178
+ The full file path for the log file in the specified directory.
179
+ """
180
+
181
+ now = datetime.now()
182
+ one_hour_ago = now - timedelta(hours=1)
183
+ files = self.__listFilesInDirectory(directory)
184
+
185
+ # Find the most recent file created within the last hour
186
+ recent_file = None
187
+ for file in sorted(files, key=lambda x: x['modified'], reverse=True):
188
+ if file['modified'] >= one_hour_ago and 'hourly' in file['name']:
189
+ recent_file = file['path']
190
+ break
191
+
192
+ # If a recent file is found, return its path
193
+ if recent_file:
194
+ return recent_file
195
+
196
+ # No recent file found, create a new one with timestamp
197
+ timestamp = now.strftime("%Y%m%d_%H%M%S")
198
+
199
+ # Ensure the filename starts with 'hourly_' if not already present
200
+ if 'hourly' not in filename:
201
+ filename = f"hourly_{filename}"
202
+
203
+ # Construct the new file name with the timestamp prefix
204
+ new_filename = f"{timestamp}_{filename}{ext}"
205
+ return os.path.join(directory, new_filename)
206
+
207
+ def __daily(
208
+ self,
209
+ directory: str,
210
+ filename: str,
211
+ ext: str
212
+ ) -> str:
213
+ """
214
+ Construct the log file path for the 'daily' channel.
215
+
216
+ This private method generates the full file path for a log file when the
217
+ 'daily' channel is specified. If a log file has already been created for
218
+ the current day, it returns its path; otherwise, it creates a new file name
219
+ with the current date and a high-resolution timestamp to ensure uniqueness.
220
+
221
+ Parameters
222
+ ----------
223
+ directory : str
224
+ The directory path where the log file should be stored.
225
+ filename : str
226
+ The base name of the log file (without extension).
227
+ ext : str
228
+ The file extension, including the dot (e.g., '.log').
67
229
 
68
- # Ensure the log directory exists; create it if it does not
69
- log_dir = Path(full_path).parent
70
- if not log_dir.exists():
71
- log_dir.mkdir(parents=True, exist_ok=True)
230
+ Returns
231
+ -------
232
+ str
233
+ The full file path for the log file in the specified directory. If a log
234
+ file for the current day already exists, its path is returned; otherwise,
235
+ a new file path is generated with the current date and a unique timestamp.
236
+
237
+ Notes
238
+ -----
239
+ - The method checks for existing log files for the current day and reuses them if found.
240
+ - If no such file exists, a new file name is generated using the current date and a Unix timestamp.
241
+ - The resulting path is platform-independent.
242
+ """
243
+
244
+ # Get the current date in YYYY_MM_DD format
245
+ date = datetime.now().strftime("%Y_%m_%d")
246
+
247
+ # List all files in the target directory
248
+ files = self.__listFilesInDirectory(directory)
249
+
250
+ # Search for the most recent file created today
251
+ recent_file = None
252
+ for file in sorted(files, key=lambda x: x['modified'], reverse=True):
253
+ if str(file['name']).startswith(date) and 'daily' in file['name']:
254
+ recent_file = file['path']
255
+ break
256
+
257
+ # If a file for today exists, return its path
258
+ if recent_file:
259
+ return recent_file
260
+
261
+ # Prefix the filename with 'daily_' if not already present
262
+ if 'daily' not in filename:
263
+ filename = f"daily_{filename}"
264
+
265
+ # Generate a unique filename using the current date and Unix timestamp
266
+ unix_time = int(datetime.now().timestamp())
267
+ new_filename = f"{date}_{unix_time}_{filename}{ext}"
268
+ return os.path.join(directory, new_filename)
269
+
270
+ def __weekly(
271
+ self,
272
+ directory: str,
273
+ filename: str,
274
+ ext: str
275
+ ) -> str:
276
+ """
277
+ Construct the log file path for the 'weekly' channel.
278
+
279
+ This private method generates the full file path for a log file when the
280
+ 'weekly' channel is specified. If a log file has already been created for
281
+ the current week, it returns its path; otherwise, it creates a new file name
282
+ with the current year, ISO week number, and a Unix timestamp to ensure uniqueness.
283
+
284
+ Parameters
285
+ ----------
286
+ directory : str
287
+ The directory path where the log file should be stored.
288
+ filename : str
289
+ The base name of the log file (without extension).
290
+ ext : str
291
+ The file extension, including the dot (e.g., '.log').
72
292
 
73
- # Return the full path to the timestamped log file
74
- return full_path
293
+ Returns
294
+ -------
295
+ str
296
+ The full file path for the log file in the specified directory. If a log
297
+ file for the current week already exists, its path is returned; otherwise,
298
+ a new file path is generated using the current year, ISO week number, and a unique timestamp.
299
+
300
+ Notes
301
+ -----
302
+ - The method checks for existing log files for the current week and reuses them if found.
303
+ - If no such file exists, a new file name is generated using the current year, ISO week number, and Unix timestamp.
304
+ - The resulting path is platform-independent.
305
+ """
306
+
307
+ # Get the current week number and year using ISO calendar
308
+ now = datetime.now()
309
+ year, week_num, _ = now.isocalendar()
310
+ week = f"{year}_W{week_num:02d}"
311
+
312
+ # List all files in the target directory
313
+ files = self.__listFilesInDirectory(directory)
314
+
315
+ # Search for the most recent file created this week with 'weekly' in its name
316
+ recent_file = None
317
+ for file in sorted(files, key=lambda x: x['modified'], reverse=True):
318
+ if str(file['name']).startswith(week) and 'weekly' in file['name']:
319
+ recent_file = file['path']
320
+ break
321
+
322
+ # If a file for this week exists, return its path
323
+ if recent_file:
324
+ return recent_file
325
+
326
+ # Prefix the filename with 'weekly_' if not already present
327
+ if 'weekly' not in filename:
328
+ filename = f"weekly_{filename}"
329
+
330
+ # Generate a unique filename using the current year, week, and Unix timestamp
331
+ unix_time = int(datetime.now().timestamp())
332
+ new_filename = f"{week}_{unix_time}_{filename}{ext}"
333
+
334
+ # Return the full path to the new weekly log file
335
+ return os.path.join(directory, new_filename)
336
+
337
+ def __monthly(
338
+ self,
339
+ directory: str,
340
+ filename: str,
341
+ ext: str
342
+ ) -> str:
343
+ """
344
+ Construct the log file path for the 'monthly' channel.
345
+
346
+ This private method generates the full file path for a log file when the
347
+ 'monthly' channel is specified. If a log file has already been created for
348
+ the current month, it returns its path; otherwise, it creates a new file name
349
+ with the current year, month, and a Unix timestamp to ensure uniqueness.
350
+
351
+ Parameters
352
+ ----------
353
+ directory : str
354
+ The directory path where the log file should be stored.
355
+ filename : str
356
+ The base name of the log file (without extension).
357
+ ext : str
358
+ The file extension, including the dot (e.g., '.log').
359
+
360
+ Returns
361
+ -------
362
+ str
363
+ The full file path for the log file in the specified directory. If a log
364
+ file for the current month already exists, its path is returned; otherwise,
365
+ a new file path is generated using the current year, month, and a unique timestamp.
366
+
367
+ Notes
368
+ -----
369
+ - The method checks for existing log files for the current month and reuses them if found.
370
+ - If no such file exists, a new file name is generated using the current year, month, and Unix timestamp.
371
+ - The resulting path is platform-independent.
372
+ """
373
+
374
+ # Get the current year and month
375
+ now = datetime.now()
376
+ year = now.year
377
+ month = now.month
378
+ month_str = f"{year}_M{month:02d}"
379
+
380
+ # List all files in the target directory
381
+ files = self.__listFilesInDirectory(directory)
382
+
383
+ # Search for the most recent file created this month with 'monthly' in its name
384
+ recent_file = None
385
+ for file in sorted(files, key=lambda x: x['modified'], reverse=True):
386
+ if str(file['name']).startswith(month_str) and 'monthly' in file['name']:
387
+ recent_file = file['path']
388
+ break
389
+
390
+ # If a file for this month exists, return its path
391
+ if recent_file:
392
+ return recent_file
393
+
394
+ # Prefix the filename with 'monthly_' if not already present
395
+ if 'monthly' not in filename:
396
+ filename = f"monthly_{filename}"
397
+
398
+ # Generate a unique filename using the current year, month, and Unix timestamp
399
+ unix_time = int(datetime.now().timestamp())
400
+ new_filename = f"{month_str}_{unix_time}_{filename}{ext}"
401
+
402
+ # Return the full path to the new monthly log file
403
+ return os.path.join(directory, new_filename)
404
+
405
+ def __chunked(
406
+ self,
407
+ directory: str,
408
+ filename: str,
409
+ ext: str,
410
+ max_bytes: int = 5242880
411
+ ) -> str:
412
+ """
413
+ Construct the log file path for the 'chunked' channel.
414
+
415
+ This private method generates the full file path for a log file when the
416
+ 'chunked' channel is specified. It checks for the most recent log file
417
+ in the specified directory with 'chunked' in its name and a size less than
418
+ the specified maximum number of bytes. If such a file exists, its path is
419
+ returned. Otherwise, a new file name is generated using the current Unix
420
+ timestamp and the 'chunked_' prefix.
421
+
422
+ Parameters
423
+ ----------
424
+ directory : str
425
+ The directory path where the log file should be stored.
426
+ filename : str
427
+ The base name of the log file (without extension).
428
+ ext : str
429
+ The file extension, including the dot (e.g., '.log').
430
+ max_bytes : int, optional
431
+ The maximum allowed size in bytes for a chunked log file. Default is 5 MB (5242880 bytes).
432
+
433
+ Returns
434
+ -------
435
+ str
436
+ The full file path for the chunked log file in the specified directory.
437
+ If a suitable file exists (with 'chunked' in its name and size less than
438
+ `max_bytes`), its path is returned. Otherwise, a new file path is generated
439
+ with a unique timestamp and returned.
440
+
441
+ Notes
442
+ -----
443
+ - The method checks for existing chunked log files and reuses them if their size
444
+ is below the specified threshold.
445
+ - If no such file exists, a new file name is generated using the current Unix timestamp.
446
+ - The resulting path is platform-independent.
447
+ """
448
+
449
+ # List all files in the target directory
450
+ files = self.__listFilesInDirectory(directory)
451
+
452
+ # Search for the most recent file with 'chunked' in its name and size less than max_bytes
453
+ recent_file = None
454
+ for file in sorted(files, key=lambda x: x['modified'], reverse=True):
455
+ if 'chunked' in file['name'] and file['size'] < max_bytes:
456
+ recent_file = file['path']
457
+ break
458
+
459
+ # If a suitable chunked file exists, return its path
460
+ if recent_file:
461
+ return recent_file
462
+
463
+ # Prefix the filename with 'chunked_' if not already present
464
+ if 'chunked' not in filename:
465
+ filename = f"chunked_{filename}"
466
+
467
+ # Generate a unique filename using the current Unix timestamp
468
+ unix_time = int(datetime.now().timestamp())
469
+ new_filename = f"{unix_time}_{filename}{ext}"
470
+
471
+ # Return the full path to the new chunked log file
472
+ return os.path.join(directory, new_filename)
473
+
474
+ def generate(self, channel: str, max_bytes: int = 5242880) -> str:
475
+ """
476
+ Generate the appropriate log file path based on the specified channel.
477
+
478
+ This method determines the log file naming strategy according to the given
479
+ channel type. It delegates the file path construction to the corresponding
480
+ private method for each channel. For the 'chunked' channel, the maximum
481
+ allowed file size can be specified.
482
+
483
+ Parameters
484
+ ----------
485
+ channel : str
486
+ The log channel type. Supported values are:
487
+ 'stack', 'hourly', 'daily', 'weekly', 'monthly', 'chunked'.
488
+ max_bytes : int, optional
489
+ The maximum allowed size in bytes for a chunked log file. Only used
490
+ when `channel` is 'chunked'. Default is 5 MB (5242880 bytes).
491
+
492
+ Returns
493
+ -------
494
+ str
495
+ The full file path for the log file according to the specified channel.
496
+
497
+ Raises
498
+ ------
499
+ ValueError
500
+ If the provided channel is not supported.
501
+
502
+ Notes
503
+ -----
504
+ - The method uses the original file path provided during initialization.
505
+ - For all channels except 'chunked', `max_bytes` is ignored.
506
+ - The resulting file path is platform-independent.
507
+ """
508
+
509
+ # Select the appropriate file path generation strategy based on the channel
510
+ if channel == 'stack':
511
+ return self.__stack(*self.__splitDirectory())
512
+ elif channel == 'hourly':
513
+ return self.__hourly(*self.__splitDirectory())
514
+ elif channel == 'daily':
515
+ return self.__daily(*self.__splitDirectory())
516
+ elif channel == 'weekly':
517
+ return self.__weekly(*self.__splitDirectory())
518
+ elif channel == 'monthly':
519
+ return self.__monthly(*self.__splitDirectory())
520
+ elif channel == 'chunked':
521
+ return self.__chunked(*self.__splitDirectory(), max_bytes=max_bytes)
522
+ else:
523
+ raise ValueError(
524
+ f"Unknown channel: {channel}. Supported channels are: "
525
+ "'stack', 'hourly', 'daily', 'weekly', 'monthly', 'chunked'."
526
+ )
@@ -25,5 +25,8 @@ class PrefixedSizeRotatingFileHandler(RotatingFileHandler):
25
25
  The timestamp prefix helps in identifying the creation time of each rotated log file.
26
26
  """
27
27
 
28
+ # Import Application to access configuration settings
29
+ from orionis.support.facades.application import Application
30
+
28
31
  # Generate the new filename using FileNameLogger, which adds a timestamp prefix.
29
- return FileNameLogger(default_name).generate()
32
+ return FileNameLogger(default_name).generate('chunked', Application.config('logging.channels.chunked.mb_size', 5))
@@ -25,5 +25,8 @@ class PrefixedTimedRotatingFileHandler(TimedRotatingFileHandler):
25
25
  The timestamp prefix helps in organizing and distinguishing rotated log files.
26
26
  """
27
27
 
28
- # Use FileNameLogger to generate a new filename with a timestamp prefix
29
- return FileNameLogger(default_name).generate()
28
+ # Import Application to access configuration settings
29
+ from orionis.support.facades.application import Application
30
+
31
+ # Generate the new filename using FileNameLogger, which adds a timestamp prefix.
32
+ return FileNameLogger(default_name).generate(Application.config('logging.default', 'stack'))
@@ -110,9 +110,6 @@ class Logger(ILogger):
110
110
  # Get the configuration object for the selected channel
111
111
  config_channels = getattr(self.__config.channels, channel)
112
112
 
113
- # Generate the log file path using the FileNameLogger utility
114
- path: str = FileNameLogger(getattr(config_channels, 'path')).generate()
115
-
116
113
  # Determine the logging level (default to DEBUG if not specified)
117
114
  level: Level | int = getattr(config_channels, 'level', 10)
118
115
  level = level if isinstance(level, int) else level.value
@@ -122,7 +119,7 @@ class Logger(ILogger):
122
119
  # Simple file handler for stack channel
123
120
  handlers = [
124
121
  logging.FileHandler(
125
- filename=path,
122
+ filename=FileNameLogger(getattr(config_channels, 'path')).generate(channel),
126
123
  encoding="utf-8"
127
124
  )
128
125
  ]
@@ -131,7 +128,7 @@ class Logger(ILogger):
131
128
  # Rotating file handler for hourly logs
132
129
  handlers = [
133
130
  PrefixedTimedRotatingFileHandler(
134
- filename=path,
131
+ filename=FileNameLogger(getattr(config_channels, 'path')).generate(channel),
135
132
  when="h",
136
133
  interval=1,
137
134
  backupCount=getattr(config_channels, 'retention_hours', 24),
@@ -144,7 +141,7 @@ class Logger(ILogger):
144
141
  # Rotating file handler for daily logs
145
142
  handlers = [
146
143
  PrefixedTimedRotatingFileHandler(
147
- filename=path,
144
+ filename=FileNameLogger(getattr(config_channels, 'path')).generate(channel),
148
145
  when="d",
149
146
  interval=1,
150
147
  backupCount=getattr(config_channels, 'retention_days', 7),
@@ -158,7 +155,7 @@ class Logger(ILogger):
158
155
  # Rotating file handler for weekly logs
159
156
  handlers = [
160
157
  PrefixedTimedRotatingFileHandler(
161
- filename=path,
158
+ filename=FileNameLogger(getattr(config_channels, 'path')).generate(channel),
162
159
  when="w0",
163
160
  interval=1,
164
161
  backupCount=getattr(config_channels, 'retention_weeks', 4),
@@ -171,7 +168,7 @@ class Logger(ILogger):
171
168
  # Rotating file handler for monthly logs
172
169
  handlers = [
173
170
  PrefixedTimedRotatingFileHandler(
174
- filename=path,
171
+ filename=FileNameLogger(getattr(config_channels, 'path')).generate(channel),
175
172
  when="midnight",
176
173
  interval=30,
177
174
  backupCount=getattr(config_channels, 'retention_months', 4),
@@ -182,9 +179,10 @@ class Logger(ILogger):
182
179
 
183
180
  elif channel == "chunked":
184
181
  # Size-based rotating file handler for chunked logs
182
+ max_bytes = getattr(config_channels, 'mb_size', 10) * 1024 * 1024
185
183
  handlers = [
186
184
  PrefixedSizeRotatingFileHandler(
187
- filename=path,
185
+ filename=FileNameLogger(getattr(config_channels, 'path')).generate(channel, max_bytes),
188
186
  maxBytes=getattr(config_channels, 'mb_size', 10) * 1024 * 1024,
189
187
  backupCount=getattr(config_channels, 'files', 5),
190
188
  encoding="utf-8"
@@ -0,0 +1,23 @@
1
+ from orionis.container.facades.facade import Facade
2
+
3
+ class Application(Facade):
4
+
5
+ @classmethod
6
+ def getFacadeAccessor(cls) -> str:
7
+ """
8
+ Get the registered service container binding key for the console component.
9
+
10
+ This method returns the specific binding key that is used to resolve the
11
+ console output service from the dependency injection container. The facade
12
+ pattern uses this key to locate and instantiate the underlying console
13
+ service implementation.
14
+
15
+ Returns
16
+ -------
17
+ str
18
+ The string identifier 'x-orionis.services.log.log_service' used as the
19
+ binding key to resolve the console service from the service container.
20
+ """
21
+
22
+ # Return the predefined binding key for the console output service
23
+ return "x-orionis.services.log.log_service"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: orionis
3
- Version: 0.481.0
3
+ Version: 0.483.0
4
4
  Summary: Orionis Framework – Elegant, Fast, and Powerful.
5
5
  Home-page: https://github.com/orionis-framework/framework
6
6
  Author: Raul Mauricio Uñate Castro
@@ -14,7 +14,8 @@ orionis/console/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
14
14
  orionis/console/commands/cache.py,sha256=8DsYoRzSBLn0P9qkGVItRbo0R6snWBDBg0_Xa7tmVhs,2322
15
15
  orionis/console/commands/help.py,sha256=ooPoenP08ArMAWiL89_oUGD9l1Faen2QjLTG90i4zCQ,3187
16
16
  orionis/console/commands/publisher.py,sha256=FUg-EUzK7LLXsla10ZUZro8V0Z5S-KjmsaSdRHSSGbA,21381
17
- orionis/console/commands/scheduler_work.py,sha256=768TntiL2dguqJ2BlMVR7zEx_xlE71CCDsZ57ZXu9As,3573
17
+ orionis/console/commands/scheduler_list.py,sha256=SXaD1XJFV1bbyjlDuQhlHz2y1kW-VVCxQxuKJMtQRtA,5956
18
+ orionis/console/commands/scheduler_work.py,sha256=cymbGHaLmwv2WtEenZTtm7iNVfs8gG8_oWXU18Jtzd0,5746
18
19
  orionis/console/commands/test.py,sha256=_Tb-I9vabiqhqGeLfRbV5Y1LEVCdhZhBAwoEQZCzQuM,2435
19
20
  orionis/console/commands/version.py,sha256=SUuNDJ40f2uq69OQUmPQXJKaa9Bm_iVRDPmBd7zc1Yc,3658
20
21
  orionis/console/commands/workflow.py,sha256=NYOmjTSvm2o6AE4h9LSTZMFSYPQreNmEJtronyOxaYk,2451
@@ -24,7 +25,7 @@ orionis/console/contracts/kernel.py,sha256=mh4LlhEYHh3FuGZZQ0GBhD6ZLa5YQvaNj2r01
24
25
  orionis/console/contracts/reactor.py,sha256=Xeq7Zrw6WE5MV_XOQfiQEchAFbb6-0TjLpjWOxYW--g,4554
25
26
  orionis/console/contracts/schedule.py,sha256=eGjcOH7kgdf0fWDZRfOFUQsIx4E8G38ayX5JwpkpN8E,4977
26
27
  orionis/console/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- orionis/console/core/reactor.py,sha256=v_y3LDydBeKmtCfA0rIi8Nicy38DEfflp_zwx2OM7NQ,30257
28
+ orionis/console/core/reactor.py,sha256=XE4VvUPnOSQ_qmkLMYEDfwKaqDZyFxCiVtoSJVqpk_M,30372
28
29
  orionis/console/dumper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
30
  orionis/console/dumper/dump.py,sha256=CATERiQ6XuIrKQsDaWcVxzTtlAJI9qLJX44fQxEX8ws,22443
30
31
  orionis/console/dumper/contracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -199,7 +200,7 @@ orionis/foundation/providers/scheduler_provider.py,sha256=72SoixFog9IOE9Ve9Xcfw6
199
200
  orionis/foundation/providers/testing_provider.py,sha256=SrJRpdvcblx9WvX7x9Y3zc7OQfiTf7la0HAJrm2ESlE,3725
200
201
  orionis/foundation/providers/workers_provider.py,sha256=oa_2NIDH6UxZrtuGkkoo_zEoNIMGgJ46vg5CCgAm7wI,3926
201
202
  orionis/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
202
- orionis/metadata/framework.py,sha256=2WrjUXc9K8IXUNZ9LsjyebMtSH23bttsYzs91v36bto,4109
203
+ orionis/metadata/framework.py,sha256=Omguwj98GlVjFh8nCMjYoLHSqVMJ57Duo0GXurO-QLk,4109
203
204
  orionis/metadata/package.py,sha256=k7Yriyp5aUcR-iR8SK2ec_lf0_Cyc-C7JczgXa-I67w,16039
204
205
  orionis/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
205
206
  orionis/services/asynchrony/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -270,15 +271,15 @@ orionis/services/introspection/modules/contracts/reflection.py,sha256=YLqKg5Ehad
270
271
  orionis/services/introspection/objects/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
271
272
  orionis/services/introspection/objects/types.py,sha256=vNKWc2b7K-X7B2X8RCimgAWQqbQlVT-aL24nUB8t_yQ,6343
272
273
  orionis/services/log/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
273
- orionis/services/log/log_service.py,sha256=PY8a7DB-kCOlwtG85J_ASQIPRw0_aRGBiqlyQho0Gxw,11316
274
+ orionis/services/log/log_service.py,sha256=y1ysxZ7MuO2NuJXGnPVp1xXsJf6TVkOiMdd-ZvgbwQI,11620
274
275
  orionis/services/log/contracts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
275
276
  orionis/services/log/contracts/log_service.py,sha256=YVWkK5z6-3jyJv6aT8fbp53nQBF8jlD-aHGKqFYPdAM,1692
276
277
  orionis/services/log/exceptions/__init__.py,sha256=PPn_LBV3U-0Yi69ZLDQmlkbmlL1iLTleLw-s88Ipg9o,84
277
278
  orionis/services/log/exceptions/runtime.py,sha256=LnaK0w0WlgxtZ9Zjn9RYIgp6fbQZmXZ_1fy9dkuA2jQ,468
278
279
  orionis/services/log/handlers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
279
- orionis/services/log/handlers/filename.py,sha256=XKkq4Gq7N2m8F3xtyUIevTQLMaYSLJ3wC04Irs45VcE,2481
280
- orionis/services/log/handlers/size_rotating.py,sha256=SLg7r-XW1NWyVxN8wJxeI8jhypzdXw_jq2zg5uy3iaQ,1131
281
- orionis/services/log/handlers/timed_rotating.py,sha256=UJicPwHcIVl2FPRPjuiKwH3-zuiPG1YV7UYLLMPm4To,1128
280
+ orionis/services/log/handlers/filename.py,sha256=722mnk075vvq02MtpKaDWzLHEUd3sQyhL0a6xd0vLeA,20122
281
+ orionis/services/log/handlers/size_rotating.py,sha256=O0rE2LkS4nfjsOjcOXL0aG1csOuK2RKGeRdBlYJ6qTs,1333
282
+ orionis/services/log/handlers/timed_rotating.py,sha256=XM72DFwKcE0AU4oiCvhy2FXxteZ7HtxpGaxisv504nY,1317
282
283
  orionis/services/system/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
283
284
  orionis/services/system/imports.py,sha256=aSXG9879ur91d6OsqV2DUYWmmwbwFHX8CHb99cPfFcU,7057
284
285
  orionis/services/system/workers.py,sha256=EfGxU_w42xRnZ1yslsui3wVG8pfe__V3GYGEIyo8JxQ,3144
@@ -291,6 +292,7 @@ orionis/support/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
291
292
  orionis/support/entities/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
292
293
  orionis/support/entities/base.py,sha256=kVx9YWZjYS6C9MraarU7U12OeT5enBp5XqizrQm4RQs,4801
293
294
  orionis/support/facades/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
295
+ orionis/support/facades/application.py,sha256=GxB6TzsCSyDEHlvJJr-WVrppJvuolXCcV1vZ0tq_H6E,883
294
296
  orionis/support/facades/console.py,sha256=Yhpgv0FpwyOXP4IMgTtDw0_nkcu-J_9nhZRPwJCetCg,875
295
297
  orionis/support/facades/dumper.py,sha256=qQPoMbkXExv6UaByoDCyJA-gt5SA1oQtym2TwpihtoI,794
296
298
  orionis/support/facades/executor.py,sha256=fRBQ447WpL2T42Ys02k_DJNqukFFk8-bbNFz4GEbRfI,931
@@ -370,7 +372,7 @@ orionis/test/validators/web_report.py,sha256=n9BfzOZz6aEiNTypXcwuWbFRG0OdHNSmCNu
370
372
  orionis/test/validators/workers.py,sha256=rWcdRexINNEmGaO7mnc1MKUxkHKxrTsVuHgbnIfJYgc,1206
371
373
  orionis/test/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
372
374
  orionis/test/view/render.py,sha256=f-zNhtKSg9R5Njqujbg2l2amAs2-mRVESneLIkWOZjU,4082
373
- orionis-0.481.0.dist-info/licenses/LICENCE,sha256=JhC-z_9mbpUrCfPjcl3DhDA8trNDMzb57cvRSam1avc,1463
375
+ orionis-0.483.0.dist-info/licenses/LICENCE,sha256=JhC-z_9mbpUrCfPjcl3DhDA8trNDMzb57cvRSam1avc,1463
374
376
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
375
377
  tests/container/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
376
378
  tests/container/context/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -517,8 +519,8 @@ tests/testing/validators/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
517
519
  tests/testing/validators/test_testing_validators.py,sha256=WPo5GxTP6xE-Dw3X1vZoqOMpb6HhokjNSbgDsDRDvy4,16588
518
520
  tests/testing/view/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
519
521
  tests/testing/view/test_render.py,sha256=tnnMBwS0iKUIbogLvu-7Rii50G6Koddp3XT4wgdFEYM,1050
520
- orionis-0.481.0.dist-info/METADATA,sha256=7qzA9IFx_3LhMmuAN45WaCaPhPy_zMtKF-4s5BvyU3c,4801
521
- orionis-0.481.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
522
- orionis-0.481.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
523
- orionis-0.481.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
524
- orionis-0.481.0.dist-info/RECORD,,
522
+ orionis-0.483.0.dist-info/METADATA,sha256=3XeBNGAmPP22h8aO4LHXJ9ynNTdYmmYjWQzrzLe5KhA,4801
523
+ orionis-0.483.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
524
+ orionis-0.483.0.dist-info/top_level.txt,sha256=2bdoHgyGZhOtLAXS6Om8OCTmL24dUMC_L1quMe_ETbk,14
525
+ orionis-0.483.0.dist-info/zip-safe,sha256=frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN_XKdLCPjaYaY,2
526
+ orionis-0.483.0.dist-info/RECORD,,