Skip to content
This repository was archived by the owner on Jul 19, 2020. It is now read-only.

Commit 300d5c5

Browse files
authored
Develop/main (#14)
migrate to **v2.1.0**: * update response schema * use only debug-level logging * rearrange and simplify code
1 parent 40bec78 commit 300d5c5

File tree

11 files changed

+323
-391
lines changed

11 files changed

+323
-391
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.pytest_cache/
12
__pycache__/
23
.vscode/
34
.cache/

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1+
# dev dependencies
12
pytest>=3.2.1
2-
requests==2.11.1
33
twine==1.9.1

ruz/__init__.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
"""
2-
Python wrapper for HSE RUZ API
2+
Python wrapper for HSE RUZ API.
33
44
Usage
55
-----
66
import ruz
77
assert ruz.person_lessons("mymail@edu.hse.ru")
88
"""
99

10-
from ruz.main import (auditoriums, buildings, chairs, faculties, find_by_str,
11-
get_formated_date, groups, is_hse_email, is_student,
12-
is_valid_hse_email, kind_of_works, lecturers,
13-
person_lessons, schedules, staff_of_group, streams,
14-
sub_groups, type_of_auditoriums)
10+
from ruz.api import (auditoriums, buildings, chairs, faculties, find_by_str,
11+
groups, kind_of_works, lecturers, person_lessons,
12+
schedules, staff_of_group, streams, sub_groups,
13+
type_of_auditoriums)
1514

1615
__author__ = "Dmitriy Pchelkin | hell03end"
17-
__version__ = (2, 0, 1)
16+
__version__ = (2, 1, 0)

ruz/main.py renamed to ruz/api.py

Lines changed: 4 additions & 184 deletions
Original file line numberDiff line numberDiff line change
@@ -1,189 +1,10 @@
1-
import json
2-
import re
1+
import logging
32
from collections import Callable, Iterable
4-
from datetime import datetime, timedelta
53
from functools import lru_cache
6-
from urllib import error, parse, request
74

8-
from ruz.logging import log, logging
9-
from ruz.schema import REQUEST_SCHEMA, RUZ_API_ENDPOINTS
10-
from ruz.utils import (CHECK_EMAIL_ONLINE, RUZ_API_URL, RUZ_API_V,
11-
USE_NONE_SAFE_VALUES, none_safe)
5+
from ruz.utils import get, get_formated_date, is_student
126

137

14-
# ===== Common methods =====
15-
16-
def is_student(email: str) -> bool or None:
17-
"""
18-
Check email belongs to student
19-
20-
:param email, required - valid HSE email addres or domain.
21-
22-
Stutent's domain: @edu.hse.ru
23-
HSE stuff' domain: @hse.ru
24-
"""
25-
email_domain = email.lower().split("@")[-1]
26-
27-
if email_domain == "edu.hse.ru":
28-
return True
29-
elif email_domain == "hse.ru":
30-
return False
31-
32-
logging.error("Wrong HSE email domain: '%s'", email_domain)
33-
34-
35-
def is_hse_email(email: str) -> bool:
36-
"""
37-
Check email is valid HSE corp. email
38-
39-
:param email, required - email address to check.
40-
"""
41-
if re.fullmatch(r"^[a-z0-9\._-]{3,}@(edu\.)?hse\.ru$", email.lower()):
42-
return True
43-
logging.debug("Incorrect HSE email '%s'.", email)
44-
return False
45-
46-
47-
def get_formated_date(day_bias: int or float=0) -> str:
48-
"""
49-
Return date in RUZ API compatible format
50-
51-
:param day_bias - number of day from now.
52-
"""
53-
return (datetime.now() + timedelta(
54-
days=float(day_bias)
55-
)).strftime("%Y.%m.%d")
56-
57-
58-
@log()
59-
def is_valid_hse_email(email: str) -> bool:
60-
"""
61-
Check email is valid via API endpoint call (schedule)
62-
63-
:param email - email address to check (for schedules only).
64-
"""
65-
@none_safe()
66-
def request_schedule_api(**params) -> list or dict:
67-
return request.urlopen(make_url(
68-
"schedule",
69-
email=email,
70-
fromDate=get_formated_date(),
71-
toDate=get_formated_date(1),
72-
**params
73-
))
74-
75-
email = email.strip().lower()
76-
if not is_hse_email(email):
77-
return False
78-
79-
try:
80-
response = request_schedule_api(
81-
receiverType=1 if not is_student(email) else None
82-
)
83-
del response
84-
except (error.HTTPError, error.URLError) as err:
85-
logging.debug("Email '%s' wasn't verified.\n%s", email, err)
86-
return False
87-
return True
88-
89-
90-
# ===== Special methods =====
91-
92-
@log()
93-
def is_valid_schema(endpoint: str,
94-
check_email_online: bool=CHECK_EMAIL_ONLINE,
95-
**params) -> bool:
96-
"""
97-
Check params fit schema for certain endpoint
98-
99-
:param endpoint - endpoint for request.
100-
:param check_email_online - use is_valid_hse_email.
101-
:param params - schema params.
102-
"""
103-
104-
if (endpoint == "schedule" and "lecturerOid" not in params and
105-
"studentOid" not in params and "email" not in params and
106-
"auditoriumOid" not in params):
107-
logging.debug("One of the followed required: lecturer_id, "
108-
"auditorium_id, student_id, email for "
109-
"schedule endpoint.")
110-
return False
111-
112-
if params.get('email') is not None:
113-
email = params['email']
114-
if not is_hse_email(email):
115-
del email
116-
return False
117-
elif check_email_online and not is_valid_hse_email(email):
118-
logging.warning("'%s' is not verified by API call.", email)
119-
del email
120-
121-
endpoint = RUZ_API_ENDPOINTS.get(endpoint)
122-
if endpoint is None:
123-
logging.warning("Can't find endpoint: '%s'.", endpoint)
124-
del endpoint
125-
return False
126-
127-
schema = REQUEST_SCHEMA[endpoint]
128-
for key, value in params.items():
129-
if key not in schema:
130-
logging.warning("Can't find '%s' schema param: '%s'",
131-
endpoint, key)
132-
del schema, endpoint
133-
return False
134-
if not isinstance(value, schema[key]):
135-
logging.warning("Expected {} for '{}'::'{}' got: {}",
136-
schema[key], endpoint, key, type(value))
137-
del schema, endpoint
138-
return False
139-
del schema, endpoint
140-
return True
141-
142-
143-
@log()
144-
def make_url(endpoint: str, **params) -> str:
145-
"""
146-
Creates URL for API requests
147-
148-
:param endpoint - endpoint for request.
149-
:param params - request params.
150-
"""
151-
url = "".join((RUZ_API_URL, RUZ_API_ENDPOINTS[endpoint]))
152-
if params:
153-
return "?".join((url, parse.urlencode(params)))
154-
return url
155-
156-
157-
@none_safe()
158-
@log()
159-
def get(endpoint: str,
160-
encoding: str="utf-8",
161-
return_none_safe: bool=USE_NONE_SAFE_VALUES,
162-
**params) -> (list, dict, None):
163-
"""
164-
Return requested data in JSON
165-
166-
Check request has correct schema.
167-
168-
:param endpoint - endpoint for request.
169-
:param encoding - encoding for received data.
170-
:param return_none_safe - return empty list on fallback.
171-
:param params - requested params
172-
"""
173-
if not is_valid_schema(endpoint, **params):
174-
return [] if return_none_safe else None
175-
176-
url = make_url(endpoint, **params)
177-
try:
178-
response = request.urlopen(url)
179-
return json.loads(response.read().decode(encoding))
180-
except (error.HTTPError, error.URLError) as err:
181-
logging.warning("Can't get '%s'.\n%s", url, err)
182-
return [] if return_none_safe else None
183-
184-
185-
# ===== API methods =====
186-
1878
def schedules(emails: Iterable=None,
1889
lecturer_ids: Iterable=None,
18910
auditorium_ids: Iterable=None,
@@ -385,8 +206,6 @@ def sub_groups(reset_cache: bool=False) -> list:
385206
return get("subGroups")
386207

387208

388-
# ===== Additional methods =====
389-
390209
def find_by_str(subject: str or Callable,
391210
query: str,
392211
by: str="name",
@@ -440,4 +259,5 @@ def find_by_str(subject: str or Callable,
440259
raise NotImplementedError(subject.__name__)
441260

442261
query = query.strip().lower()
443-
return [el for el in subject(**params) if query in el[by].lower().strip()]
262+
return [el for el in subject(**params)
263+
if query in (el[by].lower().strip() if el[by] else "")]

ruz/logging.py

Lines changed: 20 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,24 @@
11
import logging
2-
import traceback
2+
import os
33
from collections import Callable
44
from functools import wraps
5-
from logging import RootLogger
65

7-
8-
# Default logging behavior
9-
logging.basicConfig(
10-
level=logging.WARNING,
11-
format="[%(asctime)s] %(levelname)s "
12-
"[%(name)s.{%(filename)s}.%(funcName)s:%(lineno)d] %(message)s",
13-
datefmt="%H:%M:%S"
14-
)
15-
16-
17-
class Log:
18-
""" Context manager for events' logging. Handling (log) exceptions. """
19-
20-
def __init__(self,
21-
case_name: str,
22-
level: int=logging.DEBUG,
23-
logger: RootLogger=None,
24-
**kwargs) -> None:
25-
self._name = case_name
26-
self._level = level
27-
self._logger = logger if logger is not None else None
28-
self._enter_msg = kwargs.pop("enter_msg", "ENTERING::")
29-
self._exit_msg = kwargs.pop("exit_msg", "EXITING::")
30-
self._exc_msg = kwargs.pop("exc_msg", "")
31-
self._silent = kwargs.pop("silent", False)
32-
33-
def _log(self, *message, level: int=None) -> None:
34-
""" log message """
35-
if level is None:
36-
level = self._level
37-
38-
if level == logging.ERROR:
39-
if self._logger:
40-
self._logger.error(*message)
41-
else:
42-
logging.error(*message)
43-
elif level > logging.DEBUG:
44-
if self._logger:
45-
self._logger.info(*message)
46-
else:
47-
logging.info(*message)
48-
else:
49-
if self._logger:
50-
self._logger.debug(*message)
51-
else:
52-
logging.debug(*message)
53-
54-
def __enter__(self) -> object:
55-
""" Returns it's logger instance of self if silent """
56-
if not self._silent:
57-
self._log("%s%s", self._enter_msg, self._name)
58-
return self
59-
60-
def __exit__(self,
61-
exc_type: object=None,
62-
exc_val: object=None,
63-
tb: object=None) -> None:
64-
""" Handling (log) exceptions """
65-
if exc_type is not None:
66-
self._log(
67-
"%s\n%s%s: %s\n",
68-
self._exc_msg,
69-
"\n".join([s.strip(r"\n") for s in traceback.format_tb(tb)]),
70-
exc_type.__name__,
71-
exc_val
72-
)
73-
74-
if not self._silent:
75-
self._log("%s%s", self._exit_msg, self._name)
76-
77-
78-
class _FuncLog(Log):
79-
""" Context manager for logging function calls """
80-
81-
def __init__(self,
82-
case_name: str,
83-
level: int=logging.DEBUG,
84-
logger: RootLogger=None):
85-
super(_FuncLog, self).__init__(
86-
case_name=case_name,
87-
level=level,
88-
enter_msg="===> ",
89-
exit_msg="<--- ",
90-
logger=logger
91-
)
92-
93-
94-
def log(log_result: bool=True,
95-
log_args: bool=True,
96-
log_kwargs: bool=True,
97-
name: str=None,
98-
level: int=logging.DEBUG,
99-
logger: RootLogger=None) -> Callable:
100-
""" Returns decorator for logging function/method behavior """
101-
def decor(func: Callable) -> Callable:
102-
func_name = name
103-
if not func_name:
104-
func_name = func.__name__
105-
106-
@wraps(func)
107-
def wrapper(*args, **kwargs) -> object:
108-
with _FuncLog(func_name, level, logger=logger):
109-
if log_args:
110-
for arg in args:
111-
logging.debug("()::%s", arg)
112-
if log_kwargs:
113-
for key, value in kwargs.items():
114-
logging.debug(r"{}::%s=%s", key, value)
115-
116-
result = func(*args, **kwargs)
117-
if log_result:
118-
logging.debug("RETURN(%s)::%s", func_name, result)
119-
return result
120-
return wrapper
121-
return decor
6+
ENABLE_LOGGING = os.environ.get("HSE_RUZ_ENABLE_LOGGING", True)
7+
8+
9+
def log(func: Callable) -> Callable:
10+
if not ENABLE_LOGGING:
11+
return func
12+
13+
@wraps(func)
14+
def wrapper(*args, **kwargs) -> object:
15+
func_name = func.__name__
16+
logging.debug("[%s]\tENTER", func_name)
17+
for arg in args:
18+
logging.debug("[%s]\tARG\t%s", func_name, arg)
19+
for key, value in kwargs.items():
20+
logging.debug("[%s]\tKWARG\t%s=%s", func_name, key, value)
21+
result = func(*args, **kwargs)
22+
logging.debug("[%s]\tEXIT", func_name)
23+
return result
24+
return wrapper

0 commit comments

Comments
 (0)