ositah 24.4.dev1__py3-none-any.whl → 24.7.dev1__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 (47) hide show
  1. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/METADATA +1 -1
  2. ositah-24.7.dev1.dist-info/RECORD +6 -0
  3. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/WHEEL +1 -1
  4. ositah/__init__.py +0 -0
  5. ositah/app.py +0 -17
  6. ositah/apps/__init__.py +0 -0
  7. ositah/apps/analysis.py +0 -774
  8. ositah/apps/configuration/__init__.py +0 -0
  9. ositah/apps/configuration/callbacks.py +0 -917
  10. ositah/apps/configuration/main.py +0 -542
  11. ositah/apps/configuration/parameters.py +0 -74
  12. ositah/apps/configuration/tools.py +0 -112
  13. ositah/apps/export.py +0 -1172
  14. ositah/apps/validation/__init__.py +0 -0
  15. ositah/apps/validation/callbacks.py +0 -240
  16. ositah/apps/validation/main.py +0 -89
  17. ositah/apps/validation/parameters.py +0 -25
  18. ositah/apps/validation/tables.py +0 -654
  19. ositah/apps/validation/tools.py +0 -533
  20. ositah/assets/arrow_down_up.svg +0 -4
  21. ositah/assets/ositah.css +0 -54
  22. ositah/assets/sort_ascending.svg +0 -5
  23. ositah/assets/sort_descending.svg +0 -6
  24. ositah/assets/sorttable.js +0 -499
  25. ositah/main.py +0 -449
  26. ositah/ositah.example.cfg +0 -215
  27. ositah/static/style.css +0 -54
  28. ositah/templates/base.html +0 -22
  29. ositah/templates/bootstrap_login.html +0 -38
  30. ositah/templates/login_form.html +0 -27
  31. ositah/utils/__init__.py +0 -0
  32. ositah/utils/agents.py +0 -117
  33. ositah/utils/authentication.py +0 -287
  34. ositah/utils/cache.py +0 -19
  35. ositah/utils/core.py +0 -13
  36. ositah/utils/exceptions.py +0 -64
  37. ositah/utils/hito_db.py +0 -51
  38. ositah/utils/hito_db_model.py +0 -245
  39. ositah/utils/menus.py +0 -339
  40. ositah/utils/period.py +0 -135
  41. ositah/utils/projects.py +0 -1175
  42. ositah/utils/teams.py +0 -42
  43. ositah/utils/utils.py +0 -459
  44. ositah-24.4.dev1.dist-info/RECORD +0 -46
  45. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/LICENSE +0 -0
  46. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/entry_points.txt +0 -0
  47. {ositah-24.4.dev1.dist-info → ositah-24.7.dev1.dist-info}/top_level.txt +0 -0
