Skip to content

Commit a84c94f

Browse files
committedApr 26, 2023
Merge branch 'devel'
2 parents 1ac5ae5 + fc99186 commit a84c94f

13 files changed

+844
-684
lines changed
 

‎CHANGELOG.md

+61-13
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,73 @@
11
# PyPPMS Changelog
22

3+
<!-- markdownlint-disable MD024 (no-duplicate-header) -->
4+
5+
NOTE: potentially breaking changes are flagged with a 🧨 symbol.
6+
7+
## 2.2.0
8+
9+
### Added
10+
11+
- `pyppms.ppms.PpmsConnection.flush_cache()` to flush the on-disk cache with an
12+
optional argument `keep_users` (defaulting to `False`) that allows for
13+
flushing the entire cache **except** for the user **details**. This provides
14+
the opportunity of refreshing the cache on everything but *existing* users.
15+
Note that this will **not** affect **new** users, they will still be
16+
recognized and fetched from PUMAPI (and stored in the cache).
17+
18+
### Changed
19+
20+
- `pyppms.ppms.PpmsConnection.get_systems_matching()` now raises a `TypeError`
21+
in case the parameter `name_contains` is accidentially as `str` instead of a
22+
list.
23+
- `pyppms.ppms.PpmsConnection.get_running_sheet()` now has an optional parameter
24+
`ignore_uncached_users` (defaulting to `False`) that allows to process the
25+
running sheet even if it contains users that are not in the `fullname_mapping`
26+
attribute.
27+
- If the `cache_path` attribute is set for an `pyppms.ppms.PpmsConnection`
28+
instance but creating the actual subdir for an intercepted response fails
29+
(e.g. due to permission problems) the response-cache will not be updated.
30+
Before, the exception raised by the underlying code (e.g. a `PermissionError`)
31+
was passed on.
32+
- Methods of `pyppms.ppms.PpmsConnection` are now sorted in alphabetical order,
33+
making it easier to locate them e.g. in the API documentation.
34+
35+
### Removed
36+
37+
- The following previously deprecated (or not even implemented) methods of
38+
`pyppms.ppms.PpmsConnection` have been removed in favor of
39+
`pyppms.ppms.PpmsConnection.get_systems_matching()`:
40+
- `_get_system_with_name()`
41+
- `_get_machine_catalogue_from_system()`
42+
- `get_bookable_ids()`
43+
- Removed the stub `pyppms.ppms.PpmsConnection.get_system()` that was only
44+
raising a `NotImplementedError`.
45+
346
## 2.1.0
447

48+
### Changed
49+
550
- [API] `pyppms.ppms.PpmsConnection.get_user()` and
6-
`pyppms.ppms.PpmsConnection.get_user_dict()` now both accept an optional parameter
7-
`skip_cache` that is passed on to the `pyppms.ppms.PpmsConnection.request()` call
8-
- [FIX] `pyppms.ppms.PpmsConnection.update_users()` now explicitly asks for the cache
9-
to be skipped
51+
`pyppms.ppms.PpmsConnection.get_user_dict()` now both accept an optional
52+
parameter `skip_cache` that is passed on to the
53+
`pyppms.ppms.PpmsConnection.request()` call
54+
- [FIX] `pyppms.ppms.PpmsConnection.update_users()` now explicitly asks for the
55+
cache to be skipped
1056

1157
## 2.0.0
1258

