Skip to content

Commit 338626e

Browse files
committed
Merge branch 'devel'
2 parents 6032044 + 7627d08 commit 338626e

File tree

5 files changed

+95
-26
lines changed

5 files changed

+95
-26
lines changed

CHANGELOG.md

+19
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,25 @@
44

55
NOTE: potentially breaking changes are flagged with a 🧨 symbol.
66

7+
## 3.1.0
8+
9+
### Added
10+
11+
- `pyppms.common.fmt_time()` to string-format a datetime object that might also
12+
be None (in which case a fixed string is returned).
13+
- `pyppms.booking.PpmsBooking.desc` has been added as a property to retrieve a
14+
shorter description of the object than calling `str()` on it.
15+
- `pyppms.exceptions.NoDataError` has been added to indicate a PUMAPI response
16+
did *not* contain any useful data.
17+
- `pyppms.common.parse_multiline_response()` will now raise the newly added
18+
`NoDataError` in case the requested *runningsheet* for a day doesn't contain
19+
any bookings to allow for properly dealing with "empty" days.
20+
21+
### Changed
22+
23+
- Several log messages have been demoted from `debug` to `trace` level and might
24+
have been shortened / combined to reduce logging clutter.
25+
726
## 3.0.0
827

928
### Changed

src/pyppms/booking.py

+17-10
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from loguru import logger as log
66

7-
from .common import time_rel_to_abs
7+
from .common import time_rel_to_abs, fmt_time
88

99

1010
class PpmsBooking:
@@ -122,7 +122,7 @@ def starttime_fromstr(self, time_str, date=None):
122122
microsecond=0,
123123
)
124124
self.starttime = start
125-
log.debug("New starttime: {}", self)
125+
log.trace("New starttime: {}", self)
126126

127127
def endtime_fromstr(self, time_str, date=None):
128128
"""Change the ending time and / or day of a booking.
@@ -144,16 +144,9 @@ def endtime_fromstr(self, time_str, date=None):
144144
microsecond=0,
145145
)
146146
self.endtime = end
147-
log.debug("New endtime: {}", self)
147+
log.trace("New endtime: {}", self)
148148

149149
def __str__(self):
150-
def fmt_time(time):
151-
# in case a booking was created from a "nextbooking" response it will not
152-
# have the `endtime` attribute set, so treat this separately:
153-
if time is None:
154-
return "===UNDEFINED==="
155-
return datetime.strftime(time, "%Y-%m-%d %H:%M")
156-
157150
msg = (
158151
f"PpmsBooking(username=[{self.username}], "
159152
f"system_id=[{self.system_id}], "
@@ -165,3 +158,17 @@ def fmt_time(time):
165158
msg += ")"
166159

167160
return msg
161+
162+
@property
163+
def desc(self):
164+
"""Format a "short" description of the object.
165+
166+
Returns
167+
-------
168+
str
169+
A string containing `username`, `system_id` and the booking times.
170+
"""
171+
return (
172+
f"{self.username}@{self.system_id} "
173+
f"[{fmt_time(self.starttime)} -- {fmt_time(self.endtime)}]"
174+
)

src/pyppms/common.py

+33-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
from loguru import logger as log
1010

11+
from .exceptions import NoDataError
12+
1113

1214
def process_response_values(values):
1315
"""Process (in-place) a list of strings, remove quotes, detect boolean etc.
@@ -120,11 +122,15 @@ def parse_multiline_response(text, graceful=True):
120122
-------
121123
list(dict)
122124
A list with dicts where the latter ones have the same form as produced
123-
by the dict_from_single_response() function. Note that when graceful
125+
by the dict_from_single_response() function. May be empty in case the
126+
PUMAPI response didn't contain any useful data. Note that when graceful
124127
mode is requested, consistency among the dicts is not guaranteed.
125128
126129
Raises
127130
------
131+
NoDataError
132+
Raised when the response text was too short (less than two lines) and
133+
the `graceful` parameter has been set to false.
128134
ValueError
129135
Raised when the response text is inconsistent and the `graceful`
130136
parameter has been set to false, or if parsing fails for any other
@@ -134,10 +140,10 @@ def parse_multiline_response(text, graceful=True):
134140
try:
135141
lines = text.splitlines()
136142
if len(lines) < 2:
137-
log.warning("Response expected to have two or more lines: {}", text)
143+
log.info("Response has less than TWO lines: >>>{}<<<", text)
138144
if not graceful:
139-
raise ValueError("Invalid response format!")
140-
return parsed
145+
raise NoDataError("Invalid response format!")
146+
return []
141147

142148
header = lines[0].split(",")
143149
for i, entry in enumerate(header):
@@ -174,6 +180,9 @@ def parse_multiline_response(text, graceful=True):
174180
)
175181
log.warning(msg)
176182

