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