13-
- [API] the signature for `pyppms.user.PpmsUser` has been changed and now expects a
14-
single argument (the PUMAPI response text)
15-
- [API] the constructor signature for `pyppms.system.PpmsSystem()` has been changed and
16-
now expects a single argument (a dict as generated by
59+
### Changed
60+
61+
- [API] 🧨 the signature for `pyppms.user.PpmsUser` has been changed and now
62+
expects a single argument (the PUMAPI response text)
63+
- [API] 🧨 the constructor signature for `pyppms.system.PpmsSystem()` has been
64+
changed and now expects a single argument (a dict as generated by
1765
`pyppms.common.parse_multiline_response()`)
18-
- [API] the constructor signature for `pyppms.booking.PpmsBooking()` has been changed
19-
and now expects the PUMAPI response text, the booking type (if the booking is
20-
currently running or upcoming) and the system ID
21-
- [API] the following methods have been removed as their behavior is now achieved by the
22-
corresponding default constructor of the respective class:
66+
- [API] 🧨 the constructor signature for `pyppms.booking.PpmsBooking()` has been
67+
changed and now expects the PUMAPI response text, the booking type (if the
68+
booking is currently running or upcoming) and the system ID
69+
- [API] 🧨 the following methods have been removed as their behavior is now
70+
achieved by the corresponding default constructor of the respective class:
2371
- `pyppms.user.PpmsUser.from_response()`
2472
- `pyppms.system.PpmsSystem.from_parsed_response()`
2573
- `pyppms.booking.PpmsBooking.from_booking_request()`

‎Makefile

-15
This file was deleted.

‎TODO.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# PyPPMS Development ToDos
2+
3+
- all methods returning a list of user objects (get_group_users, get_admins, ...) should
4+
be refactored to return a dict with those objects instead, having the username
5+
('login') as the key.
6+
- run tests in a true *offline* environment and validate they're working
7+
- find a better solution than hard-coding a system ID in `test_ppms.__SYS_ID__`

‎pyproject.toml

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[tool.poetry]
22
description = "A Python package to communicate with Stratocore's PUMAPI."
33
name = "pyppms"
4-
version = "2.1.0-dev0"
4+
version = "0.0.0"
55

66
license = "GPLv3"
77

@@ -31,8 +31,12 @@ pytest = "^7.0"
3131
pytest-cov = "^3.0"
3232

3333
[build-system]
34-
build-backend = "poetry.core.masonry.api"
35-
requires = ["poetry-core>=1.0.0"]
34+
build-backend = "poetry_dynamic_versioning.backend"
35+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning"]
36+
37+
[tool.poetry-dynamic-versioning]
38+
enable = true
39+
pattern = "^pyppms-((?P<epoch>\\d+)!)?(?P<base>\\d+(\\.\\d+)*)"
3640

3741
[tool.pytest.ini_options]
3842
addopts = "-rs -vv --cov=pyppms --cov-report html --maxfail=1"

‎src/pyppms/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,6 @@
77
.. include:: ../../CHANGELOG.md
88
"""
99

10-
__version__ = "2.1.0-dev0"
10+
__version__ = "0.0.0"
1111

1212
from .ppms import PpmsConnection

‎src/pyppms/booking.py

+17-14
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,7 @@ def __init__(self, text, booking_type, system_id):
6565
LOG.error("Parsing booking response failed (%s), text was:\n%s", err, text)
6666
raise
6767

68-
LOG.debug(
69-
"PpmsBooking initialized: username=[%s], system=[%s], "
70-
"reservation start=[%s] end=[%s]",
71-
self.username,
72-
system_id,
73-
starttime,
74-
endtime,
75-
)
68+
LOG.debug(str(self))
7669

7770
@classmethod
7871
def from_runningsheet(cls, entry, system_id, username, date):
@@ -94,7 +87,7 @@ def from_runningsheet(cls, entry, system_id, username, date):
9487
9588
Returns
9689
-------
97-
PpmsBooking
90+
pyppms.booking.PpmsBooking
9891
The object constructed with the parsed response.
9992
"""
10093
try:
@@ -130,7 +123,7 @@ def starttime_fromstr(self, time_str, date=None):
130123
microsecond=0,
131124
)
132125
self.starttime = start
133-
LOG.debug("Updated booking starttime: %s", self)
126+
LOG.debug("New starttime: %s", self)
134127