183+
except NoDataError as err:
184+
raise err
185+
177186
except Exception as err:
178187
msg = f"Unable to parse data returned by PUMAPI: {text} - ERROR: {err}"
179188
log.error(msg)
@@ -198,3 +207,23 @@ def time_rel_to_abs(minutes_from_now):
198207
now = datetime.now().replace(second=0, microsecond=0)
199208
abstime = now + timedelta(minutes=int(minutes_from_now))
200209
return abstime
210+
211+
212+
def fmt_time(time):
213+
"""Format a `datetime` or `None` object to string.
214+
215+
This is useful to apply it to booking times as they might be `None` e.g. in
216+
case they have been created from a "nextbooking" response.
217+
218+
Parameters
219+
----------
220+
time : datetime.datetime or None
221+
222+
Returns
223+
-------
224+
str
225+
The formatted time, or a specific string in case the input was `None`.
226+
"""
227+
if time is None:
228+
return "===UNDEFINED==="
229+
return datetime.strftime(time, "%Y-%m-%d %H:%M")

src/pyppms/exceptions.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""PyPPMS exception classes."""
2+
3+
4+
class NoDataError(ValueError):
5+
"""Exception indicating no data was received from PUMAPI."""

src/pyppms/ppms.py

+21-12
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from .user import PpmsUser
2121
from .system import PpmsSystem
2222
from .booking import PpmsBooking
23+
from .exceptions import NoDataError
2324

2425

2526
class PpmsConnection:
@@ -133,7 +134,7 @@ def __authenticate(self):
133134
)
134135
self.status["auth_state"] = "attempting"
135136
response = self.request("auth")
136-
log.debug(f"Authenticate response: {response.text}")
137+
log.trace(f"Authenticate response: {response.text}")
137138
self.status["auth_response"] = response.text
138139
self.status["auth_httpstatus"] = response.status_code
139140

@@ -166,8 +167,11 @@ def __authenticate(self):
166167
log.error(msg)
167168
raise requests.exceptions.ConnectionError(msg)
168169

169-
log.info(f"Authentication succeeded, response=[{response.text}]")
170-
log.debug(f"HTTP Status: {response.status_code}")
170+
log.info(
171+
"Authentication succeeded, response=[{}], http_status=[{}]",
172+
response.text,
173+
response.status_code,
174+
)
171175
self.status["auth_state"] = "good"
172176

173177
def request(self, action, parameters={}, skip_cache=False):
@@ -249,14 +253,14 @@ def __interception_path(self, req_data, create_dir=False):
249253
action = req_data["action"]
250254

251255
if self.cache_users_only and action != "getuser":
252-
log.debug(f"NOT caching '{action}' (cache_users_only is set)")
256+
log.trace(f"NOT caching '{action}' (cache_users_only is set)")
253257
return None
254258

255259
intercept_dir = os.path.join(self.cache_path, action)
256260
if create_dir and not os.path.exists(intercept_dir): # pragma: no cover
257261
try:
258262
os.makedirs(intercept_dir)
259-
log.debug(f"Created dir to store response: {intercept_dir}")
263+
log.trace(f"Created dir to store response: {intercept_dir}")
260264
except Exception as err: # pylint: disable-msg=broad-except
261265
log.warning(f"Failed creating [{intercept_dir}]: {err}")
262266
return None
@@ -316,7 +320,10 @@ def __init__(self, text, status_code):
316320

317321
with open(intercept_file, "r", encoding="utf-8") as infile:
318322
text = infile.read()
319-
log.debug(f"Read intercepted response text from [{intercept_file}]")
323+
log.debug(
324+
"Read intercepted response text from [{}]",
325+
intercept_file[len(str(self.cache_path)) :],
326+
)
320327

321328
status_code = 200
322329
status_file = os.path.splitext(intercept_file)[0] + "_status-code.txt"
@@ -590,12 +597,14 @@ def get_running_sheet(self, core_facility_ref, date, ignore_uncached_users=False
590597
response = self.request("getrunningsheet", parameters)
591598
try:
592599
entries = parse_multiline_response(response.text, graceful=False)
600+
except NoDataError:
601+
# in case no bookings exist the response will be empty!
602+
log.debug("Runningsheet for the given day was empty!")
603+
return []
593604
except Exception as err: # pylint: disable-msg=broad-except
594605
log.error("Parsing runningsheet details failed: {}", err)
595-
# NOTE: in case no future bookings exist the response will be empty!
596-
log.error("Possibly the runningsheet is empty as no bookings exist?")
597-
log.debug("Runningsheet PUMPAI response was: {}", response.text)
598-
return bookings
606+
log.debug("Runningsheet PUMPAI response was: >>>{}<<<", response.text)
607+
return []
599608

600609
for entry in entries:
601610
full = entry["User"]
@@ -702,7 +711,7 @@ def get_systems_matching(self, localisation, name_contains):
702711
systems = self.get_systems()
703712
for sys_id, system in systems.items():
704713
if loc.lower() not in str(system.localisation).lower():
705-
log.debug(
714+
log.trace(
706715
"System [{}] location ({}) is NOT matching ({}), ignoring",
707716
system.name,
708717
system.localisation,
@@ -715,7 +724,7 @@ def get_systems_matching(self, localisation, name_contains):
715724
# system.name, loc, name_contains)
716725
for valid_name in name_contains:
717726
if valid_name in system.name:
718-
log.debug("System [{}] matches all criteria", system.name)
727+
log.trace("System [{}] matches all criteria", system.name)
719728
system_ids.append(sys_id)
720729
break
721730

0 commit comments

Comments
 (0)