From 4a07f6c1f9c370e54e94aba6c18c5ca09ae916fb Mon Sep 17 00:00:00 2001 From: megahomyak Date: Sun, 10 Oct 2021 18:28:38 +0500 Subject: [PATCH 01/12] Re-request on httpx.ReadTimeout --- netschoolapi/netschoolapi.py | 42 ++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index d810010..34b4fb3 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -97,29 +97,29 @@ async def login(self, user_name: str, password: str, school: str): async def _request_with_optional_relogin( self, path: str, method="GET", params: dict = None, json: dict = None): - try: - response = await self._client.request( - method, path, params=params, json=json - ) - except httpx.HTTPStatusError as http_status_error: - if ( - http_status_error.response.status_code - == httpx.codes.UNAUTHORIZED - ): - if self._login_data: - await self.login(*self._login_data) - return await self._client.request( - method, path, params=params, json=json - ) + while True: + try: + response = await self._client.request( + method, path, params=params, json=json + ) + except httpx.HTTPStatusError as http_status_error: + if ( + http_status_error.response.status_code + == httpx.codes.UNAUTHORIZED + ): + if self._login_data: + await self.login(*self._login_data) + else: + raise errors.AuthError( + ".login() before making requests that need " + "authorization" + ) else: - raise errors.AuthError( - ".login() before making requests that need " - "authorization" - ) + raise http_status_error + except httpx.ReadTimeout: + pass else: - raise http_status_error - else: - return response + return response async def download_attachment( self, attachment: data.Attachment, From b3d82255a288e95626cb412d083a8538849a6f21 Mon Sep 17 00:00:00 2001 From: megahomyak Date: Mon, 11 Oct 2021 22:40:54 +0500 Subject: [PATCH 02/12] Finite amount of re-requests instead of infinite --- netschoolapi/netschoolapi.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index 34b4fb3..3ca3b3f 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -11,12 +11,17 @@ __all__ = ['NetSchoolAPI'] +DEFAULT_REQUEST_REPETITIONS_AMOUNT = 5 + + async def _die_on_bad_status(response: Response): response.raise_for_status() class NetSchoolAPI: - def __init__(self, url: str): + def __init__( + self, url: str, + request_repetitions_amount=DEFAULT_REQUEST_REPETITIONS_AMOUNT): url = url.rstrip('/') self._client = AsyncClient( base_url=f'{url}/webapi', @@ -31,6 +36,8 @@ def __init__(self, url: str): self._assignment_types: Dict[int, str] = {} self._login_data = () + self._request_repetitions_amount = request_repetitions_amount + async def __aenter__(self) -> 'NetSchoolAPI': return self @@ -97,7 +104,7 @@ async def login(self, user_name: str, password: str, school: str): async def _request_with_optional_relogin( self, path: str, method="GET", params: dict = None, json: dict = None): - while True: + for _ in range(self._request_repetitions_amount): try: response = await self._client.request( method, path, params=params, json=json From 8ecff04aa49756508cc3fca7d94d3fdfbbd85990 Mon Sep 17 00:00:00 2001 From: megahomyak Date: Mon, 11 Oct 2021 22:47:11 +0500 Subject: [PATCH 03/12] +NoResponseFromServer exception --- netschoolapi/errors.py | 4 ++++ netschoolapi/netschoolapi.py | 1 + 2 files changed, 5 insertions(+) diff --git a/netschoolapi/errors.py b/netschoolapi/errors.py index 0d55e96..aac5a66 100644 --- a/netschoolapi/errors.py +++ b/netschoolapi/errors.py @@ -8,3 +8,7 @@ class AuthError(NetSchoolAPIError): class SchoolNotFoundError(NetSchoolAPIError): pass + + +class NoResponseFromServer(NetSchoolAPIError): + pass diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index 3ca3b3f..78aea69 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -127,6 +127,7 @@ async def _request_with_optional_relogin( pass else: return response + raise errors.NoResponseFromServer async def download_attachment( self, attachment: data.Attachment, From 559e19aca8d3e07b6b847e5250fb93f27ba8d9d8 Mon Sep 17 00:00:00 2001 From: megahomyak Date: Tue, 19 Oct 2021 15:52:52 +0500 Subject: [PATCH 04/12] Added request_repetitions_amount to almost every method --- netschoolapi/netschoolapi.py | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index 78aea69..d0c2ba2 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -102,9 +102,11 @@ async def login(self, user_name: str, password: str, school: str): self._login_data = (user_name, password, school) async def _request_with_optional_relogin( - self, path: str, method="GET", params: dict = None, - json: dict = None): - for _ in range(self._request_repetitions_amount): + self, path: str, request_repetitions_amount: Optional[int], + method="GET", params: dict = None, json: dict = None): + for _ in range( + request_repetitions_amount or self._request_repetitions_amount + ): try: response = await self._client.request( method, path, params=params, json=json @@ -131,7 +133,8 @@ async def _request_with_optional_relogin( async def download_attachment( self, attachment: data.Attachment, - path_or_file: Union[BytesIO, str] = None): + path_or_file: Union[BytesIO, str] = None, + request_repetitions_amount: int = None): """ If `path_to_file` is a string, it should contain absolute path to file """ @@ -143,7 +146,8 @@ async def download_attachment( file = path_or_file file.write(( await self._request_with_optional_relogin( - f"attachments/{attachment.id}" + f"attachments/{attachment.id}", + request_repetitions_amount=request_repetitions_amount ) ).content) @@ -159,6 +163,7 @@ async def diary( self, start: Optional[date] = None, end: Optional[date] = None, + request_repetitions_amount: int = None, ) -> data.Diary: if not start: monday = date.today() - timedelta(days=date.today().weekday()) @@ -174,6 +179,7 @@ async def diary( 'weekStart': start.isoformat(), 'weekEnd': end.isoformat(), }, + request_repetitions_amount=request_repetitions_amount ) diary_schema = schemas.Diary() diary_schema.context['assignment_types'] = self._assignment_types @@ -183,6 +189,7 @@ async def overdue( self, start: Optional[date] = None, end: Optional[date] = None, + request_repetitions_amount: int = None, ) -> List[data.Assignment]: if not start: monday = date.today() - timedelta(days=date.today().weekday()) @@ -198,14 +205,17 @@ async def overdue( 'weekStart': start.isoformat(), 'weekEnd': end.isoformat(), }, + request_repetitions_amount=request_repetitions_amount ) assignments = schemas.Assignment().load(response.json(), many=True) return [data.Assignment(**assignment) for assignment in assignments] async def announcements( - self, take: Optional[int] = -1) -> List[data.Announcement]: + self, take: Optional[int] = -1, + request_repetitions_amount: int = None) -> List[data.Announcement]: response = await self._request_with_optional_relogin( - 'announcements', params={'take': take} + 'announcements', params={'take': take}, + request_repetitions_amount=request_repetitions_amount ) announcements = schemas.Announcement().load(response.json(), many=True) return [ @@ -214,20 +224,23 @@ async def announcements( ] async def attachments( - self, assignment: data.Assignment) -> List[data.Attachment]: + self, assignment: data.Assignment, + request_repetitions_amount: int = None) -> List[data.Attachment]: response = await self._request_with_optional_relogin( method="POST", path='student/diary/get-attachments', params={'studentId': self._student_id}, json={'assignId': [assignment.id]}, + request_repetitions_amount=request_repetitions_amount ) attachments_json = response.json()[0]['attachments'] attachments = schemas.Attachment().load(attachments_json, many=True) return [data.Attachment(**attachment) for attachment in attachments] - async def school(self): + async def school(self, request_repetitions_amount: int = None): response = await self._request_with_optional_relogin( - 'schools/{0}/card'.format(self._school_id) + 'schools/{0}/card'.format(self._school_id), + request_repetitions_amount=request_repetitions_amount ) school = schemas.School().load(response.json()) return data.School(**school) From abe00cb954754e409d12db6652b5dc79764977d4 Mon Sep 17 00:00:00 2001 From: megahomyak Date: Tue, 19 Oct 2021 23:06:59 +0500 Subject: [PATCH 05/12] `.aclose()` in login --- netschoolapi/netschoolapi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index d0c2ba2..f30dfeb 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -71,6 +71,7 @@ async def login(self, user_name: str, password: str, school: str): }, ) except httpx.HTTPStatusError as http_status_error: + await self._client.aclose() if http_status_error.response.status_code == httpx.codes.CONFLICT: raise errors.AuthError("Incorrect username or password!") else: From 2cd93675a8502df806774f64e7690311009baeff Mon Sep 17 00:00:00 2001 From: megahomyak Date: Fri, 4 Feb 2022 23:40:14 +0500 Subject: [PATCH 06/12] Added `requests_timeout` argument --- netschoolapi/async_client_wrapper.py | 55 +++++++++++ netschoolapi/netschoolapi.py | 141 ++++++++++++++------------- 2 files changed, 128 insertions(+), 68 deletions(-) create mode 100644 netschoolapi/async_client_wrapper.py diff --git a/netschoolapi/async_client_wrapper.py b/netschoolapi/async_client_wrapper.py new file mode 100644 index 0000000..ff3ceef --- /dev/null +++ b/netschoolapi/async_client_wrapper.py @@ -0,0 +1,55 @@ +import asyncio +import functools +from typing import Optional, Awaitable, Protocol + +import httpx + +from netschoolapi import errors + +DEFAULT_REQUESTS_TIMEOUT = 5 + + +class Requester(Protocol): + + def __call__( + self, path: str, method="GET", params: dict = None, + json: dict = None, data: dict = None) -> Awaitable: + pass + + +class AsyncClientWrapper: + def __init__( + self, async_client: httpx.AsyncClient, + default_requests_timeout: int = None): + self.client = async_client + self._default_requests_timeout = ( + default_requests_timeout or DEFAULT_REQUESTS_TIMEOUT + ) + + def make_requester(self, requests_timeout: Optional[int]) -> Requester: + # noinspection PyTypeChecker + return functools.partial(self.request, requests_timeout) + + async def request( + self, requests_timeout: Optional[int], path: str, + method="GET", params: dict = None, json: dict = None, + data: dict = None): + try: + return await asyncio.wait_for(self._infinite_request( + path, method, params, json, data, + ), requests_timeout or self._default_requests_timeout) + except asyncio.TimeoutError: + raise errors.NoResponseFromServer from None + + async def _infinite_request( + self, path: str, method: str, params: Optional[dict], + json: Optional[dict], data: Optional[dict]): + while True: + try: + response = await self.client.request( + method, path, params=params, json=json, data=data + ) + except httpx.ReadTimeout: + pass + else: + return response diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index f30dfeb..7e06273 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -10,8 +10,7 @@ __all__ = ['NetSchoolAPI'] - -DEFAULT_REQUEST_REPETITIONS_AMOUNT = 5 +from netschoolapi.async_client_wrapper import AsyncClientWrapper, Requester async def _die_on_bad_status(response: Response): @@ -20,13 +19,15 @@ async def _die_on_bad_status(response: Response): class NetSchoolAPI: def __init__( - self, url: str, - request_repetitions_amount=DEFAULT_REQUEST_REPETITIONS_AMOUNT): + self, url: str, default_requests_timeout: int = None): url = url.rstrip('/') - self._client = AsyncClient( - base_url=f'{url}/webapi', - headers={'user-agent': 'NetSchoolAPI/5.0.3', 'referer': url}, - event_hooks={'response': [_die_on_bad_status]}, + self._wrapped_client = AsyncClientWrapper( + async_client=AsyncClient( + base_url=f'{url}/webapi', + headers={'user-agent': 'NetSchoolAPI/5.0.3', 'referer': url}, + event_hooks={'response': [_die_on_bad_status]}, + ), + default_requests_timeout=default_requests_timeout, ) self._student_id = -1 @@ -36,19 +37,22 @@ def __init__( self._assignment_types: Dict[int, str] = {} self._login_data = () - self._request_repetitions_amount = request_repetitions_amount - async def __aenter__(self) -> 'NetSchoolAPI': return self async def __aexit__(self, exc_type, exc_val, exc_tb): await self.logout() - async def login(self, user_name: str, password: str, school: str): - response_with_cookies = await self._client.get('logindata') - self._client.cookies.extract_cookies(response_with_cookies) + async def login( + self, user_name: str, password: str, school: str, + requests_timeout: int = None): + requester = self._wrapped_client.make_requester(requests_timeout) + response_with_cookies = await requester('logindata') + self._wrapped_client.client.cookies.extract_cookies( + response_with_cookies + ) - response = await self._client.post('auth/getdata') + response = await requester('auth/getdata', method="POST") login_meta = response.json() salt = login_meta.pop('salt') @@ -59,19 +63,20 @@ async def login(self, user_name: str, password: str, school: str): pw = pw2[: len(password)] try: - response = await self._client.post( + response = await requester( 'login', data={ 'loginType': 1, - **(await self._address(school)), + **(await self._address(school, requester)), 'un': user_name, 'pw': pw, 'pw2': pw2, **login_meta, }, + method="POST" ) except httpx.HTTPStatusError as http_status_error: - await self._client.aclose() + await self._wrapped_client.client.aclose() if http_status_error.response.status_code == httpx.codes.CONFLICT: raise errors.AuthError("Incorrect username or password!") else: @@ -81,18 +86,18 @@ async def login(self, user_name: str, password: str, school: str): if 'at' not in auth_result: raise errors.AuthError(auth_result['message']) - self._client.headers['at'] = auth_result['at'] + self._wrapped_client.client.headers['at'] = auth_result['at'] - response = await self._client.get('student/diary/init') + response = await requester('student/diary/init') diary_info = response.json() student = diary_info['students'][diary_info['currentStudentId']] self._student_id = student['studentId'] - response = await self._client.get('years/current') + response = await requester('years/current') year_reference = response.json() self._year_id = year_reference['id'] - response = await self._client.get( + response = await requester( 'grade/assignment/types', params={'all': False} ) assignment_reference = response.json() @@ -103,39 +108,33 @@ async def login(self, user_name: str, password: str, school: str): self._login_data = (user_name, password, school) async def _request_with_optional_relogin( - self, path: str, request_repetitions_amount: Optional[int], + self, requests_timeout: Optional[int], path: str, method="GET", params: dict = None, json: dict = None): - for _ in range( - request_repetitions_amount or self._request_repetitions_amount - ): - try: - response = await self._client.request( - method, path, params=params, json=json - ) - except httpx.HTTPStatusError as http_status_error: - if ( - http_status_error.response.status_code - == httpx.codes.UNAUTHORIZED - ): - if self._login_data: - await self.login(*self._login_data) - else: - raise errors.AuthError( - ".login() before making requests that need " - "authorization" - ) + try: + response = await self._wrapped_client.request( + requests_timeout, path, method, params, json, + ) + except httpx.HTTPStatusError as http_status_error: + if ( + http_status_error.response.status_code + == httpx.codes.UNAUTHORIZED + ): + if self._login_data: + await self.login(*self._login_data) else: - raise http_status_error - except httpx.ReadTimeout: - pass + raise errors.AuthError( + ".login() before making requests that need " + "authorization" + ) else: - return response - raise errors.NoResponseFromServer + raise http_status_error + else: + return response async def download_attachment( self, attachment: data.Attachment, path_or_file: Union[BytesIO, str] = None, - request_repetitions_amount: int = None): + requests_timeout: int = None): """ If `path_to_file` is a string, it should contain absolute path to file """ @@ -147,16 +146,18 @@ async def download_attachment( file = path_or_file file.write(( await self._request_with_optional_relogin( + requests_timeout, f"attachments/{attachment.id}", - request_repetitions_amount=request_repetitions_amount ) ).content) async def download_attachment_as_bytes( - self, attachment: data.Attachment) -> BytesIO: + self, attachment: data.Attachment, requests_timeout: int = None, + ) -> BytesIO: attachment_contents_buffer = BytesIO() await self.download_attachment( - attachment, path_or_file=attachment_contents_buffer + attachment, path_or_file=attachment_contents_buffer, + requests_timeout=requests_timeout ) return attachment_contents_buffer @@ -164,7 +165,7 @@ async def diary( self, start: Optional[date] = None, end: Optional[date] = None, - request_repetitions_amount: int = None, + requests_timeout: int = None, ) -> data.Diary: if not start: monday = date.today() - timedelta(days=date.today().weekday()) @@ -173,6 +174,7 @@ async def diary( end = start + timedelta(days=5) response = await self._request_with_optional_relogin( + requests_timeout, 'student/diary', params={ 'studentId': self._student_id, @@ -180,7 +182,6 @@ async def diary( 'weekStart': start.isoformat(), 'weekEnd': end.isoformat(), }, - request_repetitions_amount=request_repetitions_amount ) diary_schema = schemas.Diary() diary_schema.context['assignment_types'] = self._assignment_types @@ -190,7 +191,7 @@ async def overdue( self, start: Optional[date] = None, end: Optional[date] = None, - request_repetitions_amount: int = None, + requests_timeout: int = None, ) -> List[data.Assignment]: if not start: monday = date.today() - timedelta(days=date.today().weekday()) @@ -199,6 +200,7 @@ async def overdue( end = start + timedelta(days=5) response = await self._request_with_optional_relogin( + requests_timeout, 'student/diary/pastMandatory', params={ 'studentId': self._student_id, @@ -206,17 +208,17 @@ async def overdue( 'weekStart': start.isoformat(), 'weekEnd': end.isoformat(), }, - request_repetitions_amount=request_repetitions_amount ) assignments = schemas.Assignment().load(response.json(), many=True) return [data.Assignment(**assignment) for assignment in assignments] async def announcements( self, take: Optional[int] = -1, - request_repetitions_amount: int = None) -> List[data.Announcement]: + requests_timeout: int = None) -> List[data.Announcement]: response = await self._request_with_optional_relogin( - 'announcements', params={'take': take}, - request_repetitions_amount=request_repetitions_amount + requests_timeout, + 'announcements', + params={'take': take}, ) announcements = schemas.Announcement().load(response.json(), many=True) return [ @@ -226,34 +228,37 @@ async def announcements( async def attachments( self, assignment: data.Assignment, - request_repetitions_amount: int = None) -> List[data.Attachment]: + requests_timeout: int = None) -> List[data.Attachment]: response = await self._request_with_optional_relogin( + requests_timeout, method="POST", path='student/diary/get-attachments', params={'studentId': self._student_id}, json={'assignId': [assignment.id]}, - request_repetitions_amount=request_repetitions_amount ) attachments_json = response.json()[0]['attachments'] attachments = schemas.Attachment().load(attachments_json, many=True) return [data.Attachment(**attachment) for attachment in attachments] - async def school(self, request_repetitions_amount: int = None): + async def school(self, requests_timeout: int = None): response = await self._request_with_optional_relogin( + requests_timeout, 'schools/{0}/card'.format(self._school_id), - request_repetitions_amount=request_repetitions_amount ) school = schemas.School().load(response.json()) return data.School(**school) - async def logout(self): - await self._client.post('auth/logout') - await self._client.aclose() - - async def _address(self, school: str) -> Dict[str, int]: - response = await self._client.get( - 'addresses/schools', params={'funcType': 2} + async def logout(self, requests_timeout: int = None): + await self._wrapped_client.request( + requests_timeout, + 'auth/logout', + method="POST", ) + await self._wrapped_client.client.aclose() + + async def _address( + self, school: str, requester: Requester) -> Dict[str, int]: + response = await requester('addresses/schools', params={'funcType': 2}) schools_reference = response.json() for school_ in schools_reference: From 21ea5899b77da0fa1227226d1de6f2cc91936c2b Mon Sep 17 00:00:00 2001 From: megahomyak Date: Sat, 5 Feb 2022 04:07:35 +0500 Subject: [PATCH 07/12] `room` can sometimes be `None` --- netschoolapi/schemas.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netschoolapi/schemas.py b/netschoolapi/schemas.py index 3fbb546..5310d6f 100644 --- a/netschoolapi/schemas.py +++ b/netschoolapi/schemas.py @@ -56,7 +56,7 @@ class Lesson(NetSchoolAPISchema): day = fields.Date() start = fields.Time(data_key='startTime') end = fields.Time(data_key='endTime') - room = fields.String(missing='') + room = fields.String(missing='', allow_none=True) number = fields.Integer() subject = fields.String(data_key='subjectName') assignments = fields.List(fields.Nested(Assignment), missing=[]) From 95faeac0f3d98be73b633c73b96c302796dec8c9 Mon Sep 17 00:00:00 2001 From: megahomyak Date: Sun, 6 Feb 2022 00:03:02 +0500 Subject: [PATCH 08/12] Missing "assignment_types" added in context in `.overdue()` --- netschoolapi/netschoolapi.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index 7e06273..a8dce87 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -209,7 +209,9 @@ async def overdue( 'weekEnd': end.isoformat(), }, ) - assignments = schemas.Assignment().load(response.json(), many=True) + assignments_schema = schemas.Assignment() + assignments_schema.context['assignment_types'] = self._assignment_types + assignments = assignments_schema.load(response.json(), many=True) return [data.Assignment(**assignment) for assignment in assignments] async def announcements( From efc9f0a583c5948589b3b178bf2cd33b15506ce4 Mon Sep 17 00:00:00 2001 From: megahomyak Date: Sun, 6 Feb 2022 02:45:29 +0500 Subject: [PATCH 09/12] Little doc fix --- docs/guide.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/guide.md b/docs/guide.md index d7fafc0..e27e700 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -26,7 +26,7 @@ * `assignment: Assignment` --- урок, у которого нужно получить прикреплённые файлы -Возвращает список объектов [`Attachment`](reference.md#attachment) или пустой список, если к уроку не прикреплено файлов. Скачать вложение из файла можно с помощью `netschoolapi_client.download_attachment()` или `netschoolapi_client.download_attachment_as_bytes()`. +Возвращает список объектов [`Attachment`](reference.md#attachment) или пустой список, если к уроку не прикреплено файлов. ## Объявления {#announcements} @@ -35,3 +35,7 @@ ## Информация о школе {#school} `.school()` --- возвращает [информацию](reference.md#school) о школе. + +## Скачивание файлов из вложений {#downloading_files_from_attachments} + +Скачать файл из вложения можно с помощью `netschoolapi_client.download_attachment()` или `netschoolapi_client.download_attachment_as_bytes()`. From 530080a2e7fa4ef17eae9322c4351ded578ab946 Mon Sep 17 00:00:00 2001 From: megahomyak Date: Sun, 6 Feb 2022 02:55:07 +0500 Subject: [PATCH 10/12] Assignments without attachments are now safe to work with --- netschoolapi/netschoolapi.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/netschoolapi/netschoolapi.py b/netschoolapi/netschoolapi.py index a8dce87..6ff8451 100644 --- a/netschoolapi/netschoolapi.py +++ b/netschoolapi/netschoolapi.py @@ -238,6 +238,9 @@ async def attachments( params={'studentId': self._student_id}, json={'assignId': [assignment.id]}, ) + response = response.json() + if not response: + return [] attachments_json = response.json()[0]['attachments'] attachments = schemas.Attachment().load(attachments_json, many=True) return [data.Attachment(**attachment) for attachment in attachments] From 283e99283d08fffb6e3a85ee6f0f176ad746e61b Mon Sep 17 00:00:00 2001 From: megahomyak Date: Sun, 6 Feb 2022 04:08:27 +0500 Subject: [PATCH 11/12] Wrote some examples --- .gitignore | 3 ++ .../announcements_and_their_attachments.py | 21 ++++++++++++++ examples/config/config_maker.py | 20 +++++++++++++ examples/config/config_template.json | 6 ++++ examples/diary_and_its_attachments.py | 23 +++++++++++++++ examples/overdue.py | 11 ++++++++ examples/preparation/preparation.py | 28 +++++++++++++++++++ examples/preparation/utils.py | 7 +++++ examples/school.py | 11 ++++++++ 9 files changed, 130 insertions(+) create mode 100644 examples/announcements_and_their_attachments.py create mode 100644 examples/config/config_maker.py create mode 100644 examples/config/config_template.json create mode 100644 examples/diary_and_its_attachments.py create mode 100644 examples/overdue.py create mode 100644 examples/preparation/preparation.py create mode 100644 examples/preparation/utils.py create mode 100644 examples/school.py diff --git a/.gitignore b/.gitignore index f458ba0..53cb33f 100644 --- a/.gitignore +++ b/.gitignore @@ -267,3 +267,6 @@ pip-selfcheck.json *.code-workspace # End of https://www.toptal.com/developers/gitignore/api/linux,python,virtualenv,pycharm+all,vscode + + +examples/config/config.json diff --git a/examples/announcements_and_their_attachments.py b/examples/announcements_and_their_attachments.py new file mode 100644 index 0000000..caa7475 --- /dev/null +++ b/examples/announcements_and_their_attachments.py @@ -0,0 +1,21 @@ +import PIL.Image + +import netschoolapi +from examples.preparation.preparation import run_main +from examples.preparation.utils import pprint + + +async def main(client: netschoolapi.NetSchoolAPI): + print("Announcements:") + announcements = await client.announcements() + pprint(announcements) + print() + print("Now showing all images found in announcements.") + for announcement in announcements: + for attachment in announcement.attachments: + if attachment.name.endswith((".png", ".jpg", ".jpeg")): + image = await client.download_attachment_as_bytes(attachment) + PIL.Image.open(image).show() + + +run_main(main) diff --git a/examples/config/config_maker.py b/examples/config/config_maker.py new file mode 100644 index 0000000..01dd1fd --- /dev/null +++ b/examples/config/config_maker.py @@ -0,0 +1,20 @@ +import json +from dataclasses import dataclass + + +@dataclass +class Config: + url: str + user_name: str + password: str + school: str + + +def make_config(): + try: + return Config(**json.load(open("config/config.json", encoding="utf-8"))) + except FileNotFoundError: + raise FileNotFoundError( + "Please create a config/config.json file (use " + "config/config_template.json as a template)" + ) diff --git a/examples/config/config_template.json b/examples/config/config_template.json new file mode 100644 index 0000000..76809df --- /dev/null +++ b/examples/config/config_template.json @@ -0,0 +1,6 @@ +{ + "url": "https://edu.admoblkaluga.ru:444/", + "user_name": "Аркадий Степанович Петроненко", + "password": "||4R0Ль_йцУk3H", + "school": "МБОУ \"СОШ \"Технический лицей\"" +} \ No newline at end of file diff --git a/examples/diary_and_its_attachments.py b/examples/diary_and_its_attachments.py new file mode 100644 index 0000000..7a8ca5b --- /dev/null +++ b/examples/diary_and_its_attachments.py @@ -0,0 +1,23 @@ +import netschoolapi +from examples.preparation import utils +from preparation.preparation import run_main + + +async def main(client: netschoolapi.NetSchoolAPI): + print("Diary:") + diary = await client.diary() + utils.pprint(diary) + for day in diary.schedule: + for lesson in day.lessons: + for assignment in lesson.assignments: + attachments = await client.attachments(assignment) + if attachments: + print( + f"Some attachments found on day {day.day} on " + f"{lesson.subject} homework: {attachments}" + ) + # You can also download them using + # .download_attachment() or .download_attachment_as_bytes() + + +run_main(main) diff --git a/examples/overdue.py b/examples/overdue.py new file mode 100644 index 0000000..bb2b94e --- /dev/null +++ b/examples/overdue.py @@ -0,0 +1,11 @@ +import netschoolapi +from examples.preparation.preparation import run_main +from examples.preparation.utils import pprint + + +async def main(client: netschoolapi.NetSchoolAPI): + print("Overdue:") + pprint(await client.overdue()) + + +run_main(main) diff --git a/examples/preparation/preparation.py b/examples/preparation/preparation.py new file mode 100644 index 0000000..b1a710d --- /dev/null +++ b/examples/preparation/preparation.py @@ -0,0 +1,28 @@ +import asyncio +from typing import Callable, Awaitable + +# noinspection PyUnresolvedReferences +# because working path from an IDE project not always equals to a working path +# when launching the script (so if I launch something from examples, my working +# directory will be "examples", but my IDE thinks that it is "examples/.." +# (project root)) +from config.config_maker import Config, make_config + +import netschoolapi + + +async def login(config: Config) -> netschoolapi.NetSchoolAPI: + client = netschoolapi.NetSchoolAPI(config.url) + await client.login(config.user_name, config.password, config.school) + return client + + +async def _run_main(main): + config = make_config() + client = await login(config) + async with client: + await main(client) + + +def run_main(main: Callable[[netschoolapi.NetSchoolAPI], Awaitable]): + asyncio.get_event_loop().run_until_complete(_run_main(main)) diff --git a/examples/preparation/utils.py b/examples/preparation/utils.py new file mode 100644 index 0000000..c365411 --- /dev/null +++ b/examples/preparation/utils.py @@ -0,0 +1,7 @@ +import rich.console + +console = rich.console.Console(no_color=True, width=80) + + +def pprint(object_): + console.print(object_) diff --git a/examples/school.py b/examples/school.py new file mode 100644 index 0000000..792a5ee --- /dev/null +++ b/examples/school.py @@ -0,0 +1,11 @@ +import netschoolapi +from examples.preparation.preparation import run_main +from examples.preparation.utils import pprint + + +async def main(client: netschoolapi.NetSchoolAPI): + print("School info:") + pprint(await client.school()) + + +run_main(main) From 509f02c97e92d8a80ee04fed6c2a32fb6afa79af Mon Sep 17 00:00:00 2001 From: megahomyak Date: Sun, 6 Feb 2022 04:18:38 +0500 Subject: [PATCH 12/12] Referenced `/examples` in `README.md` --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 479295e..9ca4049 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ NetSchoolAPI — это асинхронный клиент для «Сетев [Документация](https://netschoolapi.readthedocs.io/ru/latest/) +А ещё у нас есть примеры в `/examples`. + ## Жалобы и предложения Обсуждение библиотеки ведётся в [чате](https://t.me/netschoolapi/) проекта в телеграме.