ositah/utils/teams.py DELETED
@@ -1,42 +0,0 @@
1
- import re
2
- from typing import Dict, List
3
-
4
- import pandas as pd
5
-
6
-
7
- def get_hito_teams():
8
- """
9
- Return Hito teams as a Dataframe
10
-
11
- :return: Hito teams
12
- """
13
-
14
- from .hito_db import Team, db
15
-
16
- team_query = Team.query
17
-
18
- teams = pd.read_sql(team_query.statement, con=db.session.bind)
19
-
20
- return teams
21
-
22
-
23
- def get_project_team_ids(team_config: Dict[str, str], project: str) -> List[str]:
24
- """
25
- Return the list of team ID associated with the project based on the configuration passed.
26
- The configuration is a dict where the key is a pattern applied to the project name and
27
- the value is the list of team names.
28
-
29
- :param team_config: dict describing the teams associated with a project
30
- :param project: project name
31
- :return: list of team ids or None if no match is found
32
- """
33
-
34
- for pattern, team_list in team_config.items():
35
- if re.match(pattern, project):
36
- hito_teams = get_hito_teams()
37
- hito_teams["selected"] = False
38
- for team_pattern in team_list:
39
- hito_teams.loc[hito_teams.nom.str.match(f"{team_pattern}$"), "selected"] = True
40
- return hito_teams["id"].to_list()
41
-
42
- return None
ositah/utils/utils.py DELETED
@@ -1,459 +0,0 @@
1
- # Convenience objects for OSITAH application
2
-
3
- from datetime import datetime
4
- from typing import List
5
-
6
- import dash_bootstrap_components as dbc
7
- from dash import html
8
- from dateutil.relativedelta import relativedelta
9
- from flask import session
10
- from flask_sqlalchemy import SQLAlchemy
11
- from hito_tools.exceptions import ConfigFileEmpty, ConfigMissingParam
12
- from hito_tools.nsip import nsip_session_init
13
- from hito_tools.utils import load_config_file
14
-
15
- from ositah.app import app
16
-
17
- from .core import singleton
18
- from .exceptions import SessionDataMissing
19
-
20
- CONFIG_DEFAULT_PORT = "8888"
21
-
22
- # Define the dataframe column name to use for each kind of information
23
- # The key is the kind of information, the value is the column name and must be lowercase
24
- COLUMN_NAMES = {
25
- "agent_id": "id",
26
- "activity": "project_fullname",
27
- "activity_id": "projet_id",
28
- "category": "category",
29
- "cem": "cem",
30
- "declarations_number": "declarations_number",
31
- "email": "email",
32
- "email_auth": "email_auth",
33
- "firstname": "prenom",
34
- "fullname": "fullname",
35
- "hours": "nbHeures",
36
- "lastname": "nom",
37
- "masterproject": "masterprojet",
38
- "missings_number": "missings_number",
39
- "percent": "pourcent",
40
- "project": "projet",
41
- "quotite": "quotite",
42
- "statut": "statut",
43
- "team": "team",
44
- "team_id": "team_id",
45
- "weeks": "weeks",
46
- }
47
-
48
- # Define the column names in the NSIP export and the matching column name in the validated
49
- # declarations dataframe
50
- NSIP_COLUMN_NAMES = {
51
- "email_auth": "reseda_eamil",
52
- "nsip_project_id": "projet_id",
53
- "nsip_reference_id": "reference_id",
54
- "nsip_master": "masterprojet",
55
- "nsip_project": "projet",
56
- "time": "time",
57
- "time_unit": "volume",
58
- "validation_time": "timestamp",
59
- "id_declaration": "NSIP declaration ID",
60
- }
61
-
62
- TIME_UNIT_HOURS = "h"
63
- TIME_UNIT_HOURS_EN = "hours"
64
- TIME_UNIT_HOURS_FR = "heures"
65
- TIME_UNIT_WEEKS = "w"
66
- TIME_UNIT_WEEKS_EN = "weeks"
67
- TIME_UNIT_WEEKS_FR = "semaines"
68
- TIME_UNIT_DEFAULT = TIME_UNIT_HOURS
69
-
70
- # Hours per days and per week
71
- DAY_HOURS = 7.7
72
- WEEK_HOURS = 5 * DAY_HOURS
73
- # Semester: assume no week of holidays
74
- SEMESTER_WEEKS = 26
75
- SEMESTER_HOURS = WEEK_HOURS * SEMESTER_WEEKS
76
-
77
- TEAM_LIST_ALL_AGENTS = "Tous les agents"
78
-
79
- HITO_ROLE_PROJECT_MGR = "ROLE_PROJECT_MANAGER"
80
- HITO_ROLE_SUPER_ADMIN = "ROLE_SUPER_ADMIN"
81
- HITO_ROLE_TEAM_MGR = "ROLE_RESP"
82
- # Must be in role power reverse order
83
- AUTHORIZED_ROLES = [HITO_ROLE_SUPER_ADMIN, HITO_ROLE_PROJECT_MGR, HITO_ROLE_TEAM_MGR]
84
-
85
-
86
- class OSITAHSessionData:
87
- def __init__(self):
88
- self._cache_initialisation_date = None
89
- self._category_declarations = None
90
- self._project_declarations = None
91
- self._nsip_declarations = None
92
- self._projects_data = None
93
- self._project_declarations_source = None
94
- self._hito_activities = None
95
- self._hito_projects = None
96
- self._agent_list = None
97
- self._declaration_periods = None
98
- # Use a list for agent_teams to preserve the order
99
- self._agent_teams = []
100
- self._role = None
101
-
102
- @property
103
- def agent_teams(self):
104
- return self._agent_teams
105
-
106
- def add_teams(self, teams: List[str], sort_list=True) -> None:
107
- """
108
- Add a list of teams to agent_teams, without duplicates. The list is then sorted except
109
- is sort_list=False.
110
-
111
- :param teams: a list of team names
112
- :param sort: if true sort the resulting team list
113
- :return: None
114
- """
115
- seen = set(self.agent_teams)
116
- # Ensure that that there is no duplicate in the list
117
- self._agent_teams.extend([x for x in teams if not (x in seen or seen.add(x))])
118
- if sort_list:
119
- self._agent_teams.sort()
120
-
121
- @property
122
- def agent_list(self):
123
- return self._agent_list
124
-
125
- @agent_list.setter
126
- def agent_list(self, agent_list):
127
- self._agent_list = agent_list
128
-
129
- @property
130
- def cache_date(self):
131
- return self._cache_initialisation_date
132
-
133
- @property
134
- def category_declarations(self):
135
- return self._category_declarations
136
-
137
- @category_declarations.setter
138
- def category_declarations(self, declarations):
139
- self._category_declarations = declarations
140
-
141
- @property
142
- def declaration_periods(self):
143
- return self._declaration_periods
144
-
145
- @declaration_periods.setter
146
- def declaration_periods(self, periods):
147
- self._declaration_periods = periods
148
-
149
- def get_hito_activities(self, project_activity: bool):
150
- if project_activity:
151
- return self._hito_projects
152
- else:
153
- return self._hito_activities
154
-
155
- @property
156
- def nsip_declarations(self):
157
- return self._nsip_declarations
158
-
159
- @nsip_declarations.setter
160
- def nsip_declarations(self, declarations):
161
- self._nsip_declarations = declarations
162
- if self._cache_initialisation_date is None:
163
- # Define only if the cache has not yet been initialized
164
- self._cache_initialisation_date = datetime.now()
165
-
166
- @property
167
- def projects_data(self):
168
- return self._projects_data
169
-
170
- @projects_data.setter
171
- def projects_data(self, projects_data):
172
- self._projects_data = projects_data
173
-
174
- @property
175
- def project_declarations(self):
176
- return self._project_declarations
177
-
178
- @property
179
- def project_declarations_source(self):
180
- return self._project_declarations_source
181
-
182
- @property
183
- def role(self):
184
- return self._role
185
-
186
- @role.setter
187
- def role(self, role):
188
- self._role = role
189
-
190
- @property
191
- def total_declarations_num(self):
192
- return len(self._project_declarations)
193
-
194
- def reset_caches(self):
195
- self._category_declarations = None
196
- self._project_declarations = None
197
- self._nsip_declarations = None
198
- self._projects_data = None
199
- self._hito_activities = None
200
- self._hito_projects = None
201
- self._project_declarations_source = None
202
- self._cache_initialisation_date = None
203
- self._agent_list = None
204
- self._validation_data = None
205
- self._total_declarations_num = 0
206
-
207
- def reset_validated_declarations_cache(self):
208
- self._nsip_declarations = None
209
-
210
- def set_hito_activities(self, activities, project_activity: bool):
211
- if project_activity:
212
- self._hito_projects = activities
213
- else:
214
- self._hito_activities = activities
215
-
216
- def set_project_declarations(self, declarations, source):
217
- self._project_declarations = declarations
218
- self._project_declarations_source = source
219
- if self._cache_initialisation_date is None:
220
- # Define only if the cache has not yet been initialized
221
- self._cache_initialisation_date = datetime.now()
222
-
223
-
224
- @singleton
225
- class GlobalParams:
226
- def __init__(self):
227
- self.agent_query = None
228
- self.category_patterns = {}
229
- self.columns = COLUMN_NAMES
230
- self.column_titles = None
231
- self.declaration_options = None
232
- self.project_categories = None
233
- self.reference_masterprojects = {}
234
- self.roles = {}
235
- self.time_unit = None
236
- self.ldap = None
237
- self.nsip = None
238
- self.project_teams = {}
239
- self.teaching_ratio = None
240
- self.port = CONFIG_DEFAULT_PORT
241
- self.validation_params = None
242
- self._hito_db = None
243
- self._session_data = {}
244
-
245
- @property
246
- def hito_db(self):
247
- if not self._hito_db:
248
- self._hito_db = SQLAlchemy(app.server)
249
- return self._hito_db
250
-
251
- @property
252
- def session_data(self):
253
- """
254
- Returns the session data for the current session. Must not be called if the session UID
255
- is not defined or will raise SessionDataMissing exception.
256
-
257
- :return: session data for the current session
258
- """
259
-
260
- if "uid" in session:
261
- # If 'uid' is defined, it means the user was successfully authenticated.
262
- if session["uid"] not in self._session_data:
263
- # session_data may not exist if a multi-worker configuration is used and the
264
- # authentication (done when moving from one subapp to another one) has been
265
- # done on another worker.
266
- self._session_data[session["uid"]] = OSITAHSessionData()
267
- return self._session_data[session["uid"]]
268
-
269
- else:
270
- raise SessionDataMissing()
271
-
272
- @session_data.deleter
273
- def session_data(self):
274
- if "uid" in session:
275
- if session["uid"] in self._session_data:
276
- del self._session_data[session["uid"]]
277
- else:
278
- print(
279
- (
280
- f"WARNING: attempt to delete non-existing session data for session"
281
- f" {session['uid']} (user={session['user_id']})"
282
- )
283
- )
284
-
285
- else:
286
- raise SessionDataMissing()
287
-
288
-
289
- def define_config_params(file):
290
- """
291
- Validate configuration and define appropriate defaults. Also define global parameters
292
- from configuration.
293
-
294
- :param file: configuration file
295
- :return: updated configuration hash
296
- """
297
-
298
- global_params = GlobalParams()
299
-
300
- config = load_config_file(file, required=True)
301
- if not config:
302
- raise ConfigFileEmpty(file)
303
-
304
- if "server" not in config or not config["server"]:
305
- config["server"] = {}
306
- if "port" in config["server"]:
307
- global_params.port = config["server"]["port"]
308
- if "authentication" in config["server"]:
309
- if "ldap" in config["server"]["authentication"]:
310
- global_params.ldap = config["server"]["authentication"]["ldap"]
311
- for param in ["uri", "base_dn", "bind_dn", "password"]:
312
- if param not in global_params.ldap:
313
- raise ConfigMissingParam(f"server/authentication/ldap/{param}", file)
314
- else:
315
- raise ConfigMissingParam("server/authentication/ldap", file)
316
- if "provider_name" not in config["server"]["authentication"]:
317
- raise ConfigMissingParam("server/authentication/provider_name", file)
318
- else:
319
- raise ConfigMissingParam("server/authentication", file)
320
-
321
- if "hito" not in config:
322
- raise ConfigMissingParam("hito", file)
323
- if "db" not in config["hito"]:
324
- raise ConfigMissingParam("hito/db", file)
325
- if "type" not in config["hito"]["db"]:
326
- raise ConfigMissingParam("hito/db/type", file)
327
- if "location" not in config["hito"]["db"]:
328
- raise ConfigMissingParam("hito/db/location", file)
329
- if "agent_query" not in config["hito"]["db"]:
330
- raise ConfigMissingParam("hito/db/agent_query", file)
331
- if config["hito"]["db"]["type"] == "sqlite":
332
- app.server.config["SQLALCHEMY_DATABASE_URI"] = (
333
- f"sqlite:///{config['hito']['db']['location']}"
334
- )
335
- elif config["hito"]["db"]["type"] == "mysql":
336
- if "user" not in config["hito"]["db"]:
337
- raise ConfigMissingParam("hito/db/user", file)
338
- if "password" not in config["hito"]["db"]:
339
- raise ConfigMissingParam("hito/db/passowrd", file)
340
- app.server.config["SQLALCHEMY_DATABASE_URI"] = (
341
- f"mysql+pymysql://{config['hito']['db']['user']}:{config['hito']['db']['password']}"
342
- f"@{config['hito']['db']['location']}"
343
- )
344
- else:
345
- raise Exception(f"Support for {config['hito']['db']['type']} not yet implemented...")
346
- if "inactivity_timeout" in config["hito"]["db"]:
347
- app.server.config["SQLALCHEMY_POOL_RECYCLE"] = config["hito"]["db"]["inactivity_timeout"]
348
- global_params.agent_query = contextualize_sql_query(
349
- config["hito"]["db"]["agent_query"], config["hito"]["db"]["type"]
350
- )
351
- app.server.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
352
-
353
- if "categories" in config["hito"]:
354
- global_params.project_categories = config["hito"]["categories"]
355
- global_params.category_patterns = {
356
- v: k for k, v in global_params.project_categories.items()
357
- }
358
- else:
359
- raise ConfigMissingParam("hito/categories", file)
360
-
361
- if "time_unit" in config["hito"]:
362
- global_params.time_unit = config["hito"]["time_unit"]
363
- else:
364
- global_params.time_unit = {}
365
- for category in global_params.project_categories:
366
- if category not in global_params.time_unit:
367
- global_params.time_unit[category] = TIME_UNIT_DEFAULT
368
-
369
- if "titles" in config["hito"]:
370
- global_params.column_titles = config["hito"]["titles"]
371
- else:
372
- raise ConfigMissingParam("hito/titles", file)
373
-
374
- if "declaration" in config:
375
- if "optional_statutes" not in config["declaration"]:
376
- config["declaration"]["optional_statutes"] = []
377
- if "optional_teams" not in config["declaration"]:
378
- config["declaration"]["optional_teams"] = []
379
- else:
380
- config["declaration"] = {}
381
- if "max_hours" not in config["declaration"]:
382
- # Set a very high value
383
- config["declaration"]["max_hours"] = 99999
384
- if "thresholds" in config["declaration"]:
385
- for k in ["low", "suspect", "good"]:
386
- if k not in config["declaration"]["thresholds"]:
387
- raise ConfigMissingParam(f"declaration/thresholds/{k}", file)
388
- else:
389
- config["declaration"]["thresholds"] = {"low": 50, "suspect": 75, "good": 100}
390
- # Default declaration period date defaults to current day if not explicitly defined
391
- if "default_date" not in config["declaration"]:
392
- config["declaration"]["default_date"] = datetime.now() - relativedelta(months=10)
393
- global_params.declaration_options = config["declaration"]
394
-
395
- if "validation" not in config:
396
- config["validation"] = {}
397
- if "override_period" not in config["validation"]:
398
- config["validation"]["override_period"] = []
399
- global_params.validation_params = config["validation"]
400
-
401
- if "roles" in config:
402
- global_params.roles = config["roles"]
403
- if "read-only" not in global_params.roles:
404
- global_params.roles["read-only"] = []
405
-
406
- if "project_teams" in config:
407
- global_params.project_teams = config["project_teams"]
408
-
409
- if "nsip" in config:
410
- global_params.nsip = nsip_session_init(config["nsip"])
411
- if "reference_masterprojects" in config["nsip"]:
412
- global_params.reference_masterprojects = config["nsip"]["reference_masterprojects"]
413
- if "teaching" in config["nsip"]:
414
- global_params.teaching_ratio = config["nsip"]["teaching"]
415
- if "ratio" not in global_params.teaching_ratio:
416
- raise ConfigMissingParam("nsip/teeaching/ratio", file)
417
- if "masterproject" not in global_params.teaching_ratio:
418
- global_params.teaching_ratio["masterproject"] = "Enseignement Supérieur"
419
- if "cem" not in global_params.teaching_ratio:
420
- global_params.teaching_ratio["cem"] = None
421
-
422
- return config
423
-
424
-
425
- def contextualize_sql_query(query: str, db_type: str) -> str:
426
- """
427
- Function to replace placeholders in the query by the function/statement appropriate for
428
- the actual DB back-end used.
429
-
430
- :param query: the query with the placeholders
431
- :param db_type: the DB back-end type
432
- :return: the query for the selected back-end
433
- """
434
-
435
- if db_type == "sqlite":
436
- query = query.replace("$$TODAY$$", 'DATE("now")')
437
- elif db_type == "mysql":
438
- query = query.replace("$$TODAY$$", "CURRENT_DATE()")
439
- # pymysql uses the query string as a formatter string: % characters must be esacped
440
- query = query.replace("%", "%%")
441
- else:
442
- raise Exception(f"Support for {db_type} not yet implemented...")
443
-
444
- return query
445
-
446
-
447
- def no_session_id_jumbotron(session_id=None):
448
- return html.Div([dbc.Jumbotron(html.P(["Internal error: session ID invalid or undefined"]))])
449
-
450
-
451
- def general_error_jumbotron(error):
452
- """
453
- Print an error message that can be any representable object
454
-
455
- :param error: error object, e.g. an exception
456
- :return: a jumbotron
457
- """
458
-
459
- return html.Div([dbc.Jumbotron(html.P([repr(error)]))])
@@ -1,46 +0,0 @@
1
- ositah/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- ositah/app.py,sha256=53ylXmqBSo31omZt-XBPBhOzlkGQBuDenU0Q8m7efCg,428
3
- ositah/main.py,sha256=vw8PBYP3ugtWKWUta3UKagh8lbp-ogl8LUNF-KSyo0s,15673
4
- ositah/ositah.example.cfg,sha256=-k6Bytmuml3KHQn_1gGxW2k2bBAc39zPN6Ew5rG14xw,7472
5
- ositah/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- ositah/apps/analysis.py,sha256=8bmP5m4nmfMODu1_YpzqCiFR2U2TMOOxAHbujY8od8g,28259
7
- ositah/apps/export.py,sha256=4h9_nk9uKopwfxgzqKyFk5u4qVQ9mtgGvk3zmxbhTV0,47532
8
- ositah/apps/configuration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- ositah/apps/configuration/callbacks.py,sha256=J1bWwxQF2JRX6vvkTF22XXr4R2HEEXyK3ht75hbFgAM,33477
10
- ositah/apps/configuration/main.py,sha256=K0pQePovp88r5RRJiBwXi7RNK2P7S6iTN1a-TUYUU6I,20299
11
- ositah/apps/configuration/parameters.py,sha256=KmEVdC--IJRC3yzCN97FY1SooOKFy3bMneFBo1sGa54,3862
12
- ositah/apps/configuration/tools.py,sha256=e4CYt3F_2NY34vILwxHBWtvFntq082gwpKNsZZVexHE,3982
13
- ositah/apps/validation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- ositah/apps/validation/callbacks.py,sha256=N9j0HLe8Xp1zZUrVgx0woeDBwVtJLQZ_BPVNaJ9UIZo,9557
15
- ositah/apps/validation/main.py,sha256=en9D6c65DUosbU31dxLR6BhLCU3yj7uxyoS3MYzvw30,2896
16
- ositah/apps/validation/parameters.py,sha256=tDHqDnoWv1QhcTad2yhqLT9_wCpj8sTYxKWsi1UF-uk,1003
17
- ositah/apps/validation/tables.py,sha256=Zz-yJw6Bw2fQOa87PW4EmW3nQZmmZF1kqr11NfWvQOI,24770
18
- ositah/apps/validation/tools.py,sha256=omz6M10cCI5A8aTWllpkqFDEcIQFEhjzxOCiJKhHuDc,18046
19
- ositah/assets/arrow_down_up.svg,sha256=jH7QOmbLkYnNENa2PlBktOYHKDGN5KbrrFcV6UpgkCY,503
20
- ositah/assets/ositah.css,sha256=_T3mwonAYMPoDJoHYgJ5XgdFllHMcE28vnMMDd_hdOQ,1094
21
- ositah/assets/sort_ascending.svg,sha256=Rg4wPgEBEBU9hDPgNnenFYkFLC18qKw0flaBk_8Y-uM,593
22
- ositah/assets/sort_descending.svg,sha256=puCzEAmCLJihKOXLWOzCY7C6WS7IhTi7G2pDXwmNTiw,618
23
- ositah/assets/sorttable.js,sha256=oBQl337JpkmW8fe4nUI7UqURmu-FkwnOdAYs6Ivg498,17478
24
- ositah/static/style.css,sha256=_T3mwonAYMPoDJoHYgJ5XgdFllHMcE28vnMMDd_hdOQ,1094
25
- ositah/templates/base.html,sha256=qKZLLrIOn2eZ_mIoSZEN8ZP6qyeSg1butPEVuv5lYDs,714
26
- ositah/templates/bootstrap_login.html,sha256=Gp3FTx1uWMHeVvnYcLehdYE8vfZ1RBzIjrxD6IGKPA4,1608
27
- ositah/templates/login_form.html,sha256=BjcnWd5svC0xy039-MToODcDswehe1YVQnTnctD9kdU,1033
28
- ositah/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
- ositah/utils/agents.py,sha256=Cxe2BUCP_wT63Ewduk1JUIyFVOfHJSD9BJX2QjDy7n4,4453
30
- ositah/utils/authentication.py,sha256=VfT3MrIPX6U4HAma0O0UScB86-EukBdfEg26NP5ndpk,9149
31
- ositah/utils/cache.py,sha256=mM1iKDzQbVJZvTOgfPnvbxSBWi-OVGly6MncQ09CtEY,504
32
- ositah/utils/core.py,sha256=OFML9h5JNmXXeMo7n90FSBDHtivWOY7ff2SJcG4uYEM,295
33
- ositah/utils/exceptions.py,sha256=ZVngkr28pBxO-kZAeKW9FWAFyhzuWL98-S92mieHMSk,1911
34
- ositah/utils/hito_db.py,sha256=awVGrd1DAyj6BaEkLjTVN1l9A5SjZUJ1-fQ5PAik7bI,1510
35
- ositah/utils/hito_db_model.py,sha256=gtj0URvmxg0Ms7xcEXhkf84Blb3RZl4F8nWmsqrqwXk,8988
36
- ositah/utils/menus.py,sha256=Ypb4nXVBuW3eA2iIBR4T_s3N4PAKHA5_udiCSLKcgII,12007
37
- ositah/utils/period.py,sha256=CxT77mAQasD59BmecXRUwme-_76hOr2kotOwTy6ZKfk,4058
38
- ositah/utils/projects.py,sha256=xLQ0RpJzMmCpx9Xdy8sHPQ-tijeWvSFPVSgOl5D6aSk,43479
39
- ositah/utils/teams.py,sha256=q8oRriecvM19whPWnIlScS2oqWIhGIdJHP0T1pK12W8,1186
40
- ositah/utils/utils.py,sha256=bAldpoDOCZhV2nNjelK2ByV3A77nXbS6t3DeCI2jgUY,16449
41
- ositah-24.4.dev1.dist-info/LICENSE,sha256=2C86YWCx1fvz92WySupcb6_t4NhHCVPE_ucy0YMTuoc,1550
42
- ositah-24.4.dev1.dist-info/METADATA,sha256=gY6583ihXmyZYRATx5HYbFAcEPGBdhKJTTYJgL5CkFE,8103
43
- ositah-24.4.dev1.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
44
- ositah-24.4.dev1.dist-info/entry_points.txt,sha256=t9oDDLUO1LwHJewlE862LbJMHpDTEyqbeUAPw_F7Q3I,44
45
- ositah-24.4.dev1.dist-info/top_level.txt,sha256=3kfj_oK4xoZFt0nsw6KKT_aoqshELBu0ryLXECbcqNI,7
46
- ositah-24.4.dev1.dist-info/RECORD,,