From b78c35226c9459a6177a8a6380e68c892f7bb858 Mon Sep 17 00:00:00 2001 From: bensteUEM Date: Tue, 8 Oct 2024 20:53:06 +0200 Subject: [PATCH] implementing ruff and cleaning up code #102 --- .pre-commit-config.yaml | 5 + .vscode/settings.json | 4 +- churchtools_api/__init__.py | 2 +- churchtools_api/calendar.py | 121 ++-- churchtools_api/churchtools_api.py | 289 ++++----- churchtools_api/churchtools_api_abstract.py | 51 +- churchtools_api/events.py | 647 ++++++++++---------- churchtools_api/files.py | 244 ++++---- churchtools_api/groups.py | 334 +++++----- churchtools_api/persons.py | 77 +-- churchtools_api/resources.py | 47 +- churchtools_api/songs.py | 283 ++++----- generate_pyproj.py | 91 ++- main.ipynb | 16 +- pyproject.toml | 79 ++- tests/test_churchtools_api.py | 502 +++++++-------- tests/test_churchtools_api_abstract.py | 35 +- tests/test_churchtools_api_calendars.py | 194 +++--- tests/test_churchtools_api_events.py | 340 +++++----- tests/test_churchtools_api_resources.py | 93 +-- version.py | 8 +- 21 files changed, 1839 insertions(+), 1623 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..c850bbc --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.6.7 # Use the latest stable version of Ruff + hooks: + - id: ruff diff --git a/.vscode/settings.json b/.vscode/settings.json index f51c61d..dcb900a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,8 +9,8 @@ "python.testing.pytestEnabled": true, "python.testing.unittestEnabled": false, "[python]": { - "editor.defaultFormatter": "ms-python.autopep8", - "editor.formatOnSave": true + "editor.defaultFormatter": "charliermarsh.ruff", + "editor.formatOnSave": false }, "python.formatting.provider": "none" } \ No newline at end of file diff --git a/churchtools_api/__init__.py b/churchtools_api/__init__.py index 7df3d8e..aa0c791 100644 --- a/churchtools_api/__init__.py +++ b/churchtools_api/__init__.py @@ -1 +1 @@ -__all__ = ['churchtools_api'] +__all__ = ["churchtools_api"] diff --git a/churchtools_api/calendar.py b/churchtools_api/calendar.py index c9dcea5..e3bb075 100644 --- a/churchtools_api/calendar.py +++ b/churchtools_api/calendar.py @@ -1,48 +1,47 @@ import json import logging - from datetime import datetime + from churchtools_api.churchtools_api_abstract import ChurchToolsApiAbstract logger = logging.getLogger(__name__) + class ChurchToolsApiCalendar(ChurchToolsApiAbstract): - """ Part definition of ChurchToolsApi which focuses on calendars + """Part definition of ChurchToolsApi which focuses on calendars. Args: ChurchToolsApiAbstract: template with minimum references """ - def __init__(self): + def __init__(self) -> None: super() def get_calendars(self) -> list[dict]: - """ - Function to retrieve all calendar objects - This does not include pagination yet + """Function to retrieve all calendar objects + This does not include pagination yet. Returns: Dict of calendars """ - url = self.domain + '/api/calendars' - headers = { - 'accept': 'application/json' - } + url = self.domain + "/api/calendars" + headers = {"accept": "application/json"} params = {} response = self.session.get(url=url, params=params, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - return response_content['data'].copy() - else: - logger.warning( - "%s Something went wrong fetching events: %s", response.status_code, response.content) + return response_content["data"].copy() + logger.warning( + "%s Something went wrong fetching events: %s", + response.status_code, + response.content, + ) + return None - def get_calendar_appointments( - self, calendar_ids: list, **kwargs) -> list[dict]: - """ - Retrieve a list of appointments + def get_calendar_appointments(self, calendar_ids: list, **kwargs) -> list[dict]: + """Retrieve a list of appointments. Arguments: calendar_ids: list of calendar ids to be checked @@ -60,73 +59,79 @@ def get_calendar_appointments( startDate and endDate overwritten by actual date if calculated date of series is unambiguous Nothing in case something is off or nothing exists """ - - url = self.domain + '/api/calendars' + url = self.domain + "/api/calendars" params = {} if len(calendar_ids) > 1: - url += '/appointments' - params['calendar_ids[]'] = calendar_ids - elif 'appointment_id' in kwargs.keys(): + url += "/appointments" + params["calendar_ids[]"] = calendar_ids + elif "appointment_id" in kwargs: url += f"/{calendar_ids[0]}/appointments/{kwargs['appointment_id']}" else: url += f"/{calendar_ids[0]}/appointments" - headers = { - 'accept': 'application/json' - } + headers = {"accept": "application/json"} - if 'from_' in kwargs.keys(): - from_ = kwargs['from_'] + if "from_" in kwargs: + from_ = kwargs["from_"] if isinstance(from_, datetime): from_ = from_.strftime("%Y-%m-%d") if len(from_) == 10: - params['from'] = from_ - if 'to_' in kwargs.keys() and 'from_' in kwargs.keys(): - to_ = kwargs['to_'] + params["from"] = from_ + if "to_" in kwargs and "from_" in kwargs: + to_ = kwargs["to_"] if isinstance(to_, datetime): to_ = to_.strftime("%Y-%m-%d") if len(to_) == 10: - params['to'] = to_ - elif 'to_' in kwargs.keys(): - logger.warning( - 'Use of to_ is only allowed together with from_') + params["to"] = to_ + elif "to_" in kwargs: + logger.warning("Use of to_ is only allowed together with from_") response = self.session.get(url=url, params=params, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers + response_content, + url=url, + headers=headers, ) - result = [response_data] if isinstance(response_data, dict) else response_data + result = ( + [response_data] if isinstance(response_data, dict) else response_data + ) if len(result) == 0: logger.info( - 'There are not calendar appointments with the requested params') - return + "There are not calendar appointments with the requested params", + ) + return None # clean result - elif 'base' in result[0].keys(): + if "base" in result[0]: merged_appointments = [] for appointment in result: - appointment['base']['startDate'] = appointment['calculated']['startDate'] - appointment['base']['endDate'] = appointment['calculated']['endDate'] - merged_appointments.append(appointment['base']) + appointment["base"]["startDate"] = appointment["calculated"][ + "startDate" + ] + appointment["base"]["endDate"] = appointment["calculated"][ + "endDate" + ] + merged_appointments.append(appointment["base"]) return merged_appointments - elif 'appointment' in result[0].keys(): - if len(result[0]['calculatedDates']) > 2: - logger.info('returning a series calendar appointment!') + if "appointment" in result[0]: + if len(result[0]["calculatedDates"]) > 2: + logger.info("returning a series calendar appointment!") return result - else: - logger.debug( - 'returning a simplified single calendar appointment with one date') - return [appointment['appointment'] - for appointment in result] - else: - logger.warning('unexpected result') - - else: - logger.warning( - "%s Something went wrong fetching calendar appointments: %s", response.status_code, response.content - ) + logger.debug( + "returning a simplified single calendar appointment with one date", + ) + return [appointment["appointment"] for appointment in result] + logger.warning("unexpected result") + return None + + logger.warning( + "%s Something went wrong fetching calendar appointments: %s", + response.status_code, + response.content, + ) + return None diff --git a/churchtools_api/churchtools_api.py b/churchtools_api/churchtools_api.py index 5e576cd..b8a73cb 100644 --- a/churchtools_api/churchtools_api.py +++ b/churchtools_api/churchtools_api.py @@ -1,14 +1,16 @@ import json import logging +from typing import Optional + import requests -from churchtools_api.persons import ChurchToolsApiPersons +from churchtools_api.calendar import ChurchToolsApiCalendar from churchtools_api.events import ChurchToolsApiEvents -from churchtools_api.groups import ChurchToolsApiGroups -from churchtools_api.songs import ChurchToolsApiSongs from churchtools_api.files import ChurchToolsApiFiles -from churchtools_api.calendar import ChurchToolsApiCalendar +from churchtools_api.groups import ChurchToolsApiGroups +from churchtools_api.persons import ChurchToolsApiPersons from churchtools_api.resources import ChurchToolsApiResources +from churchtools_api.songs import ChurchToolsApiSongs logger = logging.getLogger(__name__) @@ -22,7 +24,7 @@ class ChurchToolsApi( ChurchToolsApiCalendar, ChurchToolsApiResources, ): - """Main class used to combine all api functions + """Main class used to combine all api functions. Args: ChurchToolsApiPersons: all functions used for persons @@ -34,17 +36,21 @@ class ChurchToolsApi( ChurchToolsApiResources: all functions used for resources """ - def __init__(self, domain, ct_token=None, ct_user=None, ct_password=None): - """ - Setup of a ChurchToolsApi object for the specified ct_domain using a token login - :param domain: including https:// ending on e.g. .de - :type domain: str - :param ct_token: direct access using a user token - :type ct_token: str - :param ct_user: indirect login using user and password combination - :type ct_user: str - :param ct_password: indirect login using user and password combination - :type ct_password: str + def __init__( + self, + domain: str, + ct_token: Optional[str] = None, + ct_user: Optional[str] = None, + ct_password: Optional[str] = None, + ) -> None: + """Setup of a ChurchToolsApi object for the specified ct_domain using a token login. + + Arguments: + domain: including https:// ending on e.g. .de + ct_token: direct access using a user token + ct_user: indirect login using user and password combination + ct_password: indirect login using user and password combination + """ super().__init__() self.session = None @@ -57,15 +63,14 @@ def __init__(self, domain, ct_token=None, ct_user=None, ct_password=None): elif ct_user is not None and ct_password is not None: self.login_ct_rest_api(ct_user=ct_user, ct_password=ct_password) - logger.debug('ChurchToolsApi init finished') + logger.debug("ChurchToolsApi init finished") def login_ct_rest_api(self, **kwargs): - """ - Authorization: Login + """Authorization: Login If you want to authorize a request, you need to provide a Login Token as Authorization header in the format {Authorization: Login} Login Tokens are generated in "Berechtigungen" of User Settings - using REST API login as opposed to AJAX login will also save a cookie + using REST API login as opposed to AJAX login will also save a cookie. :param kwargs: optional keyword arguments as listed :keyword ct_token: str : token to be used for login into CT @@ -76,212 +81,190 @@ def login_ct_rest_api(self, **kwargs): """ self.session = requests.Session() - if 'ct_token' in kwargs.keys(): - logger.info('Trying Login with token') - url = self.domain + '/api/whoami' - headers = {"Authorization": 'Login ' + kwargs['ct_token']} + if "ct_token" in kwargs: + logger.info("Trying Login with token") + url = self.domain + "/api/whoami" + headers = {"Authorization": "Login " + kwargs["ct_token"]} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) logger.info( - 'Token Login Successful as {}'.format( - response_content['data']['email'])) - self.session.headers['CSRF-Token'] = self.get_ct_csrf_token() - return json.loads(response.content)['data']['id'] - else: - logger.warning( - "Token Login failed with {}".format( - response.content.decode())) - return False - - elif 'ct_user' in kwargs.keys() and 'ct_password' in kwargs.keys(): - logger.info('Trying Login with Username/Password') - url = self.domain + '/api/login' - data = { - 'username': kwargs['ct_user'], - 'password': kwargs['ct_password']} + "Token Login Successful as %s", + response_content["data"]["email"], + ) + self.session.headers["CSRF-Token"] = self.get_ct_csrf_token() + return json.loads(response.content)["data"]["id"] + logger.warning( + "Token Login failed with %s", + response.content.decode(), + ) + return False + + if "ct_user" in kwargs and "ct_password" in kwargs: + logger.info("Trying Login with Username/Password") + url = self.domain + "/api/login" + data = {"username": kwargs["ct_user"], "password": kwargs["ct_password"]} response = self.session.post(url=url, data=data) if response.status_code == 200: response_content = json.loads(response.content) person = self.who_am_i() - logger.info( - 'User/Password Login Successful as {}'.format(person['email'])) - return person['id'] - else: - logger.warning( - "User/Password Login failed with {}".format(response.content.decode())) - return False + logger.info("User/Password Login Successful as %s", person["email"]) + return person["id"] + logger.warning( + "User/Password Login failed with %s", + response.content.decode(), + ) + return False + return None def get_ct_csrf_token(self): - """ - Requests CSRF Token https://hilfe.church.tools/wiki/0/API-CSRF + """Requests CSRF Token https://hilfe.church.tools/wiki/0/API-CSRF Storing and transmitting CSRF token in headers is required for all legacy AJAX API calls unless disabled by admin - Therefore it is executed with each new login + Therefore it is executed with each new login. :return: token :rtype: str """ - url = self.domain + '/api/csrftoken' + url = self.domain + "/api/csrftoken" response = self.session.get(url=url) if response.status_code == 200: csrf_token = json.loads(response.content)["data"] - logger.info( - "CSRF Token erfolgreich abgerufen {}".format(csrf_token)) + logger.debug("CSRF Token erfolgreich abgerufen %s", csrf_token) return csrf_token - else: - logger.warning( - "CSRF Token not updated because of Response {}".format( - response.content.decode())) + logger.warning( + "CSRF Token not updated because of Response %s", + response.content.decode(), + ) + return None def who_am_i(self): - """ - Simple function which returns the user information for the authorized user + """Simple function which returns the user information for the authorized user :return: CT user dict if found or bool - :rtype: dict | bool + :rtype: dict | bool. """ - - url = self.domain + '/api/whoami' + url = self.domain + "/api/whoami" response = self.session.get(url=url) if response.status_code == 200: response_content = json.loads(response.content) - if 'email' in response_content['data'].keys(): - logger.info( - 'Who am I as {}'.format( - response_content['data']['email'])) - return response_content['data'] - else: - logger.warning( - 'User might not be logged in? {}'.format( - response_content['data'])) - return False - else: - logger.warning( - "Checking who am i failed with {}".format( - response.status_code)) + if "email" in response_content["data"]: + logger.info("Who am I as %s", response_content["data"]["email"]) + return response_content["data"] + logger.warning("User might not be logged in? %s", response_content["data"]) return False + logger.warning("Checking who am i failed with %s", response.status_code) + return False - def check_connection_ajax(self): - """ - Checks whether a successful connection with the given token can be initiated using the legacy AJAX API + def check_connection_ajax(self) -> bool: + """Checks whether a successful connection with the given token can be initiated using the legacy AJAX API This requires a CSRF token to be set in headers - :return: if successful + :return: if successful. """ - url = self.domain + '/?q=churchservice/ajax&func=getAllFacts' - headers = { - 'accept': 'application/json' - } + url = self.domain + "/?q=churchservice/ajax&func=getAllFacts" + headers = {"accept": "application/json"} response = self.session.post(url=url, headers=headers) if response.status_code == 200: logger.debug("Response AJAX Connection successful") return True - else: - logger.debug( - "Response AJAX Connection failed with {}".format( - json.load( - response.content))) - return False + logger.debug( + "Response AJAX Connection failed with %s", + json.load(response.content), + ) + return False def get_global_permissions(self) -> dict: + """Get global permissions of the current user + :return: dict with module names which contain dicts with individual permissions items. """ - Get global permissions of the current user - :return: dict with module names which contain dicts with individual permissions items - """ - url = self.domain + '/api/permissions/global' - headers = { - 'accept': 'application/json' - } + url = self.domain + "/api/permissions/global" + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() + response_data = response_content["data"].copy() logger.debug( - "First response of Global Permissions successful {}".format(response_content)) + "First response of Global Permissions successful %s", response_content + ) return response_data - else: - logger.warning( - "%s Something went wrong fetching global permissions: %s", - response.status_code, response_content) + logger.warning( + "%s Something went wrong fetching global permissions: %s", + response.status_code, + response_content, + ) + return None def get_services(self, **kwargs): - """ - Function to get list of all or a single services configuration item from CT + """Function to get list of all or a single services configuration item from CT :param kwargs: optional keywords as listed :keyword serviceId: id of a single item for filter :keyword returnAsDict: true if should return a dict instead of list (not combineable if serviceId) :return: list of services - :rtype: list[dict] + :rtype: list[dict]. """ - url = self.domain + '/api/services' - if 'serviceId' in kwargs.keys(): - url += '/{}'.format(kwargs['serviceId']) + url = self.domain + "/api/services" + if "serviceId" in kwargs: + url += "/{}".format(kwargs["serviceId"]) - headers = { - 'accept': 'application/json' - } + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() + response_data = response_content["data"].copy() - if 'returnAsDict' in kwargs and not 'serviceId' in kwargs: - if kwargs['returnAsDict']: - result = {} - for item in response_data: - result[item['id']] = item - response_data = result + if kwargs.get("returnAsDict", False) and "serviceId" not in kwargs: + result = {} + for item in response_data: + result[item["id"]] = item + response_data = result - logger.debug("Services load successful with {} entries".format(len(response_data))) + logger.debug( + "Services load successful with %s entries", + len(response_data), + ) return response_data - else: - logger.info( - "Services requested failed: {}".format( - response.status_code)) - return None + logger.info("Services requested failed: %s", response.status_code) + return None - def get_tags(self, type='songs'): - """ - Retrieve a list of all available tags of a specific ct_domain type from ChurchTools - Purpose: be able to find out tag-ids of all available tags for filtering by tag + def get_tags(self, type="songs"): # noqa: A002 + """Retrieve a list of all available tags of a specific ct_domain type from ChurchTools + Purpose: be able to find out tag-ids of all available tags for filtering by tag. :param type: 'songs' (default) or 'persons' :type type: str :return: list of dicts describing each tag. Each contains keys 'id' and 'name' :rtype list[dict] """ - - url = self.domain + '/api/tags' - headers = { - 'accept': 'application/json' - } + url = self.domain + "/api/tags" + headers = {"accept": "application/json"} params = { - 'type': type, + "type": type, } response = self.session.get(url=url, params=params, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() - logger.debug( - "SongTags load successful {}".format(response_content)) + response_content["data"].copy() + logger.debug("SongTags load successful %s", response_content) - return response_content['data'] - else: - logger.warning( - "%s Something went wrong fetching Song-tags: %s",response.status_code, response.content) + return response_content["data"] + logger.warning( + "%s Something went wrong fetching Song-tags: %s", + response.status_code, + response.content, + ) + return None def get_options(self) -> dict: """Helper function which returns all configurable option fields from CT. - e.g. common use is sexId + e.g. common use is sexId. Returns: dict of options - named by "name" from original list response """ - url = self.domain + "/api/dbfields" headers = {"accept": "application/json"} params = { @@ -292,13 +275,11 @@ def get_options(self) -> dict: if response.status_code == 200: response_content = json.loads(response.content) response_data = response_content["data"].copy() - - logger.debug("SongTags load successful {}".format(response_content)) - response_dict = {item["name"]: item for item in response_data} - return response_dict - else: - logger.warning( - "%s Something went wrong fetching Song-tags: %s", - response.status_code, - response.content, - ) + logger.debug("SongTags load successful %s", response_content) + return {item["name"]: item for item in response_data} + logger.warning( + "%s Something went wrong fetching Song-tags: %s", + response.status_code, + response.content, + ) + return None diff --git a/churchtools_api/churchtools_api_abstract.py b/churchtools_api/churchtools_api_abstract.py index 3c20958..37f21f6 100644 --- a/churchtools_api/churchtools_api_abstract.py +++ b/churchtools_api/churchtools_api_abstract.py @@ -1,51 +1,54 @@ -from abc import ABC, abstractmethod import json import logging +from abc import ABC, abstractmethod logger = logging.getLogger(__name__) + class ChurchToolsApiAbstract(ABC): - """This abstract is used to define minimum references available for all api parts + """This abstract is used to define minimum references available for all api parts. Args: ABC: python default abstract """ + @abstractmethod - def __init__(self): + def __init__(self) -> None: self.session = None self.domain = None def combine_paginated_response_data( - self, response_content: dict, url: str, **kwargs + self, + response_content: dict, + url: str, + **kwargs, ) -> dict: - """Helper function which combines data for requests in case of paginated responses + """Helper function which combines data for requests in case of paginated responses. Args: response_content: the original response form ChurchTools which either has meta/pagination or not url: the url used for the original request in order to repear it - kwargs - can contain headers and params passthrough + kwargs: can contain headers and params passthrough Returns: response 'data' without pagination """ response_data = response_content["data"].copy() - if meta := response_content.get("meta"): - if pagination := meta.get("pagination"): - for page in range(pagination["current"], pagination["lastPage"]): - logger.debug( - "running paginated request for page {} of {}".format( - page + 1, - pagination["lastPage"], - ) - ) - new_param = {"page": page + 1} - if kwargs.get("params"): - kwargs["params"].update(new_param) - else: - kwargs["params"] = new_param - - response = self.session.get(url=url, **kwargs) - response_content = json.loads(response.content) - response_data.extend(response_content["data"]) + if pagination := response_content.get("meta", {}).get("pagination"): + for page in range(pagination["current"], pagination["lastPage"]): + logger.debug( + "running paginated request for page %s of %s", + page + 1, + pagination["lastPage"], + ) + new_param = {"page": page + 1} + if kwargs.get("params"): + kwargs["params"].update(new_param) + else: + kwargs["params"] = new_param + + response = self.session.get(url=url, **kwargs) + response_content = json.loads(response.content) + response_data.extend(response_content["data"]) return response_data diff --git a/churchtools_api/events.py b/churchtools_api/events.py index 78d2beb..a612427 100644 --- a/churchtools_api/events.py +++ b/churchtools_api/events.py @@ -1,26 +1,29 @@ +from __future__ import annotations + import json import logging -import os from datetime import datetime, timedelta +from pathlib import Path + import docx from churchtools_api.churchtools_api_abstract import ChurchToolsApiAbstract logger = logging.getLogger(__name__) + class ChurchToolsApiEvents(ChurchToolsApiAbstract): - """ Part definition of ChurchToolsApi which focuses on events + """Part definition of ChurchToolsApi which focuses on events. Args: ChurchToolsApiAbstract: template with minimum references """ - def __init__(self): + def __init__(self) -> None: super() def get_events(self, **kwargs) -> list[dict]: - """ - Method to get all the events from given timespan or only the next event + """Method to get all the events from given timespan or only the next event. Arguments: kwargs: optional params to modify the search criteria @@ -38,61 +41,67 @@ def get_events(self, **kwargs) -> list[dict]: Returns: list of events """ - url = self.domain + '/api/events' + url = self.domain + "/api/events" - headers = { - 'accept': 'application/json' - } - params = {"limit":50} #increases default pagination size + headers = {"accept": "application/json"} + params = {"limit": 50} # increases default pagination size - if 'eventId' in kwargs.keys(): - url += '/{}'.format(kwargs['eventId']) + if "eventId" in kwargs: + url += "/{}".format(kwargs["eventId"]) else: - if 'from_' in kwargs.keys(): - from_ = kwargs['from_'] + if "from_" in kwargs: + from_ = kwargs["from_"] if isinstance(from_, datetime): from_ = from_.strftime("%Y-%m-%d") if len(from_) == 10: - params['from'] = from_ - if 'to_' in kwargs.keys() and 'from_' in kwargs.keys(): - to_ = kwargs['to_'] + params["from"] = from_ + if "to_" in kwargs and "from_" in kwargs: + to_ = kwargs["to_"] if isinstance(to_, datetime): to_ = to_.strftime("%Y-%m-%d") if len(to_) == 10: - params['to'] = to_ - elif 'to_' in kwargs.keys(): - logger.warning( - 'Use of to_ is only allowed together with from_') - if 'canceled' in kwargs.keys(): - params['canceled'] = kwargs['canceled'] - if 'direction' in kwargs.keys(): - params['direction'] = kwargs['direction'] - if 'limit' in kwargs.keys() and 'direction' in kwargs.keys(): - params['limit'] = kwargs['limit'] - elif 'direction' in kwargs.keys(): + params["to"] = to_ + elif "to_" in kwargs: + logger.warning("Use of to_ is only allowed together with from_") + if "canceled" in kwargs: + params["canceled"] = kwargs["canceled"] + if "direction" in kwargs: + params["direction"] = kwargs["direction"] + if "limit" in kwargs and "direction" in kwargs: + params["limit"] = kwargs["limit"] + elif "direction" in kwargs: logger.warning( - 'Use of limit is only allowed together with direction keyword') - if 'include' in kwargs.keys(): - params['include'] = kwargs['include'] + "Use of limit is only allowed together with direction keyword", + ) + if "include" in kwargs: + params["include"] = kwargs["include"] response = self.session.get(url=url, headers=headers, params=params) if response.status_code == 200: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers, params=params + response_content, + url=url, + headers=headers, + params=params, ) return [response_data] if isinstance(response_data, dict) else response_data - else: - logger.warning( - "%s Something went wrong fetching events: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong fetching events: %s", + response.status_code, + response.content, + ) + return None - def get_event_by_calendar_appointment(self, appointment_id: int, - start_date: str | datetime) -> dict: - """ - This method is a helper to retrieve an event for a specific calendar appointment - including it's event services + def get_event_by_calendar_appointment( + self, + appointment_id: int, + start_date: str | datetime, + ) -> dict: + """This method is a helper to retrieve an event for a specific calendar appointment + including it's event services. Args: appointment_id: _description_ @@ -101,74 +110,68 @@ def get_event_by_calendar_appointment(self, appointment_id: int, Returns: event dict with event servics """ - if not isinstance(start_date, datetime): - formats = {'iso': '%Y-%m-%dT%H:%M:%SZ', 'date': "%Y-%m-%d"} - for format in formats: + formats = {"iso": "%Y-%m-%dT%H:%M:%SZ", "date": "%Y-%m-%d"} + for date_formats in formats.values(): try: - start_date = datetime.strptime(start_date, format) + start_date = datetime.strptime(start_date, date_formats) break except ValueError: continue events = self.get_events( from_=start_date, - to_=start_date + - timedelta(days=1), - include='eventServices') + to_=start_date + timedelta(days=1), + include="eventServices", + ) for event in events: - if event['appointmentId'] == appointment_id: + if event["appointmentId"] == appointment_id: return event logger.info( - 'no event references appointment ID %s on start %s', + "no event references appointment ID %s on start %s", appointment_id, - start_date) + start_date, + ) return None - def get_AllEventData_ajax(self, eventId): - """ - Reverse engineered function from legacy AJAX API which is used to get all event data for one event + def get_AllEventData_ajax(self, eventId) -> dict: + """Reverse engineered function from legacy AJAX API which is used to get all event data for one event. + Required to read special params not yet included in REST getEvents() Legacy AJAX request might stop working with any future release ... CSRF-Token is required in session header :param eventId: number of the event to be requested :type eventId: int :return: event information - :rtype: dict """ - url = self.domain + '/index.php' - headers = { - 'accept': 'application/json' - } - params = {'q': 'churchservice/ajax'} - data = { - 'id': eventId, - 'func': 'getAllEventData' - } - response = self.session.post( - url=url, headers=headers, params=params, data=data) + url = self.domain + "/index.php" + headers = {"accept": "application/json"} + params = {"q": "churchservice/ajax"} + data = {"id": eventId, "func": "getAllEventData"} + response = self.session.post(url=url, headers=headers, params=params, data=data) if response.status_code == 200: response_content = json.loads(response.content) - if len(response_content['data']) > 0: - response_data = response_content['data'][str(eventId)] - logger.debug("AJAX Event data {}".format(response_data)) + if len(response_content["data"]) > 0: + response_data = response_content["data"][str(eventId)] + logger.debug("AJAX Event data len=%s", len(response_data)) return response_data - else: - logger.info( - "AJAX All Event data not successful - no event found: {}".format(response.status_code)) - return None - else: logger.info( - "AJAX All Event data not successful: {}".format( - response.status_code)) + "AJAX All Event data not successful - no event found:%s", + response.status_code, + ) return None + logger.info( + "AJAX All Event data not successful: %s", + response.status_code, + ) + return None def get_event_services_counts_ajax(self, eventId, **kwargs): - """ - retrieve the number of services currently set for one specific event id - optionally get the number of services for one specific id on that event only + """Retrieve the number of services currently set for one specific event id + optionally get the number of services for one specific id on that event only. + :param eventId: id number of the calendar event :type eventId: int :param kwargs: keyword arguments either serviceId or service_group_id @@ -177,38 +180,37 @@ def get_event_services_counts_ajax(self, eventId, **kwargs): :return: dict of service types and the number of services required for this event :rtype: dict """ - event = self.get_events(eventId=eventId)[0] - if 'serviceId' in kwargs.keys() and 'serviceGroupId' not in kwargs.keys(): + if "serviceId" in kwargs and "serviceGroupId" not in kwargs: service_count = 0 - for service in event['eventServices']: - if service['serviceId'] == kwargs['serviceId']: + for service in event["eventServices"]: + if service["serviceId"] == kwargs["serviceId"]: service_count += 1 - return {kwargs['serviceId']: service_count} - elif 'serviceId' not in kwargs.keys() and 'serviceGroupId' in kwargs.keys(): + return {kwargs["serviceId"]: service_count} + if "serviceId" not in kwargs and "serviceGroupId" in kwargs: all_services = self.get_services() - serviceGroupServiceIds = [service['id'] for service in all_services - if service['serviceGroupId'] == kwargs['serviceGroupId']] + serviceGroupServiceIds = [ + service["id"] + for service in all_services + if service["serviceGroupId"] == kwargs["serviceGroupId"] + ] services = {} - for service in event['eventServices']: - serviceId = service['serviceId'] + for service in event["eventServices"]: + serviceId = service["serviceId"] if serviceId in serviceGroupServiceIds: - if serviceId in services.keys(): + if serviceId in services: services[serviceId] += 1 else: services[serviceId] = 1 return services - else: - logger.warning( - 'Illegal combination of kwargs - check documentation either') + logger.warning("Illegal combination of kwargs - check documentation either") + return None - def set_event_services_counts_ajax( - self, eventId, serviceId, servicesCount): - """ - update the number of services currently set for one event specific id + def set_event_services_counts_ajax(self, eventId, serviceId, servicesCount) -> bool: + """Update the number of services currently set for one event specific id. :param eventId: id number of the calendar event :type eventId: int @@ -219,241 +221,229 @@ def set_event_services_counts_ajax( :return: successful execution :rtype: bool """ - - url = self.domain + '/index.php' - headers = { - 'accept': 'application/json' - } - params = {'q': 'churchservice/ajax'} + url = self.domain + "/index.php" + headers = {"accept": "application/json"} + params = {"q": "churchservice/ajax"} # restore other ServiceGroup assignments required for request form data services = self.get_services(returnAsDict=True) - serviceGroupId = services[serviceId]['serviceGroupId'] + serviceGroupId = services[serviceId]["serviceGroupId"] servicesOfServiceGroup = self.get_event_services_counts_ajax( - eventId, serviceGroupId=serviceGroupId) + eventId, + serviceGroupId=serviceGroupId, + ) # set new assignment servicesOfServiceGroup[serviceId] = servicesCount # Generate form specific data item_id = 0 - data = { - 'id': eventId, - 'func': 'addOrRemoveServiceToEvent' - } - for serviceIdRow, serviceCount in servicesOfServiceGroup.items(): - data['col{}'.format(item_id)] = serviceIdRow + data = {"id": eventId, "func": "addOrRemoveServiceToEvent"} + for item_id, (serviceIdRow, serviceCount) in enumerate( + servicesOfServiceGroup.items() + ): + data[f"col{item_id}"] = serviceIdRow if serviceCount > 0: - data['val{}'.format(item_id)] = 'checked' - data['count{}'.format(item_id)] = serviceCount - item_id += 1 + data[f"val{item_id}"] = "checked" + data[f"count{item_id}"] = serviceCount - response = self.session.post( - url=url, headers=headers, params=params, data=data) + response = self.session.post(url=url, headers=headers, params=params, data=data) if response.status_code == 200: response_content = json.loads(response.content) - response_success = response_content['status'] == 'success' + response_success = response_content["status"] == "success" - number_match = self.get_event_services_counts_ajax( - eventId, serviceId=serviceId)[serviceId] == servicesCount + number_match = ( + self.get_event_services_counts_ajax(eventId, serviceId=serviceId)[ + serviceId + ] + == servicesCount + ) if number_match and response_success: return True - else: - logger.warning("Request was successful but serviceId {} not changed to count {} " - .format(serviceId, servicesCount)) - return False - else: - logger.info( - "set_event_services_counts_ajax not successful: {}".format( - response.status_code)) + logger.warning( + "Request was successful but serviceId %s not changed to count %s ", + serviceId, + servicesCount, + ) return False + logger.info( + "set_event_services_counts_ajax not successful: %s", + response.status_code, + ) + return False - def get_event_admins_ajax(self, eventId): - """ - get the admin id list of an event using legacy AJAX API - :param eventId: number of the event to be changed - :type eventId: int - :return: list of admin ids - :rtype: list - """ + def get_event_admins_ajax(self, eventId: int) -> list: + """Get the admin id list of an event using legacy AJAX API. - event_data = self.get_AllEventData_ajax(eventId) + Params: + eventId: number of the event to be changed + Returns: + list of admin ids. + """ + event_data = self.get_AllEventData_ajax(eventId=eventId) if event_data is not None: - if 'admin' in event_data.keys(): - admin_ids = [int(id) for id in event_data['admin'].split(',')] + if "admin" in event_data: + admin_ids = [ + int(available_event_id) + for available_event_id in event_data["admin"].split(",") + ] else: admin_ids = [] return admin_ids - else: - logger.info('No admins found because event not found') - return [] + logger.info("No admins found because event not found") + return [] - def set_event_admins_ajax(self, eventId, admin_ids): - """ - set the admin id list of an event using legacy AJAX API - :param eventId: number of the event to be changed - :type eventId: int - :param admin_ids: list of admin user ids to be set as admin for event - :type admin_ids: list - :return: if successful - :rtype: bool - """ + def set_event_admins_ajax(self, eventId: int, admin_ids: list) -> bool: + """Set the admin id list of an event using legacy AJAX API. - url = self.domain + '/index.php' - headers = { - 'accept': 'application/json' - } - params = {'q': 'churchservice/ajax'} + Parameters: + eventId: number of the event to be changed + admin_ids: list of admin user ids to be set as admin for event + + Returns: + if successful. + """ + url = self.domain + "/index.php" + headers = {"accept": "application/json"} + params = {"q": "churchservice/ajax"} data = { - 'id': eventId, - 'admin': ", ".join([str(id) for id in admin_ids]), - 'func': 'updateEventInfo' + "id": eventId, + "admin": ", ".join([str(admin_id) for admin_id in admin_ids]), + "func": "updateEventInfo", } - response = self.session.post( - url=url, headers=headers, params=params, data=data) + response = self.session.post(url=url, headers=headers, params=params, data=data) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['status'] == 'success' + response_data = response_content["status"] == "success" logger.debug( - "Setting Admin IDs {} for event {} success".format( - admin_ids, eventId)) + "Setting Admin IDs %s for event %s success", admin_ids, eventId + ) return response_data - else: - logger.info( - "Setting Admin IDs {} for event {} failed with : {}".format(admin_ids, eventId, response.status_code)) - return False - - def get_event_agenda(self, eventId): - """ - Retrieve agenda for event by ID from ChurchTools - :param eventId: number of the event - :type eventId: int - :return: list of event agenda items - :rtype: list + logger.info( + "Setting Admin IDs %s for event %s failed with : %s", + admin_ids, + eventId, + response.status_code, + ) + return False + + def get_event_agenda(self, eventId: int) -> list: + """Retrieve agenda for event by ID from ChurchTools + Params: + eventId: number of the event + Returns: + list of event agenda items. """ - url = self.domain + '/api/events/{}/agenda'.format(eventId) - headers = { - 'accept': 'application/json' - } + url = self.domain + f"/api/events/{eventId}/agenda" + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() - logger.debug("Agenda load successful {}".format(response_content)) + response_data = response_content["data"].copy() + logger.debug("Agenda load successful %s items", len(response_content)) return response_data - else: - logger.info( - "Event requested that does not have an agenda with status: {}".format( - response.status_code)) - return None + logger.info( + "Event requested that does not have an agenda with status: %s", + response.status_code, + ) + return None - def export_event_agenda(self, target_format, - target_path='./downloads', **kwargs): - """ - Exports the agenda as zip file for imports in presenter-programs - :param target_format: fileformat or name of presentation software which should be supported. - Supported formats are 'SONG_BEAMER', 'PRO_PRESENTER6' and 'PRO_PRESENTER7' - :param target_path: Filepath of the file which should be exported (including filename) - :param kwargs: additional keywords as listed below - :key eventId: event id to check for agenda id should be exported - :key agendaId: agenda id of the agenda which should be exported - DO NOT combine with eventId because it will be overwritten! - :key append_arrangement: if True, the name of the arrangement will be included within the agenda caption - :key export_Songs: if True, the songfiles will be in the folder "Songs" within the zip file - :key with_category: has no effect when exported in target format 'SONG_BEAMER' - :return: if successful - :rtype: bool + def export_event_agenda( + self, target_format, target_path="./downloads", **kwargs + ) -> bool: + """Exports the agenda as zip file for imports in presenter-programs. + + Parameters: + target_format: fileformat or name of presentation software which should be supported. + Supported formats are 'SONG_BEAMER', 'PRO_PRESENTER6' and 'PRO_PRESENTER7' + target_path: Filepath of the file which should be exported (including filename) + kwargs: additional keywords as listed below + + Keywords: + eventId: event id to check for agenda id should be exported + agendaId: agenda id of the agenda which should be exported + DO NOT combine with eventId because it will be overwritten! + append_arrangement: if True, the name of the arrangement will be included within the agenda caption + export_Songs: if True, the songfiles will be in the folder "Songs" within the zip file + with_category: has no effect when exported in target format 'SONG_BEAMER' + Returns: + if successful. """ - if 'eventId' in kwargs.keys(): - if 'agendaId' in kwargs.keys(): + if "eventId" in kwargs: + if "agendaId" in kwargs: logger.warning( - 'Invalid use of params - can not combine eventId and agendaId!') + "Invalid use of params - can not combine eventId and agendaId!", + ) else: - agenda = self.get_event_agenda(eventId=kwargs['eventId']) - agendaId = agenda['id'] - elif 'agendaId' in kwargs.keys(): - agendaId = kwargs['agendaId'] + agenda = self.get_event_agenda(eventId=kwargs["eventId"]) + agendaId = agenda["id"] + elif "agendaId" in kwargs: + agendaId = kwargs["agendaId"] else: - logger.warning('Missing event or agendaId') + logger.warning("Missing event or agendaId") return False # note: target path can be either a zip-file defined before function # call or just a folder - is_zip = target_path.lower().endswith('.zip') + is_zip = target_path.lower().endswith(".zip") if not is_zip: - folder_exists = os.path.isdir(target_path) - # If folder doesn't exist, then create it. - if not folder_exists: - os.makedirs(target_path) - logger.debug("created folder : ", target_path) - - if 'eventId' in kwargs.keys(): - new_file_name = '{}_{}.zip'.format( - agenda['name'], target_format) + target_path = Path(target_path) + target_path.mkdir(parents=True, exist_ok=True) + + if "eventId" in kwargs: + new_file_name = "{}_{}.zip".format(agenda["name"], target_format) else: - new_file_name = '{}_agendaId_{}.zip'.format( - target_format, agendaId) + new_file_name = f"{target_format}_agendaId_{agendaId}.zip" - target_path = os.sep.join([target_path, new_file_name]) + target_path = target_path/new_file_name - url = '{}/api/agendas/{}/export'.format(self.domain, agendaId) + url = f"{self.domain}/api/agendas/{agendaId}/export" # NOTE the stream=True parameter below - params = { - 'target': target_format - } + params = {"target": target_format} json_data = {} # The following 3 parameter 'appendArrangement', 'exportSongs' and # 'withCategory' are mandatory from the churchtools API side: - if 'append_arrangement' in kwargs.keys(): - json_data['appendArrangement'] = kwargs['append_arrangement'] - else: - json_data['appendArrangement'] = True + json_data["appendArrangement"] = kwargs.get("append_arrangement", True) - if 'export_songs' in kwargs.keys(): - json_data['exportSongs'] = kwargs['export_songs'] - else: - json_data['exportSongs'] = True + json_data["exportSongs"] = kwargs.get("export_songs", True) - if 'with_category' in kwargs.keys(): - json_data['withCategory'] = kwargs['with_category'] - else: - json_data['withCategory'] = True + json_data["withCategory"] = kwargs.get("with_category", True) headers = { - 'accept': 'application/json', - 'Content-Type': 'application/json', + "accept": "application/json", + "Content-Type": "application/json", } response = self.session.post( url=url, params=params, headers=headers, - json=json_data) + json=json_data, + ) result_ok = False if response.status_code == 200: response_content = json.loads(response.content) - agenda_data = response_content['data'].copy() - logger.debug("Agenda package found {}".format(response_content)) + agenda_data = response_content["data"].copy() + logger.debug("Agenda package found %s", response_content) result_ok = self.file_download_from_url( - '{}/{}'.format(self.domain, agenda_data['url']), target_path) + "{}/{}".format(self.domain, agenda_data["url"]), + target_path, + ) if result_ok: - logger.debug('download finished') + logger.debug("download finished") else: - logger.warning( - "export of event_agenda failed: {}".format( - response.status_code)) + logger.warning("export of event_agenda failed: %s", response.status_code) return result_ok def get_event_agenda_docx(self, agenda, **kwargs): - """ - Function to generate a custom docx document with the content of the event agenda from churchtools + """Function to generate a custom docx document with the content of the event agenda from churchtools :param agenda: event agenda with services :type event: dict :param kwargs: optional keywords as listed @@ -461,90 +451,96 @@ def get_event_agenda_docx(self, agenda, **kwargs): :key excludeBeforeEvent: bool: by default pre-event parts are excluded :return: """ + excludeBeforeEvent = kwargs.get("excludeBeforeEvent", False) - if 'excludeBeforeEvent' in kwargs.keys(): - excludeBeforeEvent = kwargs['excludeBeforeEvent'] - else: - excludeBeforeEvent = False - - logger.debug('Trying to get agenda for: ' + agenda['name']) + logger.debug("Trying to get agenda for: %s", agenda["name"]) document = docx.Document() - heading = agenda['name'] - heading += '- Draft' if not agenda['isFinal'] else '' + heading = agenda["name"] + heading += "- Draft" if not agenda["isFinal"] else "" document.add_heading(heading) modifiedDate = datetime.strptime( - agenda["meta"]['modifiedDate'], - '%Y-%m-%dT%H:%M:%S%z') - modifiedDate2 = modifiedDate.astimezone().strftime('%a %d.%m (%H:%M:%S)') + agenda["meta"]["modifiedDate"], + "%Y-%m-%dT%H:%M:%S%z", + ) + modifiedDate2 = modifiedDate.astimezone().strftime("%a %d.%m (%H:%M:%S)") document.add_paragraph( - "Download from ChurchTools including changes until.: " + - modifiedDate2) + "Download from ChurchTools including changes until.: " + modifiedDate2, + ) agenda_item = 0 # Position Argument from Event Agenda is weird therefore counting manually pre_event_last_item = True # Event start is no item therefore look for change for item in agenda["items"]: - if excludeBeforeEvent and item['isBeforeEvent']: + if excludeBeforeEvent and item["isBeforeEvent"]: continue - if item['type'] == 'header': + if item["type"] == "header": document.add_heading(item["title"], level=1) continue - if pre_event_last_item: # helper for event start heading which is not part of the ct_api - if not item['isBeforeEvent']: - pre_event_last_item = False - document.add_heading('Eventstart', level=1) + # helper for event start heading which is not part of the ct_api + if pre_event_last_item and not item["isBeforeEvent"]: + pre_event_last_item = False + document.add_heading("Eventstart", level=1) agenda_item += 1 title = str(agenda_item) - title += ' ' + item["title"] + title += " " + item["title"] - if item['type'] == 'song': - title += ': ' + item['song']['title'] + if item["type"] == "song": + title += ": " + item["song"]["title"] # TODO #5 Word... check if fails on empty song items - title += ' (' + item['song']['category'] + ')' + title += " (" + item["song"]["category"] + ")" document.add_heading(title, level=2) responsible_list = [] - for responsible_item in item['responsible']['persons']: - if responsible_item['person'] is not None: - responsible_text = responsible_item['person']['title'] - if not responsible_item['accepted']: - responsible_text += ' (Angefragt)' + for responsible_item in item["responsible"]["persons"]: + if responsible_item["person"] is not None: + responsible_text = responsible_item["person"]["title"] + if not responsible_item["accepted"]: + responsible_text += " (Angefragt)" else: - responsible_text = '?' - responsible_text += ' ' + responsible_item['service'] + '' + responsible_text = "?" + responsible_text += " " + responsible_item["service"] + "" responsible_list.append(responsible_text) - if len(item['responsible']) > 0 and len( - item['responsible']['persons']) == 0: - if len(item['responsible']['text']) > 0: - responsible_list.append( - item['responsible']['text'] + ' (Person statt Rolle in ChurchTools hinterlegt!)') + if ( + len(item["responsible"]) > 0 + and len(item["responsible"]["persons"]) == 0 + and len(item["responsible"]["text"]) > 0 + ): + responsible_list.append( + item["responsible"]["text"] + + " (Person statt Rolle in ChurchTools hinterlegt!)", + ) responsible_text = ", ".join(responsible_list) document.add_paragraph(responsible_text) - if item['note'] is not None and item['note'] != '': + if item["note"] is not None and item["note"] != "": document.add_paragraph(item["note"]) - if len(item['serviceGroupNotes']) > 0: - for note in item['serviceGroupNotes']: - if note['serviceGroupId'] in kwargs['serviceGroups'].keys() and len( - note['note']) > 0: + if len(item["serviceGroupNotes"]) > 0: + for note in item["serviceGroupNotes"]: + if ( + note["serviceGroupId"] in kwargs["serviceGroups"] + and len(note["note"]) > 0 + ): document.add_heading( - "Bemerkung für {}:" - .format(kwargs['serviceGroups'][note['serviceGroupId']]['name']), level=4) - document.add_paragraph(note['note']) + "Bemerkung für {}:".format( + kwargs["serviceGroups"][note["serviceGroupId"]]["name"], + ), + level=4, + ) + document.add_paragraph(note["note"]) return document def get_persons_with_service(self, eventId: int, serviceId: int) -> list[dict]: - """helper function which should return the list of persons that are assigned a specific service on a specific event + """Helper function which should return the list of persons that are assigned a specific service on a specific event. Args: eventId: id number from Events @@ -553,48 +549,45 @@ def get_persons_with_service(self, eventId: int, serviceId: int) -> list[dict]: Returns: list of persons """ - event = self.get_events(eventId=eventId) eventServices = event[0]["eventServices"] - result = [ + return [ service for service in eventServices if service["serviceId"] == serviceId ] - return result - def get_event_masterdata(self, **kwargs): - """ - Function to get the Masterdata of the event module - This information is required to map some IDs to specific items - :param kwargs: optional keywords as listed below - :keyword type: str with name of the masterdata type (not datatype) common types are 'absenceReasons', 'songCategories', 'services', 'serviceGroups' - :keyword returnAsDict: if the list with one type should be returned as dict by ID - :return: list of masterdata items, if multiple types list of lists (by type) - :rtype: list | list[list] | dict | list[dict] + def get_event_masterdata(self, **kwargs) -> list | list[list] | dict | list[dict]: + """Function to get the Masterdata of the event module. + This information is required to map some IDs to specific items. + + Params + kwargs: optional keywords as listed below + + Keywords: + type: str with name of the masterdata type (not datatype) common types are 'absenceReasons', 'songCategories', 'services', 'serviceGroups' + returnAsDict: if the list with one type should be returned as dict by ID + + Returns: + list of masterdata items, if multiple types list of lists (by type). """ - url = self.domain + '/api/event/masterdata' + url = self.domain + "/api/event/masterdata" - headers = { - 'accept': 'application/json' - } + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() - - if 'type' in kwargs: - response_data = response_data[kwargs['type']] - if 'returnAsDict' in kwargs.keys(): - if kwargs['returnAsDict']: - response_data2 = response_data.copy() - response_data = { - item['id']: item for item in response_data2} - logger.debug( - "Event Masterdata load successful {}".format(response_data)) + response_data = response_content["data"].copy() + + if "type" in kwargs: + response_data = response_data[kwargs["type"]] + if kwargs.get("returnAsDict"): + response_data2 = response_data.copy() + response_data = {item["id"]: item for item in response_data2} + logger.debug("Event Masterdata load successful len=%s", response_data) return response_data - else: - logger.info( - "Event Masterdata requested failed: {}".format( - response.status_code)) - return None + logger.info( + "Event Masterdata requested failed: %s", + response.status_code, + ) + return None diff --git a/churchtools_api/files.py b/churchtools_api/files.py index d78363c..c06f8d8 100644 --- a/churchtools_api/files.py +++ b/churchtools_api/files.py @@ -1,67 +1,73 @@ import json import logging -import os +from pathlib import Path +from typing import Optional from churchtools_api.churchtools_api_abstract import ChurchToolsApiAbstract logger = logging.getLogger(__name__) + class ChurchToolsApiFiles(ChurchToolsApiAbstract): - """ Part definition of ChurchToolsApi which focuses on files + """Part definition of ChurchToolsApi which focuses on files. Args: ChurchToolsApiAbstract: template with minimum references """ - def __init__(self): + def __init__(self) -> None: super() - def file_upload(self, source_filepath, domain_type, - domain_identifier, custom_file_name=None, overwrite=False): - """ - Helper function to upload an attachment to any module of ChurchTools - :param source_filepath: file to be opened e.g. with open('media/pinguin.png', 'rb') - :type source_filepath: str - :param domain_type: The ct_domain type, currently supported are 'avatar', 'groupimage', 'logo', 'attatchments', + def file_upload( + self, + source_filepath: str, + domain_type: str, + domain_identifier: int, + custom_file_name: Optional[str] = None, + *, + overwrite: bool = False, + ) -> bool: + """Helper function to upload an attachment to any module of ChurchTools. + + Params: + source_filepath: file to be opened e.g. with open('media/pinguin.png', 'rb') + domain_type: The ct_domain type, currently supported are 'avatar', 'groupimage', 'logo', 'attatchments', 'html_template', 'service', 'song_arrangement', 'importtable', 'person', 'familyavatar', 'wiki_.?'. - :type domain_type: str - :param domain_identifier: ID of the object in ChurchTools - :type domain_identifier: int - :param custom_file_name: optional file name - if not specified the one from the file is used - :type custom_file_name: str - :param overwrite: if true delete existing file before upload of new one to replace \ - it's content instead of creating a copy - :type overwrite: bool - :return: if successful - :rtype: bool + domain_identifier: ID of the object in ChurchTools + custom_file_name: optional file name - if not specified the one from the file is used + :type custom_file_name: + overwrite: if true delete existing file before upload of new one to replace \ + it's content instead of creating a copy + + Returns: + if successful. """ - - source_file = open(source_filepath, 'rb') - - url = '{}/api/files/{}/{}'.format(self.domain, - domain_type, domain_identifier) - - if overwrite: - logger.debug( - "deleting old file {} before new upload".format(source_file)) - delete_file_name = source_file.name.split( - '/')[-1] if custom_file_name is None else custom_file_name - self.file_delete(domain_type, domain_identifier, delete_file_name) - - # add files as files form data with dict using 'files[]' as key and - # (tuple of filename and fileobject) - if custom_file_name is None: - files = {'files[]': (source_file.name.split('/')[-1], source_file)} - else: - if '/' in custom_file_name: + source_filepath = Path(source_filepath) + with source_filepath.open("rb") as source_file: + url = f"{self.domain}/api/files/{domain_type}/{domain_identifier}" + + if overwrite: + logger.debug("deleting old file %s before new upload", source_file) + delete_file_name = ( + source_file.name.split("/")[-1] + if custom_file_name is None + else custom_file_name + ) + self.file_delete(domain_type, domain_identifier, delete_file_name) + + # add files as files form data with dict using 'files[]' as key and + # (tuple of filename and fileobject) + if custom_file_name is None: + files = {"files[]": (source_file.name.split("/")[-1], source_file)} + elif "/" in custom_file_name: logger.warning( - '/ in file name ({}) will fail upload!'.format(custom_file_name)) + "/ in file name (%s) will fail upload!", custom_file_name + ) files = {} else: - files = {'files[]': (custom_file_name, source_file)} + files = {"files[]": (custom_file_name, source_file)} - response = self.session.post(url=url, files=files) - source_file.close() + response = self.session.post(url=url, files=files) """ # Issues with HEADERS in Request module when using non standard 'files[]' key in POST Request @@ -79,40 +85,46 @@ def file_upload(self, source_filepath, domain_type, if response.status_code == 200: try: response_content = json.loads(response.content) - logger.debug("Upload successful {}".format(response_content)) - return True - except BaseException: + logger.debug("Upload successful len=%s", response_content) + except (json.JSONDecodeError, TypeError, UnicodeDecodeError): logger.warning(response.content.decode()) return False + else: + return True else: logger.warning(response.content.decode()) return False - def file_delete(self, domain_type, domain_identifier, - filename_for_selective_delete=None): - """ - Helper function to delete ALL attachments of any specified module of ChurchTools# - or identifying individual file_name_ids and deleting specifc files only - :param domain_type: The ct_domain type, currently supported are 'avatar', 'groupimage', 'logo', 'attatchments', + def file_delete( + self, + domain_type: str, + domain_identifier: int, + filename_for_selective_delete: Optional[str] = None, + ) -> bool: + """Helper function to delete ALL attachments of any specified module of ChurchTools# + or identifying individual file_name_ids and deleting specifc files only. + + Params: + domain_type: The ct_domain type, currently supported are 'avatar', 'groupimage', 'logo', 'attatchments', 'html_template', 'service', 'song_arrangement', 'importtable', 'person', 'familyavatar', 'wiki_.?'. - :type domain_type: str - :param domain_identifier: ID of the object in ChurchTools - :type domain_identifier: int - :param filename_for_selective_delete: name of the file to be deleted - all others will be kept - :type filename_for_selective_delete: str - :return: if successful - :rtype: bool + domain_identifier: ID of the object in ChurchTools + filename_for_selective_delete: name of the file to be deleted - all others will be kept + + Returns: + if successful. """ - url = self.domain + \ - '/api/files/{}/{}'.format(domain_type, domain_identifier) + url = self.domain + f"/api/files/{domain_type}/{domain_identifier}" if filename_for_selective_delete is not None: response = self.session.get(url=url) - files = json.loads(response.content)['data'] + files = json.loads(response.content)["data"] selective_file_ids = [ - item["id"] for item in files if item['name'] == filename_for_selective_delete] + item["id"] + for item in files + if item["name"] == filename_for_selective_delete + ] for current_file_id in selective_file_ids: - url = self.domain + '/api/files/{}'.format(current_file_id) + url = self.domain + f"/api/files/{current_file_id}" response = self.session.delete(url=url) # Delete all Files for the id online @@ -121,85 +133,93 @@ def file_delete(self, domain_type, domain_identifier, return response.status_code == 204 # success code for delete action upload - def file_download(self, filename, domain_type, - domain_identifier, target_path='./downloads'): - """ - Retrieves the first file from ChurchTools for specific filename, domain_type and domain_identifier from churchtools - :param filename: display name of the file as shown in ChurchTools - :type filename: str - :param domain_type: Currently supported are either 'avatar', 'groupimage', 'logo', 'attatchments', 'html_template', 'service', 'song_arrangement', 'importtable', 'person', 'familyavatar', 'wiki_.?' - :type domain_type: str - :param domain_identifier: = Id e.g. of song_arrangement - For songs this technical number can be obtained running get_songs() - :type domain_identifier: str - :param target_path: local path as target for the download (without filename) - will be created if not exists - :type target_path: str - :return: if successful - :rtype: bool + def file_download( + self, + filename: str, + domain_type: str, + domain_identifier: str, + target_path: str = "./downloads", + ) -> bool: + """Retrieves the first file from ChurchTools for specific filename, domain_type and domain_identifier from churchtools. + + Params: + filename: display name of the file as shown in ChurchTools + domain_type: Currently supported are either 'avatar', 'groupimage', 'logo', 'attatchments', 'html_template', 'service', 'song_arrangement', 'importtable', 'person', 'familyavatar', 'wiki_.?' + domain_identifier: = Id e.g. of song_arrangement - For songs this technical number can be obtained running get_songs() + target_path: local path as target for the download (without filename) - will be created if not exists + + Returns: + if successful. """ StateOK = False - CHECK_FOLDER = os.path.isdir(target_path) - # If folder doesn't exist, then create it. - if not CHECK_FOLDER: - os.makedirs(target_path) - print("created folder : ", target_path) + target_path = Path(target_path) + target_path.mkdir(parents=True, exist_ok=True) - url = '{}/api/files/{}/{}'.format(self.domain, - domain_type, domain_identifier) + url = f"{self.domain}/api/files/{domain_type}/{domain_identifier}" response = self.session.get(url=url) if response.status_code == 200: response_content = json.loads(response.content) - arrangement_files = response_content['data'].copy() + arrangement_files = response_content["data"].copy() logger.debug( - "SongArrangement-Files load successful {}".format(response_content)) + "SongArrangement-Files load successful len=%s", + response_content, + ) file_found = False for file in arrangement_files: - filenameoriginal = str(file['name']) + filenameoriginal = str(file["name"]) if filenameoriginal == filename: file_found = True break if file_found: - logger.debug("Found File: {}".format(filename)) + logger.debug("Found File: %s", filename) # Build path OS independent - fileUrl = str(file['fileUrl']) - path_file = os.sep.join([target_path, filename]) + fileUrl = str(file["fileUrl"]) + path_file = target_path / filename StateOK = self.file_download_from_url(fileUrl, path_file) else: - logger.warning("File {} does not exist".format(filename)) + logger.warning("File %s does not exist", filename) return StateOK - else: - logger.warning( - "%s Something went wrong fetching SongArrangement-Files: %s", response.status_code, response.content) - - def file_download_from_url(self, file_url, target_path): - """ - Retrieves file from ChurchTools for specific file_url from churchtools - This function is used by file_download(...) - :param file_url: Example file_url=https://lgv-oe.church.tools/?q=public/filedownload&id=631&filename=738db42141baec592aa2f523169af772fd02c1d21f5acaaf0601678962d06a00 + logger.warning( + "%s Something went wrong fetching SongArrangement-Files: %s", + response.status_code, + response.content, + ) + return None + + def file_download_from_url(self, file_url: str, target_path: str) -> bool: + """Retrieves file from ChurchTools for specific file_url from churchtools. + This function is used by file_download(...). + + Params: + file_url: Example file_url=https://lgv-oe.church.tools/?q=public/filedownload&id=631&filename=738db42141baec592aa2f523169af772fd02c1d21f5acaaf0601678962d06a00 Pay Attention: this file-url consists of a specific / random filename which was created by churchtools - :type file_url: str - :param target_path: directory to drop the download into - must exist before use! - :type target_path: str - :return: if successful - :rtype: bool + target_path: directory to drop the download into - must exist before use! + + Returns: + if successful. """ # NOTE the stream=True parameter below + + target_path = Path(target_path) with self.session.get(url=file_url, stream=True) as response: if response.status_code == 200: - with open(target_path, 'wb') as f: + with target_path.open("wb") as f: for chunk in response.iter_content(chunk_size=8192): # If you have chunk encoded response uncomment if # and set chunk_size parameter to None. # if chunk: f.write(chunk) - logger.debug("Download of {} successful".format(file_url)) + logger.debug("Download of %s successful", file_url) return True - else: - logger.warning( - "%s Something went wrong during file_download: %s", response.status_code, response.content) - return False + logger.warning( + "%s Something went wrong during file_download: %s", + response.status_code, + response.content, + ) + return False diff --git a/churchtools_api/groups.py b/churchtools_api/groups.py index 7dce788..5ad15be 100644 --- a/churchtools_api/groups.py +++ b/churchtools_api/groups.py @@ -1,5 +1,6 @@ import json import logging +from typing import Optional from churchtools_api.churchtools_api_abstract import ChurchToolsApiAbstract @@ -7,20 +8,24 @@ class ChurchToolsApiGroups(ChurchToolsApiAbstract): - """Part definition of ChurchToolsApi which focuses on groups + """Part definition of ChurchToolsApi which focuses on groups. Args: ChurchToolsApiAbstract: template with minimum references """ - def __init__(self): + def __init__(self) -> None: super() def get_groups(self, **kwargs) -> list[dict]: - """Gets list of all groups + """Gets list of all groups. Keywords: group_id: int: optional filter by group id (only to be used on it's own) + kwargs: keyword arguments passthrough + + Keywords: + group_id Permissions: requires "view group" for all groups which should be considered @@ -30,7 +35,7 @@ def get_groups(self, **kwargs) -> list[dict]: """ url = self.domain + "/api/groups" - if "group_id" in kwargs.keys(): + if "group_id" in kwargs: url = url + "/{}".format(kwargs["group_id"]) headers = {"accept": "application/json"} @@ -40,42 +45,41 @@ def get_groups(self, **kwargs) -> list[dict]: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers - ) - response_data = ( - [response_data] if isinstance(response_data, dict) else response_data - ) - return response_data - else: - logger.warning( - "%s Something went wrong fetching groups: %s", - response.status_code, - response.content, + response_content, + url=url, + headers=headers, ) + return [response_data] if isinstance(response_data, dict) else response_data + logger.warning( + "%s Something went wrong fetching groups: %s", + response.status_code, + response.content, + ) + return None def get_groups_hierarchies(self): - """ - Get list of all group hierarchies and convert them to a dict + """Get list of all group hierarchies and convert them to a dict :return: list of all group hierarchies using groupId as key - :rtype: dict + :rtype: dict. """ - url = self.domain + '/api/groups/hierarchies' - headers = { - 'accept': 'application/json' - } + url = self.domain + "/api/groups/hierarchies" + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() + response_data = response_content["data"].copy() logger.debug( - "First response of Groups Hierarchies successful {}".format(response_content)) + "First response of Groups Hierarchies successful %s",response_content, + ) - result = {group['groupId']: group for group in response_data} - return result + return {group["groupId"]: group for group in response_data} - else: - logger.warning( - "%s Something went wrong fetching groups hierarchies: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong fetching groups hierarchies: %s", + response.status_code, + response.content, + ) + return None def get_group_statistics(self, group_id: int) -> dict: """Get statistics for the given group. @@ -86,7 +90,7 @@ def get_group_statistics(self, group_id: int) -> dict: Returns: statistics """ - url = self.domain + "/api/groups/{}/statistics".format(group_id) + url = self.domain + f"/api/groups/{group_id}/statistics" headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) @@ -95,20 +99,27 @@ def get_group_statistics(self, group_id: int) -> dict: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers + response_content, + url=url, + headers=headers, ) logger.debug( - "First response of Group Statistics successful {}".format( - response_content - ) + "First response of Group Statistics successful len=%s",response_content, ) return response_data - else: - logger.warning( - "%s Something went wrong fetching group statistics: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong fetching group statistics: %s", + response.status_code, + response.content, + ) + return None def create_group( - self, name: str, group_status_id: int, grouptype_id: int, **kwargs + self, + name: str, + group_status_id: int, + grouptype_id: int, + **kwargs, ) -> dict: """Create a new group. @@ -116,7 +127,7 @@ def create_group( name: required name group_status_id: required status id grouptype_id: required grouptype id - k + kwargs: keywords see below Kwargs: campus_id: int: optional campus id @@ -130,7 +141,6 @@ def create_group( Returns: dict with created group group - similar to get_group """ - url = self.domain + "/api/groups" headers = {"accept": "application/json"} data = { @@ -139,13 +149,13 @@ def create_group( "name": name, } - if "campus_id" in kwargs.keys(): + if "campus_id" in kwargs: data["campusId"] = kwargs["campus_id"] - if "force" in kwargs.keys(): + if "force" in kwargs: data["force"] = kwargs["force"] - if "superior_group_id" in kwargs.keys(): + if "superior_group_id" in kwargs: data["superiorGroupId"] = kwargs["superior_group_id"] response = self.session.post(url=url, headers=headers, data=data) @@ -153,24 +163,25 @@ def create_group( if response.status_code == 201: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers + response_content, + url=url, + headers=headers, ) logger.debug( - "First response of Create Group successful {}".format(response_content) + "First response of Create Group successful len=%s",response_content, ) return response_data - else: - logger.warning( - "%s Something went wrong with creating group: %s", - response.status_code, - response.content, - ) + logger.warning( + "%s Something went wrong with creating group: %s", + response.status_code, + response.content, + ) + return None def update_group(self, group_id: int, data: dict) -> dict: - """ - Update a field of the given group. - to loookup available names use get_group(group_id=xxx) + """Update a field of the given group. + to loookup available names use get_group(group_id=xxx). Arguments: group_id: number of the group to update @@ -190,16 +201,16 @@ def update_group(self, group_id: int, data: dict) -> dict: response_content = json.loads(response.content) response_data = response_content["data"].copy() logger.debug( - "First response of Update Group successful {}".format(response_content) + "First response of Update Group successful len=%s",response_content, ) return response_data - else: - logger.warning( - "%s Something went wrong updating group: %s", - response.status_code, - response.content, - ) + logger.warning( + "%s Something went wrong updating group: %s", + response.status_code, + response.content, + ) + return None def delete_group(self, group_id: int) -> bool: """Delete the given group. @@ -213,74 +224,79 @@ def delete_group(self, group_id: int) -> bool: Returns: True if successful """ - url = self.domain + "/api/groups/{}".format(group_id) + url = self.domain + f"/api/groups/{group_id}" response = self.session.delete(url=url) if response.status_code == 204: logger.debug("First response of Delete Group successful") return True - else: - logger.warning( - "%s Something went wrong deleting group: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong deleting group: %s", + response.status_code, + response.content, + ) + return None def get_grouptypes(self, **kwargs): - """ - Get list of all grouptypes + """Get list of all grouptypes :keyword grouptype_id: int: optional filter by grouptype id :return: dict with all grouptypes with id as key (even if only one) - :rtype: dict + :rtype: dict. """ - url = self.domain + '/api/group/grouptypes' - if 'grouptype_id' in kwargs.keys(): - url = url + '/{}'.format(kwargs['grouptype_id']) - headers = { - 'accept': 'application/json' - } + url = self.domain + "/api/group/grouptypes" + if "grouptype_id" in kwargs: + url = url + "/{}".format(kwargs["grouptype_id"]) + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() + response_data = response_content["data"].copy() logger.debug( - "First response of Grouptypes successful {}".format(response_content)) + "First response of Grouptypes successful len=%s",response_content, + ) if isinstance(response_data, list): - result = {group['id']: group for group in response_data} + result = {group["id"]: group for group in response_data} else: - result = {response_data['id']: response_data} + result = {response_data["id"]: response_data} return result - else: - logger.warning( - "%s Something went wrong fetching grouptypes: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong fetching grouptypes: %s", + response.status_code, + response.content, + ) + return None def get_group_permissions(self, group_id: int): - """ - Get permissions of the current user for the given group + """Get permissions of the current user for the given group :param group_id: required group_id :return: dict with permissions - :rtype: dict + :rtype: dict. """ - url = self.domain + \ - '/api/permissions/internal/groups/{}'.format(group_id) - headers = { - 'accept': 'application/json' - } + url = self.domain + f"/api/permissions/internal/groups/{group_id}" + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() + response_data = response_content["data"].copy() logger.debug( - "First response of Group Permissions successful {}".format(response_content)) + "First response of Group Permissions successful len=%s",response_content, + ) return response_data - else: - logger.warning( - "%s Something went wrong fetching group permissions: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong fetching group permissions: %s", + response.status_code, + response.content, + ) + return None def get_group_members(self, group_id: int, **kwargs) -> list[dict]: """Get list of members for the given group. Arguments: group_id: group id + kwargs: see below Kwargs: role_ids: list[int]: optional filter list of role ids @@ -288,11 +304,11 @@ def get_group_members(self, group_id: int, **kwargs) -> list[dict]: Returns: list of group member dicts """ - url = self.domain + "/api/groups/{}/members".format(group_id) + url = self.domain + f"/api/groups/{group_id}/members" headers = {"accept": "application/json"} params = {} - if "role_ids" in kwargs.keys(): + if "role_ids" in kwargs: params["role_ids[]"] = kwargs["role_ids"] response = self.session.get(url=url, headers=headers, params=params) @@ -301,28 +317,35 @@ def get_group_members(self, group_id: int, **kwargs) -> list[dict]: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers - ) - result_list = ( - [response_data] if isinstance(response_data, dict) else response_data + response_content, + url=url, + headers=headers, ) + return [response_data] if isinstance(response_data, dict) else response_data - return result_list - else: - logger.warning( - "%s Something went wrong fetching group members: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong fetching group members: %s", + response.status_code, + response.content, + ) + return None def get_groups_members( - self, group_ids: list[int] = None, with_deleted: bool = False, **kwargs + self, + group_ids: Optional[list[int]] = None, + *, + with_deleted: bool = False, + **kwargs, ) -> list[dict]: """Access to /groups/members to lookup group memberships - Similar to get_group_members but not specific to a single group + Similar to get_group_members but not specific to a single group. Args: group_ids: list of group ids to look for. Defaults to Any with_deleted: If true return also delted group members. Defaults to True + kwargs: see below - Kwargs: + Keywords: grouptype_role_ids: list[int] of grouptype_role_ids to consider person_ids: list[int]: person to consider for result @@ -342,7 +365,10 @@ def get_groups_members( response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers, params=params + response_content, + url=url, + headers=headers, + params=params, ) result_list = ( [response_data] if isinstance(response_data, dict) else response_data @@ -361,12 +387,12 @@ def get_groups_members( return result_list return result_list - else: - logger.warning( - "%s Something went wrong fetching group members: %s", - response.status_code, - response.content, - ) + logger.warning( + "%s Something went wrong fetching group members: %s", + response.status_code, + response.content, + ) + return None def add_group_member(self, group_id: int, person_id: int, **kwargs) -> dict: """Add a member to a group. @@ -374,23 +400,24 @@ def add_group_member(self, group_id: int, person_id: int, **kwargs) -> dict: Arguments: group_id: required group id person_id: required person id + kwargs: see below - Kwargs: + Keywords: grouptype_role_id: int: optional grouptype role id group_member_status: str: optional member status Returns: dict with group member """ - url = self.domain + "/api/groups/{}/members/{}".format(group_id, person_id) + url = self.domain + f"/api/groups/{group_id}/members/{person_id}" headers = { "accept": "application/json", } data = {} - if "grouptype_role_id" in kwargs.keys(): + if "grouptype_role_id" in kwargs: data["groupTypeRoleId"] = kwargs["grouptype_role_id"] - if "group_member_status" in kwargs.keys(): + if "group_member_status" in kwargs: data["group_member_status"] = kwargs["group_member_status"] response = self.session.put(url=url, data=data, headers=headers) @@ -399,12 +426,14 @@ def add_group_member(self, group_id: int, person_id: int, **kwargs) -> dict: response_content = json.loads(response.content) # For unknown reasons the endpoint returns a list of items instead # of a single item as specified in the API documentation. - response_data = response_content["data"][0].copy() + return response_content["data"][0].copy() - return response_data - else: - logger.warning( - "%s Something went wrong adding group member: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong adding group member: %s", + response.status_code, + response.content, + ) + return None def remove_group_member(self, group_id: int, person_id: int) -> bool: """Remove the given group member. @@ -418,15 +447,17 @@ def remove_group_member(self, group_id: int, person_id: int) -> bool: Returns: True if successful """ - url = self.domain + "/api/groups/{}/members/{}".format(group_id, person_id) + url = self.domain + f"/api/groups/{group_id}/members/{person_id}" response = self.session.delete(url=url) if response.status_code == 204: return True - else: - logger.warning( - "%s Something went wrong removing group member: %s", response.status_code, response.content - ) + logger.warning( + "%s Something went wrong removing group member: %s", + response.status_code, + response.content, + ) + return None def get_group_roles(self, group_id: int) -> list[dict]: """Get list of all roles for the given group. @@ -436,7 +467,7 @@ def get_group_roles(self, group_id: int) -> list[dict]: Returns: list with group roles dicts """ - url = self.domain + "/api/groups/{}/roles".format(group_id) + url = self.domain + f"/api/groups/{group_id}/roles" headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) @@ -444,15 +475,17 @@ def get_group_roles(self, group_id: int) -> list[dict]: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers - ) - result_list = ( - [response_data] if isinstance(response_data, dict) else response_data + response_content, + url=url, + headers=headers, ) - return result_list - else: - logger.warning( - "%s Something went wrong fetching group roles: %s", response.status_code, response.content) + return [response_data] if isinstance(response_data, dict) else response_data + logger.warning( + "%s Something went wrong fetching group roles: %s", + response.status_code, + response.content, + ) + return None def add_parent_group(self, group_id: int, parent_group_id: int) -> bool: """Add a parent group for a group. @@ -467,18 +500,18 @@ def add_parent_group(self, group_id: int, parent_group_id: int) -> bool: Returns: True if successful """ - url = self.domain + "/api/groups/{}/parents/{}".format( - group_id, parent_group_id - ) + url = self.domain + f"/api/groups/{group_id}/parents/{parent_group_id}" response = self.session.put(url=url) if response.status_code == 201: logger.debug("First response of Add Parent Group successful") return True - else: - logger.warning( - "%s Something went wrong adding parent group: %s",response.status_code, response.content - ) + logger.warning( + "%s Something went wrong adding parent group: %s", + response.status_code, + response.content, + ) + return None def remove_parent_group(self, group_id: int, parent_group_id: int) -> bool: """Remove a parent group from a group. @@ -490,14 +523,15 @@ def remove_parent_group(self, group_id: int, parent_group_id: int) -> bool: Returns: True if successful """ - url = self.domain + "/api/groups/{}/parents/{}".format( - group_id, parent_group_id - ) + url = self.domain + f"/api/groups/{group_id}/parents/{parent_group_id}" response = self.session.delete(url=url) if response.status_code == 204: logger.debug("First response of Remove Parent Group successful") return True - else: - logger.warning( - "%s Something went wrong removing parent group: %s", response.status_code, response.content) + logger.warning( + "%s Something went wrong removing parent group: %s", + response.status_code, + response.content, + ) + return None diff --git a/churchtools_api/persons.py b/churchtools_api/persons.py index 8f5b45c..7937d75 100644 --- a/churchtools_api/persons.py +++ b/churchtools_api/persons.py @@ -1,5 +1,6 @@ import json import logging +from typing import Optional from churchtools_api.churchtools_api_abstract import ChurchToolsApiAbstract @@ -7,18 +8,17 @@ class ChurchToolsApiPersons(ChurchToolsApiAbstract): - """ Part definition of ChurchToolsApi which focuses on persons + """Part definition of ChurchToolsApi which focuses on persons. Args: ChurchToolsApiAbstract: template with minimum references """ - def __init__(self): + def __init__(self) -> None: super() def get_persons(self, **kwargs) -> list[dict]: - """ - Function to get list of all or a person from CT. + """Function to get list of all or a person from CT. Arguments: kwargs: optional keywords as listed @@ -35,7 +35,7 @@ def get_persons(self, **kwargs) -> list[dict]: """ url = self.domain + "/api/persons" params = {"limit": 50} # increases default pagination size - if "ids" in kwargs.keys(): + if "ids" in kwargs: params["ids[]"] = kwargs["ids"] headers = {"accept": "application/json"} @@ -43,41 +43,49 @@ def get_persons(self, **kwargs) -> list[dict]: if response.status_code == 200: response_content = json.loads(response.content) - response_data = response_content['data'].copy() + response_data = response_content["data"].copy() logger.debug( - "First response of GET Persons successful {}".format(response_content)) + "len of first response of GET Persons successful len(%s)" ,response_content, + ) if len(response_data) == 0: - logger.warning('Requesting ct_users {} returned an empty response - ' - 'make sure the user has correct permissions'.format(params)) + logger.warning( + "Requesting ct_users %s returned an empty response - " + "make sure the user has correct permissions", + params + ) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers, params=params + response_content, + url=url, + headers=headers, + params=params, + ) + response_data = ( + [response_data] if isinstance(response_data, dict) else response_data ) - response_data = [response_data] if isinstance(response_data, dict) else response_data - if 'returnAsDict' in kwargs and 'serviceId' not in kwargs: - if kwargs['returnAsDict']: - result = {} - for item in response_data: - result[item['id']] = item - response_data = result + if kwargs.get("returnAsDict") and "serviceId" not in kwargs: + result = {} + for item in response_data: + result[item["id"]] = item + response_data = result - logger.debug("Persons load successful {}".format(response_data)) + logger.debug("Persons load successful %s",response_data) return response_data - else: - logger.info( - "Persons requested failed: {}".format( - response.status_code)) - return None + logger.info("Persons requested failed: %s",response.status_code) + return None def get_persons_masterdata( - self, resultClass: str = None, returnAsDict: bool = False, **kwargs + self, + *, + resultClass: Optional[str] = None, + returnAsDict: bool = False, + **kwargs, ) -> dict[list[dict]]: - """ - Function to get the Masterdata of the persons module - This information is required to map some IDs to specific items + """Function to get the Masterdata of the persons module + This information is required to map some IDs to specific items. Returns: dict of lists of masterdata items each with list of dict items used as configuration @@ -90,13 +98,12 @@ def get_persons_masterdata( if response.status_code == 200: response_content = json.loads(response.content) response_data = response_content["data"].copy() - logger.debug("Person Masterdata load successful {}".format(response_data)) + logger.debug("Person Masterdata load successful len=%s",response_data) return response_data - else: - logger.warning( - "%s Something went wrong fetching person metadata: %s", - response.status_code, - response.content, - ) - return None + logger.warning( + "%s Something went wrong fetching person metadata: %s", + response.status_code, + response.content, + ) + return None diff --git a/churchtools_api/resources.py b/churchtools_api/resources.py index c3d0494..2624143 100644 --- a/churchtools_api/resources.py +++ b/churchtools_api/resources.py @@ -13,11 +13,11 @@ class ChurchToolsApiResources(ChurchToolsApiAbstract): ChurchToolsApiAbstract: template with minimum references """ - def __init__(self): + def __init__(self) -> None: super() def get_resource_masterdata(self, result_type: str) -> dict: - """Access to resource masterdata + """Access to resource masterdata. Arguments: result_type: either "resourceTypes" or "resources" depending on expected result @@ -28,9 +28,10 @@ def get_resource_masterdata(self, result_type: str) -> dict: known_result_types = ["resourceTypes", "resources"] if result_type not in known_result_types: logger.error( - "get_resource_masterdata does not know result_type=%s", result_type + "get_resource_masterdata does not know result_type=%s", + result_type, ) - return + return None url = self.domain + "/api/resource/masterdata" headers = {"accept": "application/json"} @@ -40,21 +41,22 @@ def get_resource_masterdata(self, result_type: str) -> dict: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers + response_content, + url=url, + headers=headers, ) return response_data[result_type] - else: - logger.error(response) - return + logger.error(response) + return None def get_bookings(self, **kwargs) -> list[dict]: """Access to all Resource bookings in churchtools based on a combination of Keyword Arguments. Arguments: - kwargs - see list below - some combination limits do apply + kwargs: see list below - some combination limits do apply - KwArgs: + Keywords: booking_id: int: only one booking by id (use standalone only) resources_ids:list[int]: required if not booking_id status_ids: list[int]: filter by list of stats ids to consider (requires resource_ids) @@ -68,14 +70,14 @@ def get_bookings(self, **kwargs) -> list[dict]: # at least one of the following arguments is required required_kwargs = ["booking_id", "resource_ids"] - if not any([kwarg in kwargs for kwarg in required_kwargs]): + if not any(kwarg in kwargs for kwarg in required_kwargs): logger.error( - "invalid argument combination in get_bookings - please check docstring for requirements" + "invalid argument combination in get_bookings - please check docstring for requirements", ) - return + return None if booking_id := kwargs.get("booking_id"): - url = url + "/{}".format(kwargs["booking_id"]) + url = url + f"/{booking_id}" elif resource_ids := kwargs.get("resource_ids"): params["resource_ids[]"] = resource_ids @@ -84,7 +86,7 @@ def get_bookings(self, **kwargs) -> list[dict]: if "from_" in kwargs or "to_" in kwargs: if "from_" not in kwargs or "to_" not in kwargs: logger.info( - "missing from_ or to_ defaults to first or last day of current month" + "missing from_ or to_ defaults to first or last day of current month", ) if from_ := kwargs.get("from_"): params["from"] = from_.strftime("%Y-%m-%d") @@ -93,7 +95,7 @@ def get_bookings(self, **kwargs) -> list[dict]: if appointment_id := kwargs.get("appointment_id"): if "from" not in params: logger.warning( - "using appointment ID without date range might be incomplete if current month differs" + "using appointment ID without date range might be incomplete if current month differs", ) params["appointment_id"] = appointment_id @@ -103,7 +105,10 @@ def get_bookings(self, **kwargs) -> list[dict]: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers, params=params + response_content, + url=url, + headers=headers, + params=params, ) result_list = ( [response_data] if isinstance(response_data, dict) else response_data @@ -115,8 +120,6 @@ def get_bookings(self, **kwargs) -> list[dict]: for i in result_list if i["base"]["appointmentId"] == appointment_id ] - else: - return result_list - else: - logger.error(response.content) - return + return result_list + logger.error(response.content) + return None diff --git a/churchtools_api/songs.py b/churchtools_api/songs.py index e71350e..c129852 100644 --- a/churchtools_api/songs.py +++ b/churchtools_api/songs.py @@ -1,19 +1,21 @@ import json import logging from datetime import datetime, timedelta +from typing import Optional from churchtools_api.churchtools_api_abstract import ChurchToolsApiAbstract logger = logging.getLogger(__name__) + class ChurchToolsApiSongs(ChurchToolsApiAbstract): - """ Part definition of ChurchToolsApi which focuses on songs + """Part definition of ChurchToolsApi which focuses on songs. Args: ChurchToolsApiAbstract: template with minimum references """ - def __init__(self): + def __init__(self) -> None: super() def get_songs(self, **kwargs) -> list[dict]: @@ -21,91 +23,102 @@ def get_songs(self, **kwargs) -> list[dict]: Kwargs: song_id: int: optional filter by song id - + Returns: list of songs """ - url = self.domain + "/api/songs" - if "song_id" in kwargs.keys(): + if "song_id" in kwargs: url = url + "/{}".format(kwargs["song_id"]) headers = {"accept": "application/json"} - params = {"limit":50} #increases default pagination size + params = {"limit": 50} # increases default pagination size response = self.session.get(url=url, headers=headers, params=params) if response.status_code == 200: response_content = json.loads(response.content) response_data = self.combine_paginated_response_data( - response_content, url=url, headers=headers, params=params + response_content, + url=url, + headers=headers, + params=params, ) return [response_data] if isinstance(response_data, dict) else response_data - else: - if "song_id" in kwargs.keys(): - logger.info( - "Did not find song ({}) with CODE {}".format( - kwargs["song_id"], response.status_code)) - else: - logger.warning( - "%s Something went wrong fetching songs: %s", response.status_code, response.content) - - def get_song_ajax(self, song_id=None, require_update_after_seconds=10): - """ - Legacy AJAX function to get a specific song + if "song_id" in kwargs: + logger.info( + "Did not find song (%s) with CODE %s", + kwargs["song_id"], + response.status_code, + ) + return None + logger.warning( + "%s Something went wrong fetching songs: %s", + response.status_code, + response.content, + ) + return None + + def get_song_ajax( + self, song_id: Optional[int] = None, require_update_after_seconds: int = 10 + ) -> dict: + """Legacy AJAX function to get a specific song. used to e.g. check for tags requires requesting full song list for efficiency reasons songs are cached and not updated unless older than 15sec or update_required - Be aware that params of the returned object might differ from REST API responsens (e.g. Bezeichnung instead of name) - :param song_id: the id of the song to be searched for - :type song_id: int - :param require_update_after_seconds: number of seconds after which an update of ajax song cache is required + Be aware that params of the returned object might differ from REST API responsens (e.g. Bezeichnung instead of name). + + Params: + song_id: the id of the song to be searched for + require_update_after_seconds: number of seconds after which an update of ajax song cache is required defaults to 10 sedonds - :type require_update_after_seconds: int - :return: response content interpreted as json - :rtype: dict - """ + Returns: + response content interpreted as json + """ if self.ajax_song_last_update is None: require_update = True else: - require_update = self.ajax_song_last_update + \ - timedelta(seconds=10) < datetime.now() + require_update = ( + self.ajax_song_last_update + timedelta(seconds=10) < datetime.now() + ) if require_update: - url = self.domain + '/?q=churchservice/ajax&func=getAllSongs' + url = self.domain + "/?q=churchservice/ajax&func=getAllSongs" response = self.session.post(url=url) - self.ajax_song_cache = json.loads(response.content)[ - 'data']['songs'] + self.ajax_song_cache = json.loads(response.content)["data"]["songs"] self.ajax_song_last_update = datetime.now() - song = self.ajax_song_cache[str(song_id)] + return self.ajax_song_cache[str(song_id)] - return song + def get_song_category_map(self) -> dict: + """Helpfer function creating requesting CT metadata for mapping of categories. - def get_song_category_map(self): + Returns: + a dictionary of CategoryName:CTCategoryID. """ - Helpfer function creating requesting CT metadata for mapping of categories - :return: a dictionary of CategoryName:CTCategoryID - :rtype: dict - """ - - url = self.domain + '/api/event/masterdata' - headers = { - 'accept': 'application/json' - } + url = self.domain + "/api/event/masterdata" + headers = {"accept": "application/json"} response = self.session.get(url=url, headers=headers) response_content = json.loads(response.content) - song_categories = response_content['data']['songCategories'] + song_categories = response_content["data"]["songCategories"] song_category_dict = {} for item in song_categories: - song_category_dict[item['name']] = item['id'] + song_category_dict[item["name"]] = item["id"] return song_category_dict - def create_song(self, title: str, songcategory_id: int, author='', copyright='', ccli='', tonality='', bpm='', - beat=''): - """ - Method to create a new song using legacy AJAX API + def create_song( # noqa: PLR0913 + self, + title: str, + songcategory_id: int, + author="", + copyright="", # noqa: A002 + ccli="", + tonality="", + bpm="", + beat="", + ): + """Method to create a new song using legacy AJAX API Does not check for existing duplicates ! function endpoint see https://api.church.tools/function-churchservice_addNewSong.html - name for params reverse engineered based on web developer tools in Firefox and live churchTools instance + name for params reverse engineered based on web developer tools in Firefox and live churchTools instance. :param title: Title of the Song :param songcategory_id: int id of site specific songcategories (created in CT Metadata) - required @@ -119,39 +132,43 @@ def create_song(self, title: str, songcategory_id: int, author='', copyright='', :return: int song_id: ChurchTools song_id of the Song created or None if not successful :rtype: int | None """ - url = self.domain + '/?q=churchservice/ajax&func=addNewSong' + url = self.domain + "/?q=churchservice/ajax&func=addNewSong" data = { - 'bezeichnung': title, - 'songcategory_id': songcategory_id, - 'author': author, - 'copyright': copyright, - 'ccli': ccli, - 'tonality': tonality, - 'bpm': bpm, - 'beat': beat + "bezeichnung": title, + "songcategory_id": songcategory_id, + "author": author, + "copyright": copyright, + "ccli": ccli, + "tonality": tonality, + "bpm": bpm, + "beat": beat, } response = self.session.post(url=url, data=data) if response.status_code == 200: response_content = json.loads(response.content) - new_id = int(response_content['data']) - logger.debug("Song created successful with ID={}".format(new_id)) + new_id = int(response_content["data"]) + logger.debug("Song created successful with ID=%s", new_id) return new_id - else: - logger.info( - "Creating song failed with {}".format( - response.status_code)) - return None - - def edit_song(self, song_id: int, songcategory_id=None, title=None, author=None, copyright=None, ccli=None, - practice_yn=None): - """ - Method to EDIT an existing song using legacy AJAX API + logger.info("Creating song failed with %s", response.status_code) + return None + + def edit_song( # noqa: PLR0913 + self, + song_id: int, + songcategory_id=None, + title=None, + author=None, + copyright=None, # noqa: A002 + ccli=None, + practice_yn=None, + ): + """Method to EDIT an existing song using legacy AJAX API Changes are only applied to fields that have values in respective param - None is considered empty while '' is an empty text which clears existing values + None is considered empty while '' is an empty text which clears existing values. function endpoint see https://api.church.tools/function-churchservice_editSong.html name for params reverse engineered based on web developer tools in Firefox and live churchTools instance @@ -169,50 +186,50 @@ def edit_song(self, song_id: int, songcategory_id=None, title=None, author=None, :return: response item :rtype: dict #TODO 49 """ - # system/churchservice/churchservice_db.php - url = self.domain + '/?q=churchservice/ajax&func=editSong' + url = self.domain + "/?q=churchservice/ajax&func=editSong" existing_song = self.get_songs(song_id=song_id)[0] data = { - 'id': song_id if song_id is not None else existing_song['name'], - 'bezeichnung': title if title is not None else existing_song['name'], - 'songcategory_id': songcategory_id if songcategory_id is not None else existing_song['category']['id'], - 'author': author if author is not None else existing_song['author'], - 'copyright': copyright if copyright is not None else existing_song['copyright'], - 'ccli': ccli if ccli is not None else existing_song['ccli'], - 'practice_yn': practice_yn if practice_yn is not None else existing_song['shouldPractice'], + "id": song_id if song_id is not None else existing_song["name"], + "bezeichnung": title if title is not None else existing_song["name"], + "songcategory_id": songcategory_id + if songcategory_id is not None + else existing_song["category"]["id"], + "author": author if author is not None else existing_song["author"], + "copyright": copyright + if copyright is not None + else existing_song["copyright"], + "ccli": ccli if ccli is not None else existing_song["ccli"], + "practice_yn": practice_yn + if practice_yn is not None + else existing_song["shouldPractice"], } - response = self.session.post(url=url, data=data) - return response + return self.session.post(url=url, data=data) - def delete_song(self, song_id: int): - """ - Method to DELETE a song using legacy AJAX API - name for params reverse engineered based on web developer tools in Firefox and live churchTools instance + def delete_song(self, song_id: int) -> dict: + """Method to DELETE a song using legacy AJAX API + name for params reverse engineered based on web developer tools in Firefox and live churchTools instance. :param song_id: ChurchTools site specific song_id which should be modified - required :return: response item - :rtype: dict #TODO 49 + #TODO 49 """ - # system/churchservice/churchservice_db.php - url = self.domain + '/?q=churchservice/ajax&func=deleteSong' + url = self.domain + "/?q=churchservice/ajax&func=deleteSong" data = { - 'id': song_id, + "id": song_id, } - response = self.session.post(url=url, data=data) - return response + return self.session.post(url=url, data=data) - def add_song_tag(self, song_id: int, song_tag_id: int): - """ - Method to add a song tag using legacy AJAX API on a specific song - reverse engineered based on web developer tools in Firefox and live churchTools instance + def add_song_tag(self, song_id: int, song_tag_id: int) -> dict: + """Method to add a song tag using legacy AJAX API on a specific song + reverse engineered based on web developer tools in Firefox and live churchTools instance. re-adding existing tag does not cause any issues :param song_id: ChurchTools site specific song_id which should be modified - required @@ -221,23 +238,18 @@ def add_song_tag(self, song_id: int, song_tag_id: int): :type song_tag_id: int :return: response item - :rtype: dict #TODO 49 + #TODO 49 """ - url = self.domain + '/?q=churchservice/ajax&func=addSongTag' + url = self.domain + "/?q=churchservice/ajax&func=addSongTag" - data = { - 'id': song_id, - 'tag_id': song_tag_id - } + data = {"id": song_id, "tag_id": song_tag_id} - response = self.session.post(url=url, data=data) - return response + return self.session.post(url=url, data=data) - def remove_song_tag(self, song_id, song_tag_id): - """ - Method to remove a song tag using legacy AJAX API on a specifc song + def remove_song_tag(self, song_id, song_tag_id) -> dict: + """Method to remove a song tag using legacy AJAX API on a specifc song reverse engineered based on web developer tools in Firefox and live churchTools instance - re-removing existing tag does not cause any issues + re-removing existing tag does not cause any issues. :param song_id: ChurchTools site specific song_id which should be modified - required :type song_id: int @@ -245,58 +257,49 @@ def remove_song_tag(self, song_id, song_tag_id): :type song_tag_id: int :return: response item - :rtype: dict #TODO 49 + #TODO 49 """ - url = self.domain + '/?q=churchservice/ajax&func=delSongTag' + url = self.domain + "/?q=churchservice/ajax&func=delSongTag" - data = { - 'id': song_id, - 'tag_id': song_tag_id - } + data = {"id": song_id, "tag_id": song_tag_id} - response = self.session.post(url=url, data=data) - return response + return self.session.post(url=url, data=data) - def get_song_tags(self, song_id): - """ - Method to get a song tag workaround using legacy AJAX API for getSong + def get_song_tags(self, song_id) -> list: + """Method to get a song tag workaround using legacy AJAX API for getSong. :param song_id: ChurchTools site specific song_id which should be modified - required :type song_id: int - :return: response item - :rtype: list + :return: response item. """ song = self.get_song_ajax(song_id) - return song['tags'] + return song["tags"] + + def contains_song_tag(self, song_id, song_tag_id) -> bool: + """Helper which checks if a specific song_tag_id is present on a song. - def contains_song_tag(self, song_id, song_tag_id): - """ - Helper which checks if a specific song_tag_id is present on a song :param song_id: ChurchTools site specific song_id which should checked :type song_id: int :param song_tag_id: ChurchTools site specific song_tag_id which should be searched for :type song_tag_id: int :return: bool if present - :rtype: bool """ tags = self.get_song_tags(song_id) return str(song_tag_id) in tags - def get_songs_by_tag(self, song_tag_id): - """ - Helper which returns all songs that contain have a specific tag + def get_songs_by_tag(self, song_tag_id) -> list[dict]: + """Helper which returns all songs that contain have a specific tag. + :param song_tag_id: ChurchTools site specific song_tag_id which should be searched for :type song_tag_id: int :return: list of songs - :rtype: list[dict] """ songs = self.get_songs() - songs_dict = {song['id']: song for song in songs} - - filtered_song_ids = [] - for id in songs_dict.keys(): - if self.contains_song_tag(id, song_tag_id): - filtered_song_ids.append(id) + songs_dict = {song["id"]: song for song in songs} - result = [songs_dict[song_id] for song_id in filtered_song_ids] + filtered_song_ids = [ + song_id + for song_id in songs_dict + if self.contains_song_tag(song_id=song_id, song_tag_id=song_tag_id) + ] - return result + return [songs_dict[song_id] for song_id in filtered_song_ids] diff --git a/generate_pyproj.py b/generate_pyproj.py index 2e4b851..48761c5 100644 --- a/generate_pyproj.py +++ b/generate_pyproj.py @@ -1,4 +1,6 @@ import importlib.util +from pathlib import Path + import tomli_w # Load the version number from version.py @@ -15,13 +17,13 @@ "version": version, "description": "A python wrapper for use with ChurchToolsAPI", "authors": ["bensteUEM", "kolibri52", "fschrempf"], - "homepage": 'https://github.com/bensteUEM/ChurchToolsAPI', + "homepage": "https://github.com/bensteUEM/ChurchToolsAPI", "license": "CC-BY-SA", "readme": "README.md", "dependencies": { - "python": "^3.8", + "python": "^3.9", "python-docx": "^0.8.11", - "requests": "^2.31.0" + "requests": "^2.31.0", }, "group": { "dev": { @@ -31,17 +33,84 @@ "wheel": "^0.41.2", "setuptools": "^66.1.1", "autopep8": "^2.0.4", - "pytest" : "^8.3.3", - } - } - } - } + "pytest": "^8.3.3", + "pre-commit": "^3.8.0", + "ruff": "^0.6.9", + }, + }, + }, + }, + "ruff": { + # Exclude a variety of commonly ignored directories. + "exclude": [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + ], + # Same as Black. + "line-length": 88, + "indent-width": 4, + # Assume Python 3.9 + "target-version": "py39", + # Group violations by containing file. + "output-format": "grouped", + "lint": { + # Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. + "select": ["ALL"], + "ignore": [], + # Allow fix for all enabled rules (when `--fix`) is provided. + "fixable": ["ALL"], + "unfixable": [], + # Allow unused variables when underscore-prefixed. + "dummy-variable-rgx": "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$", + "pydocstyle": { + "convention": "google", + }, + }, + "format": { + # Like Black, use double quotes for strings. + "quote-style": "double", + # Like Black, indent with spaces, rather than tabs. + "indent-style": "space", + # Like Black, respect magic trailing commas. + "skip-magic-trailing-comma": False, + # Like Black, automatically detect the appropriate line ending. + "line-ending": "auto", + # Enable auto-formatting of code examples in docstrings. + "docstring-code-format": False, + # Set the line length limit used when formatting code snippets in docstrings. + "docstring-code-line-length": "dynamic", + }, + }, }, "build-system": { "requires": ["poetry-core"], - "build-backend": "poetry.core.masonry.api" - } + "build-backend": "poetry.core.masonry.api", + }, } -with open("pyproject.toml", "wb") as toml_file: +with Path.open("pyproject.toml", "wb") as toml_file: tomli_w.dump(pyproject_toml_content, toml_file) diff --git a/main.ipynb b/main.ipynb index cd499c9..ca1aad4 100644 --- a/main.ipynb +++ b/main.ipynb @@ -16,11 +16,13 @@ "metadata": {}, "outputs": [], "source": [ + "import json\n", "import logging\n", "import logging.config\n", - "import json\n", "from pathlib import Path\n", + "\n", "from churchtools_api.churchtools_api import ChurchToolsApi\n", + "from secure.config import ct_domain, ct_token\n", "\n", "logger = logging.getLogger(__name__)\n", "\n", @@ -33,8 +35,8 @@ " logging.config.dictConfig(config=logging_config)\n", "\n", "# Create Session\n", - "from secure.config import ct_domain\n", - "from secure.config import ct_token\n", + "\n", + "\n", "api = ChurchToolsApi(ct_domain)\n", "api.login_ct_rest_api(ct_token=ct_token)" ] @@ -54,7 +56,7 @@ "outputs": [], "source": [ "songs = api.get_songs()\n", - "print('Got {} songs and the first one is \"{}\"'.format(len(songs),songs[0]['name']))" + "print('Got {} songs and the first one is \"{}\"'.format(len(songs), songs[0][\"name\"])) # noqa: T201" ] }, { @@ -70,9 +72,9 @@ "metadata": {}, "outputs": [], "source": [ - "all_song_ids = [value['id'] for value in songs]\n", - "for id in all_song_ids:\n", - " api.add_song_tag(id, 51)" + "all_song_ids = [value[\"id\"] for value in songs]\n", + "for song_id in all_song_ids:\n", + " api.add_song_tag(song_id=song_id, song_tag_id=51)" ] } ], diff --git a/pyproject.toml b/pyproject.toml index d4d69a9..7b58291 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ license = "CC-BY-SA" readme = "README.md" [tool.poetry.dependencies] -python = "^3.8" +python = "^3.9" python-docx = "^0.8.11" requests = "^2.31.0" @@ -23,6 +23,83 @@ wheel = "^0.41.2" setuptools = "^66.1.1" autopep8 = "^2.0.4" pytest = "^8.3.3" +pre-commit = "^3.8.0" +ruff = "^0.6.9" + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] +line-length = 88 +indent-width = 4 +target-version = "py39" +output-format = "grouped" + +[tool.ruff.lint] +select = [ + "ALL", +] +ignore = [ + "S101", # TODO: Github #104 - pytest asserts + "ANN001","ANN002","ANN003","ANN201", + "FIX002", #Open ToDos + "TD002","TD003","TD004", #TODOs + "C901","PLR0912", #complexity + "ARG002",#TODO: Github #110 + "FA100", #Python version specific #102 + "D100","D101","D102","D104","D107","D205","D415", #Docstrings + "E501", #line too long + "DTZ001","DTZ005","DTZ007","DTZ002", #datetime timezone + + "PLR2004", #magic values + + "COM812", "ISC001", #disabled for formatter compatibility + + "N802", #function lowercase -> breaking change + "N803", #argument lowercase -> breaking change + "N806", #variable name lowercase -> breaking change + ] +fixable = [ + "ALL", +] +unfixable = [] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" +docstring-code-format = false +docstring-code-line-length = "dynamic" [build-system] requires = [ diff --git a/tests/test_churchtools_api.py b/tests/test_churchtools_api.py index ccf9644..0d0e026 100644 --- a/tests/test_churchtools_api.py +++ b/tests/test_churchtools_api.py @@ -3,8 +3,8 @@ import logging import logging.config import os -from pathlib import Path import unittest +from pathlib import Path from churchtools_api.churchtools_api import ChurchToolsApi @@ -20,8 +20,8 @@ class TestsChurchToolsApi(unittest.TestCase): - def __init__(self, *args, **kwargs): - super(TestsChurchToolsApi, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) if "CT_TOKEN" in os.environ: self.ct_token = os.environ["CT_TOKEN"] @@ -44,90 +44,82 @@ def __init__(self, *args, **kwargs): self.api = ChurchToolsApi(domain=self.ct_domain, ct_token=self.ct_token) logger.info("Executing Tests RUN") - def tearDown(self): - """ - Destroy the session after test execution to avoid resource issues + def tearDown(self) -> None: + """Destroy the session after test execution to avoid resource issues :return: """ self.api.session.close() - def test_init_userpwd(self): - """ - Tries to create a login with churchTools using specified username and password + def test_init_userpwd(self) -> None: + """Tries to create a login with churchTools using specified username and password :return: """ if self.api.session is not None: self.api.session.close() - username = list(self.ct_users.keys())[0] - password = list(self.ct_users.values())[0] + username = next(iter(self.ct_users.keys())) + password = next(iter(self.ct_users.values())) ct_api = ChurchToolsApi(self.ct_domain, ct_user=username, ct_password=password) - self.assertIsNotNone(ct_api) + assert ct_api is not None ct_api.session.close() - def test_login_ct_rest_api(self): - """ - Checks that Userlogin using REST is working with provided TOKEN + def test_login_ct_rest_api(self) -> None: + """Checks that Userlogin using REST is working with provided TOKEN :return: """ if self.api.session is not None: self.api.session.close() result = self.api.login_ct_rest_api(ct_token=self.ct_token) - self.assertTrue(result) + assert result - username = list(self.ct_users.keys())[0] - password = list(self.ct_users.values())[0] + username = next(iter(self.ct_users.keys())) + password = next(iter(self.ct_users.values())) if self.api.session is not None: self.api.session.close() result = self.api.login_ct_rest_api(ct_user=username, ct_password=password) - self.assertTrue(result) + assert result - def test_get_ct_csrf_token(self): - """ - Test checks that a CSRF token can be requested using the current API status + def test_get_ct_csrf_token(self) -> None: + """Test checks that a CSRF token can be requested using the current API status :return: """ token = self.api.get_ct_csrf_token() - self.assertGreater( - len(token), 0, "Token should be more than one letter but changes each time" - ) + assert ( + len(token) > 0 + ), "Token should be more than one letter but changes each time" - def test_check_connection_ajax(self): - """ - Test checks that a connection can be established using the AJAX endpoints with current session / ct_api + def test_check_connection_ajax(self) -> None: + """Test checks that a connection can be established using the AJAX endpoints with current session / ct_api :return: """ result = self.api.check_connection_ajax() - self.assertTrue(result) + assert result - def test_get_persons(self): - """ - Tries to get all and a single person from the server + def test_get_persons(self) -> None: + """Tries to get all and a single person from the server Be aware that only ct_users that are visible to the user associated with the login token can be viewed! On any elkw.KRZ.TOOLS personId 1 'firstName' starts with 'Ben' and more than 50 ct_users exist(13. Jan 2023) :return: """ - personId = 1 result1 = self.api.get_persons() - self.assertIsInstance(result1, list) - self.assertIsInstance(result1[0], dict) - self.assertGreater(len(result1), 50) + assert isinstance(result1, list) + assert isinstance(result1[0], dict) + assert len(result1) > 50 result2 = self.api.get_persons(ids=[personId]) - self.assertIsInstance(result2, list) - self.assertIsInstance(result2[0], dict) - self.assertEqual(result2[0]["firstName"][0:3], "Ben") + assert isinstance(result2, list) + assert isinstance(result2[0], dict) + assert result2[0]["firstName"][0:3] == "Ben" result3 = self.api.get_persons(returnAsDict=True) - self.assertIsInstance(result3, dict) + assert isinstance(result3, dict) result4 = self.api.get_persons(returnAsDict=False) - self.assertIsInstance(result4, list) + assert isinstance(result4, list) def test_get_persons_masterdata(self) -> None: - """ - Tries to retrieve metadata for persons module - Expected sections equal those that were available on ELKW1610.krz.tools on 4.Oct.2024 + """Tries to retrieve metadata for persons module + Expected sections equal those that were available on ELKW1610.krz.tools on 4.Oct.2024. Some items might be hidden in options instead of masterdata e.g. sex """ @@ -150,21 +142,21 @@ def test_get_persons_masterdata(self) -> None: } result = self.api.get_persons_masterdata() - self.assertIsInstance(result, dict) - self.assertEqual(EXPECTED_SECTIONS, set(result.keys())) + assert isinstance(result, dict) + assert set(result.keys()) == EXPECTED_SECTIONS for section in result.values(): - self.assertIsInstance(section, list) + assert isinstance(section, list) for item in section: - self.assertIsInstance(item, dict) + assert isinstance(item, dict) - def test_get_options(self): - """Checks that option fields can retrieved""" + def test_get_options(self) -> None: + """Checks that option fields can retrieved.""" result = self.api.get_options() - self.assertIn("sex", result) + assert "sex" in result - def test_get_persons_sex_id(self): - """Tests that persons sexId can be retrieved and converted to a human readable gender + def test_get_persons_sex_id(self) -> None: + """Tests that persons sexId can be retrieved and converted to a human readable gender. IMPORTANT - This test method and the parameters used depend on the target system! the hard coded sample exists on ELKW1610.KRZ.TOOLS @@ -180,12 +172,11 @@ def test_get_persons_sex_id(self): } result = gender_map[person[0]["sexId"]] - self.assertEqual(EXPECTED_RESULT, result) + assert result == EXPECTED_RESULT def test_get_songs(self) -> None: - """ - 1. Test requests all songs and checks that result has more than 10 elements (hence default pagination works) - 2. Test requests song 2034 and checks that result matches "sample" + """1. Test requests all songs and checks that result has more than 50 elements (hence default pagination works) + 2. Test requests song 2034 and checks that result matches "sample". IMPORTANT - This test method and the parameters used depend on the target system! the hard coded sample exists on ELKW1610.KRZ.TOOLS @@ -193,76 +184,69 @@ def test_get_songs(self) -> None: SAMPLE_SONG_ID = 2034 songs = self.api.get_songs() - self.assertGreater(len(songs), 50) + assert len(songs) > 50 song = self.api.get_songs(song_id=SAMPLE_SONG_ID)[0] - self.assertEqual(song["id"], 2034) - self.assertEqual(song["name"], "sample") + assert song["id"] == 2034 + assert song["name"] == "sample" - def test_get_song_ajax(self): - """ - Testing legacy AJAX API to request one specific song + def test_get_song_ajax(self) -> None: + """Testing legacy AJAX API to request one specific song 1. Test requests song 408 and checks that result matches Test song IMPORTANT - This test method and the parameters used depend on the target system! :return: """ SAMPLE_SONG_ID = 2034 song = self.api.get_song_ajax(song_id=SAMPLE_SONG_ID) - self.assertIsInstance(song, dict) - self.assertEqual(len(song), 14) + assert isinstance(song, dict) + assert len(song) == 14 - self.assertEqual(int(song["id"]), SAMPLE_SONG_ID) - self.assertEqual(song["bezeichnung"], "sample") + assert int(song["id"]) == SAMPLE_SONG_ID + assert song["bezeichnung"] == "sample" - def test_get_song_category_map(self): - """ - Checks that a dict with respective known values is returned when requesting song categories + def test_get_song_category_map(self) -> None: + """Checks that a dict with respective known values is returned when requesting song categories IMPORTANT - This test method and the parameters used depend on the target system! Requires the connected test system to have a category "Test" mapped as ID 13 (or changed if other system) :return: """ - song_catgegory_dict = self.api.get_song_category_map() - self.assertEqual(song_catgegory_dict["Test"], 13) + assert song_catgegory_dict["Test"] == 13 def test_get_groups(self) -> None: - """ - 1. Test requests all groups and checks that result has more than 10 elements (hence default pagination works) - 2. Test requests group 103 and checks that result matches Test song + """1. Test requests all groups and checks that result has more than 50 elements (hence default pagination works) + 2. Test requests group 103 and checks that result matches Test song. IMPORTANT - This test method and the parameters used depend on the target system! the hard coded sample exists on ELKW1610.KRZ.TOOLS """ - groups = self.api.get_groups() - self.assertTrue(isinstance(groups, list)) - self.assertTrue(isinstance(groups[0], dict)) - self.assertGreater(len(groups), 10) + assert isinstance(groups, list) + assert isinstance(groups[0], dict) + assert len(groups) > 10 groups = self.api.get_groups(group_id=103) - self.assertTrue(isinstance(groups, list)) + assert isinstance(groups, list) group = groups[0] - self.assertTrue(isinstance(group, dict)) - self.assertEqual(group["id"], 103) - self.assertEqual(group["name"], "TestGruppe") + assert isinstance(group, dict) + assert group["id"] == 103 + assert group["name"] == "TestGruppe" - def test_get_groups_hierarchies(self): - """ - Checks that the list of group hierarchies can be retrieved and each + def test_get_groups_hierarchies(self) -> None: + """Checks that the list of group hierarchies can be retrieved and each element contains the keys 'groupId', 'parents' and 'children'. The list should be accessible as dict using groupID as key :return: """ hierarchies = self.api.get_groups_hierarchies() - self.assertIsInstance(hierarchies, dict) + assert isinstance(hierarchies, dict) for hierarchy in hierarchies.values(): - self.assertTrue("groupId" in hierarchy) - self.assertTrue("parents" in hierarchy) - self.assertTrue("children" in hierarchy) + assert "groupId" in hierarchy + assert "parents" in hierarchy + assert "children" in hierarchy def test_get_group_statistics(self) -> None: - """ - Checks that the statistics for a group can be retrieved and certain keys + """Checks that the statistics for a group can be retrieved and certain keys exist in the dict. IMPORTANT - This test method and the parameters used depend on the target system! @@ -271,14 +255,13 @@ def test_get_group_statistics(self) -> None: SAMPLE_GROUP_ID = 103 stats = self.api.get_group_statistics(group_id=SAMPLE_GROUP_ID) - self.assertIsNotNone(stats) - self.assertIn("unfiltered", stats) - self.assertIn("freePlaces", stats["unfiltered"]) - self.assertIn("takenPlaces", stats["unfiltered"]) + assert stats is not None + assert "unfiltered" in stats + assert "freePlaces" in stats["unfiltered"] + assert "takenPlaces" in stats["unfiltered"] - def test_get_grouptypes(self): - """ - 1. Check that the list of grouptypes can be retrieved and each element contains the keys 'id' and 'name'. + def test_get_grouptypes(self) -> None: + """1. Check that the list of grouptypes can be retrieved and each element contains the keys 'id' and 'name'. 2. Check that a single grouptype can be retrieved and id and name are matching. IMPORTANT - This test method and the parameters used depend on the target system! @@ -286,34 +269,33 @@ def test_get_grouptypes(self): """ # multiple group types grouptypes = self.api.get_grouptypes() - self.assertIsInstance(grouptypes, dict) - self.assertGreater(len(grouptypes), 2) + assert isinstance(grouptypes, dict) + assert len(grouptypes) > 2 for grouptype in grouptypes.values(): - self.assertTrue("id" in grouptype) - self.assertTrue("name" in grouptype) + assert "id" in grouptype + assert "name" in grouptype # one type only grouptypes = self.api.get_grouptypes(grouptype_id=2) - self.assertEqual(len(grouptypes), 1) + assert len(grouptypes) == 1 for grouptype in grouptypes.values(): - self.assertTrue("id" in grouptype) - self.assertTrue("name" in grouptype) - self.assertEqual(grouptype["id"], 2) - self.assertEqual(grouptype["name"], "Dienst") + assert "id" in grouptype + assert "name" in grouptype + assert grouptype["id"] == 2 + assert grouptype["name"] == "Dienst" - def test_get_group_permissions(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_get_group_permissions(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Checks that the permissions for a group can be retrieved and matches the test permissions. :return: """ permissions = self.api.get_group_permissions(group_id=103) - self.assertEqual(permissions["churchdb"]["+see group"], 2) - self.assertTrue(permissions["churchdb"]["+edit group infos"]) + assert permissions["churchdb"]["+see group"] == 2 + assert permissions["churchdb"]["+edit group infos"] - def test_create_and_delete_group(self): + def test_create_and_delete_group(self) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS + the hard coded sample exists on ELKW1610.KRZ.TOOLS. 1. Checks if groups can be created with minimal and optional parameters. 2. Checks if a group can be created with a name of an existing group @@ -331,10 +313,10 @@ def test_create_and_delete_group(self): group_status_id=SAMPLE_GROUP_STATUS_ID, grouptype_id=SAMPLE_NEW_GROUP_TYPE, ) - self.assertIsNotNone(group1) - self.assertEqual(group1["name"], SAMPLE_GROUP_NAME) - self.assertEqual(group1["information"]["groupTypeId"], SAMPLE_NEW_GROUP_TYPE) - self.assertEqual(group1["information"]["groupStatusId"], SAMPLE_GROUP_STATUS_ID) + assert group1 is not None + assert group1["name"] == SAMPLE_GROUP_NAME + assert group1["information"]["groupTypeId"] == SAMPLE_NEW_GROUP_TYPE + assert group1["information"]["groupStatusId"] == SAMPLE_GROUP_STATUS_ID SAMPLE_GROUP_NAME2 = "TestGroup With Campus And Superior" group2 = self.api.create_group( @@ -344,18 +326,18 @@ def test_create_and_delete_group(self): campus_id=SAMPLE_CAMPUS_ID, superior_group_id=group1["id"], ) - self.assertIsNotNone(group2) - self.assertEqual(group2["name"], SAMPLE_GROUP_NAME2) - self.assertEqual(group2["information"]["groupTypeId"], SAMPLE_NEW_GROUP_TYPE) - self.assertEqual(group2["information"]["groupStatusId"], SAMPLE_GROUP_STATUS_ID) - self.assertEqual(group2["information"]["campusId"], SAMPLE_CAMPUS_ID) + assert group2 is not None + assert group2["name"] == SAMPLE_GROUP_NAME2 + assert group2["information"]["groupTypeId"] == SAMPLE_NEW_GROUP_TYPE + assert group2["information"]["groupStatusId"] == SAMPLE_GROUP_STATUS_ID + assert group2["information"]["campusId"] == SAMPLE_CAMPUS_ID group3 = self.api.create_group( SAMPLE_GROUP_NAME, group_status_id=SAMPLE_GROUP_STATUS_ID, grouptype_id=SAMPLE_NEW_GROUP_TYPE, ) - self.assertIsNone(group3) + assert group3 is None group3 = self.api.create_group( SAMPLE_GROUP_NAME, @@ -363,25 +345,24 @@ def test_create_and_delete_group(self): grouptype_id=SAMPLE_NEW_GROUP_TYPE, force=True, ) - self.assertIsNotNone(group3) - self.assertEqual(group3["name"], SAMPLE_GROUP_NAME) - self.assertEqual(group3["information"]["groupTypeId"], SAMPLE_NEW_GROUP_TYPE) - self.assertEqual(group3["information"]["groupStatusId"], SAMPLE_GROUP_STATUS_ID) + assert group3 is not None + assert group3["name"] == SAMPLE_GROUP_NAME + assert group3["information"]["groupTypeId"] == SAMPLE_NEW_GROUP_TYPE + assert group3["information"]["groupStatusId"] == SAMPLE_GROUP_STATUS_ID # cleanup after testing ret = self.api.delete_group(group_id=group1["id"]) - self.assertTrue(ret) + assert ret ret = self.api.delete_group(group_id=group2["id"]) - self.assertTrue(ret) + assert ret ret = self.api.delete_group(group_id=group3["id"]) - self.assertTrue(ret) + assert ret - def test_update_group(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! - The user needs to be able to change group information - usually "Leiter" permission enables this + def test_update_group(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! + The user needs to be able to change group information - usually "Leiter" permission enables this. Checks that a field in a group can be set to some value and the returned group has this field value set. Also cleans the field after executing the test @@ -390,13 +371,14 @@ def test_update_group(self): SAMPLE_GROUP_ID = 103 data = {"note": "TestNote - if this exists an automated test case failed"} group_update_result = self.api.update_group(group_id=SAMPLE_GROUP_ID, data=data) - self.assertEqual(group_update_result["information"]["note"], data["note"]) + assert group_update_result["information"]["note"] == data["note"] group_update_result = self.api.update_group( - group_id=SAMPLE_GROUP_ID, data={"note": ""} + group_id=SAMPLE_GROUP_ID, + data={"note": ""}, ) groups = self.api.get_groups(group_id=SAMPLE_GROUP_ID) - self.assertEqual(groups[0]["information"]["note"], "") + assert groups[0]["information"]["note"] == "" def test_get_group_members(self) -> None: """Checks if group members can be retrieved from the group and filtering @@ -409,22 +391,23 @@ def test_get_group_members(self) -> None: SAMPLE_GROUPTYPE_ROLE_ID = 16 members = self.api.get_group_members(group_id=SAMPLE_GROUP_ID) - self.assertIsNotNone(members) - self.assertNotEqual(members, []) + assert members is not None + assert members != [] for member in members: - self.assertIn("personId", member) + assert "personId" in member members = self.api.get_group_members( - group_id=SAMPLE_GROUP_ID, role_ids=[SAMPLE_GROUPTYPE_ROLE_ID] + group_id=SAMPLE_GROUP_ID, + role_ids=[SAMPLE_GROUPTYPE_ROLE_ID], ) - self.assertIsNotNone(members) - self.assertNotEqual(members, []) + assert members is not None + assert members != [] for member in members: - self.assertIn("personId", member) - self.assertEqual(member["groupTypeRoleId"], SAMPLE_GROUPTYPE_ROLE_ID) + assert "personId" in member + assert member["groupTypeRoleId"] == SAMPLE_GROUPTYPE_ROLE_ID def test_get_groups_members(self) -> None: - """Check that a list of groups is received when asking by person and optional role id + """Check that a list of groups is received when asking by person and optional role id. IMPORTANT - This test method and the parameters used depend on the target system! the hard coded sample exists on ELKW1610.KRZ.TOOLS @@ -437,25 +420,26 @@ def test_get_groups_members(self) -> None: # 1. person only group_result = self.api.get_groups_members(person_ids=SAMPLE_PERSON_IDS) compare_result = [group["groupId"] for group in group_result] - self.assertIn(EXPECTED_GROUP_ID, compare_result) + assert EXPECTED_GROUP_ID in compare_result # 2a. person user and role - non lead - no group group_result = self.api.get_groups_members( - person_ids=SAMPLE_PERSON_IDS, grouptype_role_ids=[SAMPLE_ROLE_ID_MEMBER] + person_ids=SAMPLE_PERSON_IDS, + grouptype_role_ids=[SAMPLE_ROLE_ID_MEMBER], ) - self.assertEqual(len(group_result), 0) + assert len(group_result) == 0 # 2b. person and role - lead and non lead group_result = self.api.get_groups_members( person_ids=SAMPLE_PERSON_IDS, groupTypeRoleId=[SAMPLE_ROLE_ID_LEAD, SAMPLE_ROLE_ID_MEMBER], ) - self.assertTrue(isinstance(group_result, list)) - self.assertGreater(len(group_result), 0) - self.assertTrue(isinstance(group_result[0], dict)) + assert isinstance(group_result, list) + assert len(group_result) > 0 + assert isinstance(group_result[0], dict) compare_result = [group["groupId"] for group in group_result] - self.assertIn(EXPECTED_GROUP_ID, compare_result) + assert EXPECTED_GROUP_ID in compare_result # 3. problematic result group_ids, grouptype_role_ids and person_id SAMPLE_GROUP_IDS = [99, 68, 93] @@ -483,26 +467,27 @@ def test_add_and_remove_group_members(self) -> None: grouptype_role_id=SAMPLE_GROUPTYPE_ROLE_ID, group_member_status="active", ) - self.assertIsNotNone(member) - self.assertEqual(member["personId"], SAMPLE_PERSON_ID) - self.assertEqual(member["groupTypeRoleId"], SAMPLE_GROUPTYPE_ROLE_ID) - self.assertEqual(member["groupMemberStatus"], "active") + assert member is not None + assert member["personId"] == SAMPLE_PERSON_ID + assert member["groupTypeRoleId"] == SAMPLE_GROUPTYPE_ROLE_ID + assert member["groupMemberStatus"] == "active" result = self.api.remove_group_member( - group_id=SAMPLE_GROUP_ID, person_id=SAMPLE_PERSON_ID + group_id=SAMPLE_GROUP_ID, + person_id=SAMPLE_PERSON_ID, ) - self.assertTrue(result) + assert result def test_get_group_roles(self) -> None: """Checks if group roles can be retrieved from a group.""" SAMPLE_GROUP_ID = 103 roles = self.api.get_group_roles(group_id=SAMPLE_GROUP_ID) - self.assertIsNotNone(roles) - self.assertNotEqual(roles, []) + assert roles is not None + assert roles != [] for role in roles: - self.assertIn("id", role) - self.assertIn("groupTypeId", role) - self.assertIn("name", role) + assert "id" in role + assert "groupTypeId" in role + assert "name" in role def test_add_and_remove_parent_group(self) -> None: """Checks if a parent group can be added to and removed from a group. @@ -513,34 +498,34 @@ def test_add_and_remove_parent_group(self) -> None: SAMPLE_GROUP_ID = 103 SAMPLE_PARENT_GROUP_ID = 50 result = self.api.add_parent_group( - group_id=SAMPLE_GROUP_ID, parent_group_id=SAMPLE_PARENT_GROUP_ID + group_id=SAMPLE_GROUP_ID, + parent_group_id=SAMPLE_PARENT_GROUP_ID, ) - self.assertTrue(result) + assert result result = self.api.remove_parent_group( - group_id=SAMPLE_GROUP_ID, parent_group_id=SAMPLE_PARENT_GROUP_ID + group_id=SAMPLE_GROUP_ID, + parent_group_id=SAMPLE_PARENT_GROUP_ID, ) - self.assertTrue(result) + assert result - def test_get_global_permissions(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_get_global_permissions(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Checks that the global permissions for the current user can be retrieved and one core permission and one db permission matches the expected value. :return: """ permissions = self.api.get_global_permissions() - self.assertIn("churchcore", permissions.keys()) - self.assertIn("administer settings", permissions["churchcore"].keys()) + assert "churchcore" in permissions + assert "administer settings" in permissions["churchcore"] - self.assertFalse(permissions["churchcore"]["administer settings"]) - self.assertFalse(permissions["churchdb"]["view birthdaylist"]) - self.assertTrue(permissions["churchwiki"]["view"]) + assert not permissions["churchcore"]["administer settings"] + assert not permissions["churchdb"]["view birthdaylist"] + assert permissions["churchwiki"]["view"] - def test_file_upload_replace_delete(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_file_upload_replace_delete(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! 0. Clean and delete files in test 1. Tries 3 uploads to the test song with ID 408 and arrangement 417 Adds the same file again without overwrite - should exist twice @@ -554,12 +539,10 @@ def test_file_upload_replace_delete(self): # 0. Clean and delete files in test self.api.file_delete("song_arrangement", 417) song = self.api.get_songs(song_id=408)[0] - self.assertEqual( - song["arrangements"][0]["id"], 417, "check that default arrangement exists" - ) - self.assertEqual( - len(song["arrangements"][0]["files"]), 0, "check that ono files exist" - ) + assert ( + song["arrangements"][0]["id"] == 417 + ), "check that default arrangement exists" + assert len(song["arrangements"][0]["files"]) == 0, "check that ono files exist" # 1. Tries 3 uploads to the test song with ID 408 and arrangement 417 # Adds the same file again without overwrite - should exist twice @@ -571,24 +554,25 @@ def test_file_upload_replace_delete(self): "pinguin_shell_rename.png", ) self.api.file_upload( - "samples/pinguin.png", "song_arrangement", 417, "pinguin.png" + "samples/pinguin.png", + "song_arrangement", + 417, + "pinguin.png", ) song = self.api.get_songs(song_id=408)[0] - self.assertIsInstance( - song, dict, "Should be a single song instead of list of songs" - ) - self.assertEqual( - song["arrangements"][0]["id"], 417, "check that default arrangement exsits" - ) - self.assertEqual( - len(song["arrangements"][0]["files"]), - 3, - "check that only the 3 test attachments exist", - ) + assert isinstance( + song, dict + ), "Should be a single song instead of list of songs" + assert ( + song["arrangements"][0]["id"] == 417 + ), "check that default arrangement exsits" + assert ( + len(song["arrangements"][0]["files"]) == 3 + ), "check that only the 3 test attachments exist" filenames = [i["name"] for i in song["arrangements"][0]["files"]] filenames_target = ["pinguin.png", "pinguin_shell_rename.png", "pinguin.png"] - self.assertEqual(filenames, filenames_target) + assert filenames == filenames_target # 2. Reupload pinguin.png using overwrite which will remove both old # files but keep one @@ -600,11 +584,9 @@ def test_file_upload_replace_delete(self): overwrite=True, ) song = self.api.get_songs(song_id=408)[0] - self.assertEqual( - len(song["arrangements"][0]["files"]), - 2, - "check that overwrite is applied on upload", - ) + assert ( + len(song["arrangements"][0]["files"]) == 2 + ), "check that overwrite is applied on upload" # 3. Overwrite without existing file self.api.file_upload( @@ -615,11 +597,9 @@ def test_file_upload_replace_delete(self): overwrite=True, ) song = self.api.get_songs(song_id=408)[0] - self.assertEqual( - len(song["arrangements"][0]["files"]), - 3, - "check that both file with overwrite of new file", - ) + assert ( + len(song["arrangements"][0]["files"]) == 3 + ), "check that both file with overwrite of new file" # 3.b Try overwriting again and check that number of files does not # increase @@ -631,31 +611,26 @@ def test_file_upload_replace_delete(self): overwrite=True, ) song = self.api.get_songs(song_id=408)[0] - self.assertEqual( - len(song["arrangements"][0]["files"]), - 3, - "check that still only 3 file exists", - ) + assert ( + len(song["arrangements"][0]["files"]) == 3 + ), "check that still only 3 file exists" # 4. Delete only one file self.api.file_delete("song_arrangement", 417, "pinguin.png") song = self.api.get_songs(song_id=408)[0] - self.assertEqual( - len(song["arrangements"][0]["files"]), - 2, - "check that still only 2 file exists", - ) + assert ( + len(song["arrangements"][0]["files"]) == 2 + ), "check that still only 2 file exists" # cleanup delete all files self.api.file_delete("song_arrangement", 417) song = self.api.get_songs(song_id=408)[0] - self.assertEqual( - len(song["arrangements"][0]["files"]), 0, "check that files are deleted" - ) + assert ( + len(song["arrangements"][0]["files"]) == 0 + ), "check that files are deleted" - def test_create_edit_delete_song(self): - """ - Test method used to create a new song, edit it's metadata and remove the song + def test_create_edit_delete_song(self) -> None: + """Test method used to create a new song, edit it's metadata and remove the song Does only test metadata not attachments or arrangements IMPORTANT - This test method and the parameters used depend on the target system! On ELKW1610.KRZ.TOOLS songcategory_id 13 is TEST @@ -667,18 +642,18 @@ def test_create_edit_delete_song(self): # 1. Create Song after and check it exists with all params # with self.assertNoLogs(level=logging.WARNING) as cm: #TODO #25 song_id = self.api.create_song(title, songcategory_id) - self.assertIsNotNone(song_id) + assert song_id is not None ct_song = self.api.get_songs(song_id=song_id)[0] - self.assertEqual(ct_song["name"], title) - self.assertEqual(ct_song["author"], "") - self.assertEqual(ct_song["category"]["id"], songcategory_id) + assert ct_song["name"] == title + assert ct_song["author"] == "" + assert ct_song["category"]["id"] == songcategory_id # 2. Edit Song title and check it exists with all params self.api.edit_song(song_id, title="Test_bezeichnung2") ct_song = self.api.get_songs(song_id=song_id)[0] - self.assertEqual(ct_song["author"], "") - self.assertEqual(ct_song["name"], "Test_bezeichnung2") + assert ct_song["author"] == "" + assert ct_song["name"] == "Test_bezeichnung2" # 3. Edit all fields and check it exists with all params data = { @@ -699,53 +674,49 @@ def test_create_edit_delete_song(self): data["practice_yn"], ) ct_song = self.api.get_songs(song_id=song_id)[0] - self.assertEqual(ct_song["name"], data["bezeichnung"]) - self.assertEqual(ct_song["category"]["id"], data["songcategory_id"]) - self.assertEqual(ct_song["author"], data["author"]) - self.assertEqual(ct_song["copyright"], data["copyright"]) - self.assertEqual(ct_song["ccli"], data["ccli"]) - self.assertEqual(ct_song["shouldPractice"], data["practice_yn"]) + assert ct_song["name"] == data["bezeichnung"] + assert ct_song["category"]["id"] == data["songcategory_id"] + assert ct_song["author"] == data["author"] + assert ct_song["copyright"] == data["copyright"] + assert ct_song["ccli"] == data["ccli"] + assert ct_song["shouldPractice"] == data["practice_yn"] # Delete Song self.api.delete_song(song_id) with self.assertLogs(level="INFO") as cm: ct_song = self.api.get_songs(song_id=song_id) messages = [ - "INFO:churchtools_api.songs:Did not find song ({}) with CODE 404".format( - song_id - ) + f"INFO:churchtools_api.songs:Did not find song ({song_id}) with CODE 404", ] - self.assertEqual(messages, cm.output) - self.assertIsNone(ct_song) + assert messages == cm.output + assert ct_song is None - def test_add_remove_song_tag(self): - """ - Test method used to add and remove the test tag to some song + def test_add_remove_song_tag(self) -> None: + """Test method used to add and remove the test tag to some song Tag ID and Song ID may vary depending on the server used On ELKW1610.KRZ.TOOLS song_id 408 and tag_id 53 self.api.ajax_song_last_update = None is required in order to clear the ajax song cache :return: """ self.api.ajax_song_last_update = None - self.assertTrue(self.api.contains_song_tag(408, 53)) - with self.assertNoLogs(level="INFO") as cm: + assert self.api.contains_song_tag(408, 53) + with self.assertNoLogs(level="INFO"): response = self.api.remove_song_tag(408, 53) - self.assertEqual(response.status_code, 200) + assert response.status_code == 200 self.api.ajax_song_last_update = None - self.assertFalse(self.api.contains_song_tag(408, 53)) + assert not self.api.contains_song_tag(408, 53) self.api.ajax_song_last_update = None - with self.assertNoLogs(level="INFO") as cm: + with self.assertNoLogs(level="INFO"): response = self.api.add_song_tag(408, 53) - self.assertEqual(response.status_code, 200) + assert response.status_code == 200 self.api.ajax_song_last_update = None - self.assertTrue(self.api.contains_song_tag(408, 53)) + assert self.api.contains_song_tag(408, 53) - def test_get_songs_with_tag(self): - """ - Test method to check if fetching all songs with a specific tag works + def test_get_songs_with_tag(self) -> None: + """Test method to check if fetching all songs with a specific tag works songId and tag_id will vary depending on the server used On ELKW1610.KRZ.TOOLS song ID 408 is tagged with 53 "Test" :return: @@ -756,12 +727,12 @@ def test_get_songs_with_tag(self): self.api.ajax_song_last_update = None result = self.api.get_songs_by_tag(SAMPLE_TAG_ID) result_ids = [song["id"] for song in result] - self.assertIn(SAMPLE_SONG_ID, result_ids) + assert SAMPLE_SONG_ID in result_ids - def test_file_download(self): + def test_file_download(self) -> None: """Test of file_download and file_download_from_url on https://elkw1610.krz.tools on any song IDs vary depending on the server used - On ELKW1610.KRZ.TOOLS song ID 762 has arrangement 774 does exist + On ELKW1610.KRZ.TOOLS song ID 762 has arrangement 774 does exist. Uploads a test file downloads the file via same ID @@ -772,18 +743,17 @@ def test_file_download(self): self.api.file_upload("samples/test.txt", "song_arrangement", test_id) - filePath = "downloads/test.txt" - if os.path.exists(filePath): - os.remove(filePath) + filePath = Path("downloads/test.txt") + + filePath.unlink(missing_ok=True) self.api.file_download("test.txt", "song_arrangement", test_id) - with open(filePath, "r") as file: + with filePath.open() as file: download_text = file.read() - self.assertEqual("TEST CONTENT", download_text) + assert download_text == "TEST CONTENT" self.api.file_delete("song_arrangement", test_id, "test.txt") - if os.path.exists(filePath): - os.remove(filePath) + filePath.unlink() if __name__ == "__main__": diff --git a/tests/test_churchtools_api_abstract.py b/tests/test_churchtools_api_abstract.py index 64844d2..04a2165 100644 --- a/tests/test_churchtools_api_abstract.py +++ b/tests/test_churchtools_api_abstract.py @@ -1,9 +1,9 @@ -from abc import ABC +import ast import json import logging import logging.config import os -import ast +from abc import ABC from pathlib import Path from churchtools_api.churchtools_api import ChurchToolsApi @@ -18,30 +18,29 @@ log_directory.mkdir(parents=True) logging.config.dictConfig(config=logging_config) -class TestsChurchToolsApiAbstract(ABC): + +class TestsChurchToolsApiAbstract(ABC): # noqa: B024 """This is supposed to be the base configuration for PyTest test classes that require API access.""" - def setup_class(self): - print("Setup Class") - if 'CT_TOKEN' in os.environ: - self.ct_token = os.environ['CT_TOKEN'] - self.ct_domain = os.environ['CT_DOMAIN'] - users_string = os.environ['CT_USERS'] - self.ct_users = ast.literal_eval(users_string) - logger.info( - 'using connection details provided with ENV variables') + def setup_class(self) -> None: + if "CT_TOKEN" in os.environ: + self.ct_token = os.environ["CT_TOKEN"] + self.ct_domain = os.environ["CT_DOMAIN"] + users_string = os.environ["CT_USERS"] + self.ct_users = ast.literal_eval(users_string) + logger.info("using connection details provided with ENV variables") else: from secure.config import ct_token + self.ct_token = ct_token from secure.config import ct_domain + self.ct_domain = ct_domain from secure.config import ct_users + self.ct_users = ct_users - logger.info( - 'using connection details provided from secrets folder') + logger.info("using connection details provided from secrets folder") + + self.api = ChurchToolsApi(domain=self.ct_domain, ct_token=self.ct_token) - self.api = ChurchToolsApi( - domain=self.ct_domain, - ct_token=self.ct_token) - logger.info("Executing Tests RUN") diff --git a/tests/test_churchtools_api_calendars.py b/tests/test_churchtools_api_calendars.py index b4cf05e..90e37b7 100644 --- a/tests/test_churchtools_api_calendars.py +++ b/tests/test_churchtools_api_calendars.py @@ -3,9 +3,9 @@ import logging import logging.config import os -from pathlib import Path import unittest -from datetime import datetime, timedelta +from datetime import datetime +from pathlib import Path from churchtools_api.churchtools_api import ChurchToolsApi @@ -19,152 +19,164 @@ log_directory.mkdir(parents=True) logging.config.dictConfig(config=logging_config) + class TestsChurchToolsApi(unittest.TestCase): - def __init__(self, *args, **kwargs): - super(TestsChurchToolsApi, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) - if 'CT_TOKEN' in os.environ: - self.ct_token = os.environ['CT_TOKEN'] - self.ct_domain = os.environ['CT_DOMAIN'] - users_string = os.environ['CT_USERS'] + if "CT_TOKEN" in os.environ: + self.ct_token = os.environ["CT_TOKEN"] + self.ct_domain = os.environ["CT_DOMAIN"] + users_string = os.environ["CT_USERS"] self.ct_users = ast.literal_eval(users_string) - logger.info( - 'using connection details provided with ENV variables') + logger.info("using connection details provided with ENV variables") else: from secure.config import ct_token + self.ct_token = ct_token from secure.config import ct_domain + self.ct_domain = ct_domain from secure.config import ct_users + self.ct_users = ct_users - logger.info( - 'using connection details provided from secrets folder') + logger.info("using connection details provided from secrets folder") - self.api = ChurchToolsApi( - domain=self.ct_domain, - ct_token=self.ct_token) + self.api = ChurchToolsApi(domain=self.ct_domain, ct_token=self.ct_token) logger.info("Executing Tests RUN") - def tearDown(self): - """ - Destroy the session after test execution to avoid resource issues + def tearDown(self) -> None: + """Destroy the session after test execution to avoid resource issues :return: """ self.api.session.close() - def test_get_calendar(self): - """ - Tries to retrieve a list of calendars - """ + def test_get_calendar(self) -> None: + """Tries to retrieve a list of calendars.""" result = self.api.get_calendars() - self.assertGreaterEqual(len(result), 1) - self.assertIsInstance(result, list) - self.assertIn('id', result[0].keys()) + assert len(result) >= 1 + assert isinstance(result, list) + assert "id" in result[0] - def test_get_calendar_apointments(self): - """ - Tries to retrieve calendar appointments + def test_get_calendar_apointments(self) -> None: + """Tries to retrieve calendar appointments IMPORTANT - This test method and the parameters used depend on the target system! Requires the connected test system to have a calendar mapped as ID 2 and 42 (or changed if other system) Calendar 2 should have 3 appointments on 19.11.2023 - One event should be appointment ID=327032 + One event should be appointment ID=327032. """ # Multiple calendars result = self.api.get_calendar_appointments(calendar_ids=[2, 42]) - self.assertGreaterEqual(len(result), 1) - self.assertIsInstance(result, list) - self.assertIn('id', result[0].keys()) + assert len(result) >= 1 + assert isinstance(result, list) + assert "id" in result[0] # One calendar with from date only (at least 4 appointments) result = self.api.get_calendar_appointments( - calendar_ids=[2], from_='2023-11-19') - self.assertGreater(len(result), 4) - self.assertIsInstance(result, list) - self.assertIn('id', result[0].keys()) + calendar_ids=[2], + from_="2023-11-19", + ) + assert len(result) > 4 + assert isinstance(result, list) + assert "id" in result[0] # One calendar with from and to date (exactly 4 appointments) result = self.api.get_calendar_appointments( - calendar_ids=[2], from_='2023-11-19', to_='2023-11-19') - self.assertEqual(len(result), 3) - self.assertIsInstance(result, list) - self.assertIn('id', result[0].keys()) + calendar_ids=[2], + from_="2023-11-19", + to_="2023-11-19", + ) + assert len(result) == 3 + assert isinstance(result, list) + assert "id" in result[0] # One event in a calendar test_appointment_id = 327032 result = self.api.get_calendar_appointments( - calendar_ids=[2], appointment_id=test_appointment_id) - self.assertEqual(len(result), 1) - self.assertIsInstance(result, list) - self.assertIn('id', result[0].keys()) - self.assertEqual(test_appointment_id, result[0]['id']) - - def test_get_calendar_apointments_datetime(self): - """ - Tries to retrieve calendar appointments using datetime instead of str params + calendar_ids=[2], + appointment_id=test_appointment_id, + ) + assert len(result) == 1 + assert isinstance(result, list) + assert "id" in result[0] + assert test_appointment_id == result[0]["id"] + + def test_get_calendar_apointments_datetime(self) -> None: + """Tries to retrieve calendar appointments using datetime instead of str params IMPORTANT - This test method and the parameters used depend on the target system! Requires the connected test system to have a calendar mapped as ID 2 (or changed if other system) Calendar 2 should have 3 appointments on 19.11.2023 - One event should be appointment ID=327032 + One event should be appointment ID=327032. """ - # One calendar with from and to date (exactly 4 appointments) from_ = datetime(year=2023, month=11, day=19) to_ = datetime(year=2023, month=11, day=19) result = self.api.get_calendar_appointments( - calendar_ids=[2], from_=from_, to_=to_) - self.assertEqual(len(result), 3) - self.assertIsInstance(result, list) - self.assertIn('id', result[0].keys()) - - def test_get_calendar_appoints_on_seriess(self): - """ - This test should check the behaviour of get_calendar_appointments on a series + calendar_ids=[2], + from_=from_, + to_=to_, + ) + assert len(result) == 3 + assert isinstance(result, list) + assert "id" in result[0] + + def test_get_calendar_appoints_on_seriess(self) -> None: + """This test should check the behaviour of get_calendar_appointments on a series IMPORTANT - This test method and the parameters used depend on the target system! Requires the connected test system to have a calendar mapped as ID 2 - Calendar 2 should have appointment 304973 with an instance of series on 26.11.2023 + Calendar 2 should have appointment 304973 with an instance of series on 26.11.2023. """ - # Appointment Series by ID result = self.api.get_calendar_appointments( - calendar_ids=[2], appointment_id=304973) - self.assertEqual( - result[0]['appointment']['caption'], - "Gottesdienst Friedrichstal") - self.assertEqual( - result[0]['appointment']['startDate'], - '2023-01-08T08:00:00Z') - self.assertEqual( - result[0]['appointment']['endDate'], - '2023-01-08T09:00:00Z') + calendar_ids=[2], + appointment_id=304973, + ) + assert result[0]["appointment"]["caption"] == "Gottesdienst Friedrichstal" + assert result[0]["appointment"]["startDate"] == "2023-01-08T08:00:00Z" + assert result[0]["appointment"]["endDate"] == "2023-01-08T09:00:00Z" # Appointment Instance by timeframe result = self.api.get_calendar_appointments( - calendar_ids=[2], from_="2023-11-26", to_="2023-11-26") - result = [appointment for appointment in result if appointment['caption'] - == "Gottesdienst Friedrichstal"] - self.assertEqual(result[0]['caption'], "Gottesdienst Friedrichstal") - self.assertEqual(result[0]['startDate'], "2023-11-26T08:00:00Z") - self.assertEqual(result[0]['endDate'], "2023-11-26T09:00:00Z") + calendar_ids=[2], + from_="2023-11-26", + to_="2023-11-26", + ) + result = [ + appointment + for appointment in result + if appointment["caption"] == "Gottesdienst Friedrichstal" + ] + assert result[0]["caption"] == "Gottesdienst Friedrichstal" + assert result[0]["startDate"] == "2023-11-26T08:00:00Z" + assert result[0]["endDate"] == "2023-11-26T09:00:00Z" # Multiple appointment Instances by timeframe result = self.api.get_calendar_appointments( - calendar_ids=[2], from_="2023-11-19", to_="2023-11-26") - result = [appointment for appointment in result if appointment['caption'] - == "Gottesdienst Friedrichstal"] - self.assertEqual(len(result), 2) - self.assertEqual(result[-1]['caption'], "Gottesdienst Friedrichstal") - self.assertEqual(result[-1]['startDate'], "2023-11-26T08:00:00Z") - self.assertEqual(result[-1]['endDate'], "2023-11-26T09:00:00Z") - - def test_get_calendar_appointments_none(self): - """ - Check that there is no error if no item can be found - There should be no calendar appointments on the specified day + calendar_ids=[2], + from_="2023-11-19", + to_="2023-11-26", + ) + result = [ + appointment + for appointment in result + if appointment["caption"] == "Gottesdienst Friedrichstal" + ] + assert len(result) == 2 + assert result[-1]["caption"] == "Gottesdienst Friedrichstal" + assert result[-1]["startDate"] == "2023-11-26T08:00:00Z" + assert result[-1]["endDate"] == "2023-11-26T09:00:00Z" + + def test_get_calendar_appointments_none(self) -> None: + """Check that there is no error if no item can be found + There should be no calendar appointments on the specified day. """ # Appointment Series by ID result = self.api.get_calendar_appointments( - calendar_ids=[52], from_="2023-12-01", to_="2023-12-02") + calendar_ids=[52], + from_="2023-12-01", + to_="2023-12-02", + ) - self.assertIsNone(result) + assert result is None diff --git a/tests/test_churchtools_api_events.py b/tests/test_churchtools_api_events.py index a9665a2..d3eb9cd 100644 --- a/tests/test_churchtools_api_events.py +++ b/tests/test_churchtools_api_events.py @@ -3,9 +3,9 @@ import logging import logging.config import os -from pathlib import Path import unittest from datetime import datetime, timedelta +from pathlib import Path from churchtools_api.churchtools_api import ChurchToolsApi @@ -19,122 +19,147 @@ log_directory.mkdir(parents=True) logging.config.dictConfig(config=logging_config) + class TestsChurchToolsApi(unittest.TestCase): - def __init__(self, *args, **kwargs): - super(TestsChurchToolsApi, self).__init__(*args, **kwargs) + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) - if 'CT_TOKEN' in os.environ: - self.ct_token = os.environ['CT_TOKEN'] - self.ct_domain = os.environ['CT_DOMAIN'] - users_string = os.environ['CT_USERS'] + if "CT_TOKEN" in os.environ: + self.ct_token = os.environ["CT_TOKEN"] + self.ct_domain = os.environ["CT_DOMAIN"] + users_string = os.environ["CT_USERS"] self.ct_users = ast.literal_eval(users_string) - logger.info( - 'using connection details provided with ENV variables') + logger.info("using connection details provided with ENV variables") else: from secure.config import ct_token + self.ct_token = ct_token from secure.config import ct_domain + self.ct_domain = ct_domain from secure.config import ct_users + self.ct_users = ct_users - logger.info( - 'using connection details provided from secrets folder') + logger.info("using connection details provided from secrets folder") - self.api = ChurchToolsApi( - domain=self.ct_domain, - ct_token=self.ct_token) + self.api = ChurchToolsApi(domain=self.ct_domain, ct_token=self.ct_token) logger.info("Executing Tests RUN") - def tearDown(self): - """ - Destroy the session after test execution to avoid resource issues + def tearDown(self) -> None: + """Destroy the session after test execution to avoid resource issues :return: """ self.api.session.close() - def test_get_events(self): - """ - Tries to get a list of events and a single event from CT + def test_get_events(self) -> None: + """Tries to get a list of events and a single event from CT. Event ID may vary depending on the server used On ELKW1610.KRZ.TOOLS event ID 484 is an existing Event with schedule (20th. Nov 2022) :return: """ - result = self.api.get_events() - self.assertIsNotNone(result) - self.assertIsInstance(result, list) + assert result is not None + assert isinstance(result, list) eventId = 484 result = self.api.get_events(eventId=eventId) - self.assertIsInstance(result, list) - self.assertEqual(1, len(result)) - self.assertIsInstance(result[0], dict) + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], dict) # load next event (limit) - result = self.api.get_events(limit=1, direction='forward') - self.assertIsInstance(result, list) - self.assertEqual(1, len(result)) - self.assertIsInstance(result[0], dict) - result_date = datetime.strptime( - result[0]['startDate'], - '%Y-%m-%dT%H:%M:%S%z').astimezone().date() + result = self.api.get_events(limit=1, direction="forward") + assert isinstance(result, list) + assert len(result) == 1 + assert isinstance(result[0], dict) + result_date = ( + datetime.strptime(result[0]["startDate"], "%Y-%m-%dT%H:%M:%S%z") + .astimezone() + .date() + ) today_date = datetime.today().date() - self.assertGreaterEqual(result_date, today_date) + assert result_date >= today_date # load last event (direction, limit) - result = self.api.get_events(limit=1, direction='backward') - result_date = datetime.strptime( - result[0]['startDate'], - '%Y-%m-%dT%H:%M:%S%z').astimezone().date() - self.assertLessEqual(result_date, today_date) + result = self.api.get_events(limit=1, direction="backward") + result_date = ( + datetime.strptime(result[0]["startDate"], "%Y-%m-%dT%H:%M:%S%z") + .astimezone() + .date() + ) + assert result_date <= today_date # Load events after 7 days (from) next_week_date = today_date + timedelta(days=7) - next_week_formatted = next_week_date.strftime('%Y-%m-%d') + next_week_formatted = next_week_date.strftime("%Y-%m-%d") result = self.api.get_events(from_=next_week_formatted) - result_min_date = min([datetime.strptime( - item['startDate'], '%Y-%m-%dT%H:%M:%S%z').astimezone().date() for item in result]) - result_max_date = max([datetime.strptime( - item['startDate'], '%Y-%m-%dT%H:%M:%S%z').astimezone().date() for item in result]) - self.assertGreaterEqual(result_min_date, next_week_date) - self.assertGreaterEqual(result_max_date, next_week_date) + result_min_date = min( + [ + datetime.strptime(item["startDate"], "%Y-%m-%dT%H:%M:%S%z") + .astimezone() + .date() + for item in result + ], + ) + result_max_date = max( + [ + datetime.strptime(item["startDate"], "%Y-%m-%dT%H:%M:%S%z") + .astimezone() + .date() + for item in result + ], + ) + assert result_min_date >= next_week_date + assert result_max_date >= next_week_date # load events for next 14 days (to) next2_week_date = today_date + timedelta(days=14) - next2_week_formatted = next2_week_date.strftime('%Y-%m-%d') - today_date_formatted = today_date.strftime('%Y-%m-%d') + next2_week_formatted = next2_week_date.strftime("%Y-%m-%d") + today_date_formatted = today_date.strftime("%Y-%m-%d") result = self.api.get_events( from_=today_date_formatted, - to_=next2_week_formatted) - result_min = min([datetime.strptime( - item['startDate'], '%Y-%m-%dT%H:%M:%S%z').astimezone().date() for item in result]) - result_max = max([datetime.strptime( - item['startDate'], '%Y-%m-%dT%H:%M:%S%z').astimezone().date() for item in result]) + to_=next2_week_formatted, + ) + result_min = min( + [ + datetime.strptime(item["startDate"], "%Y-%m-%dT%H:%M:%S%z") + .astimezone() + .date() + for item in result + ], + ) + result_max = max( + [ + datetime.strptime(item["startDate"], "%Y-%m-%dT%H:%M:%S%z") + .astimezone() + .date() + for item in result + ], + ) # only works if there is an event within 7 days on demo system - self.assertLessEqual(result_min, next_week_date) - self.assertLessEqual(result_max, next2_week_date) + assert result_min <= next_week_date + assert result_max <= next2_week_date # missing keyword pair warning with self.assertLogs(level=logging.WARNING) as captured: - item = self.api.get_events(to_=next2_week_formatted) - self.assertEqual(len(captured.records), 1) - self.assertEqual( - ["WARNING:churchtools_api.events:Use of to_ is only allowed together with from_"], - captured.output) + self.api.get_events(to_=next2_week_formatted) + assert len(captured.records) == 1 + assert captured.output == [ + "WARNING:churchtools_api.events:Use of to_ is only allowed together with from_" + ] # load more than 10 events (pagination #TODO #1 improve test case for # pagination - result = self.api.get_events(direction='forward', limit=11) - self.assertIsInstance(result, list) - self.assertGreaterEqual(len(result), 11) + result = self.api.get_events(direction="forward", limit=11) + assert isinstance(result, list) + assert len(result) >= 11 # TODO add test cases for uncommon parts #24 * canceled, include - def test_get_AllEventData_ajax(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_get_AllEventData_ajax(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Test function to check the get_AllEventData_ajax function for a specific ID On ELKW1610.KRZ.TOOLS event ID 3348 is an existing Test Event with schedule (29. Sept 2024) @@ -143,12 +168,11 @@ def test_get_AllEventData_ajax(self): """ SAMPLE_EVENT_ID = 3348 result = self.api.get_AllEventData_ajax(SAMPLE_EVENT_ID) - self.assertIn('id', result.keys()) - self.assertEqual(result['id'], str(SAMPLE_EVENT_ID)) + assert "id" in result + assert result["id"] == str(SAMPLE_EVENT_ID) - def test_get_set_event_services_counts(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_get_set_event_services_counts(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Test function for get and set methods related to event services counts tries to get the number of specicifc service in an id @@ -163,26 +187,32 @@ def test_get_set_event_services_counts(self): serviceId = 1 original_count_comapre = 3 - event = self.api.get_events(eventId=eventId) + self.api.get_events(eventId=eventId) original_count = self.api.get_event_services_counts_ajax( - eventId=eventId, serviceId=serviceId) - self.assertEqual(original_count, {serviceId: original_count_comapre}) + eventId=eventId, + serviceId=serviceId, + ) + assert original_count == {serviceId: original_count_comapre} result = self.api.set_event_services_counts_ajax(eventId, serviceId, 2) - self.assertTrue(result) + assert result new_count = self.api.get_event_services_counts_ajax( - eventId=eventId, serviceId=serviceId) - self.assertEqual(new_count, {serviceId: 2}) + eventId=eventId, + serviceId=serviceId, + ) + assert new_count == {serviceId: 2} result = self.api.set_event_services_counts_ajax( - eventId, serviceId, original_count[serviceId]) - self.assertTrue(result) + eventId, + serviceId, + original_count[serviceId], + ) + assert result - def test_get_set_event_admins(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_get_set_event_admins(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Test function to get list of event admins, change it and check again (and reset to original) On ELKW1610.KRZ.TOOLS event ID 3348 is an existing Event with schedule (29. Sept 2024) @@ -193,44 +223,38 @@ def test_get_set_event_admins(self): EXPECTED_ADMIN_IDS = [336] admin_ids_original = self.api.get_event_admins_ajax(SAMPLE_EVENT_ID) - self.assertEqual(admin_ids_original, EXPECTED_ADMIN_IDS) + assert admin_ids_original == EXPECTED_ADMIN_IDS admin_ids_change = [0, 1, 2] result = self.api.set_event_admins_ajax(SAMPLE_EVENT_ID, admin_ids_change) - self.assertTrue(result) + assert result admin_ids_test = self.api.get_event_admins_ajax(SAMPLE_EVENT_ID) - self.assertEqual(admin_ids_change, admin_ids_test) + assert admin_ids_change == admin_ids_test - self.assertTrue( - self.api.set_event_admins_ajax( - SAMPLE_EVENT_ID, EXPECTED_ADMIN_IDS)) + assert self.api.set_event_admins_ajax(SAMPLE_EVENT_ID, EXPECTED_ADMIN_IDS) - def test_get_event_masterdata(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_get_event_masterdata(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Tries to get a list of event masterdata and a type of masterdata from CT The values depend on your system data! - Test case is valid against ELKW1610.KRZ.TOOLS :return: """ result = self.api.get_event_masterdata() - self.assertEqual(len(result), 5) + assert len(result) == 5 - result = self.api.get_event_masterdata(type='serviceGroups') - self.assertGreater(len(result), 1) - self.assertEqual(result[0]['name'], 'Programm') + result = self.api.get_event_masterdata(type="serviceGroups") + assert len(result) > 1 + assert result[0]["name"] == "Programm" - result = self.api.get_event_masterdata( - type='serviceGroups', returnAsDict=True) - self.assertIsInstance(result, dict) - result = self.api.get_event_masterdata( - type='serviceGroups', returnAsDict=False) - self.assertIsInstance(result, list) + result = self.api.get_event_masterdata(type="serviceGroups", returnAsDict=True) + assert isinstance(result, dict) + result = self.api.get_event_masterdata(type="serviceGroups", returnAsDict=False) + assert isinstance(result, list) - def test_get_event_agenda(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_get_event_agenda(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Tries to get an event agenda from a CT Event Event ID may vary depending on the server used @@ -239,77 +263,73 @@ def test_get_event_agenda(self): """ eventId = 484 result = self.api.get_event_agenda(eventId) - self.assertIsNotNone(result) + assert result is not None - def test_export_event_agenda(self): - """ - IMPORTANT - This test method and the parameters used depend on the target system! + def test_export_event_agenda(self) -> None: + """IMPORTANT - This test method and the parameters used depend on the target system! Test function to download an Event Agenda file package for e.g. Songbeamer Event ID may vary depending on the server used On ELKW1610.KRZ.TOOLS event ID 484 is an existing Event with schedule (20th. Nov 2022) - """ + """ eventId = 484 - agendaId = self.api.get_event_agenda(eventId)['id'] + agendaId = self.api.get_event_agenda(eventId)["id"] with self.assertLogs(level=logging.WARNING) as captured: - download_result = self.api.export_event_agenda('SONG_BEAMER') - self.assertEqual(len(captured.records), 1) - self.assertFalse(download_result) + download_result = self.api.export_event_agenda("SONG_BEAMER") + assert len(captured.records) == 1 + assert not download_result - if os.path.exists('downloads'): - for file in os.listdir('downloads'): - os.remove('downloads/' + file) - self.assertEqual(len(os.listdir('downloads')), 0) + download_dir = Path("downloads") + for root, dirs, files in download_dir.walk(top_down=False): + for name in files: + (root / name).unlink() + for name in dirs: + (root / name).rmdir() - download_result = self.api.export_event_agenda( - 'SONG_BEAMER', agendaId=agendaId) - self.assertTrue(download_result) + download_result = self.api.export_event_agenda("SONG_BEAMER", agendaId=agendaId) + assert download_result - download_result = self.api.export_event_agenda( - 'SONG_BEAMER', eventId=eventId) - self.assertTrue(download_result) + download_result = self.api.export_event_agenda("SONG_BEAMER", eventId=eventId) + assert download_result - self.assertEqual(len(os.listdir('downloads')), 2) + assert len(os.listdir("downloads")) == 2 - def test_get_services(self): - """ - Tries to get all and a single services configuration from the server + def test_get_services(self) -> None: + """Tries to get all and a single services configuration from the server serviceId varies depending on the server used id 1 = Predigt and more than one item exsits On any KRZ.TOOLS serviceId 1 is named 'Predigt' and more than one service exists by default (13. Jan 2023) :return: """ serviceId = 1 result1 = self.api.get_services() - self.assertIsInstance(result1, list) - self.assertIsInstance(result1[0], dict) - self.assertGreater(len(result1), 1) + assert isinstance(result1, list) + assert isinstance(result1[0], dict) + assert len(result1) > 1 result2 = self.api.get_services(serviceId=serviceId) - self.assertIsInstance(result2, dict) - self.assertEqual(result2['name'], 'Predigt') + assert isinstance(result2, dict) + assert result2["name"] == "Predigt" result3 = self.api.get_services(returnAsDict=True) - self.assertIsInstance(result3, dict) + assert isinstance(result3, dict) result4 = self.api.get_services(returnAsDict=False) - self.assertIsInstance(result4, list) + assert isinstance(result4, list) - def test_get_tags(self): - """ - Test function for get_tags() with default type song + def test_get_tags(self) -> None: + """Test function for get_tags() with default type song On ELKW1610.KRZ.TOOLS tag ID 49 has the name To Do :return: """ result = self.api.get_tags() - self.assertGreater(len(result), 0) - test_tag = [item for item in result if item['id'] == 49][0] - self.assertEqual(test_tag['id'], 49) - self.assertEqual(test_tag['name'], 'ToDo') + assert len(result) > 0 + test_tag = next(item for item in result if item["id"] == 49) + assert test_tag["id"] == 49 + assert test_tag["name"] == "ToDo" - def test_has_event_schedule(self): - """ - Tries to get boolean if event agenda exists for a CT Event + def test_has_event_schedule(self) -> None: + """Tries to get boolean if event agenda exists for a CT Event Event ID may vary depending on the server used On ELKW1610.KRZ.TOOLS event ID 484 is an existing Event with schedule (20th. Nov 2022) 2376 does not have one @@ -317,36 +337,36 @@ def test_has_event_schedule(self): """ eventId = 484 result = self.api.get_event_agenda(eventId) - self.assertIsNotNone(result) + assert result is not None eventId = 2376 result = self.api.get_event_agenda(eventId) - self.assertIsNone(result) + assert result is None - def test_get_event_by_calendar_appointment(self): - """ - Check that event can be retrieved based on known calendar entry + def test_get_event_by_calendar_appointment(self) -> None: + """Check that event can be retrieved based on known calendar entry On ELKW1610.KRZ.TOOLS (26th. Nov 2023) sample is event_id:2261 - appointment:304976 starts on 2023-11-26T09:00:00Z + appointment:304976 starts on 2023-11-26T09:00:00Z. """ event_id = 2261 appointment_id = 304976 - start_date = '2023-11-26T09:00:00Z' - start_date = datetime.strptime(start_date, '%Y-%m-%dT%H:%M:%SZ') + start_date = "2023-11-26T09:00:00Z" + start_date = datetime.strptime(start_date, "%Y-%m-%dT%H:%M:%SZ") - result = self.api.get_event_by_calendar_appointment( - appointment_id, start_date) - self.assertEqual(event_id, result['id']) + result = self.api.get_event_by_calendar_appointment(appointment_id, start_date) + assert event_id == result["id"] - def test_get_persons_with_service(self): + def test_get_persons_with_service(self) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ SAMPLE_EVENT_ID = 3348 SAMPLE_SERVICE_ID = 1 result = self.api.get_persons_with_service( - eventId=SAMPLE_EVENT_ID, serviceId=SAMPLE_SERVICE_ID + eventId=SAMPLE_EVENT_ID, + serviceId=SAMPLE_SERVICE_ID, ) - self.assertGreaterEqual(len(result), 1) - self.assertGreaterEqual(result[0]["serviceId"], 1) \ No newline at end of file + assert len(result) >= 1 + assert result[0]["serviceId"] >= 1 diff --git a/tests/test_churchtools_api_resources.py b/tests/test_churchtools_api_resources.py index 83e9d50..a2bdbaf 100644 --- a/tests/test_churchtools_api_resources.py +++ b/tests/test_churchtools_api_resources.py @@ -1,8 +1,9 @@ -from datetime import datetime, timedelta import json import logging import logging.config +from datetime import datetime, timedelta from pathlib import Path + from tests.test_churchtools_api_abstract import TestsChurchToolsApiAbstract logger = logging.getLogger(__name__) @@ -16,8 +17,8 @@ logging.config.dictConfig(config=logging_config) -class Test_churchtools_api_resources(TestsChurchToolsApiAbstract): - def test_get_resource_masterdata_resourceTypes(self): +class TestChurchtoolsApiResources(TestsChurchToolsApiAbstract): + def test_get_resource_masterdata_resourceTypes(self) -> None: """Check resourceTypes can be retrieved. IMPORTANT - This test method and the parameters used depend on the target system! @@ -33,7 +34,7 @@ def test_get_resource_masterdata_resourceTypes(self): } assert expected_sample in result - def test_get_resource_masterdata_resources(self): + def test_get_resource_masterdata_resources(self) -> None: """Check resources can be retrieved. IMPORTANT - This test method and the parameters used depend on the target system! @@ -56,51 +57,56 @@ def test_get_resource_masterdata_resources(self): } assert expected_sample in result - def test_get_resource_masterdata_other(self, caplog): + def test_get_resource_masterdata_other(self, caplog) -> None: caplog.set_level(logging.ERROR) self.api.get_resource_masterdata(result_type="") expected_error_message = "get_resource_masterdata does not know result_type=" assert expected_error_message in caplog.messages - def test_get_booking_by_id(self): + def test_get_booking_by_id(self) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" - + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ SAMPLE_BOOKING_ID = 5108 result = self.api.get_bookings(booking_id=SAMPLE_BOOKING_ID) - assert SAMPLE_BOOKING_ID == result[0]["id"] + assert result[0]["id"] == SAMPLE_BOOKING_ID - def test_get_booking_by_resource_ids(self): + def test_get_booking_by_resource_ids(self) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ RESOURCE_ID_SAMPLES = [8, 20] result = self.api.get_bookings(resource_ids=RESOURCE_ID_SAMPLES) result_resource_ids = {i["base"]["resource"]["id"] for i in result} assert set(RESOURCE_ID_SAMPLES) == result_resource_ids - def test_get_booking_by_status_ids(self, caplog): + def test_get_booking_by_status_ids(self, caplog) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ caplog.set_level(logging.ERROR) STATUS_ID_SAMPLES = [2] self.api.get_bookings(status_ids=STATUS_ID_SAMPLES) expected_response = "invalid argument combination in get_bookings - please check docstring for requirements" assert expected_response in caplog.messages - def test_get_booking_by_resource_and_status_ids(self): + def test_get_booking_by_resource_and_status_ids(self) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ STATUS_ID_SAMPLES = [2] RESOURCE_ID_SAMPLES = [8] result = self.api.get_bookings( - resource_ids=RESOURCE_ID_SAMPLES, status_ids=STATUS_ID_SAMPLES + resource_ids=RESOURCE_ID_SAMPLES, + status_ids=STATUS_ID_SAMPLES, ) assert set(RESOURCE_ID_SAMPLES) == {i["base"]["resource"]["id"] for i in result} assert set(STATUS_ID_SAMPLES) == {i["base"]["statusId"] for i in result} - def test_get_booking_from_to_date_without_resource_id(self, caplog): + def test_get_booking_from_to_date_without_resource_id(self, caplog) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ caplog.set_level(logging.WARNING) SAMPLE_DATES = { "from_": datetime(year=2024, month=12, day=24), @@ -111,9 +117,10 @@ def test_get_booking_from_to_date_without_resource_id(self, caplog): expected_response = "invalid argument combination in get_bookings - please check docstring for requirements" assert expected_response in caplog.messages - def test_get_booking_from_date(self, caplog): + def test_get_booking_from_date(self, caplog) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ caplog.set_level(logging.INFO) RESOURCE_ID_SAMPLES = [8, 20] SAMPLE_DATES = { @@ -122,7 +129,8 @@ def test_get_booking_from_date(self, caplog): } result = self.api.get_bookings( - from_=SAMPLE_DATES["from_"], resource_ids=RESOURCE_ID_SAMPLES + from_=SAMPLE_DATES["from_"], + resource_ids=RESOURCE_ID_SAMPLES, ) assert set(RESOURCE_ID_SAMPLES) == {i["base"]["resource"]["id"] for i in result} @@ -131,7 +139,7 @@ def test_get_booking_from_date(self, caplog): for i in result } assert all( - [SAMPLE_DATES["from_"] <= compare_date for compare_date in result_dates] + SAMPLE_DATES["from_"] <= compare_date for compare_date in result_dates ) expected_response = ( @@ -139,9 +147,10 @@ def test_get_booking_from_date(self, caplog): ) assert expected_response in caplog.messages - def test_get_booking_to_date(self, caplog): + def test_get_booking_to_date(self, caplog) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ caplog.set_level(logging.INFO) RESOURCE_ID_SAMPLES = [8, 20] SAMPLE_DATES = { @@ -149,7 +158,8 @@ def test_get_booking_to_date(self, caplog): } result = self.api.get_bookings( - to_=SAMPLE_DATES["to_"], resource_ids=RESOURCE_ID_SAMPLES + to_=SAMPLE_DATES["to_"], + resource_ids=RESOURCE_ID_SAMPLES, ) assert set(RESOURCE_ID_SAMPLES) == {i["base"]["resource"]["id"] for i in result} @@ -157,18 +167,17 @@ def test_get_booking_to_date(self, caplog): datetime.strptime(i["calculated"]["startDate"][:10], "%Y-%m-%d") for i in result } - assert all( - [SAMPLE_DATES["to_"] >= compare_date for compare_date in result_dates] - ) + assert all(SAMPLE_DATES["to_"] >= compare_date for compare_date in result_dates) expected_response = ( "missing from_ or to_ defaults to first or last day of current month" ) assert expected_response in caplog.messages - def test_get_booking_from_to_date(self, caplog): + def test_get_booking_from_to_date(self, caplog) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ RESOURCE_ID_SAMPLES = [8, 20] SAMPLE_DATES = { "from_": datetime(year=2024, month=9, day=21), @@ -189,17 +198,16 @@ def test_get_booking_from_to_date(self, caplog): for i in result } assert all( - [SAMPLE_DATES["from_"] <= compare_date for compare_date in result_dates] - ) - assert all( - [SAMPLE_DATES["to_"] >= compare_date for compare_date in result_dates] + SAMPLE_DATES["from_"] <= compare_date for compare_date in result_dates ) + assert all(SAMPLE_DATES["to_"] >= compare_date for compare_date in result_dates) - assert [] == caplog.messages + assert caplog.messages == [] - def test_get_booking_appointment_id(self, caplog): + def test_get_booking_appointment_id(self, caplog) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ caplog.set_level(logging.WARNING) RESOURCE_ID_SAMPLES = [16] @@ -219,15 +227,17 @@ def test_get_booking_appointment_id(self, caplog): assert result[0]["base"]["resource"]["id"] in set(RESOURCE_ID_SAMPLES) result = self.api.get_bookings( - appointment_id=SAMPLE_APPOINTMENT_ID, resource_ids=RESOURCE_ID_SAMPLES + appointment_id=SAMPLE_APPOINTMENT_ID, + resource_ids=RESOURCE_ID_SAMPLES, ) expected_log_message = "using appointment ID without date range might be incomplete if current month differs" assert expected_log_message in caplog.messages assert len(result) == 0 - def test_get_booking_appointment_id_daterange(self): + def test_get_booking_appointment_id_daterange(self) -> None: """IMPORTANT - This test method and the parameters used depend on the target system! - the hard coded sample exists on ELKW1610.KRZ.TOOLS""" + the hard coded sample exists on ELKW1610.KRZ.TOOLS. + """ RESOURCE_ID_SAMPLES = [16] SAMPLE_APPOINTMENT_ID = 327883 @@ -244,7 +254,8 @@ def test_get_booking_appointment_id_daterange(self): ) result_date = datetime.strptime( - result[0]["calculated"]["startDate"][:10], "%Y-%m-%d" + result[0]["calculated"]["startDate"][:10], + "%Y-%m-%d", ) assert len(result) == 1 diff --git a/version.py b/version.py index cb1347b..6fcf59e 100644 --- a/version.py +++ b/version.py @@ -1,7 +1,9 @@ +"""helper script to define version number for automation.""" + import os -VERSION = '1.6.0' +VERSION = "1.6.0" __version__ = VERSION -if __name__ == '__main__': - os.environ['VERSION'] = VERSION +if __name__ == "__main__": + os.environ["VERSION"] = VERSION