135128
def endtime_fromstr(self, time_str, date=None):
136129
"""Change the ending time and / or day of a booking.
@@ -152,14 +145,24 @@ def endtime_fromstr(self, time_str, date=None):
152145
microsecond=0,
153146
)
154147
self.endtime = end
155-
LOG.debug("Updated booking endtime: %s", self)
148+
LOG.debug("New endtime: %s", self)
156149

157150
def __str__(self):
151+
def fmt_time(time):
152+
# in case a booking was created from a "nextbooking" response it will not
153+
# have the `endtime` attribute set, so treat this separately:
154+
if time is None:
155+
return "===UNDEFINED==="
156+
return datetime.strftime(time, "%Y-%m-%d %H:%M")
157+
158158
msg = (
159-
f"username: {self.username} - system: {self.system_id} - "
160-
f"reservation start / end: [ {self.starttime} / {self.endtime} ]"
159+
f"PpmsBooking(username=[{self.username}], "
160+
f"system_id=[{self.system_id}], "
161+
f"starttime=[{fmt_time(self.starttime)}], "
162+
f"endtime=[{fmt_time(self.endtime)}]"
161163
)
162164
if self.session:
163-
msg += f" - session: {self.session}"
165+
msg += f", session=[{self.session}]"
166+
msg += ")"
164167

165168
return msg

‎src/pyppms/ppms.py

+570-578
Large diffs are not rendered by default.

‎src/pyppms/system.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def __init__(self, details):
7070
self.autonomy_required = details["Autonomy Required"]
7171
self.autonomy_required_after_hours = details["Autonomy Required After Hours"]
7272
LOG.debug(
73-
"PpmsSystem created: id=%s, name=[%s], localisation=[%s], system_type=[%s]",
73+
"PpmsSystem(system_id=%s, name=[%s], localisation=[%s], system_type=[%s])",
7474
self.system_id,
7575
self.name,
7676
self.localisation,

‎tests/conftest.py

-5
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,6 @@
22

33
# pylint: disable-msg=fixme
44

5-
# TODO: pylint for Python2 complains about redefining an outer scope when using
6-
# pytest fixtures, this is supposed to be fixed in newer versions, so it should
7-
# be checked again after migration to Python3 (see pylint issue #1535):
8-
# pylint: disable-msg=redefined-outer-name
9-
105
import pytest
116

127
from ppms_values import values

‎tests/test_booking.py

+14-7
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
FMT_DATE = r"%Y-%m-%d"
11-
FMT_TIME = r"%H:%M:00"
11+
FMT_TIME = r"%H:%M"
1212
FMT = f"{FMT_DATE} {FMT_TIME}"
1313
DAY = datetime.now().strftime(FMT_DATE)
1414
TIME_START = datetime.now().strftime(FMT_TIME)
@@ -19,8 +19,8 @@
1919
USERNAME = "ppmsuser"
2020
SYS_ID = "42"
2121
SESSION_ID = "some_session_id"
22-
EXPECTED = f"username: {USERNAME} - system: {SYS_ID} - "
23-
EXPECTED += f"reservation start / end: [ %s / %s ] - session: {SESSION_ID}"
22+
EXPECTED = f"PpmsBooking(username=[{USERNAME}], system_id=[{SYS_ID}], "
23+
EXPECTED += f"starttime=[%s], endtime=[%s], session=[{SESSION_ID}])"
2424

2525

2626
def create_booking(
@@ -58,7 +58,7 @@ def test_starttime_fromstr__time():
5858
"""Test changing the starting time of a booking."""
5959
booking = create_booking()
6060

61-
newtime = "12:45:00"
61+
newtime = "12:45"
6262
booking.starttime_fromstr(newtime, date=datetime.strptime(START, FMT))
6363

6464
newstart = f"{DAY} {newtime}"
@@ -70,7 +70,7 @@ def test_starttime_fromstr__date():
7070
booking = create_booking()
7171

7272
newdate = "2019-04-01"
73-
newtime = "12:45:00"
73+
newtime = "12:45"
7474
startdate = datetime.strptime(newdate, FMT_DATE)
7575
booking.starttime_fromstr(newtime, startdate)
7676

@@ -87,7 +87,7 @@ def test_endtime_fromstr__time():
8787
"""Test changing the ending time of a booking."""
8888
booking = create_booking()
8989

90-
newtime = "12:45:00"
90+
newtime = "12:45"
9191
booking.endtime_fromstr(newtime, date=datetime.strptime(START, FMT))
9292

9393
newend = f"{DAY} {newtime}"
@@ -99,7 +99,7 @@ def test_endtime_fromstr__date():
9999
booking = create_booking()
100100

101101
newdate = "2019-06-01"
102-
newtime = "12:45:00"
102+
newtime = "12:45"
103103
enddate = datetime.strptime(newdate, FMT_DATE)
104104
booking.endtime_fromstr(newtime, enddate)
105105

@@ -112,6 +112,13 @@ def test_endtime_fromstr__date():
112112
assert booking.__str__() == EXPECTED % (START, newend)
113113

114114

115+
def test_noendtime_str():
116+
"""Test the booking object string formatting when no end time is set."""
117+
booking = create_booking()
118+
booking.endtime = None
119+
assert "endtime=[===UNDEFINED===]" in booking.__str__()
120+
121+
115122
def test_booking_from_request():
116123
"""Test the alternative from_booking_request() constructor."""
117124
time_delta = 15

‎tests/test_init.py

-8
This file was deleted.

‎tests/test_ppms.py

+164-37
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@
44

55
# pylint: disable-msg=protected-access
66

7-
import os.path
87
import logging
8+
import os.path
99
from datetime import datetime
10+
from shutil import rmtree, copytree
11+
12+
import pyppmsconf
1013
import pytest
1114
import requests.exceptions
12-
from shutil import rmtree
1315

14-
import pyppmsconf
1516
from pyppms import ppms
1617

17-
1818
# TODO: system ID is hard-coded here, so this will fail on any other instance!
1919
__SYS_ID__ = 69
2020

@@ -441,6 +441,15 @@ def test_get_systems_matching(ppms_connection, system_details_raw):
441441
assert sys_ids == []
442442

443443

444+
def test_get_systems_matching__raises(ppms_connection, system_details_raw):
445+
"""Test get_systems_matching() with a wrong parameter type."""
446+
with pytest.raises(TypeError):
447+
ppms_connection.get_systems_matching("foo", "wrong-type")
448+
449+
with pytest.raises(TypeError):
450+
ppms_connection.get_systems_matching("foo", "")
451+
452+
444453
############ system / user permissions ############
445454

446455

@@ -654,49 +663,167 @@ def test_get_running_sheet_fail(ppms_connection):
654663
assert ppms_connection.get_running_sheet("2", date=day) == []
655664

656665

657-
############ deprecated methods ############
666+
############ cache ############
658667

659668

660-
def test__get_system_with_name(ppms_connection, system_details_raw):
661-
"""Test the (deprecated) _get_system_with_name() method."""
662-
logd("Testing with a well-known system name")
663-
name = system_details_raw["Name"]
664-
sys_id = ppms_connection._get_system_with_name(name)
665-
print(f"_get_system_with_name: {sys_id}")
666-
assert sys_id == int(system_details_raw["System id"])
669+
def test_flush_cache(ppms_connection, caplog, tmp_path):
670+
"""Test flushing the on-disk PyPPMS cache.
667671
668-
logd("Testing with a non-existing system name")
669-
sys_id = ppms_connection._get_system_with_name("invalid-system-name")
670-
assert sys_id == -1
672+
- Make sure the temporary test-directory exists but doesn't contain a cache yet.
673+
- Copy over one of the cache directories provided with the tests.
674+
- Make sure the test-directory *does* contain a cache now.
675+
- Update the connection object's `cache_path` to point to the test location.
676+
- Trigger the `flush_cache()` method.
677+
- Verify the cache has been removed from the test-directory.
678+
"""
679+
orig_cache_path = os.path.join(pyppmsconf.CACHE_PATH, "stage_1")
680+
fresh_cache_path = tmp_path / "pyppms_cache"
681+
682+
assert os.path.exists(tmp_path)
683+
assert os.path.exists(orig_cache_path)
684+
685+
assert not os.path.exists(fresh_cache_path)
686+
copytree(orig_cache_path, fresh_cache_path)
687+
assert os.path.exists(fresh_cache_path)
688+
_logger.info("Cache path created: %s", fresh_cache_path)
689+
690+
ppms_connection.cache_path = fresh_cache_path
691+
_logger.info("Updated connection cache path: %s", fresh_cache_path)
692+
ppms_connection.flush_cache()
693+
_logger.info("Flushed connection cache path: %s", fresh_cache_path)
694+
assert not os.path.exists(fresh_cache_path)
695+
696+
697+
def test_flush_cache__keep_users(ppms_connection, caplog, tmp_path):
698+
"""Test flushing the on-disk PyPPMS cache while keeping the user details.
699+
700+
- Make sure the temporary test-directory exists but doesn't contain a cache yet.
701+
- Create the cache directory there.
702+
- Update the connection object's `cache_path` to point to the test location.
703+
- Copy over the subdirs listed in `to_keep` and `to_flush` from the cache provided
704+
with the tests.
705+
- Make sure the copied directories exist at the test-cache location.
706+
- Trigger the `flush_cache(keep_users=True)` method.
707+
- Verify the subdirs in `to_keep` have been retained at the test-directory.
708+
- Verify the subdirs in `to_flush` have been removed from the test-directory.
709+
"""
710+
to_keep = ["getuser"]
711+
to_flush = ["auth", "getgroups", "getusers", "getbooking"]
671712

713+
orig_cache_root = os.path.join(pyppmsconf.CACHE_PATH, "stage_0")
714+
fresh_cache_path = tmp_path / "pyppms_cache"
672715

673-
def test__get_machine_catalogue_from_system(ppms_connection, system_details_raw):
674-
"""Test the (deprecated) _get_machine_catalogue_from_system() method."""
675-
name = system_details_raw["Name"]
716+
assert os.path.exists(tmp_path)
717+
assert os.path.exists(orig_cache_root)
676718

677-
# define a list of categories we are interested in:
678-
categories = ["VDI (Development)", "VDI (CAD)", "VDI (BigMemory)"]
719+
assert not os.path.exists(fresh_cache_path)
720+
fresh_cache_path.mkdir()
721+
assert os.path.exists(fresh_cache_path)
722+
_logger.info("Cache path created: %s", fresh_cache_path)
679723

680-
# ask to which one the given machine belongs:
681-
cat = ppms_connection._get_machine_catalogue_from_system(name, categories)
724+
ppms_connection.cache_path = fresh_cache_path
725+
_logger.info("Updated connection cache path: %s", fresh_cache_path)
682726

683-
# expected be the first one:
684-
assert cat == categories[0]
727+
for subdir in to_keep + to_flush:
728+
srcdir = os.path.join(orig_cache_root, subdir)
729+
tgt_path = fresh_cache_path / subdir
730+
assert not os.path.exists(tgt_path)
731+
copytree(srcdir, tgt_path)
732+
_logger.info("Copied [%s] to [%s]", subdir, tgt_path)
733+
assert os.path.exists(tgt_path)
685734

686-
# test when no category is found:
687-
cat = ppms_connection._get_machine_catalogue_from_system(name, categories[1:])
688-
assert cat == ""
735+
ppms_connection.flush_cache(keep_users=True)
689736

690-
# test with a system name that doesn't exist
691-
name = "_invalid_pyppms_system_name_"
692-
cat = ppms_connection._get_machine_catalogue_from_system(name, categories)
693-
assert cat == ""
737+
for subdir in to_keep:
738+
tgt_path = fresh_cache_path / subdir
739+
_logger.debug("Verifying directory has been KEPT: %s", tgt_path)
740+
assert os.path.exists(tgt_path)
694741

742+
for subdir in to_flush:
743+
tgt_path = fresh_cache_path / subdir
744+
_logger.debug("Verifying directory has been FLUSHED: %s", tgt_path)
745+
assert not os.path.exists(tgt_path)
695746

696-
def test_not_implemented_errors(ppms_connection):
697-
"""Test methods raising a NotImplementedError."""
698-
with pytest.raises(NotImplementedError):
699-
ppms_connection.get_bookable_ids("", "")
700747

701-
with pytest.raises(NotImplementedError):
702-
ppms_connection.get_system(99)
748+
@pytest.mark.online
749+
def test_flush_cache__keep_users__request_new(ppms_connection, caplog, tmp_path):
750+
"""Test flush_cache() with `keep_users=True` and request a new user after.
751+
752+
This test has a huge overlap to the `test_flush_cache__keep_users()` one
753+
(that works offline, unlike this one) with the main difference being that
754+
after flushing the cache, the cached details of a known user are removed and
755+
then exactly those details are requested. This will trigger an online
756+
request for that user while requests for previously existing (cached and
757+
preserved by the `keep_users` option set to `True`) users will be served
758+
directly from the cache.
759+
760+
- Make sure the temporary test-directory exists and has no cache inside.
761+
- Create the cache directory there.
762+
- Update the connection object's `cache_path` to point to the test location.
763+
- Copy over the subdirs listed in `to_keep` and `to_flush` from the cache
764+
provided with the tests.
765+
- Trigger the `flush_cache(keep_users=True)` method.
766+
- Simulate a new user in PPMS that is not yet cached locally:
767+
- Remove a specific file of previously cached user details from the
768+
`getuser` cache.
769+
-
770+
- Then copy back the `getusers` cache which contains the list of usernames
771+
known to PPMS, which will include the username whose details have been
772+
removed explicitly in the previous step.
773+
- Request details of the preserved user, verify they're being served from
774+
the cache by inspecting the log messages.
775+
- Request details of the "pseudo-new" user, verify they're triggering an
776+
on-line request to PUMAPI.
777+
"""
778+
to_keep = ["getuser"]
779+
to_flush = ["auth", "getgroups", "getusers", "getbooking"]
780+
781+
orig_cache_root = os.path.join(pyppmsconf.CACHE_PATH, "stage_0")
782+
fresh_cache_path = tmp_path / "pyppms_cache"
783+
784+
assert os.path.exists(orig_cache_root)
785+
786+
assert not os.path.exists(fresh_cache_path)
787+
fresh_cache_path.mkdir()
788+
assert os.path.exists(fresh_cache_path)
789+
_logger.info("Cache path created: %s", fresh_cache_path)
790+
791+
ppms_connection.cache_path = fresh_cache_path
792+
_logger.info("Updated connection cache path: %s", fresh_cache_path)
793+
794+
for subdir in to_keep + to_flush:
795+
srcdir = os.path.join(orig_cache_root, subdir)
796+
tgt_path = fresh_cache_path / subdir
797+
assert not os.path.exists(tgt_path)
798+
copytree(srcdir, tgt_path)
799+
_logger.info("Copied [%s] to [%s]", subdir, tgt_path)
800+
assert os.path.exists(tgt_path)
801+
802+
ppms_connection.flush_cache(keep_users=True)
803+
804+
new_user_name = "pyppms-adm" # simulated "new" user
805+
old_user_name = "pyppms" # previously existing, cached user (preserved)
806+
807+
_logger.info("Removing preserved user-cache for [%s]...", new_user_name)
808+
new_user_cache = fresh_cache_path / "getuser" / f"login--{new_user_name}.txt"
809+
old_user_cache = fresh_cache_path / "getuser" / f"login--{old_user_name}.txt"
810+
assert os.path.isfile(new_user_cache)
811+
os.unlink(new_user_cache)
812+
assert not os.path.exists(new_user_cache)
813+
assert os.path.exists(old_user_cache)
814+
815+
_logger.info("Restoring cache of existing user names...")
816+
users_list = os.path.join(orig_cache_root, "getusers")
817+
tgt_path = fresh_cache_path / "getusers"
818+
copytree(users_list, tgt_path)
819+
assert os.path.exists(tgt_path)
820+
_logger.info("Restored user names cache to [%s].", tgt_path)
821+
822+
_logger.info("Requesting details from PUMAPI for cached user [%s]", old_user_name)
823+
ppms_connection.get_user(old_user_name)
824+
assert "No cache hit" not in caplog.text # served from the cache
825+
826+
_logger.info("Requesting details from PUMAPI for 'new' user [%s]", new_user_name)
827+
ppms_connection.get_user(new_user_name)
828+
assert "No cache hit" in caplog.text # requires an on-line request
829+
assert os.path.exists(new_user_cache)

‎tests/test_system.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
FMT_TIME = r"%H:%M:%S"
1010
FMT = f"{FMT_DATE} {FMT_TIME}"
1111
DAY = "2019-05-18"
12-
TIME_START = "12:30:00"
13-
TIME_END = "13:15:00"
12+
TIME_START = "12:30"
13+
TIME_END = "13:15"
1414
START = f"{DAY} {TIME_START}"
1515
END = f"{DAY} {TIME_END}"
1616

0 commit comments

Comments
 (0)
Please sign in to comment.