From d72340d6774b852f93f61a96ae07e3980c261f22 Mon Sep 17 00:00:00 2001 From: Guillaume Troupel Date: Tue, 24 Sep 2019 14:50:31 +0200 Subject: [PATCH 001/566] Update keycloak_admin.py automatically refresh stale token --- keycloak/keycloak_admin.py | 274 +++++++++++++++++++++++++++---------- 1 file changed, 201 insertions(+), 73 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 9fdc0407..9e7292f9 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -25,6 +25,8 @@ # internal Keycloak server ID, usually a uuid string import json +from builtins import isinstance +from typing import List, Iterable from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError @@ -45,7 +47,8 @@ class KeycloakAdmin: PAGE_SIZE = 100 - def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None): + def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None, + auto_refresh_token=None): """ :param server_url: Keycloak server url @@ -55,26 +58,41 @@ def __init__(self, server_url, username, password, realm_name='master', client_i :param client_id: client id :param verify: True if want check connection SSL :param client_secret_key: client secret key + :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] """ + self._server_url = server_url self._username = username self._password = password - self._client_id = client_id self._realm_name = realm_name + self._client_id = client_id + self._verify = verify + self._client_secret_key = client_secret_key + self._auto_refresh_token = auto_refresh_token or [] # Get token Admin - keycloak_openid = KeycloakOpenID(server_url=server_url, client_id=client_id, realm_name=realm_name, - verify=verify, client_secret_key=client_secret_key) + self.get_token() + self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, + realm_name=self.realm_name, verify=self.verify, + client_secret_key=self.client_secret_key) grant_type = ["password"] if client_secret_key: grant_type = ["client_credentials"] - self._token = keycloak_openid.token(username, password, grant_type=grant_type) + self._token = self.keycloak_openid.token(username, password, grant_type=grant_type) self._connection = ConnectionManager(base_url=server_url, headers={'Authorization': 'Bearer ' + self.token.get('access_token'), 'Content-Type': 'application/json'}, timeout=60, verify=verify) + @property + def server_url(self): + return self._server_url + + @server_url.setter + def server_url(self, value): + self._server_url = value + @property def realm_name(self): return self._realm_name @@ -99,6 +117,22 @@ def client_id(self): def client_id(self, value): self._client_id = value + @property + def client_secret_key(self): + return self._client_secret_key + + @client_secret_key.setter + def client_secret_key(self, value): + self._client_secret_key = value + + @property + def verify(self): + return self._verify + + @verify.setter + def verify(self, value): + self._verify = value + @property def username(self): return self._username @@ -123,6 +157,20 @@ def token(self): def token(self, value): self._token = value + @property + def auto_refresh_token(self): + return self._auto_refresh_token + + @auto_refresh_token.setter + def auto_refresh_token(self, value): + allowed_methods = {'get', 'post', 'put', 'delete'} + if not isinstance(value, Iterable): + raise TypeError('Expected a list of strings among {allowed}'.format(allowed=allowed_methods)) + if not any(method not in allowed_methods for method in value): + raise TypeError('Unexpected method, accepted methods are {allowed}'.format(allowed=allowed_methods)) + + self._auto_refresh_token = value + def __fetch_all(self, url, query=None): '''Wrapper function to paginate GET requests @@ -144,7 +192,7 @@ def __fetch_all(self, url, query=None): while True: query['first'] = page*self.PAGE_SIZE partial_results = raise_error_from_response( - self.connection.raw_get(url, **query), + self.raw_get(url, **query), KeycloakGetError) if not partial_results: break @@ -164,8 +212,8 @@ def import_realm(self, payload): :return: RealmRepresentation """ - data_raw = self.connection.raw_post(URL_ADMIN_REALMS, - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_REALMS, + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) def get_realms(self): @@ -174,7 +222,7 @@ def get_realms(self): :return: realms list """ - data_raw = self.connection.raw_get(URL_ADMIN_REALMS) + data_raw = self.raw_get(URL_ADMIN_REALMS) return raise_error_from_response(data_raw, KeycloakGetError) def create_realm(self, payload, skip_exists=False): @@ -188,8 +236,8 @@ def create_realm(self, payload, skip_exists=False): :return: Keycloak server response (UserRepresentation) """ - data_raw = self.connection.raw_post(URL_ADMIN_REALMS, - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_REALMS, + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) @@ -212,7 +260,7 @@ def get_idps(self): :return: array IdentityProviderRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_IDPS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_IDPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_user(self, payload): @@ -233,8 +281,8 @@ def create_user(self, payload): if exists is not None: return str(exists) - data_raw = self.connection.raw_post(URL_ADMIN_USERS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) def users_count(self): @@ -244,7 +292,7 @@ def users_count(self): :return: counter """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_id(self, username): @@ -274,7 +322,7 @@ def get_user(self, user_id): :return: UserRepresentation """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_get(URL_ADMIN_USER.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_groups(self, user_id): @@ -286,7 +334,7 @@ def get_user_groups(self, user_id): :return: user groups list """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_get(URL_ADMIN_USER_GROUPS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_USER_GROUPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_user(self, user_id, payload): @@ -299,8 +347,8 @@ def update_user(self, user_id, payload): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_put(URL_ADMIN_USER.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def delete_user(self, user_id): @@ -312,7 +360,7 @@ def delete_user(self, user_id): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_delete(URL_ADMIN_USER.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def set_user_password(self, user_id, password, temporary=True): @@ -331,8 +379,8 @@ def set_user_password(self, user_id, password, temporary=True): """ payload = {"type": "password", "temporary": temporary, "value": password} params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def consents_user(self, user_id): @@ -344,7 +392,7 @@ def consents_user(self, user_id): :return: consents """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): @@ -362,8 +410,8 @@ def send_update_account(self, user_id, payload, client_id=None, lifespan=None, r """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} - data_raw = self.connection.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), - data=payload, **params_query) + data_raw = self.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), + data=payload, **params_query) return raise_error_from_response(data_raw, KeycloakGetError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): @@ -379,8 +427,8 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "redirect_uri": redirect_uri} - data_raw = self.connection.raw_put(URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), - data={}, **params_query) + data_raw = self.raw_put(URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), + data={}, **params_query) return raise_error_from_response(data_raw, KeycloakGetError) def get_sessions(self, user_id): @@ -395,7 +443,7 @@ def get_sessions(self, user_id): :return: UserSessionRepresentation """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_get(URL_ADMIN_GET_SESSIONS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_GET_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_server_info(self): @@ -407,7 +455,7 @@ def get_server_info(self): :return: ServerInfoRepresentation """ - data_raw = self.connection.raw_get(URL_ADMIN_SERVER_INFO) + data_raw = self.raw_get(URL_ADMIN_SERVER_INFO) return raise_error_from_response(data_raw, KeycloakGetError) def get_groups(self): @@ -432,7 +480,7 @@ def get_group(self, group_id): :return: Keycloak server response (GroupRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.connection.raw_get(URL_ADMIN_GROUP.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_subgroups(self, group, path): @@ -515,12 +563,12 @@ def create_group(self, payload, parent=None, skip_exists=False): if parent is None: params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_post(URL_ADMIN_GROUPS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_GROUPS.format(**params_path), + data=json.dumps(payload)) else: params_path = {"realm-name": self.realm_name, "id": parent, } - data_raw = self.connection.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) @@ -538,8 +586,8 @@ def update_group(self, group_id, payload): """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.connection.raw_put(URL_ADMIN_GROUP.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def group_set_permissions(self, group_id, enabled=True): @@ -552,8 +600,8 @@ def group_set_permissions(self, group_id, enabled=True): """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.connection.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), - data=json.dumps({"enabled": enabled})) + data_raw = self.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), + data=json.dumps({"enabled": enabled})) return raise_error_from_response(data_raw, KeycloakGetError) def group_user_add(self, user_id, group_id): @@ -567,7 +615,7 @@ def group_user_add(self, user_id, group_id): """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} - data_raw = self.connection.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) + data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def group_user_remove(self, user_id, group_id): @@ -581,7 +629,7 @@ def group_user_remove(self, user_id, group_id): """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} - data_raw = self.connection.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def delete_group(self, group_id): @@ -593,7 +641,7 @@ def delete_group(self, group_id): """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.connection.raw_delete(URL_ADMIN_GROUP.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def get_clients(self): @@ -607,7 +655,7 @@ def get_clients(self): """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client(self, client_id): @@ -622,7 +670,7 @@ def get_client(self, client_id): """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_id(self, client_name): @@ -653,7 +701,7 @@ def get_client_authz_settings(self, client_id): """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) return data_raw def get_client_authz_resources(self, client_id): @@ -666,7 +714,7 @@ def get_client_authz_resources(self, client_id): """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) return data_raw def create_client(self, payload, skip_exists=False): @@ -681,8 +729,8 @@ def create_client(self, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_post(URL_ADMIN_CLIENTS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) def delete_client(self, client_id): @@ -697,7 +745,7 @@ def delete_client(self, client_id): """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def get_realm_roles(self): @@ -711,7 +759,7 @@ def get_realm_roles(self): """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_roles(self, client_id): @@ -727,7 +775,7 @@ def get_client_roles(self, client_id): """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role(self, client_id, role_name): @@ -744,7 +792,7 @@ def get_client_role(self, client_id, role_name): :return: role_id """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ROLE.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role_id(self, client_id, role_name): @@ -778,8 +826,8 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name, "id": client_role_id} - data_raw = self.connection.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) def delete_client_role(self, client_role_id, role_name): @@ -793,7 +841,7 @@ def delete_client_role(self, client_role_id, role_name): :param role_name: role’s name (not id!) """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} - data_raw = self.connection.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def assign_client_role(self, user_id, client_id, roles): @@ -809,8 +857,8 @@ def assign_client_role(self, user_id, client_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} - data_raw = self.connection.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def assign_realm_roles(self, user_id, client_id, roles): @@ -826,8 +874,8 @@ def assign_realm_roles(self, user_id, client_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.connection.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def get_client_roles_of_user(self, user_id, client_id): @@ -862,7 +910,7 @@ def get_composite_client_roles_of_user(self, user_id, client_id): def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} - data_raw = self.connection.raw_get(client_level_role_mapping_url.format(**params_path)) + data_raw = self.raw_get(client_level_role_mapping_url.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_client_roles_of_user(self, user_id, client_id, roles): @@ -877,8 +925,8 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} - data_raw = self.connection.raw_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def get_authentication_flows(self): @@ -891,7 +939,7 @@ def get_authentication_flows(self): :return: Keycloak server response (AuthenticationFlowRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_FLOWS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): @@ -906,8 +954,8 @@ def create_authentication_flow(self, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_post(URL_ADMIN_FLOWS.format(**params_path), - data=payload) + data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), + data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) def get_authentication_flow_executions(self, flow_alias): @@ -917,7 +965,7 @@ def get_authentication_flow_executions(self, flow_alias): :return: Response(json) """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.connection.raw_get(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_authentication_flow_executions(self, payload, flow_alias): @@ -932,8 +980,8 @@ def update_authentication_flow_executions(self, payload, flow_alias): """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.connection.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), - data=payload) + data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), + data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def sync_users(self, storage_id, action): @@ -948,8 +996,8 @@ def sync_users(self, storage_id, action): params_query = {"action": action} params_path = {"realm-name": self.realm_name, "id": storage_id} - data_raw = self.connection.raw_post(URL_ADMIN_USER_STORAGE.format(**params_path), - data=json.dumps(data), **params_query) + data_raw = self.raw_post(URL_ADMIN_USER_STORAGE.format(**params_path), + data=json.dumps(data), **params_query) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scopes(self): @@ -961,7 +1009,7 @@ def get_client_scopes(self): """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_SCOPES.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scope(self, client_scope_id): @@ -973,7 +1021,7 @@ def get_client_scope(self, client_scope_id): """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -988,7 +1036,8 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} - data_raw = self.connection.raw_post(URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) @@ -1003,5 +1052,84 @@ def get_client_secrets(self, client_id): """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_SECRETS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_SECRETS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + + def raw_get(self, *args, **kwargs): + """ + Calls connection.raw_get. + + If auto_refresh is set for *get* and *access_token* is expired, it will refresh the token + and try *get* once more. + """ + r = self.connection.raw_get(*args, **kwargs) + if 'get' in self.auto_refresh_token and r.status_code == 401: + self.refresh_token() + return self.connection.raw_get(*args, **kwargs) + return r + + def raw_post(self, *args, **kwargs): + """ + Calls connection.raw_post. + + If auto_refresh is set for *post* and *access_token* is expired, it will refresh the token + and try *post* once more. + """ + r = self.connection.raw_post(*args, **kwargs) + if 'post' in self.auto_refresh_token and r.status_code == 401: + self.refresh_token() + return self.connection.raw_post(*args, **kwargs) + return r + + def raw_put(self, *args, **kwargs): + """ + Calls connection.raw_put. + + If auto_refresh is set for *put* and *access_token* is expired, it will refresh the token + and try *put* once more. + """ + r = self.connection.raw_put(*args, **kwargs) + if 'put' in self.auto_refresh_token and r.status_code == 401: + self.refresh_token() + return self.connection.raw_put(*args, **kwargs) + return r + + def raw_delete(self, *args, **kwargs): + """ + Calls connection.raw_delete. + + If auto_refresh is set for *delete* and *access_token* is expired, it will refresh the token + and try *delete* once more. + """ + r = self.connection.raw_delete(*args, **kwargs) + if 'delete' in self.auto_refresh_token and r.status_code == 401: + self.refresh_token() + return self.connection.raw_delete(*args, **kwargs) + return r + + def get_token(self): + self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, + realm_name=self.realm_name, verify=self.verify, + client_secret_key=self.client_secret_key) + + grant_type = ["password"] + if self.client_secret_key: + grant_type = ["client_credentials"] + self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) + self._connection = ConnectionManager(base_url=self.server_url, + headers={'Authorization': 'Bearer ' + self.token.get('access_token'), + 'Content-Type': 'application/json'}, + timeout=60, + verify=self.verify) + + def refresh_token(self): + refresh_token = self.token.get('refresh_token') + try: + self.token = self.keycloak_openid.refresh_token(refresh_token) + except KeycloakGetError as e: + if e.response_code == 400 and b'Refresh token expired' in e.response_body: + self.get_token() + else: + raise + self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token')) From 79316080a6886401cab1d3bef2a2d6f3bec2ddc0 Mon Sep 17 00:00:00 2001 From: Guillaume Troupel Date: Tue, 24 Sep 2019 16:03:02 +0200 Subject: [PATCH 002/566] Update keycloak_admin.py fixes auto_refresh_token property not using setter on KeyclaokAdmin initialization --- keycloak/keycloak_admin.py | 43 +++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 9e7292f9..2ec91698 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -47,6 +47,17 @@ class KeycloakAdmin: PAGE_SIZE = 100 + _server_url = None + _username = None + _password = None + _realm_name = None + _client_id = None + _verify = None + _client_secret_key = None + _auto_refresh_token = None + _connection = None + _token = None + def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None, auto_refresh_token=None): """ @@ -60,30 +71,18 @@ def __init__(self, server_url, username, password, realm_name='master', client_i :param client_secret_key: client secret key :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] """ - self._server_url = server_url - self._username = username - self._password = password - self._realm_name = realm_name - self._client_id = client_id - self._verify = verify - self._client_secret_key = client_secret_key - self._auto_refresh_token = auto_refresh_token or [] + self.server_url = server_url + self.username = username + self.password = password + self.realm_name = realm_name + self.client_id = client_id + self.verify = verify + self.client_secret_key = client_secret_key + self.auto_refresh_token = auto_refresh_token or [] # Get token Admin self.get_token() - self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, - realm_name=self.realm_name, verify=self.verify, - client_secret_key=self.client_secret_key) - grant_type = ["password"] - if client_secret_key: - grant_type = ["client_credentials"] - self._token = self.keycloak_openid.token(username, password, grant_type=grant_type) - self._connection = ConnectionManager(base_url=server_url, - headers={'Authorization': 'Bearer ' + self.token.get('access_token'), - 'Content-Type': 'application/json'}, - timeout=60, - verify=verify) @property def server_url(self): @@ -166,8 +165,8 @@ def auto_refresh_token(self, value): allowed_methods = {'get', 'post', 'put', 'delete'} if not isinstance(value, Iterable): raise TypeError('Expected a list of strings among {allowed}'.format(allowed=allowed_methods)) - if not any(method not in allowed_methods for method in value): - raise TypeError('Unexpected method, accepted methods are {allowed}'.format(allowed=allowed_methods)) + if not all(method in allowed_methods for method in value): + raise TypeError('Unexpected method in auto_refresh_token, accepted methods are {allowed}'.format(allowed=allowed_methods)) self._auto_refresh_token = value From 9080c79a4e8c97b8c44d2013c189f5b8d69abe31 Mon Sep 17 00:00:00 2001 From: Guillaume Troupel Date: Fri, 27 Sep 2019 15:02:36 +0200 Subject: [PATCH 003/566] return user id on user creation --- keycloak/keycloak_admin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 9fdc0407..5e4e5ef0 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -235,7 +235,9 @@ def create_user(self, payload): data_raw = self.connection.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + _last_slash_idx = data_raw.headers['Location'].rindex('/') + return data_raw.headers['Location'][_last_slash_idx + 1:] def users_count(self): """ From 4404f06aa648e5e07304a8b86ded583160dd3180 Mon Sep 17 00:00:00 2001 From: Thomas Anderson Date: Thu, 17 Oct 2019 15:52:15 +1100 Subject: [PATCH 004/566] Add function to KeycloakAdmin to add a role to a realm --- keycloak/keycloak_admin.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 5d57661a..c707ba67 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -827,6 +827,21 @@ def assign_client_role(self, user_id, client_id, roles): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def create_realm_role(self, payload, skip_exists=False): + """ + Create a new role for the realm or client + + :param realm: realm name (not id) + :param rep: RoleRepresentation https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_rolerepresentation + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + + def assign_realm_roles(self, user_id, client_id, roles): """ Assign realm roles to a user From 045dfb35768b2254f6b6a0af3bb82094d08d058b Mon Sep 17 00:00:00 2001 From: Guillaume Troupel Date: Fri, 8 Nov 2019 17:47:56 +0100 Subject: [PATCH 005/566] requests' session retry once to refresh TCP connection closed by Keycloak server fixes #36 --- keycloak/connection.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keycloak/connection.py b/keycloak/connection.py index 3826936e..5e166fcc 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -27,6 +27,7 @@ from urlparse import urljoin import requests +from requests.adapters import HTTPAdapter from .exceptions import (KeycloakConnectionError) @@ -46,6 +47,10 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True): self._timeout = timeout self._verify = verify self._s = requests.Session() + # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout + # see https://github.com/marcospereirampj/python-keycloak/issues/36 + self._s.mount('https://', HTTPAdapter(max_retries=1)) + self._s.mount('http://', HTTPAdapter(max_retries=1)) @property def base_url(self): From 1e806554b554b69bab03a1a8e82778994cccb2f3 Mon Sep 17 00:00:00 2001 From: Guillaume Troupel Date: Mon, 11 Nov 2019 11:23:42 +0100 Subject: [PATCH 006/566] Also retry on POST --- keycloak/connection.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 5e166fcc..6f324396 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -47,10 +47,17 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True): self._timeout = timeout self._verify = verify self._s = requests.Session() + # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout # see https://github.com/marcospereirampj/python-keycloak/issues/36 - self._s.mount('https://', HTTPAdapter(max_retries=1)) - self._s.mount('http://', HTTPAdapter(max_retries=1)) + for protocol in ('https://', 'http://'): + adapter = HTTPAdapter(max_retries=1) + # adds POST to retry whitelist + method_whitelist = set(adapter.max_retries.method_whitelist) + method_whitelist.add('POST') + adapter.max_retries.method_whitelist = frozenset(method_whitelist) + + self._s.mount(protocol, adapter) @property def base_url(self): From e16e054bf1f06cad9bd02f53e199cc854b7f37c3 Mon Sep 17 00:00:00 2001 From: Nicolas Marcq Date: Mon, 25 Nov 2019 17:30:00 +0100 Subject: [PATCH 007/566] [Feature] add custom headers. Closes #38 --- docs/source/index.rst | 16 +++++++++++ keycloak/keycloak_admin.py | 18 +++++++++--- keycloak/keycloak_openid.py | 9 ++++-- keycloak/tests/test_connection.py | 46 +++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index f5249201..af697da3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -92,6 +92,14 @@ Main methods:: client_secret_key="secret", verify=True) + # Optionally, you can pass custom headers that will be added to all HTTP calls + # keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", + # client_id="example_client", + # realm_name="example_realm", + # client_secret_key="secret", + # verify=True, + # custom_headers={'CustomHeader': 'value'}) + # Get WellKnow config_well_know = keycloak_openid.well_know() @@ -143,6 +151,14 @@ Main methods:: realm_name="example_realm", verify=True) + # Optionally, you can pass custom headers that will be added to all HTTP calls + #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", + # username='example-admin', + # password='secret', + # realm_name="example_realm", + # verify=True, + # custom_headers={'CustomHeader': 'value'}) + # Add user new_user = keycloak_admin.create_user({"email": "example@example.com", "username": "example@example.com", diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c707ba67..5c6fe69b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -45,7 +45,8 @@ class KeycloakAdmin: PAGE_SIZE = 100 - def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None): + def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, + client_secret_key=None, custom_headers=None): """ :param server_url: Keycloak server url @@ -55,6 +56,7 @@ def __init__(self, server_url, username, password, realm_name='master', client_i :param client_id: client id :param verify: True if want check connection SSL :param client_secret_key: client secret key + :param custom_headers: dict of custom header to pass to each HTML request """ self._username = username self._password = password @@ -63,15 +65,23 @@ def __init__(self, server_url, username, password, realm_name='master', client_i # Get token Admin keycloak_openid = KeycloakOpenID(server_url=server_url, client_id=client_id, realm_name=realm_name, - verify=verify, client_secret_key=client_secret_key) + verify=verify, client_secret_key=client_secret_key, + custom_headers=custom_headers) grant_type = ["password"] if client_secret_key: grant_type = ["client_credentials"] self._token = keycloak_openid.token(username, password, grant_type=grant_type) + headers = { + 'Authorization': 'Bearer ' + self.token.get('access_token'), + 'Content-Type': 'application/json' + } + if custom_headers is not None: + # merge custom headers to main headers + headers.update(custom_headers) + self._connection = ConnectionManager(base_url=server_url, - headers={'Authorization': 'Bearer ' + self.token.get('access_token'), - 'Content-Type': 'application/json'}, + headers=headers, timeout=60, verify=verify) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 61703e7d..b196a85c 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -43,7 +43,7 @@ class KeycloakOpenID: - def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True): + def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None): """ :param server_url: Keycloak server url @@ -51,12 +51,17 @@ def __init__(self, server_url, realm_name, client_id, client_secret_key=None, ve :param realm_name: realm name :param client_secret_key: client secret key :param verify: True if want check connection SSL + :param custom_headers: dict of custom header to pass to each HTML request """ self._client_id = client_id self._client_secret_key = client_secret_key self._realm_name = realm_name + headers = dict() + if custom_headers is not None: + # merge custom headers to main headers + headers.update(custom_headers) self._connection = ConnectionManager(base_url=server_url, - headers={}, + headers=headers, timeout=60, verify=verify) diff --git a/keycloak/tests/test_connection.py b/keycloak/tests/test_connection.py index 69496f16..4c541831 100644 --- a/keycloak/tests/test_connection.py +++ b/keycloak/tests/test_connection.py @@ -14,9 +14,11 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . +from unittest import mock from httmock import urlmatch, response, HTTMock, all_requests +from keycloak import KeycloakAdmin, KeycloakOpenID from ..connection import ConnectionManager try: @@ -141,3 +143,47 @@ def test_get_headers(self): self._conn.add_param_headers("test", "value") self.assertEqual(self._conn.headers, {"test": "value"}) + + def test_KeycloakAdmin_custom_header(self): + + class FakeToken: + @staticmethod + def get(string_val): + return "faketoken" + + fake_token = FakeToken() + + with mock.patch.object(KeycloakOpenID, "__init__", return_value=None) as mock_keycloak_open_id: + with mock.patch("keycloak.keycloak_openid.KeycloakOpenID.token", return_value=fake_token): + with mock.patch("keycloak.connection.ConnectionManager.__init__", return_value=None) as mock_connection_manager: + server_url = "https://localhost/auth/" + username = "admin" + password = "secret" + realm_name = "master" + + headers = { + 'Custom': 'test-custom-header' + } + KeycloakAdmin(server_url=server_url, + username=username, + password=password, + realm_name=realm_name, + verify=False, + custom_headers=headers) + + mock_keycloak_open_id.assert_called_with(server_url=server_url, + realm_name=realm_name, + client_id='admin-cli', + client_secret_key=None, + verify=False, + custom_headers=headers) + + expected_header = {'Authorization': 'Bearer faketoken', + 'Content-Type': 'application/json', + 'Custom': 'test-custom-header' + } + + mock_connection_manager.assert_called_with(base_url=server_url, + headers=expected_header, + timeout=60, + verify=False) From bf30c0a4091bfe777eb824a918ef0f935ee4c024 Mon Sep 17 00:00:00 2001 From: Guillaume Troupel Date: Wed, 27 Nov 2019 17:36:44 +0100 Subject: [PATCH 008/566] adds user_realm_name to KeycloakAdmin fixes #41 Adds a optional new parameter _user_realm_name_ that takes _realm_name_ value if not defined. The admin token is retrieved from the given _user_realm_name_ but all methods are run under _realm_name_. This allows to have an admin user in another realm (ie: master). --- keycloak/keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 5d57661a..423687e9 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -45,7 +45,7 @@ class KeycloakAdmin: PAGE_SIZE = 100 - def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None): + def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None, user_realm_name=None): """ :param server_url: Keycloak server url @@ -62,7 +62,7 @@ def __init__(self, server_url, username, password, realm_name='master', client_i self._realm_name = realm_name # Get token Admin - keycloak_openid = KeycloakOpenID(server_url=server_url, client_id=client_id, realm_name=realm_name, + keycloak_openid = KeycloakOpenID(server_url=server_url, client_id=client_id, realm_name=user_realm_name or realm_name, verify=verify, client_secret_key=client_secret_key) grant_type = ["password"] From df51e0e0c39060e902ab0978c00d3bd0694c725b Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 5 Dec 2019 11:26:24 -0300 Subject: [PATCH 009/566] Fixed merge with external branch. --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ba578c6a..a42bd57c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1159,7 +1159,7 @@ def raw_delete(self, *args, **kwargs): def get_token(self): self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, realm_name=self.user_realm_name or self.realm_name, verify=self.verify, - client_secret_key=self.client_secret_key + client_secret_key=self.client_secret_key, custom_headers=self.custom_headers) grant_type = ["password"] From 0f8e7f6a7cdc15d6664d7b1cbdd8e7c02c800cf1 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Wed, 11 Dec 2019 00:55:12 -0200 Subject: [PATCH 010/566] Set version --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ced16471..eb0cb2c8 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.17.6' +version = '0.18.0' # The full version, including alpha/beta/rc tags. -release = '0.17.6' +release = '0.18.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 31832219..77d210bb 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.17.6', + version='0.18.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 69d29968eea72e02ffcc3b6ed652c13cafd9556a Mon Sep 17 00:00:00 2001 From: e6646 Date: Tue, 24 Dec 2019 15:57:56 -0600 Subject: [PATCH 011/566] Updated documentation and links --- keycloak/keycloak_admin.py | 150 ++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 69 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index a42bd57c..d1bf1cd1 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -72,6 +72,7 @@ def __init__(self, server_url, username, password, realm_name='master', client_i :param verify: True if want check connection SSL :param client_secret_key: client secret key :param custom_headers: dict of custom header to pass to each HTML request + :param user_realm_name: The realm name of the user, if different from realm_name :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] """ self.server_url = server_url @@ -224,7 +225,7 @@ def import_realm(self, payload): Import a new realm from a RealmRepresentation. Realm name must be unique. RealmRepresentation - https://www.keycloak.org/docs-api/4.4/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation @@ -248,10 +249,11 @@ def create_realm(self, payload, skip_exists=False): """ Create a realm - ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_realmrepresentation + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation - :param skip_exists: Skip if Realm already exist. :param payload: RealmRepresentation + :param skip_exists: Skip if Realm already exist. :return: Keycloak server response (RealmRepresentation) """ @@ -262,8 +264,12 @@ def create_realm(self, payload, skip_exists=False): def get_users(self, query=None): """ - Get users Returns a list of users, filtered according to query parameters + Return a list of users, filtered according to query parameters + UserRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + + :param query: Query parameters (optional) :return: users list """ params_path = {"realm-name": self.realm_name} @@ -274,7 +280,7 @@ def get_idps(self): Returns a list of ID Providers, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/3.3/rest-api/index.html#_identityproviderrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation :return: array IdentityProviderRepresentation """ @@ -284,10 +290,10 @@ def get_idps(self): def create_user(self, payload): """ - Create a new user Username must be unique + Create a new user. Username must be unique UserRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :param payload: UserRepresentation @@ -320,7 +326,7 @@ def get_user_id(self, username): This is required for further actions against this user. UserRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :param username: id in UserRepresentation @@ -336,7 +342,8 @@ def get_user(self, user_id): :param user_id: User id - UserRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_userrepresentation + UserRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :return: UserRepresentation """ @@ -346,7 +353,7 @@ def get_user(self, user_id): def get_user_groups(self, user_id): """ - Get user groups Returns a list of groups of which the user is a member + Returns a list of groups of which the user is a member :param user_id: User id @@ -387,8 +394,8 @@ def set_user_password(self, user_id, password, temporary=True): Set up a password for the user. If temporary is True, the user will have to reset the temporary password next time they log in. - http://www.keycloak.org/docs-api/3.2/rest-api/#_users_resource - http://www.keycloak.org/docs-api/3.2/rest-api/#_credentialrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_users_resource + https://www.keycloak.org/docs-api/8.0/rest-api/#_credentialrepresentation :param user_id: User id :param password: New password @@ -416,14 +423,14 @@ def consents_user(self, user_id): def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): """ - Send a update account email to the user An email contains a + Send an update account email to the user. An email contains a link the user can click to perform a set of required actions. - :param user_id: - :param payload: - :param client_id: - :param lifespan: - :param redirect_uri: + :param user_id: User id + :param payload: A list of actions for the user to complete + :param client_id: Client id (optional) + :param lifespan: Number of seconds after which the generated token expires (optional) + :param redirect_uri: The redirect uri (optional) :return: """ @@ -439,8 +446,8 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): link the user can click to perform a set of required actions. :param user_id: User id - :param client_id: Client id - :param redirect_uri: Redirect uri + :param client_id: Client id (optional) + :param redirect_uri: Redirect uri (optional) :return: """ @@ -457,7 +464,7 @@ def get_sessions(self, user_id): :param user_id: id of user UserSessionRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_usersessionrepresentation :return: UserSessionRepresentation """ @@ -470,7 +477,7 @@ def get_server_info(self): Get themes, social providers, auth providers, and event listeners available on this server ServerInfoRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_serverinforepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_serverinforepresentation :return: ServerInfoRepresentation """ @@ -479,10 +486,10 @@ def get_server_info(self): def get_groups(self): """ - Get groups belonging to the realm. Returns a list of groups belonging to the realm + Returns a list of groups belonging to the realm GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: array GroupRepresentation """ @@ -494,8 +501,9 @@ def get_group(self, group_id): Get group by id. Returns full group details GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + :param group_id: The group id :return: Keycloak server response (GroupRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} @@ -507,7 +515,7 @@ def get_subgroups(self, group, path): Utility function to iterate through nested group structures GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :param name: group (GroupRepresentation) :param path: group path (string) @@ -531,8 +539,10 @@ def get_group_members(self, group_id, **query): Get members by group id. Returns group members GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_userrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_userrepresentation + :param group_id: The group id + :param query: Additional query parameters (see https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getmembers) :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} @@ -545,7 +555,7 @@ def get_group_by_path(self, path, search_in_subgroups=False): Subgroups are traversed, the first to match path (or name with path) is returned. GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :param path: group path :param search_in_subgroups: True if want search in the subgroups @@ -573,9 +583,10 @@ def create_group(self, payload, parent=None, skip_exists=False): :param payload: GroupRepresentation :param parent: parent group's id. Required to create a sub-group. + :param skip_exists: If true then do not raise an error if it already exists GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: Http response """ @@ -599,7 +610,7 @@ def update_group(self, group_id, payload): :param payload: GroupRepresentation with updated information. GroupRepresentation - http://www.keycloak.org/docs-api/3.2/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation :return: Http response """ @@ -627,7 +638,6 @@ def group_user_add(self, user_id, group_id): """ Add user to group (user_id and group_id) - :param group_id: id of group :param user_id: id of user :param group_id: id of group to add to :return: Keycloak server response @@ -641,9 +651,8 @@ def group_user_remove(self, user_id, group_id): """ Remove user from group (user_id and group_id) - :param group_id: id of group :param user_id: id of user - :param group_id: id of group to add to + :param group_id: id of group to remove from :return: Keycloak server response """ @@ -665,10 +674,10 @@ def delete_group(self, group_id): def get_clients(self): """ - Get clients belonging to the realm Returns a list of clients belonging to the realm + Returns a list of clients belonging to the realm ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response (ClientRepresentation) """ @@ -682,7 +691,7 @@ def get_client(self, client_id): Get representation of the client ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) @@ -698,7 +707,7 @@ def get_client_id(self, client_name): This is required for further actions against this client. :param client_name: name in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ @@ -715,7 +724,7 @@ def get_client_authz_settings(self, client_id): Get authorization json from client. :param client_id: id in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ @@ -728,7 +737,7 @@ def get_client_authz_resources(self, client_id): Get resources from client. :param client_id: id in ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ @@ -740,9 +749,9 @@ def create_client(self, payload, skip_exists=False): """ Create a client - ClientRepresentation: http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + ClientRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation - :param skip_exists: Skip if client already exist. + :param skip_exists: If true then do not raise an error if client already exists :param payload: ClientRepresentation :return: Keycloak server response (UserRepresentation) """ @@ -771,7 +780,7 @@ def delete_client(self, client_id): Get representation of the client ClientRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation :param client_id: keycloak client id (not oauth client-id) :return: Keycloak server response (ClientRepresentation) @@ -786,7 +795,7 @@ def get_realm_roles(self): Get all roles for the realm or client RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ @@ -802,7 +811,7 @@ def get_client_roles(self, client_id): :param client_id: id of client (not client-id) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ @@ -820,7 +829,7 @@ def get_client_role(self, client_id, role_name): :param role_name: role’s name (not id!) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -839,7 +848,7 @@ def get_client_role_id(self, client_id, role_name): :param role_name: role’s name (not id!) RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -851,10 +860,11 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): Create a client role RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param payload: RoleRepresentation + :param skip_exists: If true then do not raise an error if client role already exists :return: Keycloak server response (RoleRepresentation) """ @@ -865,10 +875,10 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): def delete_client_role(self, client_role_id, role_name): """ - Create a client role + Delete a client role RoleRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param role_name: role’s name (not id!) @@ -881,9 +891,8 @@ def assign_client_role(self, user_id, client_id, roles): """ Assign a client role to a user - :param client_id: id of client (not client-id) :param user_id: id of user - :param client_id: id of client containing role, + :param client_id: id of client (not client-id) :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ @@ -898,8 +907,8 @@ def create_realm_role(self, payload, skip_exists=False): """ Create a new role for the realm or client - :param realm: realm name (not id) - :param rep: RoleRepresentation https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_rolerepresentation + :param payload: The role (use RoleRepresentation) + :param skip_exists: If true then do not raise an error if realm role already exists :return Keycloak server response """ @@ -913,9 +922,8 @@ def assign_realm_roles(self, user_id, client_id, roles): """ Assign realm roles to a user - :param client_id: id of client (not client-id) :param user_id: id of user - :param client_id: id of client containing role, + :param client_id: id of client containing role (not client-id) :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ @@ -930,8 +938,8 @@ def get_client_roles_of_user(self, user_id, client_id): """ Get all client roles for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id) @@ -940,8 +948,8 @@ def get_available_client_roles_of_user(self, user_id, client_id): """ Get available client role-mappings for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id) @@ -950,8 +958,8 @@ def get_composite_client_roles_of_user(self, user_id, client_id): """ Get composite client role-mappings for a user. - :param client_id: id of client (not client-id) :param user_id: id of user + :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id) @@ -965,9 +973,8 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): """ Delete client roles from a user. - :param client_id: id of client (not client-id) :param user_id: id of user - :param client_id: id of client containing role, + :param client_id: id of client containing role (not client-id) :param roles: roles list or role to delete (use RoleRepresentation) :return: Keycloak server response """ @@ -982,7 +989,7 @@ def get_authentication_flows(self): Get authentication flows. Returns all flow details AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation :return: Keycloak server response (AuthenticationFlowRepresentation) """ @@ -995,9 +1002,10 @@ def create_authentication_flow(self, payload, skip_exists=False): Create a new authentication flow AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation + :param skip_exists: If true then do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ @@ -1010,6 +1018,7 @@ def get_authentication_flow_executions(self, flow_alias): """ Get authentication flow executions. Returns all execution steps + :param flow_alias: the flow alias :return: Response(json) """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} @@ -1021,9 +1030,10 @@ def update_authentication_flow_executions(self, payload, flow_alias): Update an authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/4.1/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation + :param flow_alias: The flow alias :return: Keycloak server response """ @@ -1036,8 +1046,8 @@ def sync_users(self, storage_id, action): """ Function to trigger user sync from provider - :param storage_id: - :param action: + :param storage_id: The id of the user storage provider + :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" :return: """ data = {'action': action} @@ -1051,7 +1061,7 @@ def sync_users(self, storage_id, action): def get_client_scopes(self): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes :return: Keycloak server response Array of (ClientScopeRepresentation) """ @@ -1063,8 +1073,9 @@ def get_client_scopes(self): def get_client_scope(self, client_scope_id): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + :param client_scope_id: The id of the client scope :return: Keycloak server response (ClientScopeRepresentation) """ @@ -1076,8 +1087,9 @@ def get_client_scope(self, client_scope_id): def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_create_mapper + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + :param client_scope_id: The id of the client scope :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ @@ -1093,7 +1105,7 @@ def get_client_secrets(self, client_id): """ Get representation of the client secrets - https://www.keycloak.org/docs-api/4.5/rest-api/index.html#_getclientsecret + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientsecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) From 8436dbc19d4b7de0f7557b4e5013f1730150b4fd Mon Sep 17 00:00:00 2001 From: Josh Bode Date: Thu, 16 Jan 2020 12:16:19 +1100 Subject: [PATCH 012/566] Set session `auth` to trivial function Sets the session to not add "basic" auth if entry for keycloak host exists in users `.netrc` (`requests` causes `Authorization: Basic ...` header to be added if an entry exists in `.netrc` by default) see: https://requests.readthedocs.io/en/master/api/#requests.Session.auth --- keycloak/connection.py | 1 + 1 file changed, 1 insertion(+) diff --git a/keycloak/connection.py b/keycloak/connection.py index 6f324396..451082c8 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -47,6 +47,7 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True): self._timeout = timeout self._verify = verify self._s = requests.Session() + self._s.auth = lambda x: x # don't let requests add auth headers # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout # see https://github.com/marcospereirampj/python-keycloak/issues/36 From fa9f4520d5eb9f290d062095e7d445117057a531 Mon Sep 17 00:00:00 2001 From: rike e Date: Tue, 4 Feb 2020 09:30:04 +0100 Subject: [PATCH 013/566] include data in raw_delete request data was missing in raw_delete request: requests with json bodies like in "delete_client_roles_of_user" won't work --- keycloak/connection.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 6f324396..65e8a0cc 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -198,11 +198,12 @@ def raw_put(self, path, data, **kwargs): raise KeycloakConnectionError( "Can't connect to server (%s)" % e) - def raw_delete(self, path, **kwargs): + def raw_delete(self, path, data, **kwargs): """ Submit delete request to the path. :arg path (str): Path for request. + data (dict): Payload for request. :return Response the request. :exception @@ -211,6 +212,7 @@ def raw_delete(self, path, **kwargs): try: return self._s.delete(urljoin(self.base_url, path), params=kwargs, + data=data, headers=self.headers, timeout=self.timeout, verify=self.verify) From d743e43065344c7e1f96461a7f4197000715e161 Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsI@users.noreply.github.com> Date: Fri, 14 Feb 2020 00:12:00 +0100 Subject: [PATCH 014/566] Added public key method --- README.md | 2 +- keycloak/keycloak_openid.py | 14 +++++++++++++- keycloak/urls_patterns.py | 1 + 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5f39f86a..d3b71e5e 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['ac token_info = keycloak_openid.introspect(token['access_token'])) # Decode Token -KEYCLOAK_PUBLIC_KEY = "secret" +KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() options = {"verify_signature": True, "verify_aud": True, "exp": True} token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index b196a85c..2d12678c 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -30,6 +30,7 @@ from .exceptions import raise_error_from_response, KeycloakGetError, \ KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError from .urls_patterns import ( + URL_REALM, URL_AUTH, URL_TOKEN, URL_USERINFO, @@ -263,8 +264,19 @@ def certs(self): :return: """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) + data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + def public_key(self): + """ + The public key is exposed by the realm page directly. + + :return: + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] + def entitlement(self, token, resource_server_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index fad3455d..e3f4d952 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -22,6 +22,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # OPENID URLS +URL_REALM = "realms/{realm-name}" URL_WELL_KNOWN = "realms/{realm-name}/.well-known/openid-configuration" URL_TOKEN = "realms/{realm-name}/protocol/openid-connect/token" URL_USERINFO = "realms/{realm-name}/protocol/openid-connect/userinfo" From 51b7f29dd40642d18392eb541de42820c11ed584 Mon Sep 17 00:00:00 2001 From: twsl <45483159+twsI@users.noreply.github.com> Date: Fri, 14 Feb 2020 00:29:13 +0100 Subject: [PATCH 015/566] Fixed mixed up urls --- keycloak/keycloak_openid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 2d12678c..c39dbf64 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -264,7 +264,7 @@ def certs(self): :return: """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) + data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def public_key(self): @@ -274,7 +274,7 @@ def public_key(self): :return: """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) + data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] From f2c32eb5c4820c1c223e9e21302851d4287ca5c9 Mon Sep 17 00:00:00 2001 From: Hari Yerramsetty Date: Sun, 16 Feb 2020 00:22:19 -0500 Subject: [PATCH 016/566] Documentation: Add client_secret_key while instantiating KeyCloakAdmin() --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5f39f86a..a752abb4 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", username='example-admin', password='secret', realm_name="example_realm", + client_secret_key="client-secret", verify=True) # Add user From 294df15e304bc0c0ae72f751e22831eeac4bdd51 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 18 Feb 2020 21:55:17 -0300 Subject: [PATCH 017/566] Release 0.19.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index eb0cb2c8..8614386d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.18.0' +version = '0.19.0' # The full version, including alpha/beta/rc tags. -release = '0.18.0' +release = '0.19.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 77d210bb..b89cd31c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.18.0', + version='0.19.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 7dff2e2b759e6a7009b41db4011bacc9d40fa2b2 Mon Sep 17 00:00:00 2001 From: Paolo Romolini Date: Mon, 24 Feb 2020 09:30:56 +0100 Subject: [PATCH 018/566] Add group realm roles delete, get and add Signed-off-by: Paolo Romolini Signed-off-by: Tamara Noncentini --- keycloak/keycloak_admin.py | 43 ++++++++++++++++++++++++++++++++++++++ keycloak/urls_patterns.py | 2 ++ 2 files changed, 45 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6d7bd207..089416ae 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -28,6 +28,8 @@ from builtins import isinstance from typing import List, Iterable +from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ + URL_ADMIN_GET_GROUPS_REALM_ROLES from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -936,6 +938,47 @@ def assign_realm_roles(self, user_id, client_id, roles): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def assign_group_realm_roles(self, group_id, roles): + """ + Assign realm roles to a group + + :param group_id: id of groupp + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_post(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_group_realm_roles(self, group_id, roles): + """ + Delete realm roles of a group + + :param group_id: id of group + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_delete(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def get_group_realm_roles(self, group_id): + """ + Get all realm roles for a group. + + :param user_id: id of the group + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": group_id} + data_raw = self.raw_get(URL_ADMIN_GET_GROUPS_REALM_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_roles_of_user(self, user_id, client_id): """ Get all client roles for a user. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index e3f4d952..19ca28ff 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -43,6 +43,8 @@ URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions" URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" +URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" +URL_ADMIN_GET_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings" URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" From d47131d5ab9172f7a9d6830b4a10c116481ddbdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Thu, 5 Mar 2020 12:27:41 +0100 Subject: [PATCH 019/566] make data-parameter optional Fixing issue https://github.com/marcospereirampj/python-keycloak/issues/65 --- keycloak/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 48417924..12903a0b 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -199,7 +199,7 @@ def raw_put(self, path, data, **kwargs): raise KeycloakConnectionError( "Can't connect to server (%s)" % e) - def raw_delete(self, path, data, **kwargs): + def raw_delete(self, path, data={}, **kwargs): """ Submit delete request to the path. :arg From 429761b520e0af0871073d5365bdf2e672ec4d8a Mon Sep 17 00:00:00 2001 From: Joshua Adelman Date: Thu, 5 Mar 2020 11:28:33 -0500 Subject: [PATCH 020/566] add MANIFEST.in to package license file --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..1aba38f6 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include LICENSE From 505d0e5772ee690de0b0947389073f36180c59f8 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Fri, 13 Mar 2020 16:53:57 +0100 Subject: [PATCH 021/566] Add generate_client_secrets --- keycloak/keycloak_admin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6d7bd207..634019cc 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1103,6 +1103,20 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + def generate_client_secrets(self, client_id): + """ + + Generate a new secret for the client + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_regeneratesecret + + :param client_id: id of client (not client-id) + :return: Keycloak server response (ClientRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_post(URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_secrets(self, client_id): """ From 43a7a3943b3256a9dacf6dd93c0bc077e4e4fa52 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Fri, 13 Mar 2020 17:05:03 +0100 Subject: [PATCH 022/566] Add support for Client Credentials Grant in KeycloakAdmin --- docs/source/index.rst | 8 ++++++++ keycloak/keycloak_admin.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index af697da3..597cfb25 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -158,6 +158,14 @@ Main methods:: # realm_name="example_realm", # verify=True, # custom_headers={'CustomHeader': 'value'}) + # + # You can also authenticate with client_id and client_secret + #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", + # client_id="example_client", + # client_secret_key="secret", + # realm_name="example_realm", + # verify=True, + # custom_headers={'CustomHeader': 'value'}) # Add user new_user = keycloak_admin.create_user({"email": "example@example.com", diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6d7bd207..c96d3972 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -60,7 +60,7 @@ class KeycloakAdmin: _custom_headers = None _user_realm_name = None - def __init__(self, server_url, username, password, realm_name='master', client_id='admin-cli', verify=True, + def __init__(self, server_url, username=None, password=None, realm_name='master', client_id='admin-cli', verify=True, client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None): """ From 4315d90d0be73139dc4f839cc9ce7e940cc3d4d3 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Sat, 11 Apr 2020 03:11:12 -0300 Subject: [PATCH 023/566] Release 0.20.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 8614386d..ba92885f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.19.0' +version = '0.20.0' # The full version, including alpha/beta/rc tags. -release = '0.19.0' +release = '0.20.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index b89cd31c..a0a550b9 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.19.0', + version='0.20.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 488b4fa8e1f11c0e13804c84dd54d11622762eb6 Mon Sep 17 00:00:00 2001 From: Matthew DuCharme Date: Sat, 11 Apr 2020 15:21:18 -0400 Subject: [PATCH 024/566] Added routes for deleting and updating realms --- keycloak/keycloak_admin.py | 32 +++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f3..8e66d9e8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,7 @@ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM class KeycloakAdmin: @@ -263,6 +263,36 @@ def create_realm(self, payload, skip_exists=False): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + def update_realm(self, realm_name, payload): + """ + Update a realm. This wil only update top level attributes and will ignore any user, + role, or client information in the payload. + + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + + :param realm_name: Realm name (not the realm id) + :param payload: RealmRepresentation + :return: Http response + """ + + params_path = {"realm-name": realm_name} + data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_realm(self, realm_name): + """ + Delete a realm + + :param realm_name: Realm name (not the realm id) + :return: Http response + """ + + params_path = {"realm-name": realm_name} + data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_users(self, query=None): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 19ca28ff..d49a5a91 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -75,6 +75,7 @@ URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALMS = "admin/realms" +URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" From e03a1ba9bec4ad9fb7c13db9c5c2030a03ec2b9b Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Thu, 30 Apr 2020 16:51:45 +0200 Subject: [PATCH 025/566] feat: add components --- docs/source/index.rst | 20 +++++++++++ keycloak/keycloak_admin.py | 74 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 3 ++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 597cfb25..aeb87448 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -276,3 +276,23 @@ Main methods:: # Function to trigger user sync from provider sync_users(storage_id="storage_di", action="action") + + # Rotate RSA realm keys + # List existing rsa keys + components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) + components_rsa_generated = list(filter(lambda component: component["provider-id"] == "rsa-generated")) + + # Create a new one + keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) + + for component in components_rsa_generated: + component_details = keycloak_admin.get_component(component['id']) + + # Delete inactive keys + if component_details['config']['active'] == ["false"]: + keycloak_admin.delete_component(component['id']) + + # Make previous keys inactive + else: + component_details['config']['active'] = ["false"] + keycloak_admin.update_component(component['id'], component_details) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f3..c0d8ab8e 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,7 @@ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT class KeycloakAdmin: @@ -1174,6 +1174,78 @@ def get_client_secrets(self, client_id): data_raw = self.raw_get(URL_ADMIN_CLIENT_SECRETS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_components(self, query=None): + """ + Return a list of components, filtered according to query parameters + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :param query: Query parameters (optional) + :return: components list + """ + params_path = {"realm-name": self.realm_name} + return self.__fetch_all(URL_ADMIN_COMPONENTS.format(**params_path), query) + + def create_component(self, payload): + """ + Create a new component. + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :param payload: ComponentRepresentation + + :return: UserRepresentation + """ + params_path = {"realm-name": self.realm_name} + + data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + + def get_component(self, component_id): + """ + Get representation of the component + + :param component_id: Component id + + ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :return: ComponentRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": component_id} + data_raw = self.raw_get(URL_ADMIN_COMPONENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_component(self, component_id, payload): + """ + Update the component + + :param component_id: Component id + :param payload: ComponentRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "id": component_id} + data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_component(self, component_id): + """ + Delete the component + + :param component_id: Component id + + :return: Http response + """ + params_path = {"realm-name": self.realm_name, "id": component_id} + data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def raw_get(self, *args, **kwargs): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 19ca28ff..809e1f16 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -79,3 +79,6 @@ URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" + +URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" +URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/component/{component-id}" From 77d5325c07ed2f7b103c758afb17ae2a789c000a Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Thu, 30 Apr 2020 19:01:15 +0200 Subject: [PATCH 026/566] fixes + add get_keys --- docs/source/index.rst | 26 +++++++++++--------------- keycloak/keycloak_admin.py | 30 +++++++++++++++++++++++------- keycloak/urls_patterns.py | 3 ++- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index aeb87448..2b41d354 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -277,22 +277,18 @@ Main methods:: # Function to trigger user sync from provider sync_users(storage_id="storage_di", action="action") - # Rotate RSA realm keys - # List existing rsa keys - components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) - components_rsa_generated = list(filter(lambda component: component["provider-id"] == "rsa-generated")) + # List public RSA keys + components = keycloak_admin.keys - # Create a new one - keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) + # List all keys + components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) - for component in components_rsa_generated: - component_details = keycloak_admin.get_component(component['id']) + # Create a new RSA key + component = keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) - # Delete inactive keys - if component_details['config']['active'] == ["false"]: - keycloak_admin.delete_component(component['id']) + # Update the key + component_details['config']['active'] = ["false"] + keycloak_admin.update_component(component['id']) - # Make previous keys inactive - else: - component_details['config']['active'] = ["false"] - keycloak_admin.update_component(component['id'], component_details) + # Delete the key + keycloak_admin.delete_component(component['id']) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c0d8ab8e..36dca802 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -41,8 +41,8 @@ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS class KeycloakAdmin: @@ -1185,7 +1185,9 @@ def get_components(self, query=None): :return: components list """ params_path = {"realm-name": self.realm_name} - return self.__fetch_all(URL_ADMIN_COMPONENTS.format(**params_path), query) + data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), + data=None, **query) + return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): """ @@ -1202,7 +1204,7 @@ def create_component(self, payload): data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) def get_component(self, component_id): """ @@ -1215,7 +1217,7 @@ def get_component(self, component_id): :return: ComponentRepresentation """ - params_path = {"realm-name": self.realm_name, "id": component_id} + params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_get(URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1229,7 +1231,7 @@ def update_component(self, component_id, payload): :return: Http response """ - params_path = {"realm-name": self.realm_name, "id": component_id} + params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) @@ -1242,10 +1244,24 @@ def delete_component(self, component_id): :return: Http response """ - params_path = {"realm-name": self.realm_name, "id": component_id} + params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_keys(self): + """ + Return a list of keys, filtered according to query parameters + + KeysMetadataRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_key_resource + + :return: keys list + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), + data=None) + return raise_error_from_response(data_raw, KeycloakGetError) + def raw_get(self, *args, **kwargs): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 809e1f16..8cf764bc 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -81,4 +81,5 @@ URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" -URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/component/{component-id}" +URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" +URL_ADMIN_KEYS = "admin/realms/{realm-name}/components/keys" From b1ef1d3dfda04301f64466fbd959ed4892607766 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Sun, 3 May 2020 22:25:47 +0200 Subject: [PATCH 027/566] fix indent --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 36dca802..ae66d53e 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -41,7 +41,7 @@ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS From 24fc50fae89924e3e1b0c69399bbd90f2b15e977 Mon Sep 17 00:00:00 2001 From: Romain Philibert Date: Sun, 3 May 2020 22:31:06 +0200 Subject: [PATCH 028/566] fix keys url --- keycloak/urls_patterns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 8cf764bc..95fc3450 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -82,4 +82,4 @@ URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" -URL_ADMIN_KEYS = "admin/realms/{realm-name}/components/keys" +URL_ADMIN_KEYS = "admin/realms/{realm-name}/keys" From 4f7069b6754edc854d8c62945554df92989d15e9 Mon Sep 17 00:00:00 2001 From: Shady Nawara Date: Thu, 7 May 2020 04:47:59 -0500 Subject: [PATCH 029/566] Modified update_client & create_realm_role to use keycloak_admin raw_put/post Modified update_client & create_realm_role to use the keycloak_admin raw_put/post instead of the connection's raw_put/post directly to allow for token refresh --- keycloak/keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f3..f13d889d 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -775,7 +775,7 @@ def update_client(self, client_id, payload): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_put(URL_ADMIN_CLIENT.format(**params_path), + data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) @@ -917,7 +917,7 @@ def create_realm_role(self, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) From 50b65c06a2af1e374351511ab7724f589eb1f367 Mon Sep 17 00:00:00 2001 From: Paolo Romolini Date: Fri, 8 May 2020 15:39:23 +0200 Subject: [PATCH 030/566] Add update and delete role by name Add 'Update a role' and 'Delete a role' by name and without passing the client parameter --- keycloak/keycloak_admin.py | 26 +++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f3..7b87b453 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,7 @@ from typing import List, Iterable from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -921,6 +921,30 @@ def create_realm_role(self, payload, skip_exists=False): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + def update_realm_role(self, role_name, payload): + """ + Update a role for the realm by name + :param role_name: The name of the role to be updated + :param payload: The role (use RoleRepresentation) + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.connection.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_realm_role(self, role_name): + """ + Delete a role for the realm by name + :param payload: The role name {'role-name':'name-of-the-role'} + :return Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.connection.raw_delete( + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) def assign_realm_roles(self, user_id, client_id, roles): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 19ca28ff..8cb88b28 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -76,6 +76,8 @@ URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" +URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" + URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" From 1f5e9ad2e4d48abaec74c14a7de53a80cf5efafe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20M=C3=BCller?= Date: Thu, 4 Jun 2020 15:13:34 +0200 Subject: [PATCH 031/566] fixing issue #91 and be backwards compatible with older keycloak versions --- keycloak/exceptions.py | 9 ++++--- keycloak/keycloak_admin.py | 50 ++++++++++++++++++------------------- keycloak/keycloak_openid.py | 2 +- 3 files changed, 32 insertions(+), 29 deletions(-) diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index a3894e7e..381d9af7 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -73,9 +73,12 @@ class KeycloakInvalidTokenError(KeycloakOperationError): pass -def raise_error_from_response(response, error, expected_code=200, skip_exists=False): - if expected_code == response.status_code: - if expected_code == requests.codes.no_content: +def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): + if expected_codes is None: + expected_codes = [200, 201, 204] + + if response.status_code in expected_codes: + if response.status_code == requests.codes.no_content: return {} try: diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 0bcf28f3..d05ca9fd 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -236,7 +236,7 @@ def import_realm(self, payload): data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_realms(self): """ @@ -261,7 +261,7 @@ def create_realm(self, payload, skip_exists=False): data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def get_users(self, query=None): @@ -310,7 +310,7 @@ def create_user(self, payload): data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) - raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) _last_slash_idx = data_raw.headers['Location'].rindex('/') return data_raw.headers['Location'][_last_slash_idx + 1:] @@ -379,7 +379,7 @@ def update_user(self, user_id, payload): params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_user(self, user_id): """ @@ -391,7 +391,7 @@ def delete_user(self, user_id): """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): """ @@ -411,7 +411,7 @@ def set_user_password(self, user_id, password, temporary=True): params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def consents_user(self, user_id): """ @@ -604,7 +604,7 @@ def create_group(self, payload, parent=None, skip_exists=False): data_raw = self.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def update_group(self, group_id, payload): """ @@ -622,7 +622,7 @@ def update_group(self, group_id, payload): params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): """ @@ -649,7 +649,7 @@ def group_user_add(self, user_id, group_id): params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): """ @@ -662,7 +662,7 @@ def group_user_remove(self, user_id, group_id): params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group(self, group_id): """ @@ -674,7 +674,7 @@ def delete_group(self, group_id): params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_clients(self): """ @@ -763,7 +763,7 @@ def create_client(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def update_client(self, client_id, payload): """ @@ -777,7 +777,7 @@ def update_client(self, client_id, payload): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.connection.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_client(self, client_id): """ @@ -792,7 +792,7 @@ def delete_client(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_realm_roles(self): """ @@ -875,7 +875,7 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def delete_client_role(self, client_role_id, role_name): """ @@ -889,7 +889,7 @@ def delete_client_role(self, client_role_id, role_name): """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): """ @@ -905,7 +905,7 @@ def assign_client_role(self, user_id, client_id, roles): params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def create_realm_role(self, payload, skip_exists=False): """ @@ -919,7 +919,7 @@ def create_realm_role(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def assign_realm_roles(self, user_id, client_id, roles): @@ -936,7 +936,7 @@ def assign_realm_roles(self, user_id, client_id, roles): params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def assign_group_realm_roles(self, group_id, roles): """ @@ -951,7 +951,7 @@ def assign_group_realm_roles(self, group_id, roles): params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_post(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): """ @@ -966,7 +966,7 @@ def delete_group_realm_roles(self, group_id, roles): params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_group_realm_roles(self, group_id): """ @@ -1027,7 +1027,7 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_authentication_flows(self): """ @@ -1057,7 +1057,7 @@ def create_authentication_flow(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def get_authentication_flow_executions(self, flow_alias): """ @@ -1085,7 +1085,7 @@ def update_authentication_flow_executions(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def sync_users(self, storage_id, action): """ @@ -1144,7 +1144,7 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): data_raw = self.raw_post( URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def generate_client_secrets(self, client_id): """ diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index c39dbf64..9ab42f05 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -251,7 +251,7 @@ def logout(self, refresh_token): data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def certs(self): """ From c135a4ebb3f97b187f80ab5f940fc7076d852fcf Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 30 Jun 2020 00:11:22 -0300 Subject: [PATCH 032/566] Fixed linter. --- keycloak/keycloak_admin.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c3ef9a96..6293d5b2 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -293,7 +293,6 @@ def delete_realm(self, realm_name): data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) - def get_users(self, query=None): """ Return a list of users, filtered according to query parameters @@ -1182,7 +1181,6 @@ def get_client_scope(self, client_scope_id): data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope @@ -1316,7 +1314,6 @@ def get_keys(self): data=None) return raise_error_from_response(data_raw, KeycloakGetError) - def raw_get(self, *args, **kwargs): """ Calls connection.raw_get. From 3273ea9f001442b3ff081bcfddcf25c3767085bf Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 30 Jun 2020 00:12:35 -0300 Subject: [PATCH 033/566] Release version 0.21.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index ba92885f..b09b16b6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.20.0' +version = '0.21.0' # The full version, including alpha/beta/rc tags. -release = '0.20.0' +release = '0.21.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index a0a550b9..acf61873 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.20.0', + version='0.21.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From a1877c5236e3b8f9697555e6ce95dd5fbe8effb5 Mon Sep 17 00:00:00 2001 From: Hari Yerramsetty Date: Fri, 10 Jul 2020 18:31:36 -0400 Subject: [PATCH 034/566] Update README.md set_user_password is an instance method KeycloakAdmin class --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 26b9e032..03b4f64a 100644 --- a/README.md +++ b/README.md @@ -150,7 +150,7 @@ response = keycloak_admin.update_user(user_id="user-id-keycloak", payload={'firstName': 'Example Update'}) # Update User Password -response = set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) +response = keycloak_admin.set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") From 0c26781a0993e18b4667943a7151d0719edd90cc Mon Sep 17 00:00:00 2001 From: Evgeni Enchev Date: Thu, 16 Jul 2020 20:20:41 +0300 Subject: [PATCH 035/566] add client-level operations for group roles --- keycloak/keycloak_admin.py | 47 ++++++++++++++++++++++++++++++++++++++ keycloak/urls_patterns.py | 1 + 2 files changed, 48 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6293d5b2..44d69f5c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1032,6 +1032,53 @@ def get_group_realm_roles(self, group_id): data_raw = self.raw_get(URL_ADMIN_GET_GROUPS_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def assign_group_client_roles(self, group_id, client_id, roles): + """ + Assign client roles to a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_post(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + + def delete_group_client_roles(self, group_id, client_id, roles): + """ + Delete client roles of a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_get(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_group_client_roles(self, group_id, client_id, roles): + """ + Get client roles of a group + + :param group_id: id of group + :param client_id: id of client (not client-id) + :param roles: roles list or role (use GroupRoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_delete(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + def get_client_roles_of_user(self, user_id, client_id): """ Get all client roles for a user. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index f08a4226..247e6c97 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -45,6 +45,7 @@ URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" URL_ADMIN_GET_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings" +URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" From 19af6bcedab663ab22c379811011ecf056635024 Mon Sep 17 00:00:00 2001 From: Evgeni Enchev Date: Thu, 16 Jul 2020 20:24:52 +0300 Subject: [PATCH 036/566] improve comment --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 44d69f5c..27423c47 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1070,7 +1070,7 @@ def get_group_client_roles(self, group_id, client_id, roles): :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response + :return Keycloak server response (array RoleRepresentation) """ payload = roles if isinstance(roles, list) else [roles] From 68dea8051a32be3ad222a5bf30922f9d99270edb Mon Sep 17 00:00:00 2001 From: Gemini Lasswell Date: Tue, 21 Jul 2020 15:57:45 -0700 Subject: [PATCH 037/566] Handle 'Token is not active' error in refresh_token If a token is acquired but not used before it times out, Keycloak will return a 400 response with the message 'Token is not active'. Change refresh_token to handle this error by getting a new token. --- keycloak/keycloak_admin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6293d5b2..22111512 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1397,7 +1397,8 @@ def refresh_token(self): try: self.token = self.keycloak_openid.refresh_token(refresh_token) except KeycloakGetError as e: - if e.response_code == 400 and b'Refresh token expired' in e.response_body: + if e.response_code == 400 and (b'Refresh token expired' in e.response_body or + b'Token is not active' in e.response_body): self.get_token() else: raise From 0baf5116477bbccd2a143e7156182ce70707501c Mon Sep 17 00:00:00 2001 From: Jonek Date: Tue, 28 Jul 2020 12:51:44 +0200 Subject: [PATCH 038/566] Fix broken link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 03b4f64a..98af23d4 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ For review- see https://github.com/marcospereirampj/python-keycloak python-keycloak depends on: * Python 3 -* [requests](http://docs.python-requests.org/en/master/) +* [requests](https://requests.readthedocs.io) * [python-jose](http://python-jose.readthedocs.io/en/latest/) ### Tests Dependencies From b98e1533774f1d4fb9145e7904d98bd45cd0609b Mon Sep 17 00:00:00 2001 From: giordyb Date: Mon, 3 Aug 2020 09:59:43 +0200 Subject: [PATCH 039/566] Update keycloak_admin.py added capabilities to add and get user's social login --- keycloak/keycloak_admin.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 6293d5b2..232c5d39 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,8 @@ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS \ + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES class KeycloakAdmin: @@ -454,6 +455,29 @@ def consents_user(self, user_id): data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_user_social_logins(self, user_id): + """ + Returns a list of federated identities/social logins of which the user has been associated with + :param user_id: User id + :return: federated identities list + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_FEDERATED_IDENTITIES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def add_user_social_login(self, user_id, provider_id, provider_userid, provider_username): + + """ + Add a federated identity / social login provider to the user + :param user_id: User id + :param provider: Social login provider id + :param realm: realm name + :return: + """ + payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} + params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} + data_raw = self.raw_post(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload)) + def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): """ Send an update account email to the user. An email contains a From 707637bfd6e68c7eeec40d693855497a6f08848a Mon Sep 17 00:00:00 2001 From: giordyb Date: Mon, 3 Aug 2020 10:00:46 +0200 Subject: [PATCH 040/566] Update urls_patterns.py added url patterns for social login --- keycloak/urls_patterns.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index f08a4226..bb22af7f 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -86,3 +86,6 @@ URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" URL_ADMIN_KEYS = "admin/realms/{realm-name}/keys" + +URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity" +URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" From 3838ed036ba2e13dc6f5774fb7bded77bcbd015d Mon Sep 17 00:00:00 2001 From: biagio Date: Mon, 3 Aug 2020 10:08:43 +0200 Subject: [PATCH 041/566] fix comma --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 232c5d39..86e321fb 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,7 @@ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS \ + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES From 03522bb5904d48933f115b878a0c2d3c4d9d4e94 Mon Sep 17 00:00:00 2001 From: biagio Date: Mon, 3 Aug 2020 10:15:08 +0200 Subject: [PATCH 042/566] added description --- keycloak/keycloak_admin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 86e321fb..58e01e2f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -470,8 +470,9 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ """ Add a federated identity / social login provider to the user :param user_id: User id - :param provider: Social login provider id - :param realm: realm name + :param provider_id: Social login provider id + :param provider_userid: userid specified by the provider + :param provider_username: username specified by the provider :return: """ payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} From 3a45fa5d4e55b4cb3685db6726a819f8972f3d64 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Sun, 16 Aug 2020 12:09:16 -0300 Subject: [PATCH 043/566] Release version 0.22.0 --- docs/source/conf.py | 4 ++-- keycloak/keycloak_admin.py | 21 ++++++++++----------- setup.py | 2 +- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index b09b16b6..96f7777c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.21.0' +version = '0.22.0' # The full version, including alpha/beta/rc tags. -release = '0.21.0' +release = '0.22.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 2f0b03a9..ae443b6c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,7 @@ from typing import List, Iterable from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -194,7 +194,6 @@ def auto_refresh_token(self, value): self._auto_refresh_token = value - def __fetch_all(self, url, query=None): '''Wrapper function to paginate GET requests @@ -280,7 +279,7 @@ def update_realm(self, realm_name, payload): params_path = {"realm-name": realm_name} data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm(self, realm_name): """ @@ -292,7 +291,7 @@ def delete_realm(self, realm_name): params_path = {"realm-name": realm_name} data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_users(self, query=None): """ @@ -986,7 +985,7 @@ def update_realm_role(self, role_name, payload): params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.connection.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm_role(self, role_name): """ @@ -998,7 +997,7 @@ def delete_realm_role(self, role_name): params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.connection.raw_delete( URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def assign_realm_roles(self, user_id, client_id, roles): """ @@ -1071,7 +1070,7 @@ def assign_group_client_roles(self, group_id, client_id, roles): params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_post(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group_client_roles(self, group_id, client_id, roles): """ @@ -1102,7 +1101,7 @@ def get_group_client_roles(self, group_id, client_id, roles): params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_delete(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): """ @@ -1328,7 +1327,7 @@ def create_component(self, payload): data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_component(self, component_id): """ @@ -1358,7 +1357,7 @@ def update_component(self, component_id, payload): params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_component(self, component_id): """ @@ -1370,7 +1369,7 @@ def delete_component(self, component_id): """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_keys(self): """ diff --git a/setup.py b/setup.py index acf61873..9467d534 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.21.0', + version='0.22.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 7f0ed66803808c58fe2a21e37a6f498e281c4cd4 Mon Sep 17 00:00:00 2001 From: ggallard Date: Wed, 19 Aug 2020 18:31:04 -0300 Subject: [PATCH 044/566] Added create_client_scope --- keycloak/keycloak_admin.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..7603a881 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1252,6 +1252,22 @@ def get_client_scope(self, client_scope_id): data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def create_client_scope(self, payload, skip_exists=False): + """ + Create a client scope + + ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + + :param payload: ClientScopeRepresentation + :param skip_exists: If true then do not raise an error if client scope already exists + :return: Keycloak server response (ClientScopeRepresentation) + """ + + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_post(URL_ADMIN_CLIENT_SCOPES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope From a02195cfd696322de836b45497cfe3f790c04979 Mon Sep 17 00:00:00 2001 From: ggallard Date: Thu, 20 Aug 2020 17:17:49 -0300 Subject: [PATCH 045/566] fixed for updated parameter on raise_error_from_response() --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 7603a881..d265a54c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1266,7 +1266,7 @@ def create_client_scope(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def add_mapper_to_client_scope(self, client_scope_id, payload): """ From f7a358b034bc73f3966dce98cd25daddc05ea1d5 Mon Sep 17 00:00:00 2001 From: ggallard Date: Thu, 20 Aug 2020 18:38:20 -0300 Subject: [PATCH 046/566] added get_client_service_account_user --- keycloak/keycloak_admin.py | 15 ++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index d265a54c..82d8323a 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,7 @@ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ - URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER class KeycloakAdmin: @@ -802,6 +802,19 @@ def get_client_authz_resources(self, client_id): data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) return data_raw + def get_client_service_account_user(self, client_id): + """ + Get service account user from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: UserRepresentation + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def create_client(self, payload, skip_exists=False): """ Create a client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8a..3369d408 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -68,6 +68,7 @@ URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" +URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" From 7e72aec7071142683dc6b6ba9a71b3de1c1a192a Mon Sep 17 00:00:00 2001 From: Manish Verma Date: Thu, 27 Aug 2020 14:19:29 +0530 Subject: [PATCH 047/566] Added code for fetching role users from role in realm and client --- keycloak/keycloak_admin.py | 25 ++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..6857d98e 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,8 @@ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ - URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ + URL_ADMIN_REALM_ROLES_MEMBERS class KeycloakAdmin: @@ -861,6 +862,16 @@ def get_realm_roles(self): data_raw = self.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_realm_role_members(self, role_name, **query): + """ + Get role members of realm by role name. + :param role_name: Name of the role. + :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) + :return: Keycloak Server Response (UserRepresentation) + """ + params_path = {"realm-name": self.realm_name, "role-name":role_name} + return self.__fetch_all(URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query) + def get_client_roles(self, client_id): """ Get all roles for the client @@ -960,6 +971,18 @@ def assign_client_role(self, user_id, client_id, roles): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_client_role_members(self, client_id, role_name, **query): + """ + Get members by client role . + :param client_id: The client id + :param role_name: the name of role to be queried. + :param query: Additional query parameters ( see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clients_resource) + :return: Keycloak server response (UserRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id":client_id, "role-name":role_name} + return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path) , query) + + def create_realm_role(self, payload, skip_exists=False): """ Create a new role for the realm or client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8a..bd8653da 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -66,6 +66,7 @@ URL_ADMIN_CLIENT_SECRETS= URL_ADMIN_CLIENT + "/client-secret" URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" +URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" @@ -75,6 +76,7 @@ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers/models" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" +URL_ADMIN_REALM_ROLES_MEMBERS = URL_ADMIN_REALM_ROLES + "/{role-name}/users" URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" From 59c3851264ab78df8826c05e55e54a7e9e3ddb14 Mon Sep 17 00:00:00 2001 From: domste Date: Thu, 27 Aug 2020 14:25:00 +0200 Subject: [PATCH 048/566] add deprecation exception on entitlement call --- keycloak/keycloak_openid.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 9ab42f05..b4d60fd4 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -28,7 +28,8 @@ from .authorization import Authorization from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError + KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, + KeycloakDeprecationError from .urls_patterns import ( URL_REALM, URL_AUTH, @@ -291,6 +292,9 @@ def entitlement(self, token, resource_server_id): self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path)) + + if data_raw.status_code == 404: + return raise_error_from_response(data_raw, KeycloakDeprecationError) return raise_error_from_response(data_raw, KeycloakGetError) From 4da5bed75e27703d160443b7181e22a29c2d72bb Mon Sep 17 00:00:00 2001 From: domste Date: Thu, 27 Aug 2020 14:27:16 +0200 Subject: [PATCH 049/566] added KeycloakDeprecationException add exception to indicate a deprecation case --- keycloak/exceptions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index 381d9af7..67da62a1 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -53,6 +53,9 @@ class KeycloakOperationError(KeycloakError): pass +class KeycloakDeprecationError(KeycloakError): + pass + class KeycloakGetError(KeycloakOperationError): pass From 2aedfec220c47f62cbabc771a43d38ad5e34df3f Mon Sep 17 00:00:00 2001 From: Abhijeet Sharma Date: Fri, 4 Sep 2020 16:49:31 +0530 Subject: [PATCH 050/566] Add Missing Methods --- keycloak/keycloak_admin.py | 31 ++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..71964db7 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -40,7 +40,7 @@ URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ - URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ + URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES @@ -306,6 +306,35 @@ def get_users(self, query=None): params_path = {"realm-name": self.realm_name} return self.__fetch_all(URL_ADMIN_USERS.format(**params_path), query) + def create_idp(self, payload): + """ + Create an ID Provider, + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation + + :param: payload: IdentityProviderRepresentation + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def add_mapper_to_idp(self, idp_alias, payload): + """ + Create an ID Provider, + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityprovidermapperrepresentation + + :param: idp_alias: alias for Idp to add mapper in + :param: payload: IdentityProviderMapperRepresentation + """ + params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} + data_raw = self.raw_post(URL_ADMIN_IDP_MAPPERS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def get_idps(self): """ Returns a list of ID Providers, diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8a..2c6fcfc8 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -78,6 +78,7 @@ URL_ADMIN_REALMS = "admin/realms" URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" +URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" From 454a8e81a42f715b8ec6f304e2e4dfb51b032994 Mon Sep 17 00:00:00 2001 From: Attila Csoma Date: Fri, 4 Sep 2020 22:06:54 +0200 Subject: [PATCH 051/566] Add installation provider download --- keycloak/keycloak_admin.py | 20 +++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..52c84658 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -41,7 +41,7 @@ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES @@ -847,6 +847,24 @@ def delete_client(self, client_id): data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_client_installation_provider(self, client_id, provider_id): + """ + Get content for given installation provider + + Related documentation: + https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_clients_resource + + Possible provider_id list available in the ServerInfoRepresentation#clientInstallations + https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_serverinforepresentation + + :param client_id: Client id + :param provider_id: provider id to specify response format + """ + + params_path = {"realm-name": self.realm_name, "id": client_id, "provider-id": provider_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def get_realm_roles(self): """ Get all roles for the realm or client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8a..f6d7ef52 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -69,6 +69,7 @@ URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" +URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" From 65721b6c405d1e6283f7d9975ae26d10bb503b48 Mon Sep 17 00:00:00 2001 From: David-Lor <17401854+David-Lor@users.noreply.github.com> Date: Sun, 6 Sep 2020 19:10:05 +0200 Subject: [PATCH 052/566] Remove unused client_id arg from assign_realm_roles method --- keycloak/keycloak_admin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index a42bd57c..f0b3c529 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -909,13 +909,11 @@ def create_realm_role(self, payload, skip_exists=False): return raise_error_from_response(data_raw, KeycloakGetError, expected_code=201, skip_exists=skip_exists) - def assign_realm_roles(self, user_id, client_id, roles): + def assign_realm_roles(self, user_id, roles): """ Assign realm roles to a user - :param client_id: id of client (not client-id) :param user_id: id of user - :param client_id: id of client containing role, :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ From 5a3b13f256629963a687960f9da2bcc2c355c0e2 Mon Sep 17 00:00:00 2001 From: David-Lor <17401854+David-Lor@users.noreply.github.com> Date: Sun, 6 Sep 2020 19:10:29 +0200 Subject: [PATCH 053/566] docs: add example to assign realm roles to user in README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 5f39f86a..67a51d18 100644 --- a/README.md +++ b/README.md @@ -235,6 +235,10 @@ realm_roles = keycloak_admin.get_roles() # Assign client role to user. Note that BOTH role_name and role_id appear to be required. keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id=role_id, role_name="test") +# Assign realm roles to user +keycloak_admin.assign_realm_roles(user_id=user_id, roles=realm_roles) + + # Get all ID Providers idps = keycloak_admin.get_idps() From f59dc97466fb58fb1aede31359bc51e5f9199b47 Mon Sep 17 00:00:00 2001 From: Abhijeet Sharma Date: Tue, 8 Sep 2020 14:33:25 +0530 Subject: [PATCH 054/566] Added delete_idp method --- keycloak/keycloak_admin.py | 12 +++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 71964db7..a03ef929 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -39,7 +39,7 @@ URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \ URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ - URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, \ + URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ @@ -348,6 +348,16 @@ def get_idps(self): data_raw = self.raw_get(URL_ADMIN_IDPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def delete_idp(self, idp_alias): + """ + Deletes ID Provider, + + :param: idp_alias: idp alias name + """ + params_path = {"realm-name": self.realm_name, "alias": idp_alias} + data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def create_user(self, payload): """ Create a new user. Username must be unique diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 2c6fcfc8..11187c14 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -79,6 +79,7 @@ URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" +URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" From 0c0bec4e6668fcc44e6c25c654c4d7402027c803 Mon Sep 17 00:00:00 2001 From: andres Date: Wed, 23 Sep 2020 12:57:16 +0200 Subject: [PATCH 055/566] Added get_realm_roles_of_user --- keycloak/keycloak_admin.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..36c3cb82 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1015,6 +1015,11 @@ def assign_realm_roles(self, user_id, client_id, roles): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_realm_roles_of_user(self, user_id): + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_group_realm_roles(self, group_id, roles): """ Assign realm roles to a group From c17fdaa3d8522a39a5c812d8e40e60d09ca367f5 Mon Sep 17 00:00:00 2001 From: andres Date: Wed, 23 Sep 2020 12:57:54 +0200 Subject: [PATCH 056/566] Added get_realm_roles_of_user --- keycloak/keycloak_admin.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 36c3cb82..40fa977c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1016,6 +1016,13 @@ def assign_realm_roles(self, user_id, client_id, roles): return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_realm_roles_of_user(self, user_id): + """ + Get all realm roles for a user. + + :param user_id: id of user + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) From 28a2f4eb11f4a943d382b847ddde4d7864755315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Weing=C3=A4rtner?= Date: Wed, 23 Sep 2020 22:14:26 -0300 Subject: [PATCH 057/566] Create add mapper to SP Client and delete mapper from scope methods --- keycloak/keycloak_admin.py | 38 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..bfa6e553 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,8 @@ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ - URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, \ + URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS class KeycloakAdmin: @@ -1269,6 +1270,41 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): + """ + Delete a mapper from a client scope + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_delete_mapper + + :param client_scope_id: The id of the client scope + :param payload: ProtocolMapperRepresentation + :return: Keycloak server Response + """ + + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mppaer_id} + + data_raw = self.raw_delete( + URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def add_mapper_to_client(self, client_id, payload): + """ + Add a mapper to a client + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + + :param client_id: The id of the client + :param payload: ProtocolMapperRepresentation + :return: Keycloak server Response + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + + data_raw = self.raw_post( + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def generate_client_secrets(self, client_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8a..bc7fd72f 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -69,10 +69,12 @@ URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" +URL_ADMIN_CLIENT_PROTOCOL_MAPPER = URL_ADMIN_CLIENT + "/protocol-mappers/models" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER = URL_ADMIN_CLIENT_SCOPE + "/protocol-mappers/models" +URL_ADMIN_CLIENT_SCOPES_MAPPERS = URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER + "/{protocol-mapper-id}" URL_ADMIN_REALM_ROLES = "admin/realms/{realm-name}/roles" URL_ADMIN_REALMS = "admin/realms" From 6757373d29b8d0776e3ab9207c82421e0d4e8acf Mon Sep 17 00:00:00 2001 From: Bob van Luijt Date: Wed, 30 Sep 2020 15:59:22 +0200 Subject: [PATCH 058/566] Added user_realm_name to the docs --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 98af23d4..4204c56e 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,8 @@ from keycloak import KeycloakAdmin keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", username='example-admin', password='secret', - realm_name="example_realm", + realm_name="master", + user_realm_name="only_if_other_realm_than_master", client_secret_key="client-secret", verify=True) From 80729ae3ccf0cf98ceef423f690ee0c60deaf624 Mon Sep 17 00:00:00 2001 From: Bergiu Date: Thu, 1 Oct 2020 13:53:14 +0200 Subject: [PATCH 059/566] fixed #104 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98af23d4..aaae5b44 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ count_users = keycloak_admin.users_count() users = keycloak_admin.get_users({}) # Get user ID from name -user-id-keycloak = keycloak_admin.get_user_id("example@example.com") +user_id_keycloak = keycloak_admin.get_user_id("example@example.com") # Get User user = keycloak_admin.get_user("user-id-keycloak") @@ -175,7 +175,7 @@ server_info = keycloak_admin.get_server_info() clients = keycloak_admin.get_clients() # Get client - id (not client-id) from client by name -client_id=keycloak_admin.get_client_id("my-client") +client_id = keycloak_admin.get_client_id("my-client") # Get representation of the client - id of client (not client-id) client = keycloak_admin.get_client(client_id="client_id") From 171d12a675590914d708c98ff66dafb067fe79bb Mon Sep 17 00:00:00 2001 From: "A. Shpak" Date: Tue, 20 Oct 2020 20:07:57 +0300 Subject: [PATCH 060/566] fix wrong param in example from readme --- README.md | 2 +- docs/source/index.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98af23d4..ea354b48 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ token_info = keycloak_openid.introspect(token['access_token'])) # Decode Token KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() -options = {"verify_signature": True, "verify_aud": True, "exp": True} +options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) # Get permissions by token diff --git a/docs/source/index.rst b/docs/source/index.rst index 2b41d354..0cd6e2fe 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -132,7 +132,7 @@ Main methods:: # Decode Token KEYCLOAK_PUBLIC_KEY = "secret" - options = {"verify_signature": True, "verify_aud": True, "exp": True} + options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) # Get permissions by token From b293f0170a2ea58ef9e14593387ddf92eedfd9ff Mon Sep 17 00:00:00 2001 From: BartoszCki Date: Wed, 21 Oct 2020 22:40:27 +0200 Subject: [PATCH 061/566] Fetch users/groups/group members with pagination --- keycloak/keycloak_admin.py | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..b168d869 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -222,6 +222,13 @@ def __fetch_all(self, url, query=None): page += 1 return results + def __fetch_paginated(self, url, query=None): + query = query or {} + + return raise_error_from_response( + self.raw_get(url, **query), + KeycloakGetError) + def import_realm(self, payload): """ Import a new realm from a RealmRepresentation. Realm name must be unique. @@ -303,8 +310,14 @@ def get_users(self, query=None): :param query: Query parameters (optional) :return: users list """ + query = query or {} params_path = {"realm-name": self.realm_name} - return self.__fetch_all(URL_ADMIN_USERS.format(**params_path), query) + url = URL_ADMIN_USERS.format(**params_path) + + if "first" in query or "max" in query: + return self.__fetch_paginated(url, query) + + return self.__fetch_all(url, query) def get_idps(self): """ @@ -541,7 +554,7 @@ def get_server_info(self): data_raw = self.raw_get(URL_ADMIN_SERVER_INFO) return raise_error_from_response(data_raw, KeycloakGetError) - def get_groups(self): + def get_groups(self, query=None): """ Returns a list of groups belonging to the realm @@ -550,8 +563,14 @@ def get_groups(self): :return: array GroupRepresentation """ + query = query or {} params_path = {"realm-name": self.realm_name} - return self.__fetch_all(URL_ADMIN_GROUPS.format(**params_path)) + url = URL_ADMIN_USERS.format(**params_path) + + if "first" in query or "max" in query: + return self.__fetch_paginated(url, query) + + return self.__fetch_all(url, query) def get_group(self, group_id): """ @@ -603,7 +622,12 @@ def get_group_members(self, group_id, **query): :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - return self.__fetch_all(URL_ADMIN_GROUP_MEMBERS.format(**params_path), query) + url = URL_ADMIN_USERS.format(**params_path) + + if "first" in query or "max" in query: + return self.__fetch_paginated(url, query) + + return self.__fetch_all(url, query) def get_group_by_path(self, path, search_in_subgroups=False): """ From 89225eff12b5e92b29b12f782accce6f8255c7d0 Mon Sep 17 00:00:00 2001 From: rpisani5c <55719539+rpisani5c@users.noreply.github.com> Date: Tue, 27 Oct 2020 12:33:44 -0500 Subject: [PATCH 062/566] Update keycloak_admin.py Swapped the names for get_group_client_roles & delete_group_client_roles respectively. Updated usage, removed the roles object build for the get_ method. --- keycloak/keycloak_admin.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..53ddab43 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1072,9 +1072,9 @@ def assign_group_client_roles(self, group_id, client_id, roles): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def delete_group_client_roles(self, group_id, client_id, roles): + def get_group_client_roles(self, group_id, client_id): """ - Delete client roles of a group + Get client roles of a group :param group_id: id of group :param client_id: id of client (not client-id) @@ -1082,14 +1082,13 @@ def delete_group_client_roles(self, group_id, client_id, roles): :return Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_get(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_group_client_roles(self, group_id, client_id, roles): + def delete_group_client_roles(self, group_id, client_id, roles): """ - Get client roles of a group + Delete client roles of a group :param group_id: id of group :param client_id: id of client (not client-id) From 70b9efeaa30017208bd1b641637ff051142f6b21 Mon Sep 17 00:00:00 2001 From: rpisani5c <55719539+rpisani5c@users.noreply.github.com> Date: Tue, 27 Oct 2020 12:36:42 -0500 Subject: [PATCH 063/566] Update keycloak_admin.py Removed the roles param definition on the get_group_client_roles method. --- keycloak/keycloak_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 53ddab43..6356cfef 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1078,7 +1078,6 @@ def get_group_client_roles(self, group_id, client_id): :param group_id: id of group :param client_id: id of client (not client-id) - :param roles: roles list or role (use GroupRoleRepresentation) :return Keycloak server response """ From 42ee704d58287008b4e31c384510f13e02abb0ff Mon Sep 17 00:00:00 2001 From: PGijsbers Date: Wed, 28 Oct 2020 12:29:21 +0200 Subject: [PATCH 064/566] Remove excess parenthesis --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98af23d4..c8088455 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['ac token_type_hint="requesting_party_token")) # Introspect Token -token_info = keycloak_openid.introspect(token['access_token'])) +token_info = keycloak_openid.introspect(token['access_token']) # Decode Token KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() From 876640616b91d3983d203f8e4771dfd4f43a72bd Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 10:13:52 +0100 Subject: [PATCH 065/566] Adds create_authentication_flow_subflow admin method --- keycloak/keycloak_admin.py | 20 +++++++++++++++++++- keycloak/urls_patterns.py | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..f546a1ea 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,7 @@ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ - URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_FLOWS_SUBFLOWS class KeycloakAdmin: @@ -1211,6 +1211,24 @@ def update_authentication_flow_executions(self, payload, flow_alias): data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): + """ + Create a new sub authentication flow for a given authentication flow + + AuthenticationFlowRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + + :param payload: AuthenticationFlowRepresentation + :param flow_alias: The flow alias + :param skip_exists: If true then do not raise an error if authentication flow already exists + :return: Keycloak server response (RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_SUBFLOWS.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def sync_users(self, storage_id, action): """ Function to trigger user sync from provider diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8a..447d28e1 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -80,9 +80,9 @@ URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" - URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +URL_ADMIN_FLOWS_SUBFLOWS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" From 39aee83585b977c4c16512c9c8feaf16f7d85722 Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 10:42:37 +0100 Subject: [PATCH 066/566] Adds create_authentication_flow_execution admin method --- keycloak/keycloak_admin.py | 22 ++++++++++++++++++++-- keycloak/urls_patterns.py | 3 ++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f546a1ea..b0c99175 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -43,7 +43,8 @@ URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ - URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_FLOWS_SUBFLOWS + URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, \ + URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW class KeycloakAdmin: @@ -1210,6 +1211,23 @@ def update_authentication_flow_executions(self, payload, flow_alias): data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def create_authentication_flow_execution(self, payload, flow_alias): + """ + Create an authentication flow execution + + AuthenticationExecutionInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + + :param payload: AuthenticationExecutionInfoRepresentation + :param flow_alias: The flow alias + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): """ @@ -1225,7 +1243,7 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_SUBFLOWS.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 447d28e1..ea10e45a 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -82,7 +82,8 @@ URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" -URL_ADMIN_FLOWS_SUBFLOWS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" +URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" +URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" From 8dfdddc5c64c5b8c4a42d4002ca3468727d38a8f Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 11:47:03 +0100 Subject: [PATCH 067/566] Adds copy_authentication_flow admin method --- keycloak/keycloak_admin.py | 17 ++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index b0c99175..726bfa82 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -44,7 +44,7 @@ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, \ - URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW + URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY class KeycloakAdmin: @@ -1184,6 +1184,21 @@ def create_authentication_flow(self, payload, skip_exists=False): data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def copy_authentication_flow(self, payload, flow_alias): + """ + Copy existing authentication flow under a new name. The new name is given as 'newName' attribute of the passed payload. + + :param payload: JSON containing 'newName' attribute + :param flow_alias: the flow alias + :return: Keycloak server response (RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} + data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path), + data=payload) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def get_authentication_flow_executions(self, flow_alias): """ Get authentication flow executions. Returns all execution steps diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index ea10e45a..a7bc39ce 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -81,6 +81,7 @@ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" +URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" From 6c62286adefdf9b6293b3ee23fbfcf4740db09df Mon Sep 17 00:00:00 2001 From: faffeldt Date: Fri, 6 Nov 2020 11:55:31 +0100 Subject: [PATCH 068/566] Adds get_authentication_flow_for_id admin method --- keycloak/keycloak_admin.py | 18 ++++++++++++++++-- keycloak/urls_patterns.py | 1 + 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 726bfa82..a8c17f28 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -44,7 +44,8 @@ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, \ - URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY + URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ + URL_ADMIN_FLOWS_ALIAS class KeycloakAdmin: @@ -1166,6 +1167,20 @@ def get_authentication_flows(self): params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + def get_authentication_flow_for_id(self, flow_id): + """ + Get one authentication flow by it's id/alias. Returns all flow details + + AuthenticationFlowRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + + :param flow_id: the id of a flow NOT it's alias + :return: Keycloak server response (AuthenticationFlowRepresentation) + """ + params_path = {"realm-name": self.realm_name, "flow-id": flow_id} + data_raw = self.raw_get(URL_ADMIN_FLOWS_ALIAS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): """ @@ -1198,7 +1213,6 @@ def copy_authentication_flow(self, payload, flow_alias): data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) - def get_authentication_flow_executions(self, flow_alias): """ Get authentication flow executions. Returns all execution steps diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index a7bc39ce..2ea23fd8 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -81,6 +81,7 @@ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" +URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" From 4624c7b2dce0869dc4d6b2396086f82eca420a5b Mon Sep 17 00:00:00 2001 From: Tamara Nocentini Date: Wed, 11 Nov 2020 14:32:18 +0100 Subject: [PATCH 069/566] Manage composite realm roles of the realm role Signed-off-by: Tamara Nocentini Signed-off-by: Paolo Romolini --- keycloak/keycloak_admin.py | 52 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ae443b6c..81201cd3 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,10 @@ from typing import List, Iterable from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ + URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, \ + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE + from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID @@ -999,6 +1002,53 @@ def delete_realm_role(self, role_name): URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def add_composite_realm_roles_to_role(self, role_name, roles): + """ + Add composite roles to the role + + :param role_name: The name of the role + :param roles: roles list or role (use RoleRepresentation) to be updated + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_post( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, + expected_codes=[204]) + + def remove_composite_realm_roles_to_role(self, role_name, roles): + """ + Remove composite roles from the role + + :param role_name: The name of the role + :param roles: roles list or role (use RoleRepresentation) to be removed + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_delete( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, + expected_codes=[204]) + + def get_composite_realm_roles_of_role(self, role_name): + """ + Get composite roles of the role + + :param role_name: The name of the role + :return Keycloak server response (array RoleRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_get( + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles(self, user_id, client_id, roles): """ Assign realm roles to a user diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 3e28de8a..78cf34c4 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -79,6 +79,7 @@ URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" +URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" From 05b3a276543341b647f16b82fcc83d9c0bbae217 Mon Sep 17 00:00:00 2001 From: Paolo Romolini Date: Fri, 13 Nov 2020 16:11:28 +0100 Subject: [PATCH 070/566] Add "Get a role by name" API Signed-off-by: Paolo Romolini --- keycloak/keycloak_admin.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 81201cd3..f98ca84f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -977,6 +977,19 @@ def create_realm_role(self, payload, skip_exists=False): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def get_realm_role(self, role_name): + """ + Get realm role by role name + :param role_name: role's name, not id! + + RoleRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + :return: role_id + """ + params_path = {"realm-name": self.realm_name, "role-name": role_name} + data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def update_realm_role(self, role_name, payload): """ Update a role for the realm by name From d046ce1d68efa6a1e884108b6385dd8a0f94ca01 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 19 Nov 2020 16:57:16 -0300 Subject: [PATCH 071/566] Fixed IndentationError --- keycloak/keycloak_openid.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index b4d60fd4..0f801ea9 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -28,8 +28,7 @@ from .authorization import Authorization from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, - KeycloakDeprecationError + KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, KeycloakDeprecationError from .urls_patterns import ( URL_REALM, URL_AUTH, From 9bfcd1f88b077497930115841b96355a56363a70 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 19 Nov 2020 17:18:11 -0300 Subject: [PATCH 072/566] Fixed imports. --- keycloak/keycloak_admin.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index dd433fc3..f88d00d7 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -26,25 +26,22 @@ import json from builtins import isinstance -from typing import List, Iterable - -from keycloak.urls_patterns import URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ - URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, \ - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE +from typing import Iterable from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, \ + URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES,\ + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_GROUPS_CLIENT_ROLES, \ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \ URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ From 2f13f6cdf1e7ca47cc43ca0c43c39ce01dc3ddf9 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Thu, 19 Nov 2020 17:30:36 -0300 Subject: [PATCH 073/566] Release 0.23.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 96f7777c..637a63b7 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.22.0' +version = '0.23.0' # The full version, including alpha/beta/rc tags. -release = '0.22.0' +release = '0.23.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 9467d534..78b8d968 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.22.0', + version='0.23.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 0646f2819b9862fb15d84d62d782e1b849708ef1 Mon Sep 17 00:00:00 2001 From: "J. Brunswicker" Date: Thu, 3 Dec 2020 15:47:16 +0100 Subject: [PATCH 074/566] - add function to update a client-scope --- keycloak/keycloak_admin.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f88d00d7..37229507 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -52,7 +52,7 @@ class KeycloakAdmin: PAGE_SIZE = 100 - + _server_url = None _username = None _password = None @@ -519,7 +519,7 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_post(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload)) - + def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): """ Send an update account email to the user. An email contains a @@ -1330,14 +1330,14 @@ def get_authentication_flows(self): params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - + def get_authentication_flow_for_id(self, flow_id): """ Get one authentication flow by it's id/alias. Returns all flow details AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation - + :param flow_id: the id of a flow NOT it's alias :return: Keycloak server response (AuthenticationFlowRepresentation) """ @@ -1403,7 +1403,7 @@ def update_authentication_flow_executions(self, payload, flow_alias): data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - + def create_authentication_flow_execution(self, payload, flow_alias): """ Create an authentication flow execution @@ -1496,6 +1496,22 @@ def create_client_scope(self, payload, skip_exists=False): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def update_client_scope(self, client_scope_id, payload): + """ + Update a client scope + + ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_client_scopes_resource + + :param client_scope_id: The id of the client scope + :param payload: ClientScopeRepresentation + :return: Keycloak server response (ClientScopeRepresentation) + """ + + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} + data_raw = self.raw_put(URL_ADMIN_CLIENT_SCOPE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope @@ -1725,18 +1741,18 @@ def get_token(self): grant_type = ["password"] if self.client_secret_key: grant_type = ["client_credentials"] - + self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) headers = { 'Authorization': 'Bearer ' + self.token.get('access_token'), 'Content-Type': 'application/json' } - + if self.custom_headers is not None: # merge custom headers to main headers headers.update(self.custom_headers) - + self._connection = ConnectionManager(base_url=self.server_url, headers=headers, timeout=60, From fa9f1741af02012d70c0dab1830503b9c225e978 Mon Sep 17 00:00:00 2001 From: Ilya Glotov Date: Fri, 4 Dec 2020 14:09:28 +0300 Subject: [PATCH 075/566] Use the same request headers and cookies --- keycloak/keycloak_admin.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f88d00d7..e5a55cb9 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -26,6 +26,7 @@ import json from builtins import isinstance +from copy import deepcopy from typing import Iterable from .connection import ConnectionManager @@ -1737,10 +1738,8 @@ def get_token(self): # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = ConnectionManager(base_url=self.server_url, - headers=headers, - timeout=60, - verify=self.verify) + self._connection = deepcopy(self.keycloak_openid.connection()) + self._connection._headers.update(headers) def refresh_token(self): refresh_token = self.token.get('refresh_token') From 21333dc6491c4b741151d4ee44c6124b6a277ec9 Mon Sep 17 00:00:00 2001 From: pjrm <4622652+pjrm@users.noreply.github.com> Date: Fri, 18 Dec 2020 15:07:19 +0000 Subject: [PATCH 076/566] Add Keycloak authenticator config management (add, update and delete methods) --- keycloak/keycloak_admin.py | 43 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f88d00d7..3126833f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -46,7 +46,7 @@ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ - URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER + URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG class KeycloakAdmin: @@ -1439,6 +1439,47 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def get_authenticator_config(self, config_id): + """ + Get authenticator configuration. Returns all configuration details. + + :param config_id: Authenticator config id + :return: Response(json) + """ + params_path = {"realm-name": self.realm_name, "id": config_id} + data_raw = self.raw_get(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_authenticator_config(self, payload, config_id): + """ + Update an authenticator configuration. + + AuthenticatorConfigRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticatorconfigrepresentation + + :param payload: AuthenticatorConfigRepresentation + :param config_id: Authenticator config id + :return: Response(json) + """ + params_path = {"realm-name": self.realm_name, "id": config_id} + data_raw = self.raw_put(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def delete_authenticator_config(self, config_id): + """ + Delete a authenticator configuration. + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource + + :param config_id: Authenticator config id + :return: Keycloak server Response + """ + + params_path = {"realm-name": self.realm_name, "id": config_id} + data_raw = self.raw_delete(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def sync_users(self, storage_id, action): """ Function to trigger user sync from provider diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 14410d84..d45ef8dd 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -95,6 +95,7 @@ URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" +URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" URL_ADMIN_COMPONENT = "admin/realms/{realm-name}/components/{component-id}" From 354cd76cee958ffcc613dc6d2e35352f082f8091 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Fri, 18 Dec 2020 12:28:03 -0300 Subject: [PATCH 077/566] Release: 0.24.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 637a63b7..1ab699d0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.23.0' +version = '0.24.0' # The full version, including alpha/beta/rc tags. -release = '0.23.0' +release = '0.24.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 78b8d968..362f1776 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.23.0', + version='0.24.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From b07ecce9a970498da050d11d6b419c316056ab21 Mon Sep 17 00:00:00 2001 From: pjrm <4622652+pjrm@users.noreply.github.com> Date: Fri, 18 Dec 2020 15:27:13 +0000 Subject: [PATCH 078/566] Maintain consistency across all methods and force all payload content to be in JSON format --- keycloak/keycloak_admin.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 8fd19f6d..9c1155c8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -536,7 +536,7 @@ def send_update_account(self, user_id, payload, client_id=None, lifespan=None, r params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} data_raw = self.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), - data=payload, **params_query) + data=json.dumps(payload), **params_query) return raise_error_from_response(data_raw, KeycloakGetError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): @@ -1359,7 +1359,7 @@ def create_authentication_flow(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), - data=payload) + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def copy_authentication_flow(self, payload, flow_alias): @@ -1373,7 +1373,7 @@ def copy_authentication_flow(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path), - data=payload) + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_authentication_flow_executions(self, flow_alias): @@ -1401,7 +1401,7 @@ def update_authentication_flow_executions(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), - data=payload) + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def create_authentication_flow_execution(self, payload, flow_alias): @@ -1418,7 +1418,7 @@ def create_authentication_flow_execution(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION.format(**params_path), - data=payload) + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): @@ -1436,7 +1436,7 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), - data=payload) + data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) def get_authenticator_config(self, config_id): From 021c549e30a522f322cb140dec28d64e4c8952d2 Mon Sep 17 00:00:00 2001 From: hamedSh Date: Sun, 10 Jan 2021 13:48:04 +0330 Subject: [PATCH 079/566] Get sessions associated with the client. --- keycloak/keycloak_admin.py | 20 ++++++++++++++++++-- keycloak/urls_patterns.py | 1 + 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 8fd19f6d..76219c36 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -32,7 +32,7 @@ from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES,\ + URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_GROUPS_CLIENT_ROLES, \ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ @@ -46,7 +46,8 @@ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ - URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG + URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ + URL_ADMIN_CLIENT_ALL_SESSIONS class KeycloakAdmin: @@ -1810,3 +1811,18 @@ def refresh_token(self): else: raise self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token')) + + def get_client_all_sessions(self, client_id): + """ + Get sessions associated with the client + + :param client_id: id of client + + UserSessionRepresentation + http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation + + :return: UserSessionRepresentation + """ + params_path = {"realm-name": self.realm_name, "client-id": client_id} + data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index d45ef8dd..95acaf9f 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -63,6 +63,7 @@ URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}" +URL_ADMIN_CLIENT_ALL_SESSIONS = URL_ADMIN_CLIENT + "/user-sessions" URL_ADMIN_CLIENT_SECRETS= URL_ADMIN_CLIENT + "/client-secret" URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" From 6ed65ea4e51098e9eb805dbfd31f4a0ac62212a5 Mon Sep 17 00:00:00 2001 From: Lukas Martini Date: Thu, 14 Jan 2021 11:57:45 +0100 Subject: [PATCH 080/566] Add exist_ok attribute to KeycloakAdmin.create_user This attribute allows configuration of the behaviour of create_user when a user with the passed username already exists. If set to False, an exception will be raised (passed through) from the API. If set to True (default), the existing user ID will silently be returned. --- README.md | 10 ++++++++++ keycloak/keycloak_admin.py | 10 ++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c2af7fcb..095782b5 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ The documentation for python-keycloak is available on [readthedocs](http://pytho * [Josha Inglis](https://bitbucket.org/joshainglis/) * [Alex](https://bitbucket.org/alex_zel/) * [Ewan Jone](https://bitbucket.org/kisamoto/) +* [Lukas Martini](https://github.com/lutoma) ## Usage @@ -125,6 +126,15 @@ new_user = keycloak_admin.create_user({"email": "example@example.com", "enabled": True, "firstName": "Example", "lastName": "Example"}) + +# Add user and raise exception if username already exists +# exist_ok currently defaults to True for backwards compatibility reasons +new_user = keycloak_admin.create_user({"email": "example@example.com", + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example"}, + exist_ok=False) # Add user and set password new_user = keycloak_admin.create_user({"email": "example@example.com", diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 8fd19f6d..b42f6a31 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -361,7 +361,7 @@ def delete_idp(self, idp_alias): data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def create_user(self, payload): + def create_user(self, payload, exist_ok=True): """ Create a new user. Username must be unique @@ -369,15 +369,17 @@ def create_user(self, payload): https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation :param payload: UserRepresentation + :param exist_ok: If False, raise KeycloakGetError if username already exists. Otherwise, return existing user ID. :return: UserRepresentation """ params_path = {"realm-name": self.realm_name} - exists = self.get_user_id(username=payload['username']) + if exist_ok: + exists = self.get_user_id(username=payload['username']) - if exists is not None: - return str(exists) + if exists is not None: + return str(exists) data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) From 0d4815ddb2bbcf5f9c642d1e5ac1b7dd3d3beecb Mon Sep 17 00:00:00 2001 From: Hans Erik Heggem Date: Wed, 3 Feb 2021 21:54:36 +0100 Subject: [PATCH 081/566] added composite clients role feature --- keycloak/keycloak_admin.py | 21 +++++++++++++++++++-- keycloak/urls_patterns.py | 3 ++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 8fd19f6d..89653325 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -32,7 +32,7 @@ from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES,\ + URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_GROUPS_CLIENT_ROLES, \ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ @@ -46,7 +46,8 @@ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ - URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG + URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE class KeycloakAdmin: @@ -1013,6 +1014,22 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): + """ + Add composite roles to client role + + :param client_role_id: id of client (not client-id) + :param role_name: The name of the role + :param roles: roles list or role (use RoleRepresentation) to be updated + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} + data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def delete_client_role(self, client_role_id, role_name): """ Delete a client role diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index d45ef8dd..64c658c8 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -63,9 +63,10 @@ URL_ADMIN_CLIENTS = "admin/realms/{realm-name}/clients" URL_ADMIN_CLIENT = URL_ADMIN_CLIENTS + "/{id}" -URL_ADMIN_CLIENT_SECRETS= URL_ADMIN_CLIENT + "/client-secret" +URL_ADMIN_CLIENT_SECRETS = URL_ADMIN_CLIENT + "/client-secret" URL_ADMIN_CLIENT_ROLES = URL_ADMIN_CLIENT + "/roles" URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" +URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composites" URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" From c71549813c2b14b6d2f787798203c9d2997bc7a5 Mon Sep 17 00:00:00 2001 From: Bekzhan Date: Mon, 8 Feb 2021 17:42:23 +0600 Subject: [PATCH 082/566] fix pytest warning --- keycloak/connection.py | 3 ++ keycloak/tests/test_connection.py | 64 ++++++++++++++++--------------- 2 files changed, 36 insertions(+), 31 deletions(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 12903a0b..3871482c 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -60,6 +60,9 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True): self._s.mount(protocol, adapter) + def __del__(self): + self._s.close() + @property def base_url(self): """ Return base url in use for requests to the server. """ diff --git a/keycloak/tests/test_connection.py b/keycloak/tests/test_connection.py index 4c541831..cb98feba 100644 --- a/keycloak/tests/test_connection.py +++ b/keycloak/tests/test_connection.py @@ -156,34 +156,36 @@ def get(string_val): with mock.patch.object(KeycloakOpenID, "__init__", return_value=None) as mock_keycloak_open_id: with mock.patch("keycloak.keycloak_openid.KeycloakOpenID.token", return_value=fake_token): with mock.patch("keycloak.connection.ConnectionManager.__init__", return_value=None) as mock_connection_manager: - server_url = "https://localhost/auth/" - username = "admin" - password = "secret" - realm_name = "master" - - headers = { - 'Custom': 'test-custom-header' - } - KeycloakAdmin(server_url=server_url, - username=username, - password=password, - realm_name=realm_name, - verify=False, - custom_headers=headers) - - mock_keycloak_open_id.assert_called_with(server_url=server_url, - realm_name=realm_name, - client_id='admin-cli', - client_secret_key=None, - verify=False, - custom_headers=headers) - - expected_header = {'Authorization': 'Bearer faketoken', - 'Content-Type': 'application/json', - 'Custom': 'test-custom-header' - } - - mock_connection_manager.assert_called_with(base_url=server_url, - headers=expected_header, - timeout=60, - verify=False) + with mock.patch("keycloak.connection.ConnectionManager.__del__", return_value=None) as mock_connection_manager_delete: + server_url = "https://localhost/auth/" + username = "admin" + password = "secret" + realm_name = "master" + + headers = { + 'Custom': 'test-custom-header' + } + KeycloakAdmin(server_url=server_url, + username=username, + password=password, + realm_name=realm_name, + verify=False, + custom_headers=headers) + + mock_keycloak_open_id.assert_called_with(server_url=server_url, + realm_name=realm_name, + client_id='admin-cli', + client_secret_key=None, + verify=False, + custom_headers=headers) + + expected_header = {'Authorization': 'Bearer faketoken', + 'Content-Type': 'application/json', + 'Custom': 'test-custom-header' + } + + mock_connection_manager.assert_called_with(base_url=server_url, + headers=expected_header, + timeout=60, + verify=False) + mock_connection_manager_delete.assert_called_once_with() From 02d82f70bce4cb3ca0a4ea53b4cf142a03b459fb Mon Sep 17 00:00:00 2001 From: Clive Jevons Date: Fri, 12 Feb 2021 20:47:38 +0100 Subject: [PATCH 083/566] update mapper in client scope --- keycloak/keycloak_admin.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 8fd19f6d..39fbdf42 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1588,6 +1588,26 @@ def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, payload): + """ + Update an existing protocol mapper in a client scope + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_protocol_mappers_resource + + :param client_scope_id: The id of the client scope + :param protocol_mapper_id: The id of the protocol mapper which exists in the client scope + and should to be updated + :param payload: ProtocolMapperRepresentation + :return: Keycloak server Response + """ + + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mapper_id} + + data_raw = self.raw_put( + URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def add_mapper_to_client(self, client_id, payload): """ Add a mapper to a client From 04ef9a0e64c69c3a2170f05fc5d0945212d13a2c Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 3 Mar 2021 20:44:46 +0100 Subject: [PATCH 084/566] Update keycloak_admin.py Fixed typo in params_path (leads to error) --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c0ac364e..af1e7997 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1861,6 +1861,6 @@ def get_client_all_sessions(self, client_id): :return: UserSessionRepresentation """ - params_path = {"realm-name": self.realm_name, "client-id": client_id} + params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) From 9c8835c2550a55c689355e3118f8a00da924a3a4 Mon Sep 17 00:00:00 2001 From: Lilis Iskandar Date: Tue, 9 Mar 2021 18:26:10 +0800 Subject: [PATCH 085/566] Added get_events method for KeycloakAdmin Signed-off-by: Lilis Iskandar --- keycloak/keycloak_admin.py | 16 +++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c0ac364e..e2e56483 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -47,7 +47,7 @@ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS class KeycloakAdmin: @@ -1760,6 +1760,20 @@ def get_keys(self): data=None) return raise_error_from_response(data_raw, KeycloakGetError) + def get_events(self, query=None): + """ + Return a list of events, filtered according to query parameters + + EventRepresentation array + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_eventrepresentation + + :return: events list + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path), + data=None, **query) + return raise_error_from_response(data_raw, KeycloakGetError) + def raw_get(self, *args, **kwargs): """ Calls connection.raw_get. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 7aa40f85..b594efea 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -105,3 +105,5 @@ URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity" URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" + +URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' \ No newline at end of file From 6d90d48048d6bfdbb683a56e8e265959465b5ccf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 24 Mar 2021 19:05:36 +0000 Subject: [PATCH 086/566] Bump rsa from 4.0 to 4.1 Bumps [rsa](https://github.com/sybrenstuvel/python-rsa) from 4.0 to 4.1. - [Release notes](https://github.com/sybrenstuvel/python-rsa/releases) - [Changelog](https://github.com/sybrenstuvel/python-rsa/blob/main/CHANGELOG.md) - [Commits](https://github.com/sybrenstuvel/python-rsa/compare/version-4.0...version-4.1) Signed-off-by: dependabot[bot] --- Pipfile.lock | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index 172a600d..da51fb75 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,10 +18,10 @@ "default": { "certifi": { "hashes": [ - "sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50", - "sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef" + "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", + "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" ], - "version": "==2019.9.11" + "version": "==2020.12.5" }, "chardet": { "hashes": [ @@ -32,17 +32,16 @@ }, "ecdsa": { "hashes": [ - "sha256:163c80b064a763ea733870feb96f9dd9b92216cfcacd374837af18e4e8ec3d4d", - "sha256:9814e700890991abeceeb2242586024d4758c8fc18445b194a49bd62d85861db" + "sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747", + "sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff" ], - "index": "pypi", - "version": "==0.13.3" + "version": "==0.16.1" }, "future": { "hashes": [ - "sha256:6142ef79e2416e432931d527452a1cab3aa4a754a0a53d25b2589f79e1106f34" + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" ], - "version": "==0.18.0" + "version": "==0.18.2" }, "httmock": { "hashes": [ @@ -60,10 +59,10 @@ }, "pyasn1": { "hashes": [ - "sha256:62cdade8b5530f0b185e09855dd422bc05c0bbff6b72ff61381c09dac7befd8c", - "sha256:a9495356ca1d66ed197a0f72b41eb1823cf7ea8b5bd07191673e8147aecf8604" + "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", + "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" ], - "version": "==0.4.7" + "version": "==0.4.8" }, "python-jose": { "hashes": [ @@ -83,17 +82,18 @@ }, "rsa": { "hashes": [ - "sha256:14ba45700ff1ec9eeb206a2ce76b32814958a98e372006c8fb76ba820211be66", - "sha256:1a836406405730121ae9823e19c6e806c62bbad73f890574fff50efa4122c487" + "sha256:2c7c8624ef9b55bacc3be8bdcec5cefc381ddc156e968d6437fe0fd08af00eb0", + "sha256:6fa6a54eb72bfc0abca7f27880b978b14a643ba2a6ad9f4a56a95be82129ca1b" ], - "version": "==4.0" + "index": "pypi", + "version": "==4.1" }, "six": { "hashes": [ - "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", - "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", + "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" ], - "version": "==1.12.0" + "version": "==1.15.0" }, "urllib3": { "hashes": [ From 988700bc8191b9f565379e6be6e536b8b6829be9 Mon Sep 17 00:00:00 2001 From: andreas Date: Mon, 29 Mar 2021 16:22:47 +0200 Subject: [PATCH 087/566] Replaced deprecated method, improved description and added packages to requirements. --- keycloak/connection.py | 6 +++--- keycloak/keycloak_admin.py | 2 +- requirements.txt | 4 +++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 3871482c..7d5ed2f0 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -54,9 +54,9 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True): for protocol in ('https://', 'http://'): adapter = HTTPAdapter(max_retries=1) # adds POST to retry whitelist - method_whitelist = set(adapter.max_retries.method_whitelist) - method_whitelist.add('POST') - adapter.max_retries.method_whitelist = frozenset(method_whitelist) + allowed_methods = set(adapter.max_retries.allowed_methods) + allowed_methods.add('POST') + adapter.max_retries.allowed_methods = frozenset(allowed_methods) self._s.mount(protocol, adapter) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c0ac364e..e52d20e8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -77,7 +77,7 @@ def __init__(self, server_url, username=None, password=None, realm_name='master' :param realm_name: realm name :param client_id: client id :param verify: True if want check connection SSL - :param client_secret_key: client secret key + :param client_secret_key: client secret key (optional, required only for access type confidential) :param custom_headers: dict of custom header to pass to each HTML request :param user_realm_name: The realm name of the user, if different from realm_name :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] diff --git a/requirements.txt b/requirements.txt index d4776e37..f4faf6df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ requests>=2.20.0 httmock>=1.2.5 python-jose>=1.4.0 -twine==1.13.0 \ No newline at end of file +twine==1.13.0 +jose~=1.0.0 +setuptools~=54.2.0 \ No newline at end of file From 92cef305787dec2485596f769692bcaa2af2b29e Mon Sep 17 00:00:00 2001 From: obdeijn Date: Tue, 6 Apr 2021 10:24:22 +0200 Subject: [PATCH 088/566] Fix get_group_realm_roles, set correct url to get realm roles. Remove the no longer used, wrong url. --- keycloak/keycloak_admin.py | 4 ++-- keycloak/urls_patterns.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c0ac364e..238d1574 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -34,7 +34,7 @@ from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GET_GROUPS_REALM_ROLES, URL_ADMIN_GROUPS_CLIENT_ROLES, \ + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \ URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ @@ -1239,7 +1239,7 @@ def get_group_realm_roles(self, group_id): :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_get(URL_ADMIN_GET_GROUPS_REALM_ROLES.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_client_roles(self, group_id, client_id, roles): diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 7aa40f85..3fae695b 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -44,7 +44,6 @@ URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" -URL_ADMIN_GET_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings" URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" From c91df2b5c8a046f541e684ee2b70e67930d3bef2 Mon Sep 17 00:00:00 2001 From: arata-honda Date: Mon, 19 Apr 2021 22:39:49 +0900 Subject: [PATCH 089/566] Fix get_user_id to non case-sensitive --- keycloak/keycloak_admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index c0ac364e..cd9660ae 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -410,9 +410,9 @@ def get_user_id(self, username): :return: user_id """ - - users = self.get_users(query={"search": username}) - return next((user["id"] for user in users if user["username"] == username), None) + lower_user_name = username.lower() + users = self.get_users(query={"search": lower_user_name}) + return next((user["id"] for user in users if user["username"] == lower_user_name), None) def get_user(self, user_id): """ From a39ff81e6250fcc6b5bb92c23a94eebac151358b Mon Sep 17 00:00:00 2001 From: Sandro Covo Date: Fri, 30 Apr 2021 16:40:38 +0200 Subject: [PATCH 090/566] add example to create user and specify locale Usage of the `attributes`for this kind of task with not quite obvious and documentation hard to find. --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 095782b5..63abe49d 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,17 @@ new_user = keycloak_admin.create_user({"email": "example@example.com", "enabled": True, "firstName": "Example", "lastName": "Example", - "credentials": [{"value": "secret","type": "password",}]}) + "credentials": [{"value": "secret","type": "password",}]}) + +# Add user and specify a locale +new_user = keycloak_admin.create_user({"email": "example@example.fr", + "username": "example@example.fr", + "enabled": True, + "firstName": "Example", + "lastName": "Example", + "attributes": { + "locale": ["fr"] + }) # User counter count_users = keycloak_admin.users_count() From 992de97378f35af6aa88aeb5d261ff693afbe3c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 30 Apr 2021 21:02:32 +0000 Subject: [PATCH 091/566] Bump rsa from 4.1 to 4.7 Bumps [rsa](https://github.com/sybrenstuvel/python-rsa) from 4.1 to 4.7. - [Release notes](https://github.com/sybrenstuvel/python-rsa/releases) - [Changelog](https://github.com/sybrenstuvel/python-rsa/blob/main/CHANGELOG.md) - [Commits](https://github.com/sybrenstuvel/python-rsa/compare/version-4.1...version-4.7) Signed-off-by: dependabot[bot] --- Pipfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Pipfile.lock b/Pipfile.lock index da51fb75..0430b1ee 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -82,11 +82,11 @@ }, "rsa": { "hashes": [ - "sha256:2c7c8624ef9b55bacc3be8bdcec5cefc381ddc156e968d6437fe0fd08af00eb0", - "sha256:6fa6a54eb72bfc0abca7f27880b978b14a643ba2a6ad9f4a56a95be82129ca1b" + "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4", + "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b" ], "index": "pypi", - "version": "==4.1" + "version": "==4.7" }, "six": { "hashes": [ From 62dc333d8236fed9705006a07bd0d7f7c0420914 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 1 Jun 2021 14:47:35 -0300 Subject: [PATCH 092/566] Release 0.25.0 --- bin/deploy.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/deploy.sh diff --git a/bin/deploy.sh b/bin/deploy.sh old mode 100644 new mode 100755 From 0ebcf990941f600e6ff562091a81cbeafbd21f3e Mon Sep 17 00:00:00 2001 From: Marcos Pereira Date: Tue, 1 Jun 2021 14:47:54 -0300 Subject: [PATCH 093/566] Release 0.25.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 1ab699d0..61db46ea 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.24.0' +version = '0.25.0' # The full version, including alpha/beta/rc tags. -release = '0.24.0' +release = '0.25.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 362f1776..dcde1e84 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.24.0', + version='0.25.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 420779e3d25e9a49f0257ca3458d8f27943690dc Mon Sep 17 00:00:00 2001 From: phala Date: Wed, 9 Jun 2021 14:16:23 +0200 Subject: [PATCH 094/566] Remove unused client_id from assign_realm_roles --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..3db6d475 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1173,7 +1173,7 @@ def get_composite_realm_roles_of_role(self, role_name): URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def assign_realm_roles(self, user_id, client_id, roles): + def assign_realm_roles(self, user_id, roles): """ Assign realm roles to a user From 56fdb467be46de2e64c56c63b6c582ca0afaa234 Mon Sep 17 00:00:00 2001 From: phala Date: Wed, 9 Jun 2021 14:30:03 +0200 Subject: [PATCH 095/566] Add user logout --- keycloak/keycloak_admin.py | 15 ++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..9758cf06 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -47,7 +47,7 @@ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, URL_ADMIN_USER_LOGOUT class KeycloakAdmin: @@ -487,6 +487,19 @@ def set_user_password(self, user_id, password, temporary=True): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def logout(self, user_id): + """ + Logs out user. + + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_logout + + :param user_id: User id + :return: + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def consents_user(self, user_id): """ Get consents granted by the user diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 8dfbadb8..3bb0ccf0 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -50,6 +50,7 @@ URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups" URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" +URL_ADMIN_USER_LOGOUT = "admin/realms/{realm-name}/users/{id}/logout" URL_ADMIN_USER_STORAGE = "admin/realms/{realm-name}/user-storage/{id}/sync" URL_ADMIN_SERVER_INFO = "admin/serverinfo" From 4b6b076f55f5c9940de068b91e6dae208d39928c Mon Sep 17 00:00:00 2001 From: Joerg Schaarschmidt Date: Wed, 9 Jun 2021 18:11:23 +0200 Subject: [PATCH 096/566] add delete_realm_roles_of_user function --- docs/source/index.rst | 3 +++ keycloak/keycloak_admin.py | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 0cd6e2fe..0f61b502 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -262,6 +262,9 @@ Main methods:: # Assign realm roles to user. Note that BOTH role_name and role_id appear to be required. keycloak_admin.assign_realm_roles(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) + # Delete realm roles of user. Note that BOTH role_name and role_id appear to be required. + keycloak_admin.deletes_realm_roles_of_user(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) + # Create new group group = keycloak_admin.create_group(name="Example Group") diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..34c1919b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1189,6 +1189,22 @@ def assign_realm_roles(self, user_id, client_id, roles): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def delete_realm_roles_of_user(self, user_id, client_id, roles): + """ + Deletes realm roles of a user + + :param user_id: id of user + :param client_id: id of client containing role (not client-id) + :param roles: roles list or role (use RoleRepresentation) + :return Keycloak server response + """ + + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_delete(URL_ADMIN_USER_REALM_ROLES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_realm_roles_of_user(self, user_id): """ Get all realm roles for a user. From 93b9991dc84f5a62a2eabd0c348afd5a1292ab25 Mon Sep 17 00:00:00 2001 From: Joerg Schaarschmidt Date: Wed, 9 Jun 2021 18:34:20 +0200 Subject: [PATCH 097/566] Remove unused client_id from delete_realm_roles_of users --- docs/source/index.rst | 2 +- keycloak/keycloak_admin.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 0f61b502..3b3007bc 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -263,7 +263,7 @@ Main methods:: keycloak_admin.assign_realm_roles(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) # Delete realm roles of user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.deletes_realm_roles_of_user(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) + keycloak_admin.deletes_realm_roles_of_user(user_id="user_id", roles=[{"roles_representation"}]) # Create new group group = keycloak_admin.create_group(name="Example Group") diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 34c1919b..2afd1e27 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1189,12 +1189,11 @@ def assign_realm_roles(self, user_id, client_id, roles): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def delete_realm_roles_of_user(self, user_id, client_id, roles): + def delete_realm_roles_of_user(self, user_id, roles): """ Deletes realm roles of a user :param user_id: id of user - :param client_id: id of client containing role (not client-id) :param roles: roles list or role (use RoleRepresentation) :return Keycloak server response """ From 11e4a12c83754bf6a7ffe95f6bcc80266113b0a9 Mon Sep 17 00:00:00 2001 From: Adrian_Cin Date: Mon, 14 Jun 2021 16:17:47 +0700 Subject: [PATCH 098/566] Correct public key format Keycloak returns public key in PEM format --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 63abe49d..28b218db 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['ac token_info = keycloak_openid.introspect(token['access_token']) # Decode Token -KEYCLOAK_PUBLIC_KEY = keycloak_openid.public_key() +KEYCLOAK_PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\n" + keycloak_openid.public_key() + "\n-----END PUBLIC KEY-----" options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) From 31b4efd7ab903354bb844d9eec1671dbdaa9dbf0 Mon Sep 17 00:00:00 2001 From: bostonkenne Date: Thu, 17 Jun 2021 15:51:11 +0100 Subject: [PATCH 099/566] add remove user realm role --- keycloak/keycloak_admin.py | 15 ++++++++++++++- keycloak/urls_patterns.py | 4 +++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..4272482e 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -47,7 +47,8 @@ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS,\ + URL_ADMIN_DELETE_USER_ROLE class KeycloakAdmin: @@ -1878,3 +1879,15 @@ def get_client_all_sessions(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + + def delete_user_realm_role(self, user_id, payload): + """ + Delete realm-level role mappings + DELETE admin/realms/{realm-name}/users/{id}/role-mappings/realm + + """ + params_path = {"realm-name": self.realm_name, "id": str(user_id) } + data_raw = self.connection.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) \ No newline at end of file diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 8dfbadb8..460ae8b5 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -105,4 +105,6 @@ URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity" URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" -URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' \ No newline at end of file +URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' + +URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" \ No newline at end of file From d7d661a38c7a6bf5daaea513616f4f9936460bef Mon Sep 17 00:00:00 2001 From: bostonkenne Date: Thu, 17 Jun 2021 15:53:16 +0100 Subject: [PATCH 100/566] fix json response --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4272482e..15ee759c 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1890,4 +1890,4 @@ def delete_user_realm_role(self, user_id, payload): params_path = {"realm-name": self.realm_name, "id": str(user_id) } data_raw = self.connection.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_code=204) \ No newline at end of file + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) \ No newline at end of file From 8f6de6c3c84faaf354484811160c8d95c33ee3c8 Mon Sep 17 00:00:00 2001 From: Yannick Chabbert Date: Mon, 28 Jun 2021 15:42:54 +0200 Subject: [PATCH 101/566] openid - minor typo fix, change nothing yet --- keycloak/keycloak_openid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 0f801ea9..197dd26f 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -189,7 +189,7 @@ def token(self, username="", password="", grant_type=["password"], code="", redi payload = {"username": username, "password": password, "client_id": self.client_id, "grant_type": grant_type, "code": code, "redirect_uri": redirect_uri} - if payload: + if extra: payload.update(extra) if totp: From 44fe7b714aa08743fc95a8de5d9ea469dabb2799 Mon Sep 17 00:00:00 2001 From: Manjeetsinh Alonja Date: Fri, 9 Jul 2021 11:33:35 +0530 Subject: [PATCH 102/566] Added realm partial export feature --- keycloak/keycloak_admin.py | 13 ++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..41f71eba 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -47,7 +47,8 @@ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ + URL_ADMIN_REALM_EXPORT class KeycloakAdmin: @@ -242,6 +243,16 @@ def import_realm(self, payload): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def export_realm(self, export_clients=False, export_groups_and_role=False): + """ + Export the realm configurations in the json format + + :return: realm configurations JSON + """ + params_path = {"realm-name": self.realm_name, "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role } + data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="") + return raise_error_from_response(data_raw, KeycloakGetError) + def get_realms(self): """ Lists all realms in Keycloak deployment diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 8dfbadb8..65ffbebc 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -89,6 +89,7 @@ URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites" +URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" From 71a92f8b4911edfc0f7c26b6829ce519a44e9979 Mon Sep 17 00:00:00 2001 From: Manjeetsinh Alonja Date: Fri, 9 Jul 2021 12:05:58 +0530 Subject: [PATCH 103/566] Fix feature function documents --- keycloak/keycloak_admin.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 41f71eba..3abf55b4 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -247,6 +247,12 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): """ Export the realm configurations in the json format + RealmRepresentation + https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_partialexport + + :param export-clients: Skip if not want to export realm clients + :param export-groups-and-roles: Skip if not want to export realm groups and roles + :return: realm configurations JSON """ params_path = {"realm-name": self.realm_name, "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role } From 227b698b3fd1552fbac9d0e619d2ff3640ddcf8a Mon Sep 17 00:00:00 2001 From: Michael Kao Date: Tue, 20 Jul 2021 17:23:44 +0200 Subject: [PATCH 104/566] Stopping pagination requests if response count is lower than page size. --- keycloak/keycloak_admin.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..e018dbc0 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -223,6 +223,8 @@ def __fetch_all(self, url, query=None): if not partial_results: break results.extend(partial_results) + if len(partial_results) < query['max']: + break page += 1 return results From a9b39248543b521fe2a5936fe09b3d8509d681c0 Mon Sep 17 00:00:00 2001 From: Jacky Boen Date: Mon, 2 Aug 2021 10:27:17 +0800 Subject: [PATCH 105/566] Fix KeycloakAdmin using wrong realm when authenticating with a service account Signed-off-by: Jacky Boen --- keycloak/keycloak_admin.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..5feceaf1 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1827,14 +1827,17 @@ def raw_delete(self, *args, **kwargs): return r def get_token(self): + token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, - realm_name=self.user_realm_name or self.realm_name, verify=self.verify, + realm_name=token_realm_name, verify=self.verify, client_secret_key=self.client_secret_key, custom_headers=self.custom_headers) grant_type = ["password"] if self.client_secret_key: grant_type = ["client_credentials"] + if self.user_realm_name: + self.realm_name = self.user_realm_name self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) From bd8af2924e8b479428a4f2ee3000cbf9bba0e7bf Mon Sep 17 00:00:00 2001 From: Julien Bouquillon Date: Wed, 18 Aug 2021 00:45:53 +0200 Subject: [PATCH 106/566] feat: add KeycloakAdmin.set_events --- keycloak/keycloak_admin.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index ffb59687..03447213 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1774,6 +1774,20 @@ def get_events(self, query=None): data=None, **query) return raise_error_from_response(data_raw, KeycloakGetError) + def set_events(self, payload): + """ + Set realm events configuration + + RealmEventsConfigRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmeventsconfigrepresentation + + :return: Http response + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def raw_get(self, *args, **kwargs): """ Calls connection.raw_get. From 81b12d2d556883f5770e81818b9e3f5eb666a10b Mon Sep 17 00:00:00 2001 From: Bas van der Linden <33874522+BvdLind@users.noreply.github.com> Date: Thu, 19 Aug 2021 19:50:37 +0200 Subject: [PATCH 107/566] Update README example create_client_role syntax In the README the `create_client_role` function has a `client_id` argument, but in [keycloak_admin.py](https://github.com/marcospereirampj/python-keycloak/blob/0ebcf990941f600e6ff562091a81cbeafbd21f3e/keycloak/keycloak_admin.py#L1001) `client_role_id` seems to be used instead. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 63abe49d..d076555d 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ role = keycloak_admin.get_client_role(client_id="client_id", role_name="role_nam role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test") # Create client role -keycloak_admin.create_client_role(client_id='client_id', {'name': 'roleName', 'clientRole': True}) +keycloak_admin.create_client_role(client_role_id='client_id', {'name': 'roleName', 'clientRole': True}) # Assign client role to user. Note that BOTH role_name and role_id appear to be required. keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") From 6acadaca5b5804b01a91d12c43c11f213b05acb5 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Mon, 30 Aug 2021 10:12:40 -0300 Subject: [PATCH 108/566] Release 0.26.0 --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 61db46ea..78454a22 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.25.0' +version = '0.26.0' # The full version, including alpha/beta/rc tags. -release = '0.25.0' +release = '0.26.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index dcde1e84..81dc85fd 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.25.0', + version='0.26.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From dd130a0365da390ae28d8a9cb6f76103ecb5c83f Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Mon, 30 Aug 2021 10:15:58 -0300 Subject: [PATCH 109/566] Release 0.26.1 --- Pipfile | 1 + docs/source/conf.py | 4 ++-- requirements.txt | 3 ++- setup.py | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Pipfile b/Pipfile index a85e8e54..828d2983 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ name = "pypi" requests = ">=2.20.0" httmock = ">=1.2.5" python-jose = ">=1.4.0" +urllib3 = ">=1.26.5" [dev-packages] diff --git a/docs/source/conf.py b/docs/source/conf.py index 78454a22..7b52edc4 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.26.0' +version = '0.26.1' # The full version, including alpha/beta/rc tags. -release = '0.26.0' +release = '0.26.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/requirements.txt b/requirements.txt index f4faf6df..a353c7f3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ httmock>=1.2.5 python-jose>=1.4.0 twine==1.13.0 jose~=1.0.0 -setuptools~=54.2.0 \ No newline at end of file +setuptools~=54.2.0 +urllib3>=1.26.5 \ No newline at end of file diff --git a/setup.py b/setup.py index 81dc85fd..e6cb84b7 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.26.0', + version='0.26.1', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 50354e61086c69a6cf7118bee56e15c35668c2c3 Mon Sep 17 00:00:00 2001 From: Md Minhazul Haque Date: Tue, 31 Aug 2021 05:25:40 +0600 Subject: [PATCH 110/566] Add feature to list and delete user credentials --- README.md | 9 ++++++++ keycloak/keycloak_admin.py | 47 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index da88a9b1..2a2a2cd1 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,15 @@ response = keycloak_admin.update_user(user_id="user-id-keycloak", # Update User Password response = keycloak_admin.set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) + +# Get User Credentials +credentials = keycloak_admin.get_credentials(user_id='user_id') + +# Get User Credential by ID +credential = keycloak_admin.get_credential(user_id='user_id', credential_id='credential_id') + +# Delete User Credential +response = keycloak_admin.delete_credential(user_id='user_id', credential_id='credential_id') # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..5ad02123 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,7 +48,8 @@ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ - URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, \ + URL_ADMIN_USER_CREDENTIALS, URL_ADMIN_USER_CREDENTIAL class KeycloakAdmin: @@ -506,6 +507,50 @@ def set_user_password(self, user_id, password, temporary=True): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_credentials(self, user_id): + """ + Returns a list of credential belonging to the user. + + CredentialRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + + :param: user_id: user id + :return: Keycloak server response (CredentialRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIALS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_credential(self, user_id, credential_id): + """ + Get credential of the user. + + CredentialRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + + :param: user_id: user id + :param: credential_id: credential id + :return: Keycloak server response (ClientRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def delete_credential(self, user_id, credential_id): + """ + Delete credential of the user. + + CredentialRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + + :param: user_id: user id + :param: credential_id: credential id + :return: Keycloak server response (ClientRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def logout(self, user_id): """ Logs out user. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..ac0b16c3 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -50,6 +50,8 @@ URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups" URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" +URL_ADMIN_USER_CREDENTIALS = "admin/realms/{realm-name}/users/{id}/credentials" +URL_ADMIN_USER_CREDENTIAL = "admin/realms/{realm-name}/users/{id}/credentials/{credential_id}" URL_ADMIN_USER_LOGOUT = "admin/realms/{realm-name}/users/{id}/logout" URL_ADMIN_USER_STORAGE = "admin/realms/{realm-name}/user-storage/{id}/sync" From 5b202e71cb76f0e05f8820ccd5e8a56dc2d37bf4 Mon Sep 17 00:00:00 2001 From: Nicklas Sedlock Date: Fri, 3 Sep 2021 13:10:12 +0200 Subject: [PATCH 111/566] fix: handle refresh_token error "Session not active" --- keycloak/keycloak_admin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..18e3fb74 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1906,8 +1906,12 @@ def refresh_token(self): try: self.token = self.keycloak_openid.refresh_token(refresh_token) except KeycloakGetError as e: - if e.response_code == 400 and (b'Refresh token expired' in e.response_body or - b'Token is not active' in e.response_body): + list_errors = [ + b'Refresh token expired', + b'Token is not active', + b'Session not active' + ] + if e.response_code == 400 and any(err in e.response_body for err in list_errors): self.get_token() else: raise From 69d60d430a35cc2c01e3658b81287926310e2f46 Mon Sep 17 00:00:00 2001 From: Ankur Saxena Date: Thu, 16 Sep 2021 11:20:51 -0400 Subject: [PATCH 112/566] Add remove and update methods for client protocol mappers --- keycloak/keycloak_admin.py | 42 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 3 ++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..188c6e77 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1672,10 +1672,50 @@ def add_mapper_to_client(self, client_id, payload): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + + def update_client_mapper(self, client_id, mapper_id, payload): + """ + Update client mapper + :param client_id: The id of the client + :param client_mapper_id: The id of the mapper to be deleted + :param payload: ProtocolMapperRepresentation + :return: Keycloak server response + """ + + params_path = { + "realm-name": self.realm_name, + "id": self.client_id, + "protocol-mapper-id": mapper_id, + } + data_raw = self.raw_put( + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def remove_client_mapper(self, client_id, client_mapper_id): + """ + Removes a mapper from the client + https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_protocol_mappers_resource + :param client_id: The id of the client + :param client_mapper_id: The id of the mapper to be deleted + :return: Keycloak server response + """ + + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "protocol-mapper-id": mapper_id + } + + data_raw = self.raw_delete( + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) + + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def generate_client_secrets(self, client_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..fc396e9e 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -74,7 +74,8 @@ URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" -URL_ADMIN_CLIENT_PROTOCOL_MAPPER = URL_ADMIN_CLIENT + "/protocol-mappers/models" +URL_ADMIN_CLIENT_PROTOCOL_MAPPERS = URL_ADMIN_CLIENT + "/protocol-mappers/models" +URL_ADMIN_CLIENT_PROTOCOL_MAPPER = URL_ADMIN_CLIENT_PROTOCOL_MAPPERS + "/{protocol-mapper-id}" URL_ADMIN_CLIENT_SCOPES = "admin/realms/{realm-name}/client-scopes" URL_ADMIN_CLIENT_SCOPE = URL_ADMIN_CLIENT_SCOPES + "/{scope-id}" From d0894d4352a982c4c100938d301797a0719281ed Mon Sep 17 00:00:00 2001 From: Nikolay Amiantov Date: Fri, 1 Oct 2021 12:58:54 +0300 Subject: [PATCH 113/566] Don't force realm name when using secret key Using other realms can be useful, for example, to manage realm users with restricted rights. --- keycloak/keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..c934a7b8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1873,7 +1873,7 @@ def raw_delete(self, *args, **kwargs): return r def get_token(self): - token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name + token_realm_name = self.user_realm_name or self.realm_name self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, realm_name=token_realm_name, verify=self.verify, client_secret_key=self.client_secret_key, @@ -1938,4 +1938,4 @@ def delete_user_realm_role(self, user_id, payload): params_path = {"realm-name": self.realm_name, "id": str(user_id) } data_raw = self.connection.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) \ No newline at end of file + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) From a37bf45c77164d9635fec91ae099103d37cee6ab Mon Sep 17 00:00:00 2001 From: Tobias Henkel Date: Fri, 8 Oct 2021 19:01:29 +0200 Subject: [PATCH 114/566] Add delete_user_social_login This makes it possible to delete federated identities without having to use raw requests. --- keycloak/keycloak_admin.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..464ef186 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -555,6 +555,18 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_post(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload)) + def delete_user_social_login(self, user_id, provider_id): + + """ + Delete a federated identity / social login provider from the user + :param user_id: User id + :param provider_id: Social login provider id + :return: + """ + params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} + data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): """ Send an update account email to the user. An email contains a From 9e7d0d2ec5829ecd384f8fd2a3e5c7ca88db52ef Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Wed, 13 Oct 2021 11:13:31 -0300 Subject: [PATCH 115/566] Added optional proxies for requests calls --- docs/source/index.rst | 9 +++++++++ keycloak/connection.py | 6 +++++- keycloak/keycloak_openid.py | 6 ++++-- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 0cd6e2fe..7347d0e4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -100,6 +100,15 @@ Main methods:: # verify=True, # custom_headers={'CustomHeader': 'value'}) + # Optionally, you can pass proxies as well that will be used in all HTTP calls. See requests documentation for more details_ + # `requests-proxies `_. + # keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", + # client_id="example_client", + # realm_name="example_realm", + # client_secret_key="secret", + # verify=True, + # proxies={'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080'}) + # Get WellKnow config_well_know = keycloak_openid.well_know() diff --git a/keycloak/connection.py b/keycloak/connection.py index 7d5ed2f0..1ceae022 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -39,9 +39,10 @@ class ConnectionManager(object): headers (dict): The header parameters of the requests to the server. timeout (int): Timeout to use for requests to the server. verify (bool): Verify server SSL. + proxies (dict): The proxies servers requests is sent by. """ - def __init__(self, base_url, headers={}, timeout=60, verify=True): + def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): self._base_url = base_url self._headers = headers self._timeout = timeout @@ -59,6 +60,9 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True): adapter.max_retries.allowed_methods = frozenset(allowed_methods) self._s.mount(protocol, adapter) + + if proxies: + self._s.proxies = proxies def __del__(self): self._s.close() diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 197dd26f..1d6ed289 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -44,7 +44,7 @@ class KeycloakOpenID: - def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None): + def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None, proxies=None): """ :param server_url: Keycloak server url @@ -53,6 +53,7 @@ def __init__(self, server_url, realm_name, client_id, client_secret_key=None, ve :param client_secret_key: client secret key :param verify: True if want check connection SSL :param custom_headers: dict of custom header to pass to each HTML request + :param proxies: dict of proxies to sent the request by. """ self._client_id = client_id self._client_secret_key = client_secret_key @@ -64,7 +65,8 @@ def __init__(self, server_url, realm_name, client_id, client_secret_key=None, ve self._connection = ConnectionManager(base_url=server_url, headers=headers, timeout=60, - verify=verify) + verify=verify, + proxies=proxies) self._authorization = Authorization() From fc079d8fb73a889ade397818f3e32a49a57a59dd Mon Sep 17 00:00:00 2001 From: Leandro de Souza Date: Wed, 13 Oct 2021 11:28:53 -0300 Subject: [PATCH 116/566] Using session.proxies.update method instead of seting it as an dict --- keycloak/connection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/connection.py b/keycloak/connection.py index 1ceae022..bdecfce7 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -62,7 +62,7 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): self._s.mount(protocol, adapter) if proxies: - self._s.proxies = proxies + self._s.proxies.update(proxies) def __del__(self): self._s.close() From fd0577c7303f2cfe2620fc06d3b037e3fb9d99aa Mon Sep 17 00:00:00 2001 From: Aarno Aukia Date: Thu, 21 Oct 2021 21:11:39 +0300 Subject: [PATCH 117/566] add_group needs a dict of GroupRepresentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da88a9b1..fa6c4a3f 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ keycloak_admin.delete_client_roles_of_user(client_id="client_id", user_id="user_ keycloak_admin.delete_client_roles_of_user(client_id="client_id", user_id="user_id", roles=[{"id": "role-id_1"}, {"id": "role-id_2"}]) # Create new group -group = keycloak_admin.create_group(name="Example Group") +group = keycloak_admin.create_group({"name": "Example Group"}) # Get all groups groups = keycloak_admin.get_groups() From 3b93754d27d9aa7d76213131e79055fda568563e Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 10:59:28 +0800 Subject: [PATCH 118/566] Add new AuthZ API support NOTE: These are private API, will be changed later. --- keycloak/keycloak_admin.py | 65 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 3 ++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..371897b9 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -32,6 +32,7 @@ from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ + URL_ADMIN_CLIENT_AUTHZ_POLICIES, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION, \ URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ @@ -866,6 +867,22 @@ def get_client_authz_settings(self, client_id): data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) return data_raw + def create_client_authz_resource(self, client_id, payload, skip_exists=False): + """ + Create resources of client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, + "id": client_id} + + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + def get_client_authz_resources(self, client_id): """ Get resources from client. @@ -877,7 +894,53 @@ def get_client_authz_resources(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) + + def create_client_authz_role_based_policy(self, client_id, payload, skip_exists=False): + """ + Create role-based policy of client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, + "id": client_id} + + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + + def create_client_authz_role_based_permission(self, client_id, payload, skip_exists=False): + """ + Create role-based permission of client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, + "id": client_id} + + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION.format(**params_path), + data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + + def get_client_authz_policies(self, client_id): + """ + Get policies from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + params_query = {"first": 0, "max": 20, "permission": False} + data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path), **params_query) + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): """ diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..91221160 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -71,6 +71,9 @@ URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" +URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy" +URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT_AUTHZ_POLICIES + "/role" +URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION = URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource" URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" From 656de1c4669ce60600f33eb07ce5fadcab4a1885 Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 11:27:33 +0800 Subject: [PATCH 119/566] Add more comments --- keycloak/keycloak_admin.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 371897b9..d5b6df81 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -873,6 +873,9 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: ResourceRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_resourcerepresentation + :return: Keycloak server response """ @@ -902,6 +905,9 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: PolicyRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation + :return: Keycloak server response """ @@ -918,6 +924,20 @@ def create_client_authz_role_based_permission(self, client_id, payload, skip_exi :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: No Document + payload example: + payload={ + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Permission-Name", + "resources": [ + resource_id + ], + "policies": [ + policy_id + ] + :return: Keycloak server response """ @@ -934,6 +954,9 @@ def get_client_authz_policies(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :param payload: PolicyRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation + :return: Keycloak server response """ From 10c212cce5e07609fef89783925bf6bac2c9059e Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 11:40:18 +0800 Subject: [PATCH 120/566] Refine comments --- keycloak/keycloak_admin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index d5b6df81..2453d5c9 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -905,8 +905,19 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation - :param payload: PolicyRepresentation - https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation + :param payload: No Document + payload example: + payload={ + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Policy-1", + "roles": [ + { + "id": id + } + ] + } :return: Keycloak server response """ @@ -924,7 +935,8 @@ def create_client_authz_role_based_permission(self, client_id, payload, skip_exi :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation - :param payload: No Document + :param payload: PolicyRepresentation + https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation payload example: payload={ "type": "resource", From 80bb7a56b58557a8dc30521b257dbc2ada124a6a Mon Sep 17 00:00:00 2001 From: lcgkm Date: Sun, 7 Nov 2021 11:53:27 +0800 Subject: [PATCH 121/566] Fix invalid permission type A permission associates the object being protected and the policies that must be evaluated to decide whether access should be granted. Permissions can be created to protect two main types of objects: 1. Resources 2. Scopes --- keycloak/keycloak_admin.py | 8 ++++---- keycloak/urls_patterns.py | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 2453d5c9..69da3d8a 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -32,7 +32,7 @@ from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_CLIENT_AUTHZ_POLICIES, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION, \ + URL_ADMIN_CLIENT_AUTHZ_POLICIES, URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, \ URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ @@ -929,9 +929,9 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) - def create_client_authz_role_based_permission(self, client_id, payload, skip_exists=False): + def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): """ - Create role-based permission of client. + Create resource-based permission of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation @@ -956,7 +956,7 @@ def create_client_authz_role_based_permission(self, client_id, payload, skip_exi params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 91221160..b58c4bbf 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -73,7 +73,8 @@ URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy" URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT_AUTHZ_POLICIES + "/role" -URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_PERMISSION = URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource" +URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT + "/authz/resource-server/permission" +URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS + "/resource" URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" From 7ee625d063580f95c09b7eefe5865bae8d025214 Mon Sep 17 00:00:00 2001 From: Ryan Gard Date: Mon, 15 Nov 2021 09:28:03 -0800 Subject: [PATCH 122/566] Enable Keycloak Admin for Non-master Realms Allow the 'KeycloakAdmin' class to instantiate against non-master realms using an Authorization header for a non-admin user that is granted permissions to inspect or manage Keycloak admin resources. Example: kca = KeycloakAdmin( 'https://auth.keycloak.local/auth/', realm_name='my-realm', client_id='admin', custom_headers=auth_headers ) --- keycloak/keycloak_admin.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..8c31e959 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -254,7 +254,7 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): :param export-clients: Skip if not want to export realm clients :param export-groups-and-roles: Skip if not want to export realm groups and roles - + :return: realm configurations JSON """ params_path = {"realm-name": self.realm_name, "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role } @@ -1885,12 +1885,16 @@ def get_token(self): if self.user_realm_name: self.realm_name = self.user_realm_name - self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) + if self.username and self.password: + self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) - headers = { - 'Authorization': 'Bearer ' + self.token.get('access_token'), - 'Content-Type': 'application/json' - } + headers = { + 'Authorization': 'Bearer ' + self.token.get('access_token'), + 'Content-Type': 'application/json' + } + else: + self._token = None + headers = {} if self.custom_headers is not None: # merge custom headers to main headers From a6ad87d62c62bc3ab7bac5bf1ebd935a3a314488 Mon Sep 17 00:00:00 2001 From: robsonyeg Date: Wed, 17 Nov 2021 14:51:37 -0700 Subject: [PATCH 123/566] fix invalid credential error and refresh_code not exist error --- keycloak/keycloak_admin.py | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..5dad4570 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1873,7 +1873,14 @@ def raw_delete(self, *args, **kwargs): return r def get_token(self): - token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name + # token_realm_name = 'master' if self.client_secret_key else self.user_realm_name or self.realm_name + if self.user_realm_name: + token_realm_name = self.user_realm_name + elif self.realm_name: + token_realm_name = self.realm_name + else: + token_realm_name = "master" + self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, realm_name=token_realm_name, verify=self.verify, client_secret_key=self.client_secret_key, @@ -1902,15 +1909,19 @@ def get_token(self): verify=self.verify) def refresh_token(self): - refresh_token = self.token.get('refresh_token') - try: - self.token = self.keycloak_openid.refresh_token(refresh_token) - except KeycloakGetError as e: - if e.response_code == 400 and (b'Refresh token expired' in e.response_body or - b'Token is not active' in e.response_body): - self.get_token() - else: - raise + refresh_token = self.token.get('refresh_token', None) + if refresh_token is None: + self.get_token() + else: + try: + self.token = self.keycloak_openid.refresh_token(refresh_token) + except KeycloakGetError as e: + if e.response_code == 400 and (b'Refresh token expired' in e.response_body or + b'Token is not active' in e.response_body): + self.get_token() + else: + raise + self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token')) def get_client_all_sessions(self, client_id): From c538d8fb3232b84ba5e033b919e636116b8ed84a Mon Sep 17 00:00:00 2001 From: ggallard Date: Sat, 20 Nov 2021 16:24:57 -0300 Subject: [PATCH 124/566] added get/add/delete default (default/optional) client scopes --- keycloak/keycloak_admin.py | 76 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 5 +++ 2 files changed, 80 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..f5576bd8 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,7 +48,9 @@ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ - URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, \ + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, \ + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE class KeycloakAdmin: @@ -1659,6 +1661,78 @@ def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, pay return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_default_default_client_scopes(self): + """ + Return list of default default client scopes + + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + + def delete_default_default_client_scope(self, scope_id): + """ + Delete default default client scope + + :param scope_id: default default client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + + def add_default_default_client_scope(self, scope_id): + """ + Add default default client scope + + :param scope_id: default default client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + payload = {"realm": self.realm_name, "clientScopeId": scope_id} + data_raw = self.raw_put(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + + def get_default_optional_client_scopes(self): + """ + Return list of default optional client scopes + + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + + def delete_default_optional_client_scope(self, scope_id): + """ + Delete default optional client scope + + :param scope_id: default optional client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + + def add_default_optional_client_scope(self, scope_id): + """ + Add default optional client scope + + :param scope_id: default optional client scope id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": scope_id} + payload = {"realm": self.realm_name, "clientScopeId": scope_id} + data_raw = self.raw_put(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def add_mapper_to_client(self, client_id, payload): """ Add a mapper to a client diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..bbc5fe07 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -92,6 +92,11 @@ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites" URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" +URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-default-client-scopes" +URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE = URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES + "/{id}" +URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-optional-client-scopes" +URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE = URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES + "/{id}" + URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" From 76b6798429b5cec876b1b4e66d1d7337e75859cf Mon Sep 17 00:00:00 2001 From: ggallard Date: Tue, 23 Nov 2021 13:21:15 -0300 Subject: [PATCH 125/566] added get/delete authentication_flow_execution(execution_id) --- keycloak/keycloak_admin.py | 31 +++++++++++++++++++++++++++++-- keycloak/urls_patterns.py | 1 + 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..d7dfc714 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,8 +48,7 @@ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ - URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT - + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION class KeycloakAdmin: @@ -1455,6 +1454,20 @@ def update_authentication_flow_executions(self, payload, flow_alias): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_authentication_flow_execution(self, execution_id): + """ + Get authentication flow execution. + + AuthenticationExecutionInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + + :param execution_id: the execution ID + :return: Response(json) + """ + params_path = {"realm-name": self.realm_name, "id": execution_id} + data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def create_authentication_flow_execution(self, payload, flow_alias): """ Create an authentication flow execution @@ -1472,6 +1485,20 @@ def create_authentication_flow_execution(self, payload, flow_alias): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def delete_authentication_flow_execution(self, execution_id): + """ + Delete authentication flow execution + + AuthenticationExecutionInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + + :param execution_id: keycloak client id (not oauth client-id) + :return: Keycloak server response (json) + """ + params_path = {"realm-name": self.realm_name, "id": execution_id} + data_raw = self.raw_delete(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): """ Create a new sub authentication flow for a given authentication flow diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..148fe4fb 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -96,6 +96,7 @@ URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +URL_ADMIN_FLOWS_EXECUTION = "admin/realms/{realm-name}/authentication/executions/{id}" URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" From 95c4a7ee2dce930e086e83fca85bc4774e47df88 Mon Sep 17 00:00:00 2001 From: ggallard Date: Tue, 23 Nov 2021 13:22:46 -0300 Subject: [PATCH 126/566] added delete_authentication_flow(flow_id) --- keycloak/keycloak_admin.py | 17 ++++++++++++++++- keycloak/urls_patterns.py | 1 + 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index d7dfc714..fc96e879 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -48,7 +48,8 @@ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ - URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION + URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION, \ + URL_ADMIN_FLOW class KeycloakAdmin: @@ -1426,6 +1427,20 @@ def copy_authentication_flow(self, payload, flow_alias): data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + def delete_authentication_flow(self, flow_id): + """ + Delete authentication flow + + AuthenticationInfoRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationinforepresentation + + :param flow_id: authentication flow id + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": flow_id} + data_raw = self.raw_delete(URL_ADMIN_FLOW.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + def get_authentication_flow_executions(self, flow_alias): """ Get authentication flow executions. Returns all execution steps diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 148fe4fb..1fcdb590 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -93,6 +93,7 @@ URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" URL_ADMIN_FLOWS = "admin/realms/{realm-name}/authentication/flows" +URL_ADMIN_FLOW = URL_ADMIN_FLOWS + "/{id}" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" From 9777a85fa09ec8339ca3b68d1f2a58ce433a4a6f Mon Sep 17 00:00:00 2001 From: ggallard Date: Tue, 23 Nov 2021 13:25:12 -0300 Subject: [PATCH 127/566] fixed typo in URL pattern --- keycloak/keycloak_admin.py | 4 ++-- keycloak/urls_patterns.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index fc96e879..19c83a5b 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -45,7 +45,7 @@ URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ - URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ + URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION, \ @@ -1496,7 +1496,7 @@ def create_authentication_flow_execution(self, payload, flow_alias): """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION.format(**params_path), + data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 1fcdb590..f89fd7c9 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -98,7 +98,7 @@ URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" URL_ADMIN_FLOWS_EXECUTION = "admin/realms/{realm-name}/authentication/executions/{id}" -URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" +URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" From 2dcf68fcef691ffc43ad45d110cb1fea6d9d675e Mon Sep 17 00:00:00 2001 From: Matthew Martin Date: Sat, 4 Dec 2021 21:30:45 -0500 Subject: [PATCH 128/566] always publish wheels --- bin/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/deploy.sh b/bin/deploy.sh index e4b4d021..4c45a77b 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -9,5 +9,5 @@ username=${PYPI_USERNAME} password=${PYPI_PASSWORD} EOF -python setup.py sdist +python setup.py sdist bdist_wheel --universal twine upload dist/* From 58c4426a4540fe1c067391986482a3f1c0d4be3f Mon Sep 17 00:00:00 2001 From: Matthew Martin Date: Sat, 4 Dec 2021 21:54:15 -0500 Subject: [PATCH 129/566] pure, not universal. --- bin/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/deploy.sh b/bin/deploy.sh index 4c45a77b..9086dec9 100755 --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -9,5 +9,5 @@ username=${PYPI_USERNAME} password=${PYPI_PASSWORD} EOF -python setup.py sdist bdist_wheel --universal +python setup.py sdist bdist_wheel twine upload dist/* From 9abbf559ac32bc3beb9e6d6612e0f9d1aa5697dc Mon Sep 17 00:00:00 2001 From: modularTaco <37046961+modularTaco@users.noreply.github.com> Date: Mon, 10 Jan 2022 12:09:17 +0100 Subject: [PATCH 130/566] add client session stats Signed-off-by: modularTaco <37046961+modularTaco@users.noreply.github.com> --- keycloak/keycloak_admin.py | 18 ++++++++++++++++-- keycloak/urls_patterns.py | 4 +++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..6a744255 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1928,7 +1928,6 @@ def get_client_all_sessions(self, client_id): data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def delete_user_realm_role(self, user_id, payload): """ Delete realm-level role mappings @@ -1938,4 +1937,19 @@ def delete_user_realm_role(self, user_id, payload): params_path = {"realm-name": self.realm_name, "id": str(user_id) } data_raw = self.connection.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) \ No newline at end of file + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + + def get_client_sessions_stats(self): + """ + Get current session count for all clients with active sessions + + https://www.keycloak.org/docs-api/16.1/rest-api/index.html#_getclientsessionstats + + :return: Dict of clients and session count + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get( + self.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..54026cfc 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -109,4 +109,6 @@ URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' -URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" \ No newline at end of file +URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" + +URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" From e13dd54c2b32fbd9c9800c5ce2bc2a1c595474d0 Mon Sep 17 00:00:00 2001 From: Salem Wafi <32916450+SalemWafi@users.noreply.github.com> Date: Sat, 5 Feb 2022 01:50:12 -0600 Subject: [PATCH 131/566] bug fix for Issue #260 --- keycloak/urls_patterns.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..74b82119 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -70,7 +70,7 @@ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composites" URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" -URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" +URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" @@ -109,4 +109,4 @@ URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' -URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" \ No newline at end of file +URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" From a29f282c69a1cb17cf640d9f8194987e96c6dcbe Mon Sep 17 00:00:00 2001 From: Salem Wafi Date: Sat, 5 Feb 2022 12:06:55 -0600 Subject: [PATCH 132/566] #262 Added three functions to get all the client's authorization scopes, permissions, policies using Keycloak Admin --- keycloak/keycloak_admin.py | 42 +++++++++++++++++++++++++++++++++++++- keycloak/urls_patterns.py | 3 +++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..c58189c1 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -31,7 +31,8 @@ from .connection import ConnectionManager from .exceptions import raise_error_from_response, KeycloakGetError from .keycloak_openid import KeycloakOpenID -from .urls_patterns import URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ +from .urls_patterns import URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS, URL_ADMIN_CLIENT_AUTHZ_POLICIES, \ + URL_ADMIN_CLIENT_AUTHZ_SCOPES, URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ @@ -879,6 +880,45 @@ def get_client_authz_resources(self, client_id): data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) return data_raw + def get_client_authz_scopes(self, client_id): + """ + Get scopes from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) + return data_raw + + def get_client_authz_permissions(self, client_id): + """ + Get permissions from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path)) + return data_raw + + def get_client_authz_policies(self, client_id): + """ + Get policies from client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path)) + return data_raw + def get_client_service_account_user(self, client_id): """ Get service account user from client. diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..c9fcf9e0 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -71,6 +71,9 @@ URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource" +URL_ADMIN_CLIENT_AUTHZ_SCOPES = URL_ADMIN_CLIENT + "/authz/resource-server/scope?max=-1" +URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT + "/authz/resource-server/permission?max=-1" +URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy?max=-1" URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" URL_ADMIN_CLIENT_INSTALLATION_PROVIDER = URL_ADMIN_CLIENT + "/installation/providers/{provider-id}" From 6e8af8ff4924aba7f4df62385425c74f9673a7eb Mon Sep 17 00:00:00 2001 From: Salem Wafi Date: Sun, 6 Feb 2022 09:29:50 -0600 Subject: [PATCH 133/566] Updated README --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index da88a9b1..bb762d5a 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,18 @@ keycloak_admin.get_composite_client_roles_of_user(user_id="user_id", client_id=" keycloak_admin.delete_client_roles_of_user(client_id="client_id", user_id="user_id", roles={"id": "role-id"}) keycloak_admin.delete_client_roles_of_user(client_id="client_id", user_id="user_id", roles=[{"id": "role-id_1"}, {"id": "role-id_2"}]) +# Get all client authorization resources +client_resources = get_client_authz_resources(client_id="client_id") + +# Get all client authorization scopes +client_scopes = get_client_authz_scopes(client_id="client_id") + +# Get all client authorization permissions +client_permissions = get_client_authz_permissions(client_id="client_id") + +# Get all client authorization policies +client_policies = get_client_authz_policies(client_id="client_id") + # Create new group group = keycloak_admin.create_group(name="Example Group") From 738e9ecc7094853ec95f092788042f4333a7e801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konstantin=20K=C3=B6hring?= Date: Fri, 11 Feb 2022 16:26:22 +0100 Subject: [PATCH 134/566] Add methods to get available and composite realm roles of a user --- keycloak/keycloak_admin.py | 23 ++++++++++++++++++++++- keycloak/urls_patterns.py | 2 ++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f567be19..d2513b1f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -42,7 +42,8 @@ URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ + URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_USER_REALM_ROLES_AVAILABLE, URL_ADMIN_USER_REALM_ROLES_COMPOSITE, \ + URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ URL_ADMIN_FLOWS_EXECUTIONS_EXEUCUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ @@ -1233,6 +1234,26 @@ def get_realm_roles_of_user(self, user_id): data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_available_realm_roles_of_user(self, user_id): + """ + Get all available (i.e. unassigned) realm roles for a user. + :param user_id: id of user + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES_AVAILABLE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_composite_realm_roles_of_user(self, user_id): + """ + Get all composite (i.e. implicit) realm roles for a user. + :param user_id: id of user + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_group_realm_roles(self, group_id, roles): """ Assign realm roles to a group diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 13325867..69da536e 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -43,6 +43,8 @@ URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions" URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" +URL_ADMIN_USER_REALM_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm/available" +URL_ADMIN_USER_REALM_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm/composite" URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" From 4ba3007e412997365f3e53523c821fd36939cc38 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Mon, 14 Feb 2022 12:04:40 -0300 Subject: [PATCH 135/566] Reverted connection deepcopy on KeycloakAdmin --- keycloak/keycloak_admin.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index f3ddbcd0..4f7356f6 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1920,8 +1920,10 @@ def get_token(self): # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = deepcopy(self.keycloak_openid.connection()) - self._connection._headers.update(headers) + self._connection = ConnectionManager(base_url=self.server_url, + headers=headers, + timeout=60, + verify=self.verify) def refresh_token(self): refresh_token = self.token.get('refresh_token') From 28c82d10510426ea6be79450595c8e8d55fd77a8 Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Mon, 14 Feb 2022 12:08:02 -0300 Subject: [PATCH 136/566] Removed unsed import. --- keycloak/keycloak_admin.py | 1 - 1 file changed, 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4f7356f6..ab3ef200 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -26,7 +26,6 @@ import json from builtins import isinstance -from copy import deepcopy from typing import Iterable from .connection import ConnectionManager From d208fb60b15bc01245bddd681c86e61fc47c186a Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Wed, 16 Feb 2022 10:47:30 -0300 Subject: [PATCH 137/566] Fixed conflict. --- keycloak/urls_patterns.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 933acb1e..d7dd16a5 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -129,8 +129,5 @@ URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" -<<<<<<< HEAD -======= - URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" ->>>>>>> 28f67a0d1c47a9fed090157ea7e519e35fa20b56 + From 47021057a77379d08249efa015549b64584e174b Mon Sep 17 00:00:00 2001 From: Marcos Pereira Jr Date: Wed, 16 Feb 2022 10:54:13 -0300 Subject: [PATCH 138/566] New release. --- docs/source/conf.py | 4 ++-- docs/source/index.rst | 1 + setup.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7b52edc4..9dc7661e 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.26.1' +version = '0.27.0' # The full version, including alpha/beta/rc tags. -release = '0.26.1' +release = '0.27.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/index.rst b/docs/source/index.rst index 15217721..66753529 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -304,3 +304,4 @@ Main methods:: # Delete the key keycloak_admin.delete_component(component['id']) + diff --git a/setup.py b/setup.py index e6cb84b7..556882fb 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.26.1', + version='0.27.0', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From 7cef48f7259edb6950f595e157ae334cff701530 Mon Sep 17 00:00:00 2001 From: carlos Date: Wed, 16 Feb 2022 18:02:17 +0000 Subject: [PATCH 139/566] Fix get_groups() so that it returns groups and not users --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4890c3c0..38daa632 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -705,7 +705,7 @@ def get_groups(self, query=None): """ query = query or {} params_path = {"realm-name": self.realm_name} - url = URL_ADMIN_USERS.format(**params_path) + url = URL_ADMIN_GROUPS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) From b57d88471824749f5f959bcd182f65f4ae94afc0 Mon Sep 17 00:00:00 2001 From: Salem Wafi Date: Sat, 5 Mar 2022 10:40:14 -0600 Subject: [PATCH 140/566] Fix the issue of the token getting expire for some functions in the keycloak admin --- keycloak/keycloak_admin.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4890c3c0..b77234e4 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1348,7 +1348,7 @@ def update_realm_role(self, role_name, payload): """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.connection.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), + data_raw = self.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -1360,7 +1360,7 @@ def delete_realm_role(self, role_name): """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.connection.raw_delete( + data_raw = self.raw_delete( URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -2337,7 +2337,7 @@ def get_client_all_sessions(self, client_id): :return: UserSessionRepresentation """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.connection.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_user_realm_role(self, user_id, payload): @@ -2347,7 +2347,7 @@ def delete_user_realm_role(self, user_id, payload): """ params_path = {"realm-name": self.realm_name, "id": str(user_id) } - data_raw = self.connection.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), + data_raw = self.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) From 17b507427f8feea239dfa9eb59523868599b2082 Mon Sep 17 00:00:00 2001 From: maxnoto Date: Fri, 25 Mar 2022 17:46:16 +0100 Subject: [PATCH 141/566] Bugfix get_group_members --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4890c3c0..53bec0ff 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -762,7 +762,7 @@ def get_group_members(self, group_id, **query): :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - url = URL_ADMIN_USERS.format(**params_path) + url = URL_ADMIN_GROUP_MEMBERS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) From 64d74be6afd3e546551429eac047d497caa5c759 Mon Sep 17 00:00:00 2001 From: Bruno Bonfils Date: Tue, 3 May 2022 10:55:38 +0200 Subject: [PATCH 142/566] Add support of TOTP to KeycloakAdmin --- keycloak/keycloak_admin.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4890c3c0..bf09ecdc 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -65,6 +65,7 @@ class KeycloakAdmin: _server_url = None _username = None _password = None + _totp = None _realm_name = None _client_id = None _verify = None @@ -75,13 +76,14 @@ class KeycloakAdmin: _custom_headers = None _user_realm_name = None - def __init__(self, server_url, username=None, password=None, realm_name='master', client_id='admin-cli', verify=True, - client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None): + def __init__(self, server_url, username=None, password=None, totp=None, realm_name='master', client_id='admin-cli', + verify=True, client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None): """ :param server_url: Keycloak server url :param username: admin username :param password: admin password + :param totp: Time based OTP :param realm_name: realm name :param client_id: client id :param verify: True if want check connection SSL @@ -93,6 +95,7 @@ def __init__(self, server_url, username=None, password=None, realm_name='master' self.server_url = server_url self.username = username self.password = password + self.totp = totp self.realm_name = realm_name self.client_id = client_id self.verify = verify @@ -168,6 +171,14 @@ def password(self): def password(self, value): self._password = value + @property + def totp(self): + return self._totp + + @totp.setter + def totp(self, value): + self._totp = value + @property def token(self): return self._token @@ -2286,7 +2297,8 @@ def get_token(self): self.realm_name = self.user_realm_name if self.username and self.password: - self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) + self._token = self.keycloak_openid.token(self.username, self.password, + grant_type=grant_type, totp=self.totp) headers = { 'Authorization': 'Bearer ' + self.token.get('access_token'), From 25b9097fd10569df023ddf875d3160803e2fc150 Mon Sep 17 00:00:00 2001 From: Bruno Bonfils Date: Tue, 3 May 2022 16:26:43 +0200 Subject: [PATCH 143/566] Add 202 expected return code when update flow #288 --- keycloak/keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 4890c3c0..45e3374f 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1707,7 +1707,7 @@ def update_authentication_flow_executions(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[202,204]) def get_authentication_flow_execution(self, execution_id): """ From 1f574ab5d8f2faed0b7326b35264268f0572e6d0 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 18 May 2022 20:43:33 +0200 Subject: [PATCH 144/566] fix(release): version bumps for hotfix release Applied version bumps --- docs/source/conf.py | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9dc7661e..166a8f45 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -60,9 +60,9 @@ # built documents. # # The short X.Y version. -version = '0.27.0' +version = '0.27.1' # The full version, including alpha/beta/rc tags. -release = '0.27.0' +release = '0.27.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/setup.py b/setup.py index 556882fb..0ffc5aa6 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name='python-keycloak', - version='0.27.0', + version='0.27.1', url='https://github.com/marcospereirampj/python-keycloak', license='The MIT License', author='Marcos Pereira', From cc82e6a8749da1188a8ce57fa2bd9d4e899233b6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 11:51:53 +0200 Subject: [PATCH 145/566] feat: initial setup of CICD and linting --- .circleci/config.yml | 37 - .github/workflows/bump.yaml | 32 + .github/workflows/daily.yaml | 27 + .github/workflows/lint.yaml | 90 ++ .github/workflows/publish.yaml | 33 + .gitignore | 3 +- .pre-commit-config.yaml | 16 + .readthedocs.yaml | 10 + .releaserc.json | 8 + CHANGELOG.md | 31 +- CODEOWNERS | 1 + CONTRIBUTING.md | 86 ++ MANIFEST.in | 3 + dev-requirements.txt | 5 + docs-requirements.txt | 9 + docs/source/conf.py | 70 +- keycloak/_version.py | 1 + keycloak/authorization/__init__.py | 53 +- keycloak/authorization/policy.py | 5 +- keycloak/connection.py | 102 ++- keycloak/exceptions.py | 26 +- keycloak/keycloak_admin.py | 626 ++++++++----- keycloak/keycloak_openid.py | 140 +-- keycloak/tests/test_connection.py | 191 ---- keycloak/urls_patterns.py | 59 +- pyproject.toml | 6 + requirements.txt | 6 +- setup.py | 65 +- test_keycloak_init.sh | 35 + {keycloak/tests => tests}/__init__.py | 0 tests/conftest.py | 61 ++ tests/test_keycloak_admin.py | 1201 +++++++++++++++++++++++++ tests/test_urls_patterns.py | 26 + tox.env | 4 + tox.ini | 48 + 35 files changed, 2416 insertions(+), 700 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/bump.yaml create mode 100644 .github/workflows/daily.yaml create mode 100644 .github/workflows/lint.yaml create mode 100644 .github/workflows/publish.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 .releaserc.json create mode 100644 CODEOWNERS create mode 100644 CONTRIBUTING.md create mode 100644 dev-requirements.txt create mode 100644 docs-requirements.txt create mode 100644 keycloak/_version.py delete mode 100644 keycloak/tests/test_connection.py create mode 100644 pyproject.toml create mode 100755 test_keycloak_init.sh rename {keycloak/tests => tests}/__init__.py (100%) create mode 100644 tests/conftest.py create mode 100644 tests/test_keycloak_admin.py create mode 100644 tests/test_urls_patterns.py create mode 100644 tox.env create mode 100644 tox.ini diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index e2ab2c35..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: 2 -jobs: - build: - docker: - - image: circleci/python:3.6.1 - - working_directory: ~/repo - - steps: - - checkout - - restore_cache: - keys: - - v1-dependencies-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: - name: install dependencies - command: | - python3 -m venv venv - . venv/bin/activate - pip install -r requirements.txt - - - save_cache: - paths: - - ./venv - key: v1-dependencies-{{ checksum "requirements.txt" }} - - - run: - name: run tests - command: | - . venv/bin/activate - python3 -m unittest discover - - - store_artifacts: - path: test-reports - destination: test-reports \ No newline at end of file diff --git a/.github/workflows/bump.yaml b/.github/workflows/bump.yaml new file mode 100644 index 00000000..e5623469 --- /dev/null +++ b/.github/workflows/bump.yaml @@ -0,0 +1,32 @@ +name: Bump version + +on: + workflow_run: + workflows: [ "Lint" ] + branches: [ master ] + types: + - completed + +jobs: + tag-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT_TOKEN }} + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: determine-version + run: | + VERSION=$(npx semantic-release --branches master --dry-run | { grep -i 'the next release version is' || test $? = 1; } | sed -E 's/.* ([[:digit:].]+)$/\1/') + echo "VERSION=$VERSION" >> $GITHUB_ENV + id: version + - uses: rickstaa/action-create-tag@v1 + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + with: + tag: v${{ env.VERSION }} + message: "Releasing v${{ env.VERSION }}" + github_token: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml new file mode 100644 index 00000000..7ddc6227 --- /dev/null +++ b/.github/workflows/daily.yaml @@ -0,0 +1,27 @@ +name: Daily check + +on: + schedule: + - cron: '0 4 * * *' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - uses: docker-practice/actions-setup-docker@master + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run tests + run: | + tox -e tests diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 00000000..2cade19a --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,90 @@ +name: Lint + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + check-commits: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: webiny/action-conventional-commits@v1.0.3 + + check-linting: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Check linting, formatting + run: | + tox -e check + + check-docs: + runs-on: ubuntu-latest + needs: + - check-commits + - check-linting + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Check documentation build + run: | + tox -e docs + + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.7", "3.8", "3.9", "3.10"] + needs: + - check-commits + - check-linting + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - uses: docker-practice/actions-setup-docker@master + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run tests + run: | + tox -e tests + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run build + run: | + tox -e build diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 00000000..95198296 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,33 @@ +name: Publish + +on: + push: + tags: + - 'v*' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox wheel twine + - name: Apply the tag version + run: | + version=${{ github.ref_name }} + sed -i 's/__version__ = .*/__version__ = "'${version:1}'"/' keycloak/_version.py + - name: Run build + run: | + tox -e build + - name: Publish to PyPi + env: + TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + run: | + twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* diff --git a/.gitignore b/.gitignore index 7ea99027..4c8d46d3 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,5 @@ ENV/ .idea/ main.py main2.py -s3air-authz-config.json \ No newline at end of file +s3air-authz-config.json +.vscode \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..806a12ce --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,16 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v1.2.0 + hooks: + - id: conventional-pre-commit + stages: [ commit-msg ] + args: [ ] # optional: list of Conventional Commits types to allow diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..7aa6ce59 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,10 @@ +version: 2 + +build: + os: "ubuntu-20.04" + tools: + python: "3.10" + +python: + install: + - requirements: docs-requirements.txt diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 00000000..c89e61ef --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,8 @@ +{ + "plugins": ["@semantic-release/commit-analyzer"], + "verifyConditions": false, + "npmPublish": false, + "publish": false, + "fail": false, + "success": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md index c8891db5..4492c3dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,44 @@ -Changelog -============ +# Changelog All notable changes to this project will be documented in this file. ## [0.5.0] - 2017-08-21 -* Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, -entitlement, instropect) +- Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, + entitlement, instropect) ## [0.6.0] - 2017-08-23 -* Added load authorization settings +- Added load authorization settings ## [0.7.0] - 2017-08-23 -* Added polices +- Added polices ## [0.8.0] - 2017-08-23 -* Added permissions +- Added permissions ## [0.9.0] - 2017-09-05 -* Added functions for Admin Keycloak API +- Added functions for Admin Keycloak API ## [0.10.0] - 2017-10-23 -* Updated libraries versions -* Updated Docs +- Updated libraries versions +- Updated Docs ## [0.11.0] - 2017-12-12 -* Changed Instropect RPT +- Changed Instropect RPT ## [0.12.0] - 2018-01-25 -* Add groups functions -* Add Admin Tasks for user and client role management -* Function to trigger user sync from provider +- Add groups functions +- Add Admin Tasks for user and client role management +- Function to trigger user sync from provider ## [0.12.1] - 2018-08-04 -* Add get_idps -* Rework group functions +- Add get_idps +- Rework group functions diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 00000000..853ebe53 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1 @@ +* @ryshoooo @marcospereirampj diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..683f7eec --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,86 @@ +# Contributing + +Welcome to the Python Keycloak contributing guidelines. We are all more than happy to receive +any contributions to the repository and want to thank you in advance for your contributions! +This document outlines the process and the guidelines on how contributions work for this repository. + +## Setting up the dev environment + +The development environment is mainly up to the developer. Our recommendations are to create a python +virtual environment and install the necessary requirements. Example + +```sh +python -m venv venv +source venv/bin/activate +python -m pip install -U pip +python -m pip install -r requirements.txt +python -m pip install -r dev-requirements.txt +``` + +## Running checks and tests + +We're utilizing `tox` for most of the testing workflows. However we also have an external dependency on `docker`. +We're using docker to spin up a local keycloak instance which we run our test cases against. This is to avoid +a lot of unnecessary mocking and yet have immediate feedback from the actual Keycloak instance. All of the setup +is done for you with the tox environments, all you need is to have both tox and docker installed +(`tox` is included in the `dev-requirements.txt`). + +To run the unit tests, simply run + +```sh +tox -e tests +``` + +The project is also adhering to strict linting (flake8) and formatting (black + isort). You can always check that +your code changes adhere to the format by running + +```sh +tox -e check +``` + +If the check fails, you'll see an error message specifying what went wrong. To simplify things, you can also run + +```sh +tox -e apply-check +``` + +which will apply isort and black formatting for you in the repository. The flake8 problems however need to be resolved +manually by the developer. + +Additionally we require that the documentation pages are built without warnings. This check is also run via tox, using +the command + +```sh +tox -e docs +``` + +The check is also run in the CICD pipelines. We require that the documentation pages built from the code docstrings +do not create visually "bad" pages. + +## Conventional commits + +Commits to this project must adhere to the [Conventional Commits +specification](https://www.conventionalcommits.org/en/v1.0.0/) that will allow +us to automate version bumps and changelog entry creation. + +After cloning this repository, you must install the pre-commit hook for +conventional commits (this is included in the `dev-requirements.txt`) + +```sh +python3 -m venv .venv +source .venv/bin/activate +python3 -m pip install pre-commit +pre-commit install --install-hooks -t pre-commit -t pre-push -t commit-msg +``` + +## How to contribute + +1. Fork this repository, develop and test your changes +2. Make sure that your changes do not decrease the test coverage +3. Make sure you're commits follow the conventional commits +4. Submit a pull request + +## How to release + +The CICD pipelines are set up for the repository. When a PR is merged, a new version of the library +will be automatically deployed to the PyPi server, meaning you'll be able to see your changes immediately. diff --git a/MANIFEST.in b/MANIFEST.in index 1aba38f6..acf84afb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,4 @@ include LICENSE +include requirements.txt +include dev-requirements.txt +include docs-requirements.txt diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 00000000..d2f6981e --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,5 @@ +tox +pytest +pytest-cov +wheel +pre-commit diff --git a/docs-requirements.txt b/docs-requirements.txt new file mode 100644 index 00000000..343bf895 --- /dev/null +++ b/docs-requirements.txt @@ -0,0 +1,9 @@ +mock +alabaster +commonmark +recommonmark +sphinx +sphinx-rtd-theme +readthedocs-sphinx-ext +m2r2 +sphinx-autoapi diff --git a/docs/source/conf.py b/docs/source/conf.py index 166a8f45..23e9bbf1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,37 +32,37 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.viewcode', + "sphinx.ext.autodoc", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.viewcode", ] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = 'python-keycloak' -copyright = '2017, Marcos Pereira' -author = 'Marcos Pereira' +project = "python-keycloak" +copyright = "2017, Marcos Pereira" +author = "Marcos Pereira" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '0.27.1' +version = "0.27.1" # The full version, including alpha/beta/rc tags. -release = '0.27.1' +release = "0.27.1" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -74,13 +74,13 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] add_function_parentheses = False add_module_names = True # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True @@ -91,7 +91,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Theme options are theme-specific and customize the look and feel of a theme @@ -103,7 +103,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] html_use_smartypants = False @@ -116,7 +116,7 @@ # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -#html_sidebars = { +# html_sidebars = { # '**': [ # 'about.html', # 'navigation.html', @@ -124,13 +124,13 @@ # 'searchbox.html', # 'donate.html', # ] -#} +# } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. -htmlhelp_basename = 'python-keycloakdoc' +htmlhelp_basename = "python-keycloakdoc" # -- Options for LaTeX output --------------------------------------------- @@ -139,15 +139,12 @@ # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. # # 'preamble': '', - # Latex figure (float) alignment # # 'figure_align': 'htbp', @@ -157,8 +154,13 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - (master_doc, 'python-keycloak.tex', 'python-keycloak Documentation', - 'Marcos Pereira', 'manual'), + ( + master_doc, + "python-keycloak.tex", + "python-keycloak Documentation", + "Marcos Pereira", + "manual", + ) ] @@ -166,10 +168,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'python-keycloak', 'python-keycloak Documentation', - [author], 1) -] +man_pages = [(master_doc, "python-keycloak", "python-keycloak Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- @@ -178,10 +177,13 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'python-keycloak', 'python-keycloak Documentation', - author, 'python-keycloak', 'One line description of project.', - 'Miscellaneous'), + ( + master_doc, + "python-keycloak", + "python-keycloak Documentation", + author, + "python-keycloak", + "One line description of project.", + "Miscellaneous", + ) ] - - - diff --git a/keycloak/_version.py b/keycloak/_version.py new file mode 100644 index 00000000..6c8e6b97 --- /dev/null +++ b/keycloak/_version.py @@ -0,0 +1 @@ +__version__ = "0.0.0" diff --git a/keycloak/authorization/__init__.py b/keycloak/authorization/__init__.py index 219687bd..dad10780 100644 --- a/keycloak/authorization/__init__.py +++ b/keycloak/authorization/__init__.py @@ -55,39 +55,44 @@ def load_config(self, data): :param data: keycloak authorization data (dict) :return: """ - for pol in data['policies']: - if pol['type'] == 'role': - policy = Policy(name=pol['name'], - type=pol['type'], - logic=pol['logic'], - decision_strategy=pol['decisionStrategy']) - - config_roles = json.loads(pol['config']['roles']) + for pol in data["policies"]: + if pol["type"] == "role": + policy = Policy( + name=pol["name"], + type=pol["type"], + logic=pol["logic"], + decision_strategy=pol["decisionStrategy"], + ) + + config_roles = json.loads(pol["config"]["roles"]) for role in config_roles: - policy.add_role(Role(name=role['id'], - required=role['required'])) + policy.add_role(Role(name=role["id"], required=role["required"])) self.policies[policy.name] = policy - if pol['type'] == 'scope': - permission = Permission(name=pol['name'], - type=pol['type'], - logic=pol['logic'], - decision_strategy=pol['decisionStrategy']) + if pol["type"] == "scope": + permission = Permission( + name=pol["name"], + type=pol["type"], + logic=pol["logic"], + decision_strategy=pol["decisionStrategy"], + ) - permission.scopes = ast.literal_eval(pol['config']['scopes']) + permission.scopes = ast.literal_eval(pol["config"]["scopes"]) - for policy_name in ast.literal_eval(pol['config']['applyPolicies']): + for policy_name in ast.literal_eval(pol["config"]["applyPolicies"]): self.policies[policy_name].add_permission(permission) - if pol['type'] == 'resource': - permission = Permission(name=pol['name'], - type=pol['type'], - logic=pol['logic'], - decision_strategy=pol['decisionStrategy']) + if pol["type"] == "resource": + permission = Permission( + name=pol["name"], + type=pol["type"], + logic=pol["logic"], + decision_strategy=pol["decisionStrategy"], + ) - permission.resources = ast.literal_eval(pol['config'].get('resources', "[]")) + permission.resources = ast.literal_eval(pol["config"].get("resources", "[]")) - for policy_name in ast.literal_eval(pol['config']['applyPolicies']): + for policy_name in ast.literal_eval(pol["config"]["applyPolicies"]): if self.policies.get(policy_name) is not None: self.policies[policy_name].add_permission(permission) diff --git a/keycloak/authorization/policy.py b/keycloak/authorization/policy.py index 9f688f76..4fbe9138 100644 --- a/keycloak/authorization/policy.py +++ b/keycloak/authorization/policy.py @@ -98,9 +98,10 @@ def add_role(self, role): :param role: keycloak role. :return: """ - if self.type != 'role': + if self.type != "role": raise KeycloakAuthorizationConfigError( - "Can't add role. Policy type is different of role") + "Can't add role. Policy type is different of role" + ) self._roles.append(role) def add_permission(self, permission): diff --git a/keycloak/connection.py b/keycloak/connection.py index bdecfce7..8ef45b18 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -29,11 +29,11 @@ import requests from requests.adapters import HTTPAdapter -from .exceptions import (KeycloakConnectionError) +from .exceptions import KeycloakConnectionError class ConnectionManager(object): - """ Represents a simple server connection. + """Represents a simple server connection. Args: base_url (str): The server URL. headers (dict): The header parameters of the requests to the server. @@ -52,15 +52,15 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): # retry once to reset connection with Keycloak after tomcat's ConnectionTimeout # see https://github.com/marcospereirampj/python-keycloak/issues/36 - for protocol in ('https://', 'http://'): + for protocol in ("https://", "http://"): adapter = HTTPAdapter(max_retries=1) # adds POST to retry whitelist allowed_methods = set(adapter.max_retries.allowed_methods) - allowed_methods.add('POST') + allowed_methods.add("POST") adapter.max_retries.allowed_methods = frozenset(allowed_methods) self._s.mount(protocol, adapter) - + if proxies: self._s.proxies.update(proxies) @@ -69,7 +69,7 @@ def __del__(self): @property def base_url(self): - """ Return base url in use for requests to the server. """ + """Return base url in use for requests to the server.""" return self._base_url @base_url.setter @@ -79,7 +79,7 @@ def base_url(self, value): @property def timeout(self): - """ Return timeout in use for request to the server. """ + """Return timeout in use for request to the server.""" return self._timeout @timeout.setter @@ -89,7 +89,7 @@ def timeout(self, value): @property def verify(self): - """ Return verify in use for request to the server. """ + """Return verify in use for request to the server.""" return self._verify @verify.setter @@ -99,7 +99,7 @@ def verify(self, value): @property def headers(self): - """ Return header request to the server. """ + """Return header request to the server.""" return self._headers @headers.setter @@ -108,7 +108,7 @@ def headers(self, value): self._headers = value def param_headers(self, key): - """ Return a specific header parameter. + """Return a specific header parameter. :arg key (str): Header parameters key. :return: @@ -117,11 +117,11 @@ def param_headers(self, key): return self.headers.get(key) def clean_headers(self): - """ Clear header parameters. """ + """Clear header parameters.""" self.headers = {} def exist_param_headers(self, key): - """ Check if the parameter exists in the header. + """Check if the parameter exists in the header. :arg key (str): Header parameters key. :return: @@ -130,7 +130,7 @@ def exist_param_headers(self, key): return self.param_headers(key) is not None def add_param_headers(self, key, value): - """ Add a single parameter inside the header. + """Add a single parameter inside the header. :arg key (str): Header parameters key. value (str): Value to be added. @@ -138,14 +138,14 @@ def add_param_headers(self, key, value): self.headers[key] = value def del_param_headers(self, key): - """ Remove a specific parameter. + """Remove a specific parameter. :arg key (str): Key of the header parameters. """ self.headers.pop(key, None) def raw_get(self, path, **kwargs): - """ Submit get request to the path. + """Submit get request to the path. :arg path (str): Path for request. :return @@ -155,17 +155,18 @@ def raw_get(self, path, **kwargs): """ try: - return self._s.get(urljoin(self.base_url, path), - params=kwargs, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.get( + urljoin(self.base_url, path), + params=kwargs, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) def raw_post(self, path, data, **kwargs): - """ Submit post request to the path. + """Submit post request to the path. :arg path (str): Path for request. data (dict): Payload for request. @@ -175,18 +176,19 @@ def raw_post(self, path, data, **kwargs): HttpError: Can't connect to server. """ try: - return self._s.post(urljoin(self.base_url, path), - params=kwargs, - data=data, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.post( + urljoin(self.base_url, path), + params=kwargs, + data=data, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) def raw_put(self, path, data, **kwargs): - """ Submit put request to the path. + """Submit put request to the path. :arg path (str): Path for request. data (dict): Payload for request. @@ -196,18 +198,19 @@ def raw_put(self, path, data, **kwargs): HttpError: Can't connect to server. """ try: - return self._s.put(urljoin(self.base_url, path), - params=kwargs, - data=data, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.put( + urljoin(self.base_url, path), + params=kwargs, + data=data, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) def raw_delete(self, path, data={}, **kwargs): - """ Submit delete request to the path. + """Submit delete request to the path. :arg path (str): Path for request. @@ -218,12 +221,13 @@ def raw_delete(self, path, data={}, **kwargs): HttpError: Can't connect to server. """ try: - return self._s.delete(urljoin(self.base_url, path), - params=kwargs, - data=data, - headers=self.headers, - timeout=self.timeout, - verify=self.verify) + return self._s.delete( + urljoin(self.base_url, path), + params=kwargs, + data=data, + headers=self.headers, + timeout=self.timeout, + verify=self.verify, + ) except Exception as e: - raise KeycloakConnectionError( - "Can't connect to server (%s)" % e) + raise KeycloakConnectionError("Can't connect to server (%s)" % e) diff --git a/keycloak/exceptions.py b/keycloak/exceptions.py index 67da62a1..a9c1b2b7 100644 --- a/keycloak/exceptions.py +++ b/keycloak/exceptions.py @@ -25,8 +25,7 @@ class KeycloakError(Exception): - def __init__(self, error_message="", response_code=None, - response_body=None): + def __init__(self, error_message="", response_code=None, response_body=None): Exception.__init__(self, error_message) @@ -56,10 +55,23 @@ class KeycloakOperationError(KeycloakError): class KeycloakDeprecationError(KeycloakError): pass + class KeycloakGetError(KeycloakOperationError): pass +class KeycloakPostError(KeycloakOperationError): + pass + + +class KeycloakPutError(KeycloakOperationError): + pass + + +class KeycloakDeleteError(KeycloakOperationError): + pass + + class KeycloakSecretNotFound(KeycloakOperationError): pass @@ -90,10 +102,10 @@ def raise_error_from_response(response, error, expected_codes=None, skip_exists= return response.content if skip_exists and response.status_code == 409: - return {"Already exists"} + return {"msg": "Already exists"} try: - message = response.json()['message'] + message = response.json()["message"] except (KeyError, ValueError): message = response.content @@ -103,6 +115,6 @@ def raise_error_from_response(response, error, expected_codes=None, skip_exists= if response.status_code == 401: error = KeycloakAuthenticationError - raise error(error_message=message, - response_code=response.status_code, - response_body=response.content) + raise error( + error_message=message, response_code=response.status_code, response_body=response.content + ) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 41ecab8f..41f45679 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,33 +29,91 @@ from typing import Iterable from .connection import ConnectionManager -from .exceptions import raise_error_from_response, KeycloakGetError +from .exceptions import KeycloakGetError, raise_error_from_response from .keycloak_openid import KeycloakOpenID - -from .urls_patterns import URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS, URL_ADMIN_CLIENT_AUTHZ_POLICIES, \ - URL_ADMIN_CLIENT_AUTHZ_SCOPES, URL_ADMIN_SERVER_INFO, URL_ADMIN_CLIENT_AUTHZ_RESOURCES, URL_ADMIN_CLIENT_ROLES, \ - URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, \ - URL_ADMIN_GET_SESSIONS, URL_ADMIN_RESET_PASSWORD, URL_ADMIN_SEND_UPDATE_ACCOUNT, URL_ADMIN_GROUPS_REALM_ROLES, \ - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, \ - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, URL_ADMIN_GROUPS_CLIENT_ROLES, \ - URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, URL_ADMIN_USER_GROUP, URL_ADMIN_REALM_ROLES, URL_ADMIN_GROUP_CHILD, \ - URL_ADMIN_USER_CONSENTS, URL_ADMIN_SEND_VERIFY_EMAIL, URL_ADMIN_CLIENT, URL_ADMIN_USER, URL_ADMIN_CLIENT_ROLE, \ - URL_ADMIN_USER_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_FLOWS_EXECUTIONS, URL_ADMIN_GROUPS, URL_ADMIN_USER_CLIENT_ROLES, \ - URL_ADMIN_REALMS, URL_ADMIN_USERS_COUNT, URL_ADMIN_FLOWS, URL_ADMIN_GROUP, URL_ADMIN_CLIENT_AUTHZ_SETTINGS, \ - URL_ADMIN_GROUP_MEMBERS, URL_ADMIN_USER_STORAGE, URL_ADMIN_GROUP_PERMISSIONS, URL_ADMIN_IDPS, URL_ADMIN_IDP, \ - URL_ADMIN_IDP_MAPPERS, URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, URL_ADMIN_USERS, URL_ADMIN_CLIENT_SCOPES, \ - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, URL_ADMIN_CLIENT_SCOPE, URL_ADMIN_CLIENT_SECRETS, \ - URL_ADMIN_USER_REALM_ROLES, URL_ADMIN_USER_REALM_ROLES_AVAILABLE, URL_ADMIN_USER_REALM_ROLES_COMPOSITE, \ - URL_ADMIN_REALM, URL_ADMIN_COMPONENTS, URL_ADMIN_COMPONENT, URL_ADMIN_KEYS, \ - URL_ADMIN_USER_FEDERATED_IDENTITY, URL_ADMIN_USER_FEDERATED_IDENTITIES, URL_ADMIN_CLIENT_ROLE_MEMBERS, \ - URL_ADMIN_REALM_ROLES_MEMBERS, URL_ADMIN_CLIENT_PROTOCOL_MAPPER, URL_ADMIN_CLIENT_SCOPES_MAPPERS, \ - URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, URL_ADMIN_FLOWS_EXECUTIONS_FLOW, URL_ADMIN_FLOWS_COPY, \ - URL_ADMIN_FLOWS_ALIAS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, URL_ADMIN_AUTHENTICATOR_CONFIG, \ - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, URL_ADMIN_CLIENT_ALL_SESSIONS, URL_ADMIN_EVENTS, \ - URL_ADMIN_REALM_EXPORT, URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_USER_LOGOUT, URL_ADMIN_FLOWS_EXECUTION, \ - URL_ADMIN_FLOW, URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, \ - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, \ - URL_ADMIN_USER_CREDENTIALS, URL_ADMIN_USER_CREDENTIAL, URL_ADMIN_CLIENT_PROTOCOL_MAPPERS +from .urls_patterns import ( + URL_ADMIN_AUTHENTICATOR_CONFIG, + URL_ADMIN_CLIENT, + URL_ADMIN_CLIENT_ALL_SESSIONS, + URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS, + URL_ADMIN_CLIENT_AUTHZ_POLICIES, + URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, + URL_ADMIN_CLIENT_AUTHZ_RESOURCES, + URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, + URL_ADMIN_CLIENT_AUTHZ_SCOPES, + URL_ADMIN_CLIENT_AUTHZ_SETTINGS, + URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, + URL_ADMIN_CLIENT_PROTOCOL_MAPPER, + URL_ADMIN_CLIENT_PROTOCOL_MAPPERS, + URL_ADMIN_CLIENT_ROLE, + URL_ADMIN_CLIENT_ROLE_MEMBERS, + URL_ADMIN_CLIENT_ROLES, + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, + URL_ADMIN_CLIENT_SCOPE, + URL_ADMIN_CLIENT_SCOPES, + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, + URL_ADMIN_CLIENT_SCOPES_MAPPERS, + URL_ADMIN_CLIENT_SECRETS, + URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, + URL_ADMIN_CLIENTS, + URL_ADMIN_COMPONENT, + URL_ADMIN_COMPONENTS, + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, + URL_ADMIN_DELETE_USER_ROLE, + URL_ADMIN_EVENTS, + URL_ADMIN_FLOW, + URL_ADMIN_FLOWS, + URL_ADMIN_FLOWS_ALIAS, + URL_ADMIN_FLOWS_COPY, + URL_ADMIN_FLOWS_EXECUTION, + URL_ADMIN_FLOWS_EXECUTIONS, + URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, + URL_ADMIN_FLOWS_EXECUTIONS_FLOW, + URL_ADMIN_GET_SESSIONS, + URL_ADMIN_GROUP, + URL_ADMIN_GROUP_CHILD, + URL_ADMIN_GROUP_MEMBERS, + URL_ADMIN_GROUP_PERMISSIONS, + URL_ADMIN_GROUPS, + URL_ADMIN_GROUPS_CLIENT_ROLES, + URL_ADMIN_GROUPS_REALM_ROLES, + URL_ADMIN_IDP, + URL_ADMIN_IDP_MAPPERS, + URL_ADMIN_IDPS, + URL_ADMIN_KEYS, + URL_ADMIN_REALM, + URL_ADMIN_REALM_EXPORT, + URL_ADMIN_REALM_ROLES, + URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, + URL_ADMIN_REALM_ROLES_MEMBERS, + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, + URL_ADMIN_REALMS, + URL_ADMIN_RESET_PASSWORD, + URL_ADMIN_SEND_UPDATE_ACCOUNT, + URL_ADMIN_SEND_VERIFY_EMAIL, + URL_ADMIN_SERVER_INFO, + URL_ADMIN_USER, + URL_ADMIN_USER_CLIENT_ROLES, + URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, + URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, + URL_ADMIN_USER_CONSENTS, + URL_ADMIN_USER_CREDENTIAL, + URL_ADMIN_USER_CREDENTIALS, + URL_ADMIN_USER_FEDERATED_IDENTITIES, + URL_ADMIN_USER_FEDERATED_IDENTITY, + URL_ADMIN_USER_GROUP, + URL_ADMIN_USER_GROUPS, + URL_ADMIN_USER_LOGOUT, + URL_ADMIN_USER_REALM_ROLES, + URL_ADMIN_USER_REALM_ROLES_AVAILABLE, + URL_ADMIN_USER_REALM_ROLES_COMPOSITE, + URL_ADMIN_USER_STORAGE, + URL_ADMIN_USERS, + URL_ADMIN_USERS_COUNT, +) class KeycloakAdmin: @@ -75,8 +133,19 @@ class KeycloakAdmin: _custom_headers = None _user_realm_name = None - def __init__(self, server_url, username=None, password=None, realm_name='master', client_id='admin-cli', verify=True, - client_secret_key=None, custom_headers=None, user_realm_name=None, auto_refresh_token=None): + def __init__( + self, + server_url, + username=None, + password=None, + realm_name="master", + client_id="admin-cli", + verify=True, + client_secret_key=None, + custom_headers=None, + user_realm_name=None, + auto_refresh_token=None, + ): """ :param server_url: Keycloak server url @@ -198,40 +267,46 @@ def custom_headers(self, value): @auto_refresh_token.setter def auto_refresh_token(self, value): - allowed_methods = {'get', 'post', 'put', 'delete'} + allowed_methods = {"get", "post", "put", "delete"} if not isinstance(value, Iterable): - raise TypeError('Expected a list of strings among {allowed}'.format(allowed=allowed_methods)) + raise TypeError( + "Expected a list of strings among {allowed}".format(allowed=allowed_methods) + ) if not all(method in allowed_methods for method in value): - raise TypeError('Unexpected method in auto_refresh_token, accepted methods are {allowed}'.format(allowed=allowed_methods)) + raise TypeError( + "Unexpected method in auto_refresh_token, accepted methods are {allowed}".format( + allowed=allowed_methods + ) + ) self._auto_refresh_token = value def __fetch_all(self, url, query=None): - '''Wrapper function to paginate GET requests + """Wrapper function to paginate GET requests :param url: The url on which the query is executed :param query: Existing query parameters (optional) :return: Combined results of paginated queries - ''' + """ results = [] # initalize query if it was called with None if not query: query = {} page = 0 - query['max'] = self.PAGE_SIZE + query["max"] = self.PAGE_SIZE # fetch until we can while True: - query['first'] = page*self.PAGE_SIZE + query["first"] = page * self.PAGE_SIZE partial_results = raise_error_from_response( - self.raw_get(url, **query), - KeycloakGetError) + self.raw_get(url, **query), KeycloakGetError + ) if not partial_results: break results.extend(partial_results) - if len(partial_results) < query['max']: + if len(partial_results) < query["max"]: break page += 1 return results @@ -239,9 +314,7 @@ def __fetch_all(self, url, query=None): def __fetch_paginated(self, url, query=None): query = query or {} - return raise_error_from_response( - self.raw_get(url, **query), - KeycloakGetError) + return raise_error_from_response(self.raw_get(url, **query), KeycloakGetError) def import_realm(self, payload): """ @@ -255,8 +328,7 @@ def import_realm(self, payload): :return: RealmRepresentation """ - data_raw = self.raw_post(URL_ADMIN_REALMS, - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def export_realm(self, export_clients=False, export_groups_and_role=False): @@ -271,7 +343,11 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): :return: realm configurations JSON """ - params_path = {"realm-name": self.realm_name, "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role } + params_path = { + "realm-name": self.realm_name, + "export-clients": export_clients, + "export-groups-and-roles": export_groups_and_role, + } data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="") return raise_error_from_response(data_raw, KeycloakGetError) @@ -296,9 +372,10 @@ def create_realm(self, payload, skip_exists=False): :return: Keycloak server response (RealmRepresentation) """ - data_raw = self.raw_post(URL_ADMIN_REALMS, - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_realm(self, realm_name, payload): """ @@ -314,8 +391,7 @@ def update_realm(self, realm_name, payload): """ params_path = {"realm-name": realm_name} - data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm(self, realm_name): @@ -359,8 +435,7 @@ def create_idp(self, payload): :param: payload: IdentityProviderRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def add_mapper_to_idp(self, idp_alias, payload): @@ -374,8 +449,9 @@ def add_mapper_to_idp(self, idp_alias, payload): :param: payload: IdentityProviderMapperRepresentation """ params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} - data_raw = self.raw_post(URL_ADMIN_IDP_MAPPERS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_idps(self): @@ -416,16 +492,15 @@ def create_user(self, payload, exist_ok=True): params_path = {"realm-name": self.realm_name} if exist_ok: - exists = self.get_user_id(username=payload['username']) + exists = self.get_user_id(username=payload["username"]) if exists is not None: return str(exists) - data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) - _last_slash_idx = data_raw.headers['Location'].rindex('/') - return data_raw.headers['Location'][_last_slash_idx + 1:] + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] def users_count(self): """ @@ -490,8 +565,7 @@ def update_user(self, user_id, payload): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_user(self, user_id): @@ -522,8 +596,9 @@ def set_user_password(self, user_id, password, temporary=True): """ payload = {"type": "password", "temporary": temporary, "value": password} params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_put(URL_ADMIN_RESET_PASSWORD.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_credentials(self, user_id): @@ -551,7 +626,11 @@ def get_credential(self, user_id, credential_id): :param: credential_id: credential id :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + params_path = { + "realm-name": self.realm_name, + "id": user_id, + "credential_id": credential_id, + } data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -566,7 +645,11 @@ def delete_credential(self, user_id, credential_id): :param: credential_id: credential id :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": user_id, "credential_id": credential_id} + params_path = { + "realm-name": self.realm_name, + "id": user_id, + "credential_id": credential_id, + } data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -615,9 +698,15 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ :param provider_username: username specified by the provider :return: """ - payload = {"identityProvider": provider_id, "userId": provider_userid, "userName": provider_username} + payload = { + "identityProvider": provider_id, + "userId": provider_userid, + "userName": provider_username, + } params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} - data_raw = self.raw_post(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload) + ) def delete_user_social_login(self, user_id, provider_id): @@ -631,7 +720,9 @@ def delete_user_social_login(self, user_id, provider_id): data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def send_update_account(self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None): + def send_update_account( + self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None + ): """ Send an update account email to the user. An email contains a link the user can click to perform a set of required actions. @@ -646,8 +737,11 @@ def send_update_account(self, user_id, payload, client_id=None, lifespan=None, r """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} - data_raw = self.raw_put(URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), - data=json.dumps(payload), **params_query) + data_raw = self.raw_put( + URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), + data=json.dumps(payload), + **params_query + ) return raise_error_from_response(data_raw, KeycloakGetError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): @@ -663,8 +757,9 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "redirect_uri": redirect_uri} - data_raw = self.raw_put(URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), - data={}, **params_query) + data_raw = self.raw_put( + URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, **params_query + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_sessions(self, user_id): @@ -740,7 +835,7 @@ def get_subgroups(self, group, path): """ for subgroup in group["subGroups"]: - if subgroup['path'] == path: + if subgroup["path"] == path: return subgroup elif subgroup["subGroups"]: for subgroup in group["subGroups"]: @@ -787,11 +882,11 @@ def get_group_by_path(self, path, search_in_subgroups=False): # TODO: Review this code is necessary for group in groups: - if group['path'] == path: + if group["path"] == path: return group elif search_in_subgroups and group["subGroups"]: for group in group["subGroups"]: - if group['path'] == path: + if group["path"] == path: return group res = self.get_subgroups(group, path) if res != None: @@ -814,14 +909,18 @@ def create_group(self, payload, parent=None, skip_exists=False): if parent is None: params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_GROUPS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_GROUPS.format(**params_path), data=json.dumps(payload) + ) else: - params_path = {"realm-name": self.realm_name, "id": parent, } - data_raw = self.raw_post(URL_ADMIN_GROUP_CHILD.format(**params_path), - data=json.dumps(payload)) + params_path = {"realm-name": self.realm_name, "id": parent} + data_raw = self.raw_post( + URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) + ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_group(self, group_id, payload): """ @@ -837,8 +936,7 @@ def update_group(self, group_id, payload): """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): @@ -851,8 +949,10 @@ def group_set_permissions(self, group_id, enabled=True): """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_put(URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), - data=json.dumps({"enabled": enabled})) + data_raw = self.raw_put( + URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), + data=json.dumps({"enabled": enabled}), + ) return raise_error_from_response(data_raw, KeycloakGetError) def group_user_add(self, user_id, group_id): @@ -935,7 +1035,7 @@ def get_client_id(self, client_name): clients = self.get_clients() for client in clients: - if client_name == client.get('name') or client_name == client.get('clientId'): + if client_name == client.get("name") or client_name == client.get("clientId"): return client["id"] return None @@ -965,12 +1065,14 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, - "id": client_id} + params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_client_authz_resources(self, client_id): """ @@ -1008,12 +1110,15 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, - "id": client_id} + params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): """ @@ -1039,12 +1144,15 @@ def create_client_authz_resource_based_permission(self, client_id, payload, skip :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, - "id": client_id} + params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_client_authz_scopes(self, client_id): """ @@ -1110,9 +1218,10 @@ def create_client(self, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_client(self, client_id, payload): """ @@ -1124,8 +1233,7 @@ def update_client(self, client_id, payload): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_client(self, client_id): @@ -1182,7 +1290,7 @@ def get_realm_role_members(self, role_name, **query): :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) :return: Keycloak Server Response (UserRepresentation) """ - params_path = {"realm-name": self.realm_name, "role-name":role_name} + params_path = {"realm-name": self.realm_name, "role-name": role_name} return self.__fetch_all(URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query) def get_client_roles(self, client_id): @@ -1250,9 +1358,12 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name, "id": client_role_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): """ @@ -1266,8 +1377,10 @@ def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} - data_raw = self.raw_post(URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), + data=json.dumps(payload), + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_client_role(self, client_role_id, role_name): @@ -1296,8 +1409,9 @@ def assign_client_role(self, user_id, client_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} - data_raw = self.raw_post(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_role_members(self, client_id, role_name, **query): @@ -1308,9 +1422,8 @@ def get_client_role_members(self, client_id, role_name, **query): :param query: Additional query parameters ( see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clients_resource) :return: Keycloak server response (UserRepresentation) """ - params_path = {"realm-name": self.realm_name, "id":client_id, "role-name":role_name} - return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path) , query) - + params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} + return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query) def create_realm_role(self, payload, skip_exists=False): """ @@ -1322,9 +1435,12 @@ def create_realm_role(self, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_realm_role(self, role_name): """ @@ -1348,8 +1464,9 @@ def update_realm_role(self, role_name, payload): """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_put(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm_role(self, role_name): @@ -1360,8 +1477,7 @@ def delete_realm_role(self, role_name): """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_delete( - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): @@ -1377,9 +1493,9 @@ def add_composite_realm_roles_to_role(self, role_name, roles): params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_post( URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, - expected_codes=[204]) + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def remove_composite_realm_roles_to_role(self, role_name, roles): """ @@ -1394,9 +1510,9 @@ def remove_composite_realm_roles_to_role(self, role_name, roles): params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, - expected_codes=[204]) + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_composite_realm_roles_of_role(self, role_name): """ @@ -1407,8 +1523,7 @@ def get_composite_realm_roles_of_role(self, role_name): """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_get( - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) + data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def assign_realm_roles(self, user_id, roles): @@ -1422,8 +1537,9 @@ def assign_realm_roles(self, user_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_post(URL_ADMIN_USER_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_realm_roles_of_user(self, user_id, roles): @@ -1437,8 +1553,9 @@ def delete_realm_roles_of_user(self, user_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_delete(URL_ADMIN_USER_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_realm_roles_of_user(self, user_id): @@ -1484,8 +1601,9 @@ def assign_group_realm_roles(self, group_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_post(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): @@ -1499,8 +1617,9 @@ def delete_group_realm_roles(self, group_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_delete(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_group_realm_roles(self, group_id): @@ -1526,8 +1645,9 @@ def assign_group_client_roles(self, group_id, client_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} - data_raw = self.raw_post(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_group_client_roles(self, group_id, client_id): @@ -1555,8 +1675,9 @@ def delete_group_client_roles(self, group_id, client_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} - data_raw = self.raw_delete(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): @@ -1577,7 +1698,9 @@ def get_available_client_roles_of_user(self, user_id, client_id): :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ - return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id) + return self._get_client_roles_of_user( + URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id + ) def get_composite_client_roles_of_user(self, user_id, client_id): """ @@ -1587,7 +1710,9 @@ def get_composite_client_roles_of_user(self, user_id, client_id): :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ - return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id) + return self._get_client_roles_of_user( + URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id + ) def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} @@ -1605,8 +1730,9 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} - data_raw = self.raw_delete(URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_delete( + URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_authentication_flows(self): @@ -1649,9 +1775,10 @@ def create_authentication_flow(self, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload)) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def copy_authentication_flow(self, payload, flow_alias): """ @@ -1663,8 +1790,9 @@ def copy_authentication_flow(self, payload, flow_alias): """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_COPY.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def delete_authentication_flow(self, flow_id): @@ -1705,9 +1833,10 @@ def update_authentication_flow_executions(self, payload, flow_alias): """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_put(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[202,204]) + data_raw = self.raw_put( + URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[202, 204]) def get_authentication_flow_execution(self, execution_id): """ @@ -1736,8 +1865,9 @@ def create_authentication_flow_execution(self, payload, flow_alias): """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def delete_authentication_flow_execution(self, execution_id): @@ -1768,9 +1898,12 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_post(URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def get_authenticator_config(self, config_id): """ @@ -1795,8 +1928,9 @@ def update_authenticator_config(self, payload, config_id): :return: Response(json) """ params_path = {"realm-name": self.realm_name, "id": config_id} - data_raw = self.raw_put(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_authenticator_config(self, config_id): @@ -1821,12 +1955,13 @@ def sync_users(self, storage_id, action): :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" :return: """ - data = {'action': action} + data = {"action": action} params_query = {"action": action} params_path = {"realm-name": self.realm_name, "id": storage_id} - data_raw = self.raw_post(URL_ADMIN_USER_STORAGE.format(**params_path), - data=json.dumps(data), **params_query) + data_raw = self.raw_post( + URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), **params_query + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scopes(self): @@ -1866,9 +2001,12 @@ def create_client_scope(self, payload, skip_exists=False): """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_CLIENT_SCOPES.format(**params_path), - data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists) + data_raw = self.raw_post( + URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response( + data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + ) def update_client_scope(self, client_scope_id, payload): """ @@ -1882,8 +2020,9 @@ def update_client_scope(self, client_scope_id, payload): """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} - data_raw = self.raw_put(URL_ADMIN_CLIENT_SCOPE.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def add_mapper_to_client_scope(self, client_scope_id, payload): @@ -1899,7 +2038,8 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) @@ -1913,11 +2053,13 @@ def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, - "protocol-mapper-id": protocol_mppaer_id} + params_path = { + "realm-name": self.realm_name, + "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mppaer_id, + } - data_raw = self.raw_delete( - URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) + data_raw = self.raw_delete(URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -1933,11 +2075,15 @@ def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, pay :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id, - "protocol-mapper-id": protocol_mapper_id} + params_path = { + "realm-name": self.realm_name, + "scope-id": client_scope_id, + "protocol-mapper-id": protocol_mapper_id, + } data_raw = self.raw_put( - URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -1951,7 +2097,6 @@ def get_default_default_client_scopes(self): data_raw = self.raw_get(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def delete_default_default_client_scope(self, scope_id): """ Delete default default client scope @@ -1963,7 +2108,6 @@ def delete_default_default_client_scope(self, scope_id): data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def add_default_default_client_scope(self, scope_id): """ Add default default client scope @@ -1973,10 +2117,11 @@ def add_default_default_client_scope(self, scope_id): """ params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} - data_raw = self.raw_put(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def get_default_optional_client_scopes(self): """ Return list of default optional client scopes @@ -1987,7 +2132,6 @@ def get_default_optional_client_scopes(self): data_raw = self.raw_get(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def delete_default_optional_client_scope(self, scope_id): """ Delete default optional client scope @@ -1999,7 +2143,6 @@ def delete_default_optional_client_scope(self, scope_id): data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def add_default_optional_client_scope(self, scope_id): """ Add default optional client scope @@ -2009,10 +2152,11 @@ def add_default_optional_client_scope(self, scope_id): """ params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} - data_raw = self.raw_put(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def add_mapper_to_client(self, client_id, payload): """ Add a mapper to a client @@ -2026,28 +2170,30 @@ def add_mapper_to_client(self, client_id, payload): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload)) + URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) - + def update_client_mapper(self, client_id, mapper_id, payload): """ Update client mapper :param client_id: The id of the client :param client_mapper_id: The id of the mapper to be deleted :param payload: ProtocolMapperRepresentation - :return: Keycloak server response + :return: Keycloak server response """ params_path = { "realm-name": self.realm_name, - "id": self.client_id, + "id": self.client_id, "protocol-mapper-id": mapper_id, } data_raw = self.raw_put( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload)) - + URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def remove_client_mapper(self, client_id, client_mapper_id): @@ -2062,14 +2208,13 @@ def remove_client_mapper(self, client_id, client_mapper_id): params_path = { "realm-name": self.realm_name, "id": client_id, - "protocol-mapper-id": client_mapper_id + "protocol-mapper-id": client_mapper_id, } - data_raw = self.raw_delete( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) - + data_raw = self.raw_delete(URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - + def generate_client_secrets(self, client_id): """ @@ -2109,8 +2254,7 @@ def get_components(self, query=None): :return: components list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), - data=None, **query) + data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query) return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): @@ -2126,8 +2270,9 @@ def create_component(self, payload): """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_COMPONENTS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_post( + URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) def get_component(self, component_id): @@ -2156,8 +2301,9 @@ def update_component(self, component_id, payload): :return: Http response """ params_path = {"realm-name": self.realm_name, "component-id": component_id} - data_raw = self.raw_put(URL_ADMIN_COMPONENT.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put( + URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def delete_component(self, component_id): @@ -2182,8 +2328,7 @@ def get_keys(self): :return: keys list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), - data=None) + data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), data=None) return raise_error_from_response(data_raw, KeycloakGetError) def get_events(self, query=None): @@ -2196,8 +2341,7 @@ def get_events(self, query=None): :return: events list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path), - data=None, **query) + data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path), data=None, **query) return raise_error_from_response(data_raw, KeycloakGetError) def set_events(self, payload): @@ -2210,8 +2354,7 @@ def set_events(self, payload): :return: Http response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), - data=json.dumps(payload)) + data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def raw_get(self, *args, **kwargs): @@ -2222,7 +2365,7 @@ def raw_get(self, *args, **kwargs): and try *get* once more. """ r = self.connection.raw_get(*args, **kwargs) - if 'get' in self.auto_refresh_token and r.status_code == 401: + if "get" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_get(*args, **kwargs) return r @@ -2235,7 +2378,7 @@ def raw_post(self, *args, **kwargs): and try *post* once more. """ r = self.connection.raw_post(*args, **kwargs) - if 'post' in self.auto_refresh_token and r.status_code == 401: + if "post" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_post(*args, **kwargs) return r @@ -2248,7 +2391,7 @@ def raw_put(self, *args, **kwargs): and try *put* once more. """ r = self.connection.raw_put(*args, **kwargs) - if 'put' in self.auto_refresh_token and r.status_code == 401: + if "put" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_put(*args, **kwargs) return r @@ -2261,7 +2404,7 @@ def raw_delete(self, *args, **kwargs): and try *delete* once more. """ r = self.connection.raw_delete(*args, **kwargs) - if 'delete' in self.auto_refresh_token and r.status_code == 401: + if "delete" in self.auto_refresh_token and r.status_code == 401: self.refresh_token() return self.connection.raw_delete(*args, **kwargs) return r @@ -2273,11 +2416,15 @@ def get_token(self): token_realm_name = self.realm_name else: token_realm_name = "master" - - self.keycloak_openid = KeycloakOpenID(server_url=self.server_url, client_id=self.client_id, - realm_name=token_realm_name, verify=self.verify, - client_secret_key=self.client_secret_key, - custom_headers=self.custom_headers) + + self.keycloak_openid = KeycloakOpenID( + server_url=self.server_url, + client_id=self.client_id, + realm_name=token_realm_name, + verify=self.verify, + client_secret_key=self.client_secret_key, + custom_headers=self.custom_headers, + ) grant_type = ["password"] if self.client_secret_key: @@ -2286,11 +2433,13 @@ def get_token(self): self.realm_name = self.user_realm_name if self.username and self.password: - self._token = self.keycloak_openid.token(self.username, self.password, grant_type=grant_type) + self._token = self.keycloak_openid.token( + self.username, self.password, grant_type=grant_type + ) headers = { - 'Authorization': 'Bearer ' + self.token.get('access_token'), - 'Content-Type': 'application/json' + "Authorization": "Bearer " + self.token.get("access_token"), + "Content-Type": "application/json", } else: self._token = None @@ -2300,13 +2449,12 @@ def get_token(self): # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = ConnectionManager(base_url=self.server_url, - headers=headers, - timeout=60, - verify=self.verify) + self._connection = ConnectionManager( + base_url=self.server_url, headers=headers, timeout=60, verify=self.verify + ) def refresh_token(self): - refresh_token = self.token.get('refresh_token', None) + refresh_token = self.token.get("refresh_token", None) if refresh_token is None: self.get_token() else: @@ -2314,16 +2462,18 @@ def refresh_token(self): self.token = self.keycloak_openid.refresh_token(refresh_token) except KeycloakGetError as e: list_errors = [ - b'Refresh token expired', - b'Token is not active', - b'Session not active' + b"Refresh token expired", + b"Token is not active", + b"Session not active", ] if e.response_code == 400 and any(err in e.response_body for err in list_errors): - self.get_token() + self.get_token() else: raise - - self.connection.add_param_headers('Authorization', 'Bearer ' + self.token.get('access_token')) + + self.connection.add_param_headers( + "Authorization", "Bearer " + self.token.get("access_token") + ) def get_client_all_sessions(self, client_id): """ @@ -2346,9 +2496,10 @@ def delete_user_realm_role(self, user_id, payload): DELETE admin/realms/{realm-name}/users/{id}/role-mappings/realm """ - params_path = {"realm-name": self.realm_name, "id": str(user_id) } - data_raw = self.raw_delete(URL_ADMIN_DELETE_USER_ROLE.format(**params_path), - data=json.dumps(payload)) + params_path = {"realm-name": self.realm_name, "id": str(user_id)} + data_raw = self.raw_delete( + URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) def get_client_sessions_stats(self): @@ -2360,8 +2511,5 @@ def get_client_sessions_stats(self): :return: Dict of clients and session count """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get( - self.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path) - ) + data_raw = self.raw_get(self.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index 1d6ed289..d9c068fa 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -27,24 +27,38 @@ from .authorization import Authorization from .connection import ConnectionManager -from .exceptions import raise_error_from_response, KeycloakGetError, \ - KeycloakRPTNotFound, KeycloakAuthorizationConfigError, KeycloakInvalidTokenError, KeycloakDeprecationError +from .exceptions import ( + KeycloakAuthorizationConfigError, + KeycloakDeprecationError, + KeycloakGetError, + KeycloakInvalidTokenError, + KeycloakRPTNotFound, + raise_error_from_response, +) from .urls_patterns import ( - URL_REALM, URL_AUTH, + URL_CERTS, + URL_ENTITLEMENT, + URL_INTROSPECT, + URL_LOGOUT, + URL_REALM, URL_TOKEN, URL_USERINFO, URL_WELL_KNOWN, - URL_LOGOUT, - URL_CERTS, - URL_ENTITLEMENT, - URL_INTROSPECT ) class KeycloakOpenID: - - def __init__(self, server_url, realm_name, client_id, client_secret_key=None, verify=True, custom_headers=None, proxies=None): + def __init__( + self, + server_url, + realm_name, + client_id, + client_secret_key=None, + verify=True, + custom_headers=None, + proxies=None, + ): """ :param server_url: Keycloak server url @@ -62,11 +76,9 @@ def __init__(self, server_url, realm_name, client_id, client_secret_key=None, ve if custom_headers is not None: # merge custom headers to main headers headers.update(custom_headers) - self._connection = ConnectionManager(base_url=server_url, - headers=headers, - timeout=60, - verify=verify, - proxies=proxies) + self._connection = ConnectionManager( + base_url=server_url, headers=headers, timeout=60, verify=verify, proxies=proxies + ) self._authorization = Authorization() @@ -138,7 +150,7 @@ def _token_info(self, token, method_token_info, **kwargs): :param kwargs: :return: """ - if method_token_info == 'introspect': + if method_token_info == "introspect": token_info = self.introspect(token) else: token_info = self.decode_token(token, **kwargs) @@ -146,11 +158,11 @@ def _token_info(self, token, method_token_info, **kwargs): return token_info def well_know(self): - """ The most important endpoint to understand is the well-known configuration - endpoint. It lists endpoints and other configuration options relevant to - the OpenID Connect implementation in Keycloak. + """The most important endpoint to understand is the well-known configuration + endpoint. It lists endpoints and other configuration options relevant to + the OpenID Connect implementation in Keycloak. - :return It lists endpoints and other configuration options relevant. + :return It lists endpoints and other configuration options relevant. """ params_path = {"realm-name": self.realm_name} @@ -165,12 +177,23 @@ def auth_url(self, redirect_uri): :return: """ - params_path = {"authorization-endpoint": self.well_know()['authorization_endpoint'], - "client-id": self.client_id, - "redirect-uri": redirect_uri} + params_path = { + "authorization-endpoint": self.well_know()["authorization_endpoint"], + "client-id": self.client_id, + "redirect-uri": redirect_uri, + } return URL_AUTH.format(**params_path) - def token(self, username="", password="", grant_type=["password"], code="", redirect_uri="", totp=None, **extra): + def token( + self, + username="", + password="", + grant_type=["password"], + code="", + redirect_uri="", + totp=None, + **extra + ): """ The token endpoint is used to obtain tokens. Tokens can either be obtained by exchanging an authorization code or by supplying credentials directly depending on @@ -188,9 +211,14 @@ def token(self, username="", password="", grant_type=["password"], code="", redi :return: """ params_path = {"realm-name": self.realm_name} - payload = {"username": username, "password": password, - "client_id": self.client_id, "grant_type": grant_type, - "code": code, "redirect_uri": redirect_uri} + payload = { + "username": username, + "password": password, + "client_id": self.client_id, + "grant_type": grant_type, + "code": code, + "redirect_uri": redirect_uri, + } if extra: payload.update(extra) @@ -198,8 +226,7 @@ def token(self, username="", password="", grant_type=["password"], code="", redi payload["totp"] = totp payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError) def refresh_token(self, refresh_token, grant_type=["refresh_token"]): @@ -216,10 +243,13 @@ def refresh_token(self, refresh_token, grant_type=["refresh_token"]): :return: """ params_path = {"realm-name": self.realm_name} - payload = {"client_id": self.client_id, "grant_type": grant_type, "refresh_token": refresh_token} + payload = { + "client_id": self.client_id, + "grant_type": grant_type, + "refresh_token": refresh_token, + } payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError) def userinfo(self, token): @@ -250,8 +280,7 @@ def logout(self, refresh_token): payload = {"client_id": self.client_id, "refresh_token": refresh_token} payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) @@ -268,7 +297,7 @@ def certs(self): params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - + def public_key(self): """ The public key is exposed by the realm page directly. @@ -277,8 +306,7 @@ def public_key(self): """ params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError)['public_key'] - + return raise_error_from_response(data_raw, KeycloakGetError)["public_key"] def entitlement(self, token, resource_server_id): """ @@ -293,8 +321,8 @@ def entitlement(self, token, resource_server_id): self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} data_raw = self.connection.raw_get(URL_ENTITLEMENT.format(**params_path)) - - if data_raw.status_code == 404: + + if data_raw.status_code == 404: return raise_error_from_response(data_raw, KeycloakDeprecationError) return raise_error_from_response(data_raw, KeycloakGetError) @@ -316,7 +344,7 @@ def introspect(self, token, rpt=None, token_type_hint=None): payload = {"client_id": self.client_id, "token": token} - if token_type_hint == 'requesting_party_token': + if token_type_hint == "requesting_party_token": if rpt: payload.update({"token": rpt, "token_type_hint": token_type_hint}) self.connection.add_param_headers("Authorization", "Bearer " + token) @@ -325,12 +353,11 @@ def introspect(self, token, rpt=None, token_type_hint=None): payload = self._add_secret_key(payload) - data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), - data=payload) + data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError) - def decode_token(self, token, key, algorithms=['RS256'], **kwargs): + def decode_token(self, token, key, algorithms=["RS256"], **kwargs): """ A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key. This specification @@ -347,8 +374,7 @@ def decode_token(self, token, key, algorithms=['RS256'], **kwargs): :return: """ - return jwt.decode(token, key, algorithms=algorithms, - audience=self.client_id, **kwargs) + return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) def load_authorization_config(self, path): """ @@ -357,12 +383,12 @@ def load_authorization_config(self, path): :param path: settings file (json) :return: """ - authorization_file = open(path, 'r') + authorization_file = open(path, "r") authorization_json = json.loads(authorization_file.read()) self.authorization.load_config(authorization_json) authorization_file.close() - def get_policies(self, token, method_token_info='introspect', **kwargs): + def get_policies(self, token, method_token_info="introspect", **kwargs): """ Get policies by user token @@ -377,12 +403,10 @@ def get_policies(self, token, method_token_info='introspect', **kwargs): token_info = self._token_info(token, method_token_info, **kwargs) - if method_token_info == 'introspect' and not token_info['active']: - raise KeycloakInvalidTokenError( - "Token expired or invalid." - ) + if method_token_info == "introspect" and not token_info["active"]: + raise KeycloakInvalidTokenError("Token expired or invalid.") - user_resources = token_info['resource_access'].get(self.client_id) + user_resources = token_info["resource_access"].get(self.client_id) if not user_resources: return None @@ -390,13 +414,13 @@ def get_policies(self, token, method_token_info='introspect', **kwargs): policies = [] for policy_name, policy in self.authorization.policies.items(): - for role in user_resources['roles']: + for role in user_resources["roles"]: if self._build_name_role(role) in policy.roles: policies.append(policy) return list(set(policies)) - def get_permissions(self, token, method_token_info='introspect', **kwargs): + def get_permissions(self, token, method_token_info="introspect", **kwargs): """ Get permission by user token @@ -413,12 +437,10 @@ def get_permissions(self, token, method_token_info='introspect', **kwargs): token_info = self._token_info(token, method_token_info, **kwargs) - if method_token_info == 'introspect' and not token_info['active']: - raise KeycloakInvalidTokenError( - "Token expired or invalid." - ) + if method_token_info == "introspect" and not token_info["active"]: + raise KeycloakInvalidTokenError("Token expired or invalid.") - user_resources = token_info['resource_access'].get(self.client_id) + user_resources = token_info["resource_access"].get(self.client_id) if not user_resources: return None @@ -426,7 +448,7 @@ def get_permissions(self, token, method_token_info='introspect', **kwargs): permissions = [] for policy_name, policy in self.authorization.policies.items(): - for role in user_resources['roles']: + for role in user_resources["roles"]: if self._build_name_role(role) in policy.roles: permissions += policy.permissions diff --git a/keycloak/tests/test_connection.py b/keycloak/tests/test_connection.py deleted file mode 100644 index cb98feba..00000000 --- a/keycloak/tests/test_connection.py +++ /dev/null @@ -1,191 +0,0 @@ -# -*- coding: utf-8 -*- -# -# Copyright (C) 2017 Marcos Pereira -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with this program. If not, see . -from unittest import mock - -from httmock import urlmatch, response, HTTMock, all_requests - -from keycloak import KeycloakAdmin, KeycloakOpenID -from ..connection import ConnectionManager - -try: - import unittest -except ImportError: - import unittest2 as unittest - - -class TestConnection(unittest.TestCase): - - def setUp(self): - self._conn = ConnectionManager( - base_url="http://localhost/", - headers={}, - timeout=60) - - @all_requests - def response_content_success(self, url, request): - headers = {'content-type': 'application/json'} - content = b'response_ok' - return response(200, content, headers, None, 5, request) - - def test_raw_get(self): - with HTTMock(self.response_content_success): - resp = self._conn.raw_get("/known_path") - self.assertEqual(resp.content, b'response_ok') - self.assertEqual(resp.status_code, 200) - - def test_raw_post(self): - @urlmatch(path="/known_path", method="post") - def response_post_success(url, request): - headers = {'content-type': 'application/json'} - content = 'response'.encode("utf-8") - return response(201, content, headers, None, 5, request) - - with HTTMock(response_post_success): - resp = self._conn.raw_post("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, b'response') - self.assertEqual(resp.status_code, 201) - - def test_raw_put(self): - @urlmatch(netloc="localhost", path="/known_path", method="put") - def response_put_success(url, request): - headers = {'content-type': 'application/json'} - content = 'response'.encode("utf-8") - return response(200, content, headers, None, 5, request) - - with HTTMock(response_put_success): - resp = self._conn.raw_put("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, b'response') - self.assertEqual(resp.status_code, 200) - - def test_raw_get_fail(self): - @urlmatch(netloc="localhost", path="/known_path", method="get") - def response_get_fail(url, request): - headers = {'content-type': 'application/json'} - content = "404 page not found".encode("utf-8") - return response(404, content, headers, None, 5, request) - - with HTTMock(response_get_fail): - resp = self._conn.raw_get("/known_path") - - self.assertEqual(resp.content, b"404 page not found") - self.assertEqual(resp.status_code, 404) - - def test_raw_post_fail(self): - @urlmatch(netloc="localhost", path="/known_path", method="post") - def response_post_fail(url, request): - headers = {'content-type': 'application/json'} - content = str(["Start can't be blank"]).encode("utf-8") - return response(404, content, headers, None, 5, request) - - with HTTMock(response_post_fail): - resp = self._conn.raw_post("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, str(["Start can't be blank"]).encode("utf-8")) - self.assertEqual(resp.status_code, 404) - - def test_raw_put_fail(self): - @urlmatch(netloc="localhost", path="/known_path", method="put") - def response_put_fail(url, request): - headers = {'content-type': 'application/json'} - content = str(["Start can't be blank"]).encode("utf-8") - return response(404, content, headers, None, 5, request) - - with HTTMock(response_put_fail): - resp = self._conn.raw_put("/known_path", - {'field': 'value'}) - self.assertEqual(resp.content, str(["Start can't be blank"]).encode("utf-8")) - self.assertEqual(resp.status_code, 404) - - def test_add_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertEqual(self._conn.headers, - {"test": "value"}) - - def test_del_param_headers(self): - self._conn.add_param_headers("test", "value") - self._conn.del_param_headers("test") - self.assertEqual(self._conn.headers, {}) - - def test_clean_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertEqual(self._conn.headers, - {"test": "value"}) - self._conn.clean_headers() - self.assertEqual(self._conn.headers, {}) - - def test_exist_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertTrue(self._conn.exist_param_headers("test")) - self.assertFalse(self._conn.exist_param_headers("test_no")) - - def test_get_param_headers(self): - self._conn.add_param_headers("test", "value") - self.assertTrue(self._conn.exist_param_headers("test")) - self.assertFalse(self._conn.exist_param_headers("test_no")) - - def test_get_headers(self): - self._conn.add_param_headers("test", "value") - self.assertEqual(self._conn.headers, - {"test": "value"}) - - def test_KeycloakAdmin_custom_header(self): - - class FakeToken: - @staticmethod - def get(string_val): - return "faketoken" - - fake_token = FakeToken() - - with mock.patch.object(KeycloakOpenID, "__init__", return_value=None) as mock_keycloak_open_id: - with mock.patch("keycloak.keycloak_openid.KeycloakOpenID.token", return_value=fake_token): - with mock.patch("keycloak.connection.ConnectionManager.__init__", return_value=None) as mock_connection_manager: - with mock.patch("keycloak.connection.ConnectionManager.__del__", return_value=None) as mock_connection_manager_delete: - server_url = "https://localhost/auth/" - username = "admin" - password = "secret" - realm_name = "master" - - headers = { - 'Custom': 'test-custom-header' - } - KeycloakAdmin(server_url=server_url, - username=username, - password=password, - realm_name=realm_name, - verify=False, - custom_headers=headers) - - mock_keycloak_open_id.assert_called_with(server_url=server_url, - realm_name=realm_name, - client_id='admin-cli', - client_secret_key=None, - verify=False, - custom_headers=headers) - - expected_header = {'Authorization': 'Bearer faketoken', - 'Content-Type': 'application/json', - 'Custom': 'test-custom-header' - } - - mock_connection_manager.assert_called_with(base_url=server_url, - headers=expected_header, - timeout=60, - verify=False) - mock_connection_manager_delete.assert_called_once_with() diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index d7dd16a5..43699eb5 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -30,7 +30,9 @@ URL_CERTS = "realms/{realm-name}/protocol/openid-connect/certs" URL_INTROSPECT = "realms/{realm-name}/protocol/openid-connect/token/introspect" URL_ENTITLEMENT = "realms/{realm-name}/authz/entitlement/{resource-server-id}" -URL_AUTH = "{authorization-endpoint}?client_id={client-id}&response_type=code&redirect_uri={redirect-uri}" +URL_AUTH = ( + "{authorization-endpoint}?client_id={client-id}&response_type=code&redirect_uri={redirect-uri}" +) # ADMIN URLS URL_ADMIN_USERS = "admin/realms/{realm-name}/users" @@ -41,14 +43,26 @@ URL_ADMIN_SEND_VERIFY_EMAIL = "admin/realms/{realm-name}/users/{id}/send-verify-email" URL_ADMIN_RESET_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_GET_SESSIONS = "admin/realms/{realm-name}/users/{id}/sessions" -URL_ADMIN_USER_CLIENT_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" +URL_ADMIN_USER_CLIENT_ROLES = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}" +) URL_ADMIN_USER_REALM_ROLES = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" -URL_ADMIN_USER_REALM_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm/available" -URL_ADMIN_USER_REALM_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm/composite" +URL_ADMIN_USER_REALM_ROLES_AVAILABLE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/realm/available" +) +URL_ADMIN_USER_REALM_ROLES_COMPOSITE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/realm/composite" +) URL_ADMIN_GROUPS_REALM_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/realm" -URL_ADMIN_GROUPS_CLIENT_ROLES = "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" -URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" -URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" +URL_ADMIN_GROUPS_CLIENT_ROLES = ( + "admin/realms/{realm-name}/groups/{id}/role-mappings/clients/{client-id}" +) +URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/available" +) +URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE = ( + "admin/realms/{realm-name}/users/{id}/role-mappings/clients/{client-id}/composite" +) URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups" URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" @@ -79,8 +93,12 @@ URL_ADMIN_CLIENT_AUTHZ_SCOPES = URL_ADMIN_CLIENT + "/authz/resource-server/scope?max=-1" URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT + "/authz/resource-server/permission?max=-1" URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy?max=-1" -URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = URL_ADMIN_CLIENT + "/authz/resource-server/policy/role?max=-1" -URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource?max=-1" +URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = ( + URL_ADMIN_CLIENT + "/authz/resource-server/policy/role?max=-1" +) +URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = ( + URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource?max=-1" +) URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" @@ -101,7 +119,9 @@ URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" -URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = "admin/realms/{realm-name}/roles/{role-name}/composites" +URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = ( + "admin/realms/{realm-name}/roles/{role-name}/composites" +) URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-default-client-scopes" @@ -113,10 +133,16 @@ URL_ADMIN_FLOW = URL_ADMIN_FLOWS + "/{id}" URL_ADMIN_FLOWS_ALIAS = "admin/realms/{realm-name}/authentication/flows/{flow-id}" URL_ADMIN_FLOWS_COPY = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/copy" -URL_ADMIN_FLOWS_EXECUTIONS = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +URL_ADMIN_FLOWS_EXECUTIONS = ( + "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions" +) URL_ADMIN_FLOWS_EXECUTION = "admin/realms/{realm-name}/authentication/executions/{id}" -URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" -URL_ADMIN_FLOWS_EXECUTIONS_FLOW = "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" +URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION = ( + "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/execution" +) +URL_ADMIN_FLOWS_EXECUTIONS_FLOW = ( + "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" +) URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" @@ -124,10 +150,11 @@ URL_ADMIN_KEYS = "admin/realms/{realm-name}/keys" URL_ADMIN_USER_FEDERATED_IDENTITIES = "admin/realms/{realm-name}/users/{id}/federated-identity" -URL_ADMIN_USER_FEDERATED_IDENTITY = "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" +URL_ADMIN_USER_FEDERATED_IDENTITY = ( + "admin/realms/{realm-name}/users/{id}/federated-identity/{provider}" +) -URL_ADMIN_EVENTS = 'admin/realms/{realm-name}/events' +URL_ADMIN_EVENTS = "admin/realms/{realm-name}/events" URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" - diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f9474507 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 99 + +[tool.isort] +line_length = 99 +profile = "black" diff --git a/requirements.txt b/requirements.txt index a353c7f3..5474982a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,3 @@ requests>=2.20.0 -httmock>=1.2.5 python-jose>=1.4.0 -twine==1.13.0 -jose~=1.0.0 -setuptools~=54.2.0 -urllib3>=1.26.5 \ No newline at end of file +urllib3>=1.26.0 diff --git a/setup.py b/setup.py index 0ffc5aa6..8f3b7fca 100644 --- a/setup.py +++ b/setup.py @@ -1,31 +1,56 @@ # -*- coding: utf-8 -*- - +import re from setuptools import setup with open("README.md", "r") as fh: long_description = fh.read() +with open("requirements.txt", "r") as fh: + reqs = fh.read().split("\n") + +with open("dev-requirements.txt", "r") as fh: + dev_reqs = fh.read().split("\n") + +with open("docs-requirements.txt", "r") as fh: + docs_reqs = fh.read().split("\n") + + +VERSIONFILE = "keycloak/_version.py" +verstrline = open(VERSIONFILE, "rt").read() +VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" +mo = re.search(VSRE, verstrline, re.M) +if mo: + verstr = mo.group(1) +else: + raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) + setup( - name='python-keycloak', - version='0.27.1', - url='https://github.com/marcospereirampj/python-keycloak', - license='The MIT License', - author='Marcos Pereira', - author_email='marcospereira.mpj@gmail.com', - keywords='keycloak openid', - description='python-keycloak is a Python package providing access to the Keycloak API.', + name="python-keycloak", + version=verstr, + url="https://github.com/marcospereirampj/python-keycloak", + license="The MIT License", + author="Marcos Pereira, Richard Nemeth", + author_email="marcospereira.mpj@gmail.com; ryshoooo@gmail.com", + keywords="keycloak openid oidc", + description="python-keycloak is a Python package providing access to the Keycloak API.", long_description=long_description, long_description_content_type="text/markdown", - packages=['keycloak', 'keycloak.authorization', 'keycloak.tests'], - install_requires=['requests>=2.20.0', 'python-jose>=1.4.0'], - tests_require=['httmock>=1.2.5'], + packages=["keycloak"], + install_requires=reqs, + tests_require=dev_reqs, + extras_require={"docs": docs_reqs}, + python_requires=">=3.7", + project_urls={ + "Documentation": "https://python-keycloak.readthedocs.io/en/latest/", + "Issue tracker": "https://github.com/marcospereirampj/python-keycloak/issues", + }, classifiers=[ - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Development Status :: 3 - Alpha', - 'Operating System :: MacOS', - 'Operating System :: Unix', - 'Operating System :: Microsoft :: Windows', - 'Topic :: Utilities' - ] + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Operating System :: MacOS", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Topic :: Utilities", + ], ) diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh new file mode 100755 index 00000000..bee88308 --- /dev/null +++ b/test_keycloak_init.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +CMD_ARGS=$1 +KEYCLOAK_DOCKER_IMAGE="quay.io/keycloak/keycloak:latest" + +echo "${CMD_ARGS}" + +function keycloak_stop() { + docker stop unittest_keycloak &> /dev/null + docker rm unittest_keycloak &> /dev/null +} + +function keycloak_start() { + echo "Starting keycloak docker container" + docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev + SECONDS=0 + until curl localhost:$KEYCLOAK_PORT; do + sleep 5; + if [ ${SECONDS} -gt 180 ]; then + echo "Timeout exceeded"; + exit 1; + fi + done +} + +# Ensuring that keycloak is stopped in case of CTRL-C +trap keycloak_stop err exit + +keycloak_stop # In case it did not shut down correctly last time. +keycloak_start + +eval ${CMD_ARGS} +RETURN_VALUE=$? + +exit ${RETURN_VALUE} diff --git a/keycloak/tests/__init__.py b/tests/__init__.py similarity index 100% rename from keycloak/tests/__init__.py rename to tests/__init__.py diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..b9c266a2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,61 @@ +import os +import uuid + +import pytest + +from keycloak import KeycloakAdmin + + +@pytest.fixture +def env(): + class KeycloakTestEnv(object): + KEYCLOAK_HOST = os.environ["KEYCLOAK_HOST"] + KEYCLOAK_PORT = os.environ["KEYCLOAK_PORT"] + KEYCLOAK_ADMIN = os.environ["KEYCLOAK_ADMIN"] + KEYCLOAK_ADMIN_PASSWORD = os.environ["KEYCLOAK_ADMIN_PASSWORD"] + + return KeycloakTestEnv() + + +@pytest.fixture +def admin(env): + return KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + ) + + +@pytest.fixture +def realm(admin: KeycloakAdmin) -> str: + realm_name = str(uuid.uuid4()) + admin.create_realm(payload={"realm": realm_name}) + yield realm_name + admin.delete_realm(realm_name=realm_name) + + +@pytest.fixture +def user(admin: KeycloakAdmin, realm: str) -> str: + admin.realm_name = realm + username = str(uuid.uuid4()) + user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"}) + yield user_id + admin.delete_user(user_id=user_id) + + +@pytest.fixture +def group(admin: KeycloakAdmin, realm: str) -> str: + admin.realm_name = realm + group_name = str(uuid.uuid4()) + group_id = admin.create_group(payload={"name": group_name}) + yield group_id + admin.delete_group(group_id=group_id) + + +@pytest.fixture +def client(admin: KeycloakAdmin, realm: str) -> str: + admin.realm_name = realm + client = str(uuid.uuid4()) + client_id = admin.create_client(payload={"name": client, "clientId": client}) + yield client_id + admin.delete_client(client_id=client_id) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py new file mode 100644 index 00000000..29d3b15f --- /dev/null +++ b/tests/test_keycloak_admin.py @@ -0,0 +1,1201 @@ +import pytest + +import keycloak +from keycloak import KeycloakAdmin +from keycloak.connection import ConnectionManager +from keycloak.exceptions import ( + KeycloakDeleteError, + KeycloakGetError, + KeycloakPostError, + KeycloakPutError, +) + + +def test_keycloak_version(): + assert keycloak.__version__, keycloak.__version__ + + +def test_keycloak_admin_bad_init(env): + with pytest.raises(TypeError) as err: + KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + auto_refresh_token=1, + ) + assert err.match("Expected a list of strings") + + with pytest.raises(TypeError) as err: + KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + auto_refresh_token=["patch"], + ) + assert err.match("Unexpected method in auto_refresh_token") + + +def test_keycloak_admin_init(env): + admin = KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + ) + assert admin.server_url == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", admin.server_url + assert admin.realm_name == "master", admin.realm_name + assert isinstance(admin.connection, ConnectionManager), type(admin.connection) + assert admin.client_id == "admin-cli", admin.client_id + assert admin.client_secret_key is None, admin.client_secret_key + assert admin.verify, admin.verify + assert admin.username == env.KEYCLOAK_ADMIN, admin.username + assert admin.password == env.KEYCLOAK_ADMIN_PASSWORD, admin.password + assert admin.totp is None, admin.totp + assert admin.token is not None, admin.token + assert admin.auto_refresh_token == list(), admin.auto_refresh_token + assert admin.user_realm_name is None, admin.user_realm_name + assert admin.custom_headers is None, admin.custom_headers + + +def test_realms(admin: KeycloakAdmin): + # Get realms + realms = admin.get_realms() + assert len(realms) == 1, realms + assert "master" == realms[0]["realm"] + + # Create a test realm + res = admin.create_realm(payload={"realm": "test"}) + assert res == b"", res + + # Create the same realm, should fail + with pytest.raises(KeycloakPostError) as err: + res = admin.create_realm(payload={"realm": "test"}) + assert err.match('409: b\'{"errorMessage":"Conflict detected. See logs for details"}\'') + + # Create the same realm, skip_exists true + res = admin.create_realm(payload={"realm": "test"}, skip_exists=True) + assert res == {"msg": "Already exists"}, res + + # Get a single realm + res = admin.get_realm(realm_name="test") + assert res["realm"] == "test" + + # Get non-existing realm + with pytest.raises(KeycloakGetError) as err: + admin.get_realm(realm_name="non-existent") + assert err.match('404: b\'{"error":"Realm not found."}\'') + + # Update realm + res = admin.update_realm(realm_name="test", payload={"accountTheme": "test"}) + assert res == dict(), res + + # Check that the update worked + res = admin.get_realm(realm_name="test") + assert res["realm"] == "test" + assert res["accountTheme"] == "test" + + # Update wrong payload + with pytest.raises(KeycloakPutError) as err: + admin.update_realm(realm_name="test", payload={"wrong": "payload"}) + assert err.match('400: b\'{"error":"Unrecognized field') + + # Check that get realms returns both realms + realms = admin.get_realms() + realm_names = [x["realm"] for x in realms] + assert len(realms) == 2, realms + assert "master" in realm_names, realm_names + assert "test" in realm_names, realm_names + + # Delete the realm + res = admin.delete_realm(realm_name="test") + assert res == dict(), res + + # Check that the realm does not exist anymore + with pytest.raises(KeycloakGetError) as err: + admin.get_realm(realm_name="test") + assert err.match('404: b\'{"error":"Realm not found."}\'') + + # Delete non-existing realm + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm(realm_name="non-existent") + assert err.match('404: b\'{"error":"Realm not found."}\'') + + +def test_import_export_realms(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + realm_export = admin.export_realm(export_clients=True, export_groups_and_role=True) + assert realm_export != dict(), realm_export + + admin.delete_realm(realm_name=realm) + admin.realm_name = "master" + res = admin.import_realm(payload=realm_export) + assert res == b"", res + + # Test bad import + with pytest.raises(KeycloakPostError) as err: + admin.import_realm(payload=dict()) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + +def test_users(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Check no users present + users = admin.get_users() + assert users == list(), users + + # Test create user + user_id = admin.create_user(payload={"username": "test", "email": "test@test.test"}) + assert user_id is not None, user_id + + # Test create the same user + with pytest.raises(KeycloakPostError) as err: + admin.create_user(payload={"username": "test", "email": "test@test.test"}) + assert err.match('409: b\'{"errorMessage":"User exists with same username"}\'') + + # Test create the same user, exists_ok true + user_id_2 = admin.create_user( + payload={"username": "test", "email": "test@test.test"}, exist_ok=True + ) + assert user_id == user_id_2 + + # Test get user + user = admin.get_user(user_id=user_id) + assert user["username"] == "test", user["username"] + assert user["email"] == "test@test.test", user["email"] + + # Test update user + res = admin.update_user(user_id=user_id, payload={"firstName": "Test"}) + assert res == dict(), res + user = admin.get_user(user_id=user_id) + assert user["firstName"] == "Test" + + # Test update user fail + with pytest.raises(KeycloakPutError) as err: + admin.update_user(user_id=user_id, payload={"wrong": "payload"}) + assert err.match('400: b\'{"error":"Unrecognized field') + + # Test get users again + users = admin.get_users() + usernames = [x["username"] for x in users] + assert "test" in usernames + + # Test users counts + count = admin.users_count() + assert count == 1, count + + # Test user groups + groups = admin.get_user_groups(user_id=user["id"]) + assert len(groups) == 0 + + # Test user groups bad id + with pytest.raises(KeycloakGetError) as err: + admin.get_user_groups(user_id="does-not-exist") + assert err.match('404: b\'{"error":"User not found"}\'') + + # Test logout + res = admin.user_logout(user_id=user["id"]) + assert res == dict(), res + + # Test logout fail + with pytest.raises(KeycloakPostError) as err: + admin.user_logout(user_id="non-existent-id") + assert err.match('404: b\'{"error":"User not found"}\'') + + # Test consents + res = admin.user_consents(user_id=user["id"]) + assert len(res) == 0, res + + # Test consents fail + with pytest.raises(KeycloakGetError) as err: + admin.user_consents(user_id="non-existent-id") + assert err.match('404: b\'{"error":"User not found"}\'') + + # Test delete user + res = admin.delete_user(user_id=user_id) + assert res == dict(), res + with pytest.raises(KeycloakGetError) as err: + admin.get_user(user_id=user_id) + err.match('404: b\'{"error":"User not found"}\'') + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_user(user_id="non-existent-id") + assert err.match('404: b\'{"error":"User not found"}\'') + + +def test_users_pagination(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + for ind in range(admin.PAGE_SIZE + 50): + username = f"user_{ind}" + admin.create_user(payload={"username": username, "email": f"{username}@test.test"}) + + users = admin.get_users() + assert len(users) == admin.PAGE_SIZE + 50, len(users) + + users = admin.get_users(query={"first": 100}) + assert len(users) == 50, len(users) + + users = admin.get_users(query={"max": 20}) + assert len(users) == 20, len(users) + + +def test_idps(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Create IDP + res = admin.create_idp( + payload=dict( + providerId="github", alias="github", config=dict(clientId="test", clientSecret="test") + ) + ) + assert res == b"", res + + # Test create idp fail + with pytest.raises(KeycloakPostError) as err: + admin.create_idp(payload={"providerId": "does-not-exist", "alias": "something"}) + assert err.match("Invalid identity provider id"), err + + # Test listing + idps = admin.get_idps() + assert len(idps) == 1 + assert "github" == idps[0]["alias"] + + # Test adding a mapper + res = admin.add_mapper_to_idp( + idp_alias="github", + payload={ + "identityProviderAlias": "github", + "identityProviderMapper": "github-user-attribute-mapper", + "name": "test", + }, + ) + assert res == b"", res + + # Test mapper fail + with pytest.raises(KeycloakPostError) as err: + admin.add_mapper_to_idp(idp_alias="does-no-texist", payload=dict()) + assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') + + # Test delete + res = admin.delete_idp(idp_alias="github") + assert res == dict(), res + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_idp(idp_alias="does-not-exist") + assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') + + +def test_user_credentials(admin: KeycloakAdmin, user: str): + res = admin.set_user_password(user_id=user, password="booya", temporary=True) + assert res == dict(), res + + # Test user password set fail + with pytest.raises(KeycloakPutError) as err: + admin.set_user_password(user_id="does-not-exist", password="") + assert err.match('404: b\'{"error":"User not found"}\'') + + credentials = admin.get_credentials(user_id=user) + assert len(credentials) == 1 + assert credentials[0]["type"] == "password", credentials + + # Test get credentials fail + with pytest.raises(KeycloakGetError) as err: + admin.get_credentials(user_id="does-not-exist") + assert err.match('404: b\'{"error":"User not found"}\'') + + res = admin.delete_credential(user_id=user, credential_id=credentials[0]["id"]) + assert res == dict(), res + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_credential(user_id=user, credential_id="does-not-exist") + assert err.match('404: b\'{"error":"Credential not found"}\'') + + +def test_social_logins(admin: KeycloakAdmin, user: str): + res = admin.add_user_social_login( + user_id=user, provider_id="gitlab", provider_userid="test", provider_username="test" + ) + assert res == dict(), res + admin.add_user_social_login( + user_id=user, provider_id="github", provider_userid="test", provider_username="test" + ) + assert res == dict(), res + + # Test add social login fail + with pytest.raises(KeycloakPostError) as err: + admin.add_user_social_login( + user_id="does-not-exist", + provider_id="does-not-exist", + provider_userid="test", + provider_username="test", + ) + assert err.match('404: b\'{"error":"User not found"}\'') + + res = admin.get_user_social_logins(user_id=user) + assert res == list(), res + + # Test get social logins fail + with pytest.raises(KeycloakGetError) as err: + admin.get_user_social_logins(user_id="does-not-exist") + assert err.match('404: b\'{"error":"User not found"}\'') + + res = admin.delete_user_social_login(user_id=user, provider_id="gitlab") + assert res == {}, res + + res = admin.delete_user_social_login(user_id=user, provider_id="github") + assert res == {}, res + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_user_social_login(user_id=user, provider_id="instagram") + assert err.match('404: b\'{"error":"Link not found"}\''), err + + +def test_server_info(admin: KeycloakAdmin): + info = admin.get_server_info() + assert set(info.keys()) == { + "systemInfo", + "memoryInfo", + "profileInfo", + "themes", + "socialProviders", + "identityProviders", + "providers", + "protocolMapperTypes", + "builtinProtocolMappers", + "clientInstallations", + "componentTypes", + "passwordPolicies", + "enums", + }, info.keys() + + +def test_groups(admin: KeycloakAdmin, user: str): + # Test get groups + groups = admin.get_groups() + assert len(groups) == 0 + + # Test create group + group_id = admin.create_group(payload={"name": "main-group"}) + assert group_id is not None, group_id + + # Test create subgroups + subgroup_id_1 = admin.create_group(payload={"name": "subgroup-1"}, parent=group_id) + subgroup_id_2 = admin.create_group(payload={"name": "subgroup-2"}, parent=group_id) + + # Test create group fail + with pytest.raises(KeycloakPostError) as err: + admin.create_group(payload={"name": "subgroup-1"}, parent=group_id) + assert err.match('409: b\'{"error":"unknown_error"}\''), err + + # Test skip exists OK + subgroup_id_1_eq = admin.create_group( + payload={"name": "subgroup-1"}, parent=group_id, skip_exists=True + ) + assert subgroup_id_1_eq is None + + # Test get groups again + groups = admin.get_groups() + assert len(groups) == 1, groups + assert len(groups[0]["subGroups"]) == 2, groups["subGroups"] + assert groups[0]["id"] == group_id + assert {x["id"] for x in groups[0]["subGroups"]} == {subgroup_id_1, subgroup_id_2} + + # Test get groups query + groups = admin.get_groups(query={"max": 10}) + assert len(groups) == 1, groups + assert len(groups[0]["subGroups"]) == 2, groups["subGroups"] + assert groups[0]["id"] == group_id + assert {x["id"] for x in groups[0]["subGroups"]} == {subgroup_id_1, subgroup_id_2} + + # Test get group + res = admin.get_group(group_id=subgroup_id_1) + assert res["id"] == subgroup_id_1, res + assert res["name"] == "subgroup-1" + assert res["path"] == "/main-group/subgroup-1" + + # Test get group fail + with pytest.raises(KeycloakGetError) as err: + admin.get_group(group_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find group by id"}\''), err + + # Create 1 more subgroup + subsubgroup_id_1 = admin.create_group(payload={"name": "subsubgroup-1"}, parent=subgroup_id_2) + main_group = admin.get_group(group_id=group_id) + + # Test nested searches + res = admin.get_subgroups(group=main_group, path="/main-group/subgroup-2/subsubgroup-1") + assert res is not None, res + assert res["id"] == subsubgroup_id_1 + + # Test empty search + res = admin.get_subgroups(group=main_group, path="/none") + assert res is None, res + + # Test get group by path + res = admin.get_group_by_path(path="/main-group/subgroup-1") + assert res is None, res + + res = admin.get_group_by_path(path="/main-group/subgroup-1", search_in_subgroups=True) + assert res is not None, res + assert res["id"] == subgroup_id_1, res + + res = admin.get_group_by_path( + path="/main-group/subgroup-2/subsubgroup-1/test", search_in_subgroups=True + ) + assert res is None, res + + res = admin.get_group_by_path( + path="/main-group/subgroup-2/subsubgroup-1", search_in_subgroups=True + ) + assert res is not None, res + assert res["id"] == subsubgroup_id_1 + + res = admin.get_group_by_path(path="/main-group") + assert res is not None, res + assert res["id"] == group_id, res + + # Test group members + res = admin.get_group_members(group_id=subgroup_id_2) + assert len(res) == 0, res + + # Test fail group members + with pytest.raises(KeycloakGetError) as err: + admin.get_group_members(group_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find group by id"}\'') + + res = admin.group_user_add(user_id=user, group_id=subgroup_id_2) + assert res == dict(), res + + res = admin.get_group_members(group_id=subgroup_id_2) + assert len(res) == 1, res + assert res[0]["id"] == user + + # Test get group members query + res = admin.get_group_members(group_id=subgroup_id_2, query={"max": 10}) + assert len(res) == 1, res + assert res[0]["id"] == user + + with pytest.raises(KeycloakDeleteError) as err: + admin.group_user_remove(user_id="does-not-exist", group_id=subgroup_id_2) + assert err.match('404: b\'{"error":"User not found"}\''), err + + res = admin.group_user_remove(user_id=user, group_id=subgroup_id_2) + assert res == dict(), res + + # Test set permissions + res = admin.group_set_permissions(group_id=subgroup_id_2, enabled=True) + assert res["enabled"], res + res = admin.group_set_permissions(group_id=subgroup_id_2, enabled=False) + assert not res["enabled"], res + with pytest.raises(KeycloakPutError) as err: + admin.group_set_permissions(group_id=subgroup_id_2, enabled="blah") + assert err.match('500: b\'{"error":"unknown_error"}\''), err + + # Test update group + res = admin.update_group(group_id=subgroup_id_2, payload={"name": "new-subgroup-2"}) + assert res == dict(), res + assert admin.get_group(group_id=subgroup_id_2)["name"] == "new-subgroup-2" + + # test update fail + with pytest.raises(KeycloakPutError) as err: + admin.update_group(group_id="does-not-exist", payload=dict()) + assert err.match('404: b\'{"error":"Could not find group by id"}\''), err + + # Test delete + res = admin.delete_group(group_id=group_id) + assert res == dict(), res + assert len(admin.get_groups()) == 0 + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_group(group_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find group by id"}\''), err + + +def test_clients(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test get clients + clients = admin.get_clients() + assert len(clients) == 6, clients + assert {x["name"] for x in clients} == set( + [ + "${client_admin-cli}", + "${client_security-admin-console}", + "${client_account-console}", + "${client_broker}", + "${client_account}", + "${client_realm-management}", + ] + ), clients + + # Test create client + client_id = admin.create_client(payload={"name": "test-client", "clientId": "test-client"}) + assert client_id, client_id + + with pytest.raises(KeycloakPostError) as err: + admin.create_client(payload={"name": "test-client", "clientId": "test-client"}) + assert err.match('409: b\'{"errorMessage":"Client test-client already exists"}\''), err + + client_id_2 = admin.create_client( + payload={"name": "test-client", "clientId": "test-client"}, skip_exists=True + ) + assert client_id == client_id_2, client_id_2 + + # Test get client + res = admin.get_client(client_id=client_id) + assert res["clientId"] == "test-client", res + assert res["name"] == "test-client", res + assert res["id"] == client_id, res + + with pytest.raises(KeycloakGetError) as err: + admin.get_client(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + assert len(admin.get_clients()) == 7 + + # Test get client id + assert admin.get_client_id(client_name="test-client") == client_id + assert admin.get_client_id(client_name="does-not-exist") is None + + # Test update client + res = admin.update_client(client_id=client_id, payload={"name": "test-client-change"}) + assert res == dict(), res + + with pytest.raises(KeycloakPutError) as err: + admin.update_client(client_id="does-not-exist", payload={"name": "test-client-change"}) + assert err.match('404: b\'{"error":"Could not find client"}\'') + + # Test authz + auth_client_id = admin.create_client( + payload={ + "name": "authz-client", + "clientId": "authz-client", + "authorizationServicesEnabled": True, + "serviceAccountsEnabled": True, + } + ) + res = admin.get_client_authz_settings(client_id=auth_client_id) + assert res["allowRemoteResourceManagement"] + assert res["decisionStrategy"] == "UNANIMOUS" + assert len(res["policies"]) >= 0 + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_settings(client_id=client_id) + assert err.match('500: b\'{"error":"HTTP 500 Internal Server Error"}\'') + + # Authz resources + res = admin.get_client_authz_resources(client_id=auth_client_id) + assert len(res) == 1 + assert res[0]["name"] == "Default Resource" + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_resources(client_id=client_id) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + res = admin.create_client_authz_resource( + client_id=auth_client_id, payload={"name": "test-resource"} + ) + assert res["name"] == "test-resource", res + test_resource_id = res["_id"] + + with pytest.raises(KeycloakPostError) as err: + admin.create_client_authz_resource( + client_id=auth_client_id, payload={"name": "test-resource"} + ) + assert err.match('409: b\'{"error":"invalid_request"') + assert admin.create_client_authz_resource( + client_id=auth_client_id, payload={"name": "test-resource"}, skip_exists=True + ) == {"msg": "Already exists"} + + res = admin.get_client_authz_resources(client_id=auth_client_id) + assert len(res) == 2 + assert {x["name"] for x in res} == {"Default Resource", "test-resource"} + + # Authz policies + res = admin.get_client_authz_policies(client_id=auth_client_id) + assert len(res) == 1, res + assert res[0]["name"] == "Default Policy" + assert len(admin.get_client_authz_policies(client_id=client_id)) == 1 + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_policies(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + role_id = admin.get_realm_role(role_name="offline_access")["id"] + res = admin.create_client_authz_role_based_policy( + client_id=auth_client_id, + payload={"name": "test-authz-rb-policy", "roles": [{"id": role_id}]}, + ) + assert res["name"] == "test-authz-rb-policy", res + + with pytest.raises(KeycloakPostError) as err: + admin.create_client_authz_role_based_policy( + client_id=auth_client_id, + payload={"name": "test-authz-rb-policy", "roles": [{"id": role_id}]}, + ) + assert err.match('409: b\'{"error":"Policy with name') + assert admin.create_client_authz_role_based_policy( + client_id=auth_client_id, + payload={"name": "test-authz-rb-policy", "roles": [{"id": role_id}]}, + skip_exists=True, + ) == {"msg": "Already exists"} + assert len(admin.get_client_authz_policies(client_id=auth_client_id)) == 2 + + # Test authz permissions + res = admin.get_client_authz_permissions(client_id=auth_client_id) + assert len(res) == 1, res + assert res[0]["name"] == "Default Permission" + assert len(admin.get_client_authz_permissions(client_id=client_id)) == 1 + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_permissions(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + res = admin.create_client_authz_resource_based_permission( + client_id=auth_client_id, + payload={"name": "test-permission-rb", "resources": [test_resource_id]}, + ) + assert res, res + assert res["name"] == "test-permission-rb" + assert res["resources"] == [test_resource_id] + + with pytest.raises(KeycloakPostError) as err: + admin.create_client_authz_resource_based_permission( + client_id=auth_client_id, + payload={"name": "test-permission-rb", "resources": [test_resource_id]}, + ) + assert err.match('409: b\'{"error":"Policy with name') + assert admin.create_client_authz_resource_based_permission( + client_id=auth_client_id, + payload={"name": "test-permission-rb", "resources": [test_resource_id]}, + skip_exists=True, + ) == {"msg": "Already exists"} + assert len(admin.get_client_authz_permissions(client_id=auth_client_id)) == 2 + + # Test authz scopes + res = admin.get_client_authz_scopes(client_id=auth_client_id) + assert len(res) == 0, res + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_authz_scopes(client_id=client_id) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + # Test service account user + res = admin.get_client_service_account_user(client_id=auth_client_id) + assert res["username"] == "service-account-authz-client", res + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_service_account_user(client_id=client_id) + assert err.match('400: b\'{"error":"unknown_error"}\'') + + # Test delete client + res = admin.delete_client(client_id=auth_client_id) + assert res == dict(), res + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client(client_id=auth_client_id) + assert err.match('404: b\'{"error":"Could not find client"}\'') + + +def test_realm_roles(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test get realm roles + roles = admin.get_realm_roles() + assert len(roles) == 3, roles + role_names = [x["name"] for x in roles] + assert "uma_authorization" in role_names, role_names + assert "offline_access" in role_names, role_names + + # Test empty members + with pytest.raises(KeycloakGetError) as err: + admin.get_realm_role_members(role_name="does-not-exist") + assert err.match('404: b\'{"error":"Could not find role"}\'') + members = admin.get_realm_role_members(role_name="offline_access") + assert members == list(), members + + # Test create realm role + role_id = admin.create_realm_role(payload={"name": "test-realm-role"}) + assert role_id, role_id + with pytest.raises(KeycloakPostError) as err: + admin.create_realm_role(payload={"name": "test-realm-role"}) + assert err.match('409: b\'{"errorMessage":"Role with name test-realm-role already exists"}\'') + role_id_2 = admin.create_realm_role(payload={"name": "test-realm-role"}, skip_exists=True) + assert role_id == role_id_2 + + # Test update realm role + res = admin.update_realm_role( + role_name="test-realm-role", payload={"name": "test-realm-role-update"} + ) + assert res == dict(), res + with pytest.raises(KeycloakPutError) as err: + admin.update_realm_role( + role_name="test-realm-role", payload={"name": "test-realm-role-update"} + ) + assert err.match('404: b\'{"error":"Could not find role"}\''), err + + # Test realm role user assignment + user_id = admin.create_user(payload={"username": "role-testing", "email": "test@test.test"}) + with pytest.raises(KeycloakPostError) as err: + admin.assign_realm_roles(user_id=user_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_realm_roles( + user_id=user_id, + roles=[ + admin.get_realm_role(role_name="offline_access"), + admin.get_realm_role(role_name="test-realm-role-update"), + ], + ) + assert res == dict(), res + assert admin.get_user(user_id=user_id)["username"] in [ + x["username"] for x in admin.get_realm_role_members(role_name="offline_access") + ] + assert admin.get_user(user_id=user_id)["username"] in [ + x["username"] for x in admin.get_realm_role_members(role_name="test-realm-role-update") + ] + + roles = admin.get_realm_roles_of_user(user_id=user_id) + assert len(roles) == 3 + assert "offline_access" in [x["name"] for x in roles] + assert "test-realm-role-update" in [x["name"] for x in roles] + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm_roles_of_user(user_id=user_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_realm_roles_of_user( + user_id=user_id, roles=[admin.get_realm_role(role_name="offline_access")] + ) + assert res == dict(), res + assert admin.get_realm_role_members(role_name="offline_access") == list() + roles = admin.get_realm_roles_of_user(user_id=user_id) + assert len(roles) == 2 + assert "offline_access" not in [x["name"] for x in roles] + assert "test-realm-role-update" in [x["name"] for x in roles] + + roles = admin.get_available_realm_roles_of_user(user_id=user_id) + assert len(roles) == 2 + assert "offline_access" in [x["name"] for x in roles] + assert "uma_authorization" in [x["name"] for x in roles] + + # Test realm role group assignment + group_id = admin.create_group(payload={"name": "test-group"}) + with pytest.raises(KeycloakPostError) as err: + admin.assign_group_realm_roles(group_id=group_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_group_realm_roles( + group_id=group_id, + roles=[ + admin.get_realm_role(role_name="offline_access"), + admin.get_realm_role(role_name="test-realm-role-update"), + ], + ) + assert res == dict(), res + + roles = admin.get_group_realm_roles(group_id=group_id) + assert len(roles) == 2 + assert "offline_access" in [x["name"] for x in roles] + assert "test-realm-role-update" in [x["name"] for x in roles] + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_group_realm_roles(group_id=group_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_group_realm_roles( + group_id=group_id, roles=[admin.get_realm_role(role_name="offline_access")] + ) + assert res == dict(), res + roles = admin.get_group_realm_roles(group_id=group_id) + assert len(roles) == 1 + assert "test-realm-role-update" in [x["name"] for x in roles] + + # Test composite realm roles + composite_role = admin.create_realm_role(payload={"name": "test-composite-role"}) + with pytest.raises(KeycloakPostError) as err: + admin.add_composite_realm_roles_to_role(role_name=composite_role, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.add_composite_realm_roles_to_role( + role_name=composite_role, roles=[admin.get_realm_role(role_name="test-realm-role-update")] + ) + assert res == dict(), res + + res = admin.get_composite_realm_roles_of_role(role_name=composite_role) + assert len(res) == 1 + assert "test-realm-role-update" in res[0]["name"] + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_realm_roles_of_role(role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + res = admin.get_composite_realm_roles_of_user(user_id=user_id) + assert len(res) == 4 + assert "offline_access" in {x["name"] for x in res} + assert "test-realm-role-update" in {x["name"] for x in res} + assert "uma_authorization" in {x["name"] for x in res} + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_realm_roles_of_user(user_id="bad") + assert err.match('404: b\'{"error":"User not found"}\'') + + with pytest.raises(KeycloakDeleteError) as err: + admin.remove_composite_realm_roles_to_role(role_name=composite_role, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.remove_composite_realm_roles_to_role( + role_name=composite_role, roles=[admin.get_realm_role(role_name="test-realm-role-update")] + ) + assert res == dict(), res + + res = admin.get_composite_realm_roles_of_role(role_name=composite_role) + assert len(res) == 0 + + # Test delete realm role + res = admin.delete_realm_role(role_name=composite_role) + assert res == dict(), res + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm_role(role_name=composite_role) + assert err.match('404: b\'{"error":"Could not find role"}\'') + + +def test_client_roles(admin: KeycloakAdmin, client: str): + # Test get client roles + res = admin.get_client_roles(client_id=client) + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_client_roles(client_id="bad") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + # Test create client role + client_role_id = admin.create_client_role( + client_role_id=client, payload={"name": "client-role-test"} + ) + with pytest.raises(KeycloakPostError) as err: + admin.create_client_role(client_role_id=client, payload={"name": "client-role-test"}) + assert err.match('409: b\'{"errorMessage":"Role with name client-role-test already exists"}\'') + client_role_id_2 = admin.create_client_role( + client_role_id=client, payload={"name": "client-role-test"}, skip_exists=True + ) + assert client_role_id == client_role_id_2 + + # Test get client role + res = admin.get_client_role(client_id=client, role_name="client-role-test") + assert res["name"] == client_role_id + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + res_ = admin.get_client_role_id(client_id=client, role_name="client-role-test") + assert res_ == res["id"] + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role_id(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + assert len(admin.get_client_roles(client_id=client)) == 1 + + # Test update client role + res = admin.update_client_role( + client_role_id=client, + role_name="client-role-test", + payload={"name": "client-role-test-update"}, + ) + assert res == dict() + with pytest.raises(KeycloakPutError) as err: + res = admin.update_client_role( + client_role_id=client, + role_name="client-role-test", + payload={"name": "client-role-test-update"}, + ) + assert err.match('404: b\'{"error":"Could not find role"}\'') + + # Test user with client role + res = admin.get_client_role_members(client_id=client, role_name="client-role-test-update") + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role_members(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + user_id = admin.create_user(payload={"username": "test", "email": "test@test.test"}) + with pytest.raises(KeycloakPostError) as err: + admin.assign_client_role(user_id=user_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_client_role( + user_id=user_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert res == dict() + assert ( + len(admin.get_client_role_members(client_id=client, role_name="client-role-test-update")) + == 1 + ) + + roles = admin.get_client_roles_of_user(user_id=user_id, client_id=client) + assert len(roles) == 1, roles + with pytest.raises(KeycloakGetError) as err: + admin.get_client_roles_of_user(user_id=user_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + roles = admin.get_composite_client_roles_of_user(user_id=user_id, client_id=client) + assert len(roles) == 1, roles + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_client_roles_of_user(user_id=user_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + roles = admin.get_available_client_roles_of_user(user_id=user_id, client_id=client) + assert len(roles) == 0, roles + with pytest.raises(KeycloakGetError) as err: + admin.get_composite_client_roles_of_user(user_id=user_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_roles_of_user(user_id=user_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + admin.delete_client_roles_of_user( + user_id=user_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert len(admin.get_client_roles_of_user(user_id=user_id, client_id=client)) == 0 + + # Test groups and client roles + res = admin.get_client_role_groups(client_id=client, role_name="client-role-test-update") + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_client_role_groups(client_id=client, role_name="bad") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + group_id = admin.create_group(payload={"name": "test-group"}) + res = admin.get_group_client_roles(group_id=group_id, client_id=client) + assert len(res) == 0 + with pytest.raises(KeycloakGetError) as err: + admin.get_group_client_roles(group_id=group_id, client_id="bad") + assert err.match('404: b\'{"error":"Client not found"}\'') + + with pytest.raises(KeycloakPostError) as err: + admin.assign_group_client_roles(group_id=group_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_group_client_roles( + group_id=group_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert res == dict() + assert ( + len(admin.get_client_role_groups(client_id=client, role_name="client-role-test-update")) + == 1 + ) + assert len(admin.get_group_client_roles(group_id=group_id, client_id=client)) == 1 + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_group_client_roles(group_id=group_id, client_id=client, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_group_client_roles( + group_id=group_id, + client_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test-update")], + ) + assert res == dict() + + # Test composite client roles + with pytest.raises(KeycloakPostError) as err: + admin.add_composite_client_roles_to_role( + client_role_id=client, role_name="client-role-test-update", roles=["bad"] + ) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.add_composite_client_roles_to_role( + client_role_id=client, + role_name="client-role-test-update", + roles=[admin.get_realm_role(role_name="offline_access")], + ) + assert res == dict() + assert admin.get_client_role(client_id=client, role_name="client-role-test-update")[ + "composite" + ] + + # Test delete of client role + res = admin.delete_client_role(client_role_id=client, role_name="client-role-test-update") + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_role(client_role_id=client, role_name="client-role-test-update") + assert err.match('404: b\'{"error":"Could not find role"}\'') + + +def test_email(admin: KeycloakAdmin, user: str): + # Emails will fail as we don't have SMTP test setup + with pytest.raises(KeycloakPutError) as err: + admin.send_update_account(user_id=user, payload=dict()) + assert err.match('500: b\'{"error":"unknown_error"}\'') + + admin.update_user(user_id=user, payload={"enabled": True}) + with pytest.raises(KeycloakPutError) as err: + admin.send_verify_email(user_id=user) + assert err.match('500: b\'{"errorMessage":"Failed to send execute actions email"}\'') + + +def test_get_sessions(admin: KeycloakAdmin): + sessions = admin.get_sessions(user_id=admin.get_user_id(username=admin.username)) + assert len(sessions) >= 1 + with pytest.raises(KeycloakGetError) as err: + admin.get_sessions(user_id="bad") + assert err.match('404: b\'{"error":"User not found"}\'') + + +def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): + with pytest.raises(KeycloakGetError) as err: + admin.get_client_installation_provider(client_id=client, provider_id="bad") + assert err.match('404: b\'{"error":"Unknown Provider"}\'') + + installation = admin.get_client_installation_provider( + client_id=client, provider_id="keycloak-oidc-keycloak-json" + ) + assert set(installation.keys()) == { + "auth-server-url", + "confidential-port", + "credentials", + "realm", + "resource", + "ssl-required", + } + + +def test_auth_flows(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + res = admin.get_authentication_flows() + assert len(res) == 8, res + assert set(res[0].keys()) == { + "alias", + "authenticationExecutions", + "builtIn", + "description", + "id", + "providerId", + "topLevel", + } + assert {x["alias"] for x in res} == { + "reset credentials", + "browser", + "http challenge", + "registration", + "docker auth", + "direct grant", + "first broker login", + "clients", + } + + with pytest.raises(KeycloakGetError) as err: + admin.get_authentication_flow_for_id(flow_id="bad") + assert err.match('404: b\'{"error":"Could not find flow with id"}\'') + browser_flow_id = [x for x in res if x["alias"] == "browser"][0]["id"] + res = admin.get_authentication_flow_for_id(flow_id=browser_flow_id) + assert res["alias"] == "browser" + + # Test copying + with pytest.raises(KeycloakPostError) as err: + admin.copy_authentication_flow(payload=dict(), flow_alias="bad") + assert err.match("404: b''") + + res = admin.copy_authentication_flow(payload={"newName": "test-browser"}, flow_alias="browser") + assert res == b"", res + assert len(admin.get_authentication_flows()) == 9 + + # Test create + res = admin.create_authentication_flow( + payload={"alias": "test-create", "providerId": "basic-flow"} + ) + assert res == b"" + with pytest.raises(KeycloakPostError) as err: + admin.create_authentication_flow(payload={"alias": "test-create", "builtIn": False}) + assert err.match('409: b\'{"errorMessage":"Flow test-create already exists"}\'') + assert admin.create_authentication_flow( + payload={"alias": "test-create"}, skip_exists=True + ) == {"msg": "Already exists"} + + # Test flow executions + res = admin.get_authentication_flow_executions(flow_alias="browser") + assert len(res) == 8, res + with pytest.raises(KeycloakGetError) as err: + admin.get_authentication_flow_executions(flow_alias="bad") + assert err.match("404: b''") + exec_id = res[0]["id"] + + res = admin.get_authentication_flow_execution(execution_id=exec_id) + assert set(res.keys()) == { + "alternative", + "authenticator", + "authenticatorFlow", + "conditional", + "disabled", + "enabled", + "id", + "parentFlow", + "priority", + "required", + "requirement", + }, res + with pytest.raises(KeycloakGetError) as err: + admin.get_authentication_flow_execution(execution_id="bad") + assert err.match('404: b\'{"error":"Illegal execution"}\'') + + with pytest.raises(KeycloakPostError) as err: + admin.create_authentication_flow_execution(payload=dict(), flow_alias="browser") + assert err.match('400: b\'{"error":"It is illegal to add execution to a built in flow"}\'') + + res = admin.create_authentication_flow_execution( + payload={"provider": "auth-cookie"}, flow_alias="test-create" + ) + assert res == b"" + assert len(admin.get_authentication_flow_executions(flow_alias="test-create")) == 1 + + with pytest.raises(KeycloakPutError) as err: + admin.update_authentication_flow_executions( + payload={"required": "yes"}, flow_alias="test-create" + ) + assert err.match('400: b\'{"error":"Unrecognized field') + payload = admin.get_authentication_flow_executions(flow_alias="test-create")[0] + payload["displayName"] = "test" + res = admin.update_authentication_flow_executions(payload=payload, flow_alias="test-create") + assert res + + exec_id = admin.get_authentication_flow_executions(flow_alias="test-create")[0]["id"] + res = admin.delete_authentication_flow_execution(execution_id=exec_id) + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_authentication_flow_execution(execution_id=exec_id) + assert err.match('404: b\'{"error":"Illegal execution"}\'') + + # Test subflows + res = admin.create_authentication_flow_subflow( + payload={ + "alias": "test-subflow", + "provider": "basic-flow", + "type": "something", + "description": "something", + }, + flow_alias="test-browser", + ) + assert res == b"" + with pytest.raises(KeycloakPostError) as err: + admin.create_authentication_flow_subflow( + payload={"alias": "test-subflow", "providerId": "basic-flow"}, + flow_alias="test-browser", + ) + assert err.match('409: b\'{"errorMessage":"New flow alias name already exists"}\'') + res = admin.create_authentication_flow_subflow( + payload={ + "alias": "test-subflow", + "provider": "basic-flow", + "type": "something", + "description": "something", + }, + flow_alias="test-create", + skip_exists=True, + ) + assert res == {"msg": "Already exists"} + + # Test delete auth flow + flow_id = [x for x in admin.get_authentication_flows() if x["alias"] == "test-browser"][0][ + "id" + ] + res = admin.delete_authentication_flow(flow_id=flow_id) + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_authentication_flow(flow_id=flow_id) + assert err.match('404: b\'{"error":"Could not find flow with id"}\'') diff --git a/tests/test_urls_patterns.py b/tests/test_urls_patterns.py new file mode 100644 index 00000000..6fa5a871 --- /dev/null +++ b/tests/test_urls_patterns.py @@ -0,0 +1,26 @@ +from keycloak import urls_patterns + + +def test_correctness_of_patterns(): + """Test that there are no duplicate url patterns.""" + + # Test that the patterns are present + urls = [x for x in dir(urls_patterns) if not x.startswith("__")] + assert len(urls) >= 0 + + # Test that all patterns start with URL_ + for url in urls: + assert url.startswith("URL_"), f"The url pattern {url} does not begin with URL_" + + # Test that the patterns have unique names + seen_urls = list() + for url in urls: + assert url not in seen_urls, f"The url pattern {url} is present twice." + seen_urls.append(url) + + # Test that the pattern values are unique + seen_url_values = list() + for url in urls: + url_value = urls_patterns.__dict__[url] + assert url_value not in seen_url_values, f"The url {url} has a duplicate value {url_value}" + seen_url_values.append(url_value) diff --git a/tox.env b/tox.env new file mode 100644 index 00000000..49cea83b --- /dev/null +++ b/tox.env @@ -0,0 +1,4 @@ +KEYCLOAK_ADMIN=admin +KEYCLOAK_ADMIN_PASSWORD=admin +KEYCLOAK_HOST={env:KEYCLOAK_HOST:localhost} +KEYCLOAK_PORT=8080 diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..2726f668 --- /dev/null +++ b/tox.ini @@ -0,0 +1,48 @@ +[tox] +envlist = check, apply-check, docs, tests, build + +[testenv] +install_command = pip install {opts} {packages} + +[testenv:check] +deps = + black + isort + flake8 +commands = + black --check --diff keycloak tests docs + isort -c --df keycloak tests docs + flake8 keycloak tests docs + +[testenv:apply-check] +deps = + black + isort + flake8 +commands = + black -C keycloak tests docs + black keycloak tests docs + isort keycloak tests docs + +[testenv:docs] +deps = + .[docs] +commands = + python -m sphinx -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html + +[testenv:tests] +setenv = file|tox.env +deps = + -rrequirements.txt + -rdev-requirements.txt +commands = + ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" + +[testenv:build] +deps = + -rdev-requirements.txt +commands = + python setup.py sdist bdist_wheel + +[flake8] +max-line-length = 99 From 8d19ea8180b494832d36126615bbcb9c5c9de159 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:06:04 +0200 Subject: [PATCH 146/566] fix: raise correct errors --- keycloak/__init__.py | 7 +- keycloak/keycloak_admin.py | 164 +++++++++++++++++++------------------ 2 files changed, 89 insertions(+), 82 deletions(-) diff --git a/keycloak/__init__.py b/keycloak/__init__.py index 987ce1c5..62c47a8e 100644 --- a/keycloak/__init__.py +++ b/keycloak/__init__.py @@ -21,5 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -from .keycloak_admin import * -from .keycloak_openid import * +from ._version import __version__ +from .keycloak_admin import KeycloakAdmin +from .keycloak_openid import KeycloakOpenID + +__all__ = ["KeycloakAdmin", "KeycloakOpenID", "__version__"] diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 5725a1a0..3e30722d 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -29,7 +29,13 @@ from typing import Iterable from .connection import ConnectionManager -from .exceptions import KeycloakGetError, raise_error_from_response +from .exceptions import ( + KeycloakDeleteError, + KeycloakGetError, + KeycloakPostError, + KeycloakPutError, + raise_error_from_response, +) from .keycloak_openid import KeycloakOpenID from .urls_patterns import ( URL_ADMIN_AUTHENTICATOR_CONFIG, @@ -341,7 +347,7 @@ def import_realm(self, payload): """ data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def export_realm(self, export_clients=False, export_groups_and_role=False): """ @@ -361,7 +367,7 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): "export-groups-and-roles": export_groups_and_role, } data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="") - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def get_realms(self): """ @@ -386,7 +392,7 @@ def create_realm(self, payload, skip_exists=False): data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_realm(self, realm_name, payload): @@ -404,7 +410,7 @@ def update_realm(self, realm_name, payload): params_path = {"realm-name": realm_name} data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm(self, realm_name): """ @@ -416,7 +422,7 @@ def delete_realm(self, realm_name): params_path = {"realm-name": realm_name} data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_users(self, query=None): """ @@ -448,7 +454,7 @@ def create_idp(self, payload): """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def add_mapper_to_idp(self, idp_alias, payload): """ @@ -464,7 +470,7 @@ def add_mapper_to_idp(self, idp_alias, payload): data_raw = self.raw_post( URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def get_idps(self): """ @@ -487,7 +493,7 @@ def delete_idp(self, idp_alias): """ params_path = {"realm-name": self.realm_name, "alias": idp_alias} data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_user(self, payload, exist_ok=True): """ @@ -510,7 +516,7 @@ def create_user(self, payload, exist_ok=True): return str(exists) data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) - raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) _last_slash_idx = data_raw.headers["Location"].rindex("/") return data_raw.headers["Location"][_last_slash_idx + 1 :] @@ -578,7 +584,7 @@ def update_user(self, user_id, payload): """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_user(self, user_id): """ @@ -590,7 +596,7 @@ def delete_user(self, user_id): """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): """ @@ -611,7 +617,7 @@ def set_user_password(self, user_id, password, temporary=True): data_raw = self.raw_put( URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_credentials(self, user_id): """ @@ -663,7 +669,7 @@ def delete_credential(self, user_id, credential_id): "credential_id": credential_id, } data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakDeleteError) def logout(self, user_id): """ @@ -676,7 +682,7 @@ def logout(self, user_id): """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def consents_user(self, user_id): """ @@ -719,6 +725,7 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ data_raw = self.raw_post( URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload) ) + return raise_error_from_response(data_raw, KeycloakPostError) def delete_user_social_login(self, user_id, provider_id): @@ -730,7 +737,7 @@ def delete_user_social_login(self, user_id, provider_id): """ params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def send_update_account( self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None @@ -754,7 +761,7 @@ def send_update_account( data=json.dumps(payload), **params_query ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPutError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): """ @@ -772,7 +779,7 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): data_raw = self.raw_put( URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, **params_query ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPutError) def get_sessions(self, user_id): """ @@ -931,7 +938,7 @@ def create_group(self, payload, parent=None, skip_exists=False): ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_group(self, group_id, payload): @@ -949,7 +956,7 @@ def update_group(self, group_id, payload): params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): """ @@ -965,7 +972,7 @@ def group_set_permissions(self, group_id, enabled=True): URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), data=json.dumps({"enabled": enabled}), ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPutError) def group_user_add(self, user_id, group_id): """ @@ -978,7 +985,7 @@ def group_user_add(self, user_id, group_id): params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): """ @@ -991,7 +998,7 @@ def group_user_remove(self, user_id, group_id): params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def delete_group(self, group_id): """ @@ -1003,7 +1010,7 @@ def delete_group(self, group_id): params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_clients(self): """ @@ -1063,7 +1070,7 @@ def get_client_authz_settings(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_resource(self, client_id, payload, skip_exists=False): """ @@ -1083,7 +1090,7 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_client_authz_resources(self, client_id): @@ -1129,7 +1136,7 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= data=json.dumps(payload), ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): @@ -1163,7 +1170,7 @@ def create_client_authz_resource_based_permission(self, client_id, payload, skip data=json.dumps(payload), ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_client_authz_scopes(self, client_id): @@ -1177,7 +1184,7 @@ def get_client_authz_scopes(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_permissions(self, client_id): """ @@ -1190,7 +1197,7 @@ def get_client_authz_permissions(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policies(self, client_id): """ @@ -1203,7 +1210,7 @@ def get_client_authz_policies(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path)) - return data_raw + return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): """ @@ -1232,7 +1239,7 @@ def create_client(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_client(self, client_id, payload): @@ -1246,7 +1253,7 @@ def update_client(self, client_id, payload): """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client(self, client_id): """ @@ -1261,7 +1268,7 @@ def delete_client(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_installation_provider(self, client_id, provider_id): """ @@ -1374,7 +1381,7 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): @@ -1393,7 +1400,7 @@ def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_client_role(self, client_role_id, role_name): """ @@ -1407,7 +1414,7 @@ def delete_client_role(self, client_role_id, role_name): """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): """ @@ -1424,7 +1431,7 @@ def assign_client_role(self, user_id, client_id, roles): data_raw = self.raw_post( URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_client_role_members(self, client_id, role_name, **query): """ @@ -1451,7 +1458,7 @@ def create_realm_role(self, payload, skip_exists=False): URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_realm_role(self, role_name): @@ -1479,7 +1486,7 @@ def update_realm_role(self, role_name, payload): data_raw = self.raw_put( URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm_role(self, role_name): """ @@ -1490,7 +1497,7 @@ def delete_realm_role(self, role_name): params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): """ @@ -1507,7 +1514,7 @@ def add_composite_realm_roles_to_role(self, role_name, roles): URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def remove_composite_realm_roles_to_role(self, role_name, roles): """ @@ -1524,7 +1531,7 @@ def remove_composite_realm_roles_to_role(self, role_name, roles): URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_composite_realm_roles_of_role(self, role_name): """ @@ -1552,7 +1559,7 @@ def assign_realm_roles(self, user_id, roles): data_raw = self.raw_post( URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_realm_roles_of_user(self, user_id, roles): """ @@ -1568,7 +1575,7 @@ def delete_realm_roles_of_user(self, user_id, roles): data_raw = self.raw_delete( URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_realm_roles_of_user(self, user_id): """ @@ -1616,7 +1623,7 @@ def assign_group_realm_roles(self, group_id, roles): data_raw = self.raw_post( URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): """ @@ -1632,7 +1639,7 @@ def delete_group_realm_roles(self, group_id, roles): data_raw = self.raw_delete( URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_group_realm_roles(self, group_id): """ @@ -1660,7 +1667,7 @@ def assign_group_client_roles(self, group_id, client_id, roles): data_raw = self.raw_post( URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_group_client_roles(self, group_id, client_id): """ @@ -1690,7 +1697,7 @@ def delete_group_client_roles(self, group_id, client_id, roles): data_raw = self.raw_delete( URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): """ @@ -1745,7 +1752,7 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): data_raw = self.raw_delete( URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flows(self): """ @@ -1789,7 +1796,7 @@ def create_authentication_flow(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload)) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def copy_authentication_flow(self, payload, flow_alias): @@ -1805,7 +1812,7 @@ def copy_authentication_flow(self, payload, flow_alias): data_raw = self.raw_post( URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow(self, flow_id): """ @@ -1819,7 +1826,7 @@ def delete_authentication_flow(self, flow_id): """ params_path = {"realm-name": self.realm_name, "id": flow_id} data_raw = self.raw_delete(URL_ADMIN_FLOW.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flow_executions(self, flow_alias): """ @@ -1848,7 +1855,7 @@ def update_authentication_flow_executions(self, payload, flow_alias): data_raw = self.raw_put( URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[202, 204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[202, 204]) def get_authentication_flow_execution(self, execution_id): """ @@ -1880,7 +1887,7 @@ def create_authentication_flow_execution(self, payload, flow_alias): data_raw = self.raw_post( URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow_execution(self, execution_id): """ @@ -1894,7 +1901,7 @@ def delete_authentication_flow_execution(self, execution_id): """ params_path = {"realm-name": self.realm_name, "id": execution_id} data_raw = self.raw_delete(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): """ @@ -1914,7 +1921,7 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def get_authenticator_config(self, config_id): @@ -1943,7 +1950,7 @@ def update_authenticator_config(self, payload, config_id): data_raw = self.raw_put( URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_authenticator_config(self, config_id): """ @@ -1956,8 +1963,7 @@ def delete_authenticator_config(self, config_id): params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_delete(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def sync_users(self, storage_id, action): """ @@ -1974,7 +1980,7 @@ def sync_users(self, storage_id, action): data_raw = self.raw_post( URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), **params_query ) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def get_client_scopes(self): """ @@ -2017,7 +2023,7 @@ def create_client_scope(self, payload, skip_exists=False): URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( - data_raw, KeycloakGetError, expected_codes=[201], skip_exists=skip_exists + data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_client_scope(self, client_scope_id, payload): @@ -2035,7 +2041,7 @@ def update_client_scope(self, client_scope_id, payload): data_raw = self.raw_put( URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client_scope(self, client_scope_id, payload): """ @@ -2053,7 +2059,7 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): """ @@ -2072,8 +2078,7 @@ def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): } data_raw = self.raw_delete(URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, payload): """ @@ -2097,7 +2102,7 @@ def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, pay URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_default_client_scopes(self): """ @@ -2118,7 +2123,7 @@ def delete_default_default_client_scope(self, scope_id): """ params_path = {"realm-name": self.realm_name, "id": scope_id} data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_default_client_scope(self, scope_id): """ @@ -2132,7 +2137,7 @@ def add_default_default_client_scope(self, scope_id): data_raw = self.raw_put( URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_optional_client_scopes(self): """ @@ -2153,7 +2158,7 @@ def delete_default_optional_client_scope(self, scope_id): """ params_path = {"realm-name": self.realm_name, "id": scope_id} data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_optional_client_scope(self, scope_id): """ @@ -2167,7 +2172,7 @@ def add_default_optional_client_scope(self, scope_id): data_raw = self.raw_put( URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client(self, client_id, payload): """ @@ -2185,7 +2190,7 @@ def add_mapper_to_client(self, client_id, payload): URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_client_mapper(self, client_id, mapper_id, payload): """ @@ -2206,7 +2211,7 @@ def update_client_mapper(self, client_id, mapper_id, payload): URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def remove_client_mapper(self, client_id, client_mapper_id): """ @@ -2224,8 +2229,7 @@ def remove_client_mapper(self, client_id, client_mapper_id): } data_raw = self.raw_delete(URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def generate_client_secrets(self, client_id): """ @@ -2239,7 +2243,7 @@ def generate_client_secrets(self, client_id): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post(URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def get_client_secrets(self, client_id): """ @@ -2285,7 +2289,7 @@ def create_component(self, payload): data_raw = self.raw_post( URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[201]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def get_component(self, component_id): """ @@ -2316,7 +2320,7 @@ def update_component(self, component_id, payload): data_raw = self.raw_put( URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_component(self, component_id): """ @@ -2328,7 +2332,7 @@ def delete_component(self, component_id): """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_keys(self): """ @@ -2367,7 +2371,7 @@ def set_events(self, payload): """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def raw_get(self, *args, **kwargs): """ From aff3051ffa33c2c50a2504fc42b5e176be955918 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:40:15 +0200 Subject: [PATCH 147/566] docs: fix docstrings --- keycloak/authorization/__init__.py | 4 +- keycloak/authorization/permission.py | 14 +++-- keycloak/authorization/policy.py | 7 ++- keycloak/connection.py | 87 +++++++++++++--------------- keycloak/keycloak_openid.py | 51 ++++++++-------- 5 files changed, 79 insertions(+), 84 deletions(-) diff --git a/keycloak/authorization/__init__.py b/keycloak/authorization/__init__.py index dad10780..789656d8 100644 --- a/keycloak/authorization/__init__.py +++ b/keycloak/authorization/__init__.py @@ -38,7 +38,7 @@ class Authorization: """ def __init__(self): - self._policies = {} + self.policies = {} @property def policies(self): @@ -53,7 +53,7 @@ def load_config(self, data): Load policies, roles and permissions (scope/resources). :param data: keycloak authorization data (dict) - :return: + :returns: None """ for pol in data["policies"]: if pol["type"] == "role": diff --git a/keycloak/authorization/permission.py b/keycloak/authorization/permission.py index 9988730c..a200afec 100644 --- a/keycloak/authorization/permission.py +++ b/keycloak/authorization/permission.py @@ -26,15 +26,19 @@ class Permission: """ Consider this simple and very common permission: - A permission associates the object being protected with the policies that must be evaluated to determine whether access is granted. + A permission associates the object being protected with the policies that must be evaluated to + determine whether access is granted. X CAN DO Y ON RESOURCE Z - where … - X represents one or more users, roles, or groups, or a combination of them. You can + where + + - X represents one or more users, roles, or groups, or a combination of them. You can also use claims and context here. - Y represents an action to be performed, for example, write, view, and so on. - Z represents a protected resource, for example, "/accounts". + + - Y represents an action to be performed, for example, write, view, and so on. + + - Z represents a protected resource, for example, "/accounts". https://keycloak.gitbooks.io/documentation/authorization_services/topics/permission/overview.html diff --git a/keycloak/authorization/policy.py b/keycloak/authorization/policy.py index 4fbe9138..4014b7a8 100644 --- a/keycloak/authorization/policy.py +++ b/keycloak/authorization/policy.py @@ -29,9 +29,10 @@ class Policy: A policy defines the conditions that must be satisfied to grant access to an object. Unlike permissions, you do not specify the object being protected but rather the conditions that must be satisfied for access to a given object (for example, resource, scope, or both). - Policies are strongly related to the different access control mechanisms (ACMs) that you can use to - protect your resources. With policies, you can implement strategies for attribute-based access control - (ABAC), role-based access control (RBAC), context-based access control, or any combination of these. + Policies are strongly related to the different access control mechanisms (ACMs) that you can + use to protect your resources. With policies, you can implement strategies for attribute-based + access control (ABAC), role-based access control (RBAC), context-based access control, or any + combination of these. https://keycloak.gitbooks.io/documentation/authorization_services/topics/policy/overview.html diff --git a/keycloak/connection.py b/keycloak/connection.py index 8ef45b18..07573778 100644 --- a/keycloak/connection.py +++ b/keycloak/connection.py @@ -33,13 +33,14 @@ class ConnectionManager(object): - """Represents a simple server connection. - Args: - base_url (str): The server URL. - headers (dict): The header parameters of the requests to the server. - timeout (int): Timeout to use for requests to the server. - verify (bool): Verify server SSL. - proxies (dict): The proxies servers requests is sent by. + """ + Represents a simple server connection. + + :param base_url: (str) The server URL. + :param headers: (dict) The header parameters of the requests to the server. + :param timeout: (int) Timeout to use for requests to the server. + :param verify: (bool) Verify server SSL. + :param proxies: (dict) The proxies servers requests is sent by. """ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): @@ -108,11 +109,11 @@ def headers(self, value): self._headers = value def param_headers(self, key): - """Return a specific header parameter. - :arg - key (str): Header parameters key. - :return: - If the header parameters exist, return its value. + """ + Return a specific header parameter. + + :param key: (str) Header parameters key. + :returns: If the header parameters exist, return its value. """ return self.headers.get(key) @@ -122,36 +123,33 @@ def clean_headers(self): def exist_param_headers(self, key): """Check if the parameter exists in the header. - :arg - key (str): Header parameters key. - :return: - If the header parameters exist, return True. + + :param key: (str) Header parameters key. + :returns: If the header parameters exist, return True. """ return self.param_headers(key) is not None def add_param_headers(self, key, value): """Add a single parameter inside the header. - :arg - key (str): Header parameters key. - value (str): Value to be added. + + :param key: (str) Header parameters key. + :param value: (str) Value to be added. """ self.headers[key] = value def del_param_headers(self, key): """Remove a specific parameter. - :arg - key (str): Key of the header parameters. + + :param key: (str) Key of the header parameters. """ self.headers.pop(key, None) def raw_get(self, path, **kwargs): """Submit get request to the path. - :arg - path (str): Path for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + + :param path: (str) Path for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: @@ -167,13 +165,11 @@ def raw_get(self, path, **kwargs): def raw_post(self, path, data, **kwargs): """Submit post request to the path. - :arg - path (str): Path for request. - data (dict): Payload for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + + :param path: (str) Path for request. + :param data: (dict) Payload for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: return self._s.post( @@ -189,13 +185,11 @@ def raw_post(self, path, data, **kwargs): def raw_put(self, path, data, **kwargs): """Submit put request to the path. - :arg - path (str): Path for request. - data (dict): Payload for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + + :param path: (str) Path for request. + :param data: (dict) Payload for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: return self._s.put( @@ -212,13 +206,10 @@ def raw_put(self, path, data, **kwargs): def raw_delete(self, path, data={}, **kwargs): """Submit delete request to the path. - :arg - path (str): Path for request. - data (dict): Payload for request. - :return - Response the request. - :exception - HttpError: Can't connect to server. + :param path: (str) Path for request. + :param data: (dict) Payload for request. + :returns: Response the request. + :raises: HttpError Can't connect to server. """ try: return self._s.delete( diff --git a/keycloak/keycloak_openid.py b/keycloak/keycloak_openid.py index d9c068fa..4205b0b5 100644 --- a/keycloak/keycloak_openid.py +++ b/keycloak/keycloak_openid.py @@ -49,6 +49,18 @@ class KeycloakOpenID: + """ + Keycloak OpenID client. + + :param server_url: Keycloak server url + :param client_id: client id + :param realm_name: realm name + :param client_secret_key: client secret key + :param verify: True if want check connection SSL + :param custom_headers: dict of custom header to pass to each HTML request + :param proxies: dict of proxies to sent the request by. + """ + def __init__( self, server_url, @@ -59,28 +71,15 @@ def __init__( custom_headers=None, proxies=None, ): - """ - - :param server_url: Keycloak server url - :param client_id: client id - :param realm_name: realm name - :param client_secret_key: client secret key - :param verify: True if want check connection SSL - :param custom_headers: dict of custom header to pass to each HTML request - :param proxies: dict of proxies to sent the request by. - """ - self._client_id = client_id - self._client_secret_key = client_secret_key - self._realm_name = realm_name - headers = dict() - if custom_headers is not None: - # merge custom headers to main headers - headers.update(custom_headers) - self._connection = ConnectionManager( + self.client_id = client_id + self.client_secret_key = client_secret_key + self.realm_name = realm_name + headers = custom_headers if custom_headers is not None else dict() + self.connection = ConnectionManager( base_url=server_url, headers=headers, timeout=60, verify=verify, proxies=proxies ) - self._authorization = Authorization() + self.authorization = Authorization() @property def client_id(self): @@ -206,8 +205,8 @@ def token( :param password: :param grant_type: :param code: - :param redirect_uri - :param totp + :param redirect_uri: + :param totp: :return: """ params_path = {"realm-name": self.realm_name} @@ -312,9 +311,9 @@ def entitlement(self, token, resource_server_id): """ Client applications can use a specific endpoint to obtain a special security token called a requesting party token (RPT). This token consists of all the entitlements - (or permissions) for a user as a result of the evaluation of the permissions and authorization - policies associated with the resources being requested. With an RPT, client applications can - gain access to protected resources at the resource server. + (or permissions) for a user as a result of the evaluation of the permissions and + authorization policies associated with the resources being requested. With an RPT, + client applications can gain access to protected resources at the resource server. :return: """ @@ -329,8 +328,8 @@ def entitlement(self, token, resource_server_id): def introspect(self, token, rpt=None, token_type_hint=None): """ - The introspection endpoint is used to retrieve the active state of a token. It is can only be - invoked by confidential clients. + The introspection endpoint is used to retrieve the active state of a token. + It is can only be invoked by confidential clients. https://tools.ietf.org/html/rfc7662 From b911d94db9b44257cb8535457062da33525f7f11 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:41:38 +0200 Subject: [PATCH 148/566] feat: fixed admin client to pass the tests --- keycloak/_version.py | 23 +++++ keycloak/keycloak_admin.py | 166 ++++++++++++++++++++++++++++--------- keycloak/urls_patterns.py | 8 +- 3 files changed, 155 insertions(+), 42 deletions(-) diff --git a/keycloak/_version.py b/keycloak/_version.py index 6c8e6b97..f3403b2c 100644 --- a/keycloak/_version.py +++ b/keycloak/_version.py @@ -1 +1,24 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (C) 2017 Marcos Pereira +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + __version__ = "0.0.0" diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 3e30722d..8d6463b0 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -61,6 +61,7 @@ URL_ADMIN_CLIENT_SCOPES_MAPPERS, URL_ADMIN_CLIENT_SECRETS, URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, + URL_ADMIN_CLIENT_ROLE_GROUPS, URL_ADMIN_CLIENTS, URL_ADMIN_COMPONENT, URL_ADMIN_COMPONENTS, @@ -68,7 +69,6 @@ URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, - URL_ADMIN_DELETE_USER_ROLE, URL_ADMIN_EVENTS, URL_ADMIN_FLOW, URL_ADMIN_FLOWS, @@ -123,6 +123,23 @@ class KeycloakAdmin: + """ + Keycloak Admin client. + + :param server_url: Keycloak server url + :param username: admin username + :param password: admin password + :param totp: Time based OTP + :param realm_name: realm name + :param client_id: client id + :param verify: True if want check connection SSL + :param client_secret_key: client secret key + (optional, required only for access type confidential) + :param custom_headers: dict of custom header to pass to each HTML request + :param user_realm_name: The realm name of the user, if different from realm_name + :param auto_refresh_token: list of methods that allows automatic token refresh. + Ex: ['get', 'put', 'post', 'delete'] + """ PAGE_SIZE = 100 @@ -154,20 +171,6 @@ def __init__( user_realm_name=None, auto_refresh_token=None, ): - """ - - :param server_url: Keycloak server url - :param username: admin username - :param password: admin password - :param totp: Time based OTP - :param realm_name: realm name - :param client_id: client id - :param verify: True if want check connection SSL - :param client_secret_key: client secret key (optional, required only for access type confidential) - :param custom_headers: dict of custom header to pass to each HTML request - :param user_realm_name: The realm name of the user, if different from realm_name - :param auto_refresh_token: list of methods that allows automatic token refresh. ex: ['get', 'put', 'post', 'delete'] - """ self.server_url = server_url self.username = username self.password = password @@ -378,6 +381,20 @@ def get_realms(self): data_raw = self.raw_get(URL_ADMIN_REALMS) return raise_error_from_response(data_raw, KeycloakGetError) + def get_realm(self, realm_name): + """ + Get a specific realm. + + RealmRepresentation: + https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + + :param realm_name: Realm name (not the realm id) + :return: RealmRepresentation + """ + params_path = {"realm-name": realm_name} + data_raw = self.raw_get(URL_ADMIN_REALM.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def create_realm(self, payload, skip_exists=False): """ Create a realm @@ -495,7 +512,7 @@ def delete_idp(self, idp_alias): data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) - def create_user(self, payload, exist_ok=True): + def create_user(self, payload, exist_ok=False): """ Create a new user. Username must be unique @@ -530,6 +547,33 @@ def users_count(self): data_raw = self.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def user_logout(self, user_id): + """ + Logs out user. + + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout + + :param user_id: User id + :return: + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) + + def user_consents(self, user_id): + """ + Get consents granted by the user + + UserConsentRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation + + :param user_id: User id + :return: List of UserConsentRepresentations + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_user_id(self, username): """ Get internal keycloak user id from username @@ -923,7 +967,7 @@ def create_group(self, payload, parent=None, skip_exists=False): GroupRepresentation https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation - :return: Http response + :return: Group ID for newly created group, otherwise None """ if parent is None: @@ -937,9 +981,14 @@ def create_group(self, payload, parent=None, skip_exists=False): URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + try: + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 + except KeyError: + return def update_group(self, group_id, payload): """ @@ -1229,18 +1278,27 @@ def create_client(self, payload, skip_exists=False): """ Create a client - ClientRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + ClientRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param skip_exists: If true then do not raise an error if client already exists :param payload: ClientRepresentation - :return: Keycloak server response (UserRepresentation) + :return: Client ID """ + if skip_exists: + client_id = self.get_client_id(client_name=payload["name"]) + + if client_id is not None: + return client_id + params_path = {"realm-name": self.realm_name} data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client(self, client_id, payload): """ @@ -1368,21 +1426,46 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): Create a client role RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) :param payload: RoleRepresentation :param skip_exists: If true then do not raise an error if client role already exists - :return: Keycloak server response (RoleRepresentation) + :return: Client role name """ + if skip_exists: + res = self.get_client_role(client_id=client_role_id, role_name=payload["name"]) + if res: + return res["name"] + params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post( URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 + + def update_client_role(self, client_role_id, role_name, payload): + """ + Update a client role + + RoleRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + + :param client_role_id: id of client (not client-id) + :param role_name: role's name (not id!) + :param payload: RoleRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} + data_raw = self.raw_put( + URL_ADMIN_CLIENT_ROLE.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): """ @@ -1444,22 +1527,41 @@ def get_client_role_members(self, client_id, role_name, **query): params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query) + def get_client_role_groups(self, client_id, role_name, **query): + """ + Get group members by client role . + :param client_id: The client id + :param role_name: the name of role to be queried. + :param query: Additional query parameters + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} + return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_GROUPS.format(**params_path), query) + def create_realm_role(self, payload, skip_exists=False): """ Create a new role for the realm or client :param payload: The role (use RoleRepresentation) :param skip_exists: If true then do not raise an error if realm role already exists - :return Keycloak server response + :return: Realm role name """ + if skip_exists: + role = self.get_realm_role(role_name=payload["name"]) + if role is not None: + return role["name"] + params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def get_realm_role(self, role_name): """ @@ -2506,18 +2608,6 @@ def get_client_all_sessions(self, client_id): data_raw = self.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def delete_user_realm_role(self, user_id, payload): - """ - Delete realm-level role mappings - DELETE admin/realms/{realm-name}/users/{id}/role-mappings/realm - - """ - params_path = {"realm-name": self.realm_name, "id": str(user_id)} - data_raw = self.raw_delete( - URL_ADMIN_DELETE_USER_ROLE.format(**params_path), data=json.dumps(payload) - ) - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) - def get_client_sessions_stats(self): """ Get current session count for all clients with active sessions diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 43699eb5..34d85140 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -65,7 +65,6 @@ ) URL_ADMIN_USER_GROUP = "admin/realms/{realm-name}/users/{id}/groups/{group-id}" URL_ADMIN_USER_GROUPS = "admin/realms/{realm-name}/users/{id}/groups" -URL_ADMIN_USER_PASSWORD = "admin/realms/{realm-name}/users/{id}/reset-password" URL_ADMIN_USER_CREDENTIALS = "admin/realms/{realm-name}/users/{id}/credentials" URL_ADMIN_USER_CREDENTIAL = "admin/realms/{realm-name}/users/{id}/credentials/{credential_id}" URL_ADMIN_USER_LOGOUT = "admin/realms/{realm-name}/users/{id}/logout" @@ -87,12 +86,15 @@ URL_ADMIN_CLIENT_ROLE = URL_ADMIN_CLIENT + "/roles/{role-name}" URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composites" URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" +URL_ADMIN_CLIENT_ROLE_GROUPS = URL_ADMIN_CLIENT + "/roles/{role-name}/groups" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" URL_ADMIN_CLIENT_AUTHZ_SCOPES = URL_ADMIN_CLIENT + "/authz/resource-server/scope?max=-1" URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS = URL_ADMIN_CLIENT + "/authz/resource-server/permission?max=-1" -URL_ADMIN_CLIENT_AUTHZ_POLICIES = URL_ADMIN_CLIENT + "/authz/resource-server/policy?max=-1" +URL_ADMIN_CLIENT_AUTHZ_POLICIES = ( + URL_ADMIN_CLIENT + "/authz/resource-server/policy?max=-1&permission=false" +) URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY = ( URL_ADMIN_CLIENT + "/authz/resource-server/policy/role?max=-1" ) @@ -155,6 +157,4 @@ ) URL_ADMIN_EVENTS = "admin/realms/{realm-name}/events" - -URL_ADMIN_DELETE_USER_ROLE = "admin/realms/{realm-name}/users/{id}/role-mappings/realm" URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" From 6cce29f26b2f0735937f3f6b247dcb6538a3bc05 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:51:07 +0200 Subject: [PATCH 149/566] fix: full tox fix ready --- keycloak/keycloak_admin.py | 826 ++++++++++++++++++------------------- keycloak/urls_patterns.py | 5 +- 2 files changed, 412 insertions(+), 419 deletions(-) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 8d6463b0..7b92d802 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -28,6 +28,7 @@ from builtins import isinstance from typing import Iterable +from . import urls_patterns from .connection import ConnectionManager from .exceptions import ( KeycloakDeleteError, @@ -37,89 +38,6 @@ raise_error_from_response, ) from .keycloak_openid import KeycloakOpenID -from .urls_patterns import ( - URL_ADMIN_AUTHENTICATOR_CONFIG, - URL_ADMIN_CLIENT, - URL_ADMIN_CLIENT_ALL_SESSIONS, - URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS, - URL_ADMIN_CLIENT_AUTHZ_POLICIES, - URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION, - URL_ADMIN_CLIENT_AUTHZ_RESOURCES, - URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY, - URL_ADMIN_CLIENT_AUTHZ_SCOPES, - URL_ADMIN_CLIENT_AUTHZ_SETTINGS, - URL_ADMIN_CLIENT_INSTALLATION_PROVIDER, - URL_ADMIN_CLIENT_PROTOCOL_MAPPER, - URL_ADMIN_CLIENT_PROTOCOL_MAPPERS, - URL_ADMIN_CLIENT_ROLE, - URL_ADMIN_CLIENT_ROLE_MEMBERS, - URL_ADMIN_CLIENT_ROLES, - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE, - URL_ADMIN_CLIENT_SCOPE, - URL_ADMIN_CLIENT_SCOPES, - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER, - URL_ADMIN_CLIENT_SCOPES_MAPPERS, - URL_ADMIN_CLIENT_SECRETS, - URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER, - URL_ADMIN_CLIENT_ROLE_GROUPS, - URL_ADMIN_CLIENTS, - URL_ADMIN_COMPONENT, - URL_ADMIN_COMPONENTS, - URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE, - URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES, - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE, - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES, - URL_ADMIN_EVENTS, - URL_ADMIN_FLOW, - URL_ADMIN_FLOWS, - URL_ADMIN_FLOWS_ALIAS, - URL_ADMIN_FLOWS_COPY, - URL_ADMIN_FLOWS_EXECUTION, - URL_ADMIN_FLOWS_EXECUTIONS, - URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION, - URL_ADMIN_FLOWS_EXECUTIONS_FLOW, - URL_ADMIN_GET_SESSIONS, - URL_ADMIN_GROUP, - URL_ADMIN_GROUP_CHILD, - URL_ADMIN_GROUP_MEMBERS, - URL_ADMIN_GROUP_PERMISSIONS, - URL_ADMIN_GROUPS, - URL_ADMIN_GROUPS_CLIENT_ROLES, - URL_ADMIN_GROUPS_REALM_ROLES, - URL_ADMIN_IDP, - URL_ADMIN_IDP_MAPPERS, - URL_ADMIN_IDPS, - URL_ADMIN_KEYS, - URL_ADMIN_REALM, - URL_ADMIN_REALM_EXPORT, - URL_ADMIN_REALM_ROLES, - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE, - URL_ADMIN_REALM_ROLES_MEMBERS, - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME, - URL_ADMIN_REALMS, - URL_ADMIN_RESET_PASSWORD, - URL_ADMIN_SEND_UPDATE_ACCOUNT, - URL_ADMIN_SEND_VERIFY_EMAIL, - URL_ADMIN_SERVER_INFO, - URL_ADMIN_USER, - URL_ADMIN_USER_CLIENT_ROLES, - URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, - URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, - URL_ADMIN_USER_CONSENTS, - URL_ADMIN_USER_CREDENTIAL, - URL_ADMIN_USER_CREDENTIALS, - URL_ADMIN_USER_FEDERATED_IDENTITIES, - URL_ADMIN_USER_FEDERATED_IDENTITY, - URL_ADMIN_USER_GROUP, - URL_ADMIN_USER_GROUPS, - URL_ADMIN_USER_LOGOUT, - URL_ADMIN_USER_REALM_ROLES, - URL_ADMIN_USER_REALM_ROLES_AVAILABLE, - URL_ADMIN_USER_REALM_ROLES_COMPOSITE, - URL_ADMIN_USER_STORAGE, - URL_ADMIN_USERS, - URL_ADMIN_USERS_COUNT, -) class KeycloakAdmin: @@ -342,14 +260,14 @@ def import_realm(self, payload): Import a new realm from a RealmRepresentation. Realm name must be unique. RealmRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation :return: RealmRepresentation """ - data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) + data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def export_realm(self, export_clients=False, export_groups_and_role=False): @@ -357,7 +275,7 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): Export the realm configurations in the json format RealmRepresentation - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_partialexport + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_partialexport :param export-clients: Skip if not want to export realm clients :param export-groups-and-roles: Skip if not want to export realm groups and roles @@ -369,7 +287,9 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): "export-clients": export_clients, "export-groups-and-roles": export_groups_and_role, } - data_raw = self.raw_post(URL_ADMIN_REALM_EXPORT.format(**params_path), data="") + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_REALM_EXPORT.format(**params_path), data="" + ) return raise_error_from_response(data_raw, KeycloakPostError) def get_realms(self): @@ -378,7 +298,7 @@ def get_realms(self): :return: realms list """ - data_raw = self.raw_get(URL_ADMIN_REALMS) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALMS) return raise_error_from_response(data_raw, KeycloakGetError) def get_realm(self, realm_name): @@ -392,7 +312,7 @@ def get_realm(self, realm_name): :return: RealmRepresentation """ params_path = {"realm-name": realm_name} - data_raw = self.raw_get(URL_ADMIN_REALM.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def create_realm(self, payload, skip_exists=False): @@ -400,14 +320,14 @@ def create_realm(self, payload, skip_exists=False): Create a realm RealmRepresentation: - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation :param skip_exists: Skip if Realm already exist. :return: Keycloak server response (RealmRepresentation) """ - data_raw = self.raw_post(URL_ADMIN_REALMS, data=json.dumps(payload)) + data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) @@ -418,7 +338,7 @@ def update_realm(self, realm_name, payload): role, or client information in the payload. RealmRepresentation: - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param realm_name: Realm name (not the realm id) :param payload: RealmRepresentation @@ -426,7 +346,9 @@ def update_realm(self, realm_name, payload): """ params_path = {"realm-name": realm_name} - data_raw = self.raw_put(URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm(self, realm_name): @@ -438,7 +360,7 @@ def delete_realm(self, realm_name): """ params_path = {"realm-name": realm_name} - data_raw = self.raw_delete(URL_ADMIN_REALM.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_users(self, query=None): @@ -446,14 +368,14 @@ def get_users(self, query=None): Return a list of users, filtered according to query parameters UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param query: Query parameters (optional) :return: users list """ query = query or {} params_path = {"realm-name": self.realm_name} - url = URL_ADMIN_USERS.format(**params_path) + url = urls_patterns.URL_ADMIN_USERS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) @@ -465,12 +387,14 @@ def create_idp(self, payload): Create an ID Provider, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation :param: payload: IdentityProviderRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_IDPS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def add_mapper_to_idp(self, idp_alias, payload): @@ -478,14 +402,14 @@ def add_mapper_to_idp(self, idp_alias, payload): Create an ID Provider, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityprovidermapperrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityprovidermapperrepresentation :param: idp_alias: alias for Idp to add mapper in :param: payload: IdentityProviderMapperRepresentation """ params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} data_raw = self.raw_post( - URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_IDP_MAPPERS.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -494,12 +418,12 @@ def get_idps(self): Returns a list of ID Providers, IdentityProviderRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_identityproviderrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation :return: array IdentityProviderRepresentation """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_IDPS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_IDPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_idp(self, idp_alias): @@ -509,7 +433,7 @@ def delete_idp(self, idp_alias): :param: idp_alias: idp alias name """ params_path = {"realm-name": self.realm_name, "alias": idp_alias} - data_raw = self.raw_delete(URL_ADMIN_IDP.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_IDP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_user(self, payload, exist_ok=False): @@ -517,10 +441,11 @@ def create_user(self, payload, exist_ok=False): Create a new user. Username must be unique UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param payload: UserRepresentation - :param exist_ok: If False, raise KeycloakGetError if username already exists. Otherwise, return existing user ID. + :param exist_ok: If False, raise KeycloakGetError if username already exists. + Otherwise, return existing user ID. :return: UserRepresentation """ @@ -532,10 +457,12 @@ def create_user(self, payload, exist_ok=False): if exists is not None: return str(exists) - data_raw = self.raw_post(URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_USERS.format(**params_path), data=json.dumps(payload) + ) raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) _last_slash_idx = data_raw.headers["Location"].rindex("/") - return data_raw.headers["Location"][_last_slash_idx + 1 :] + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def users_count(self): """ @@ -544,34 +471,7 @@ def users_count(self): :return: counter """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_USERS_COUNT.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) - - def user_logout(self, user_id): - """ - Logs out user. - - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout - - :param user_id: User id - :return: - """ - params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") - return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) - - def user_consents(self, user_id): - """ - Get consents granted by the user - - UserConsentRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation - - :param user_id: User id - :return: List of UserConsentRepresentations - """ - params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USERS_COUNT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_id(self, username): @@ -580,7 +480,7 @@ def get_user_id(self, username): This is required for further actions against this user. UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param username: id in UserRepresentation @@ -597,12 +497,12 @@ def get_user(self, user_id): :param user_id: User id UserRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :return: UserRepresentation """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_groups(self, user_id): @@ -614,7 +514,7 @@ def get_user_groups(self, user_id): :return: user groups list """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_GROUPS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_GROUPS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_user(self, user_id, payload): @@ -627,7 +527,9 @@ def update_user(self, user_id, payload): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_put(URL_ADMIN_USER.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_USER.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_user(self, user_id): @@ -639,7 +541,7 @@ def delete_user(self, user_id): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_delete(URL_ADMIN_USER.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): @@ -647,8 +549,8 @@ def set_user_password(self, user_id, password, temporary=True): Set up a password for the user. If temporary is True, the user will have to reset the temporary password next time they log in. - https://www.keycloak.org/docs-api/8.0/rest-api/#_users_resource - https://www.keycloak.org/docs-api/8.0/rest-api/#_credentialrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_users_resource + https://www.keycloak.org/docs-api/18.0/rest-api/#_credentialrepresentation :param user_id: User id :param password: New password @@ -659,7 +561,7 @@ def set_user_password(self, user_id, password, temporary=True): payload = {"type": "password", "temporary": temporary, "value": password} params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put( - URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_RESET_PASSWORD.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -668,32 +570,13 @@ def get_credentials(self, user_id): Returns a list of credential belonging to the user. CredentialRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation :param: user_id: user id :return: Keycloak server response (CredentialRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIALS.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) - - def get_credential(self, user_id, credential_id): - """ - Get credential of the user. - - CredentialRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation - - :param: user_id: user id - :param: credential_id: credential id - :return: Keycloak server response (ClientRepresentation) - """ - params_path = { - "realm-name": self.realm_name, - "id": user_id, - "credential_id": credential_id, - } - data_raw = self.raw_get(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CREDENTIALS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_credential(self, user_id, credential_id): @@ -701,7 +584,7 @@ def delete_credential(self, user_id, credential_id): Delete credential of the user. CredentialRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_credentialrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation :param: user_id: user id :param: credential_id: credential id @@ -712,42 +595,49 @@ def delete_credential(self, user_id, credential_id): "id": user_id, "credential_id": credential_id, } - data_raw = self.raw_delete(URL_ADMIN_USER_CREDENTIAL.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER_CREDENTIAL.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError) - def logout(self, user_id): + def user_logout(self, user_id): """ Logs out user. - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_logout + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout :param user_id: User id :return: """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_post(URL_ADMIN_USER_LOGOUT.format(**params_path), data="") + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_USER_LOGOUT.format(**params_path), data="" + ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) - def consents_user(self, user_id): + def user_consents(self, user_id): """ Get consents granted by the user - :param user_id: User id + UserConsentRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation - :return: consents + :param user_id: User id + :return: List of UserConsentRepresentations """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_CONSENTS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CONSENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_social_logins(self, user_id): """ - Returns a list of federated identities/social logins of which the user has been associated with + Returns a list of federated identities/social logins of which the user has been associated + with :param user_id: User id :return: federated identities list """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_FEDERATED_IDENTITIES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_FEDERATED_IDENTITIES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def add_user_social_login(self, user_id, provider_id, provider_userid, provider_username): @@ -767,9 +657,10 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ } params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_post( - URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path), + data=json.dumps(payload), ) - return raise_error_from_response(data_raw, KeycloakPostError) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201, 204]) def delete_user_social_login(self, user_id, provider_id): @@ -780,7 +671,9 @@ def delete_user_social_login(self, user_id, provider_id): :return: """ params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} - data_raw = self.raw_delete(URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_USER_FEDERATED_IDENTITY.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def send_update_account( @@ -801,7 +694,7 @@ def send_update_account( params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} data_raw = self.raw_put( - URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), + urls_patterns.URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), data=json.dumps(payload), **params_query ) @@ -821,7 +714,9 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "redirect_uri": redirect_uri} data_raw = self.raw_put( - URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, **params_query + urls_patterns.URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), + data={}, + **params_query ) return raise_error_from_response(data_raw, KeycloakPutError) @@ -832,12 +727,12 @@ def get_sessions(self, user_id): :param user_id: id of user UserSessionRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_usersessionrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_usersessionrepresentation :return: UserSessionRepresentation """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_GET_SESSIONS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GET_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_server_info(self): @@ -845,11 +740,11 @@ def get_server_info(self): Get themes, social providers, auth providers, and event listeners available on this server ServerInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_serverinforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation :return: ServerInfoRepresentation """ - data_raw = self.raw_get(URL_ADMIN_SERVER_INFO) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_SERVER_INFO) return raise_error_from_response(data_raw, KeycloakGetError) def get_groups(self, query=None): @@ -857,13 +752,13 @@ def get_groups(self, query=None): Returns a list of groups belonging to the realm GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :return: array GroupRepresentation """ query = query or {} params_path = {"realm-name": self.realm_name} - url = URL_ADMIN_GROUPS.format(**params_path) + url = urls_patterns.URL_ADMIN_GROUPS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) @@ -875,13 +770,13 @@ def get_group(self, group_id): Get group by id. Returns full group details GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param group_id: The group id :return: Keycloak server response (GroupRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_get(URL_ADMIN_GROUP.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_subgroups(self, group, path): @@ -889,7 +784,7 @@ def get_subgroups(self, group, path): Utility function to iterate through nested group structures GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param name: group (GroupRepresentation) :param path: group path (string) @@ -908,19 +803,21 @@ def get_subgroups(self, group, path): # went through the tree without hits return None - def get_group_members(self, group_id, **query): + def get_group_members(self, group_id, query=None): """ Get members by group id. Returns group members GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_userrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_userrepresentation :param group_id: The group id - :param query: Additional query parameters (see https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getmembers) + :param query: Additional query parameters + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getmembers) :return: Keycloak server response (UserRepresentation) """ + query = query or {} params_path = {"realm-name": self.realm_name, "id": group_id} - url = URL_ADMIN_GROUP_MEMBERS.format(**params_path) + url = urls_patterns.URL_ADMIN_GROUP_MEMBERS.format(**params_path) if "first" in query or "max" in query: return self.__fetch_paginated(url, query) @@ -934,7 +831,7 @@ def get_group_by_path(self, path, search_in_subgroups=False): Subgroups are traversed, the first to match path (or name with path) is returned. GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param path: group path :param search_in_subgroups: True if want search in the subgroups @@ -952,7 +849,7 @@ def get_group_by_path(self, path, search_in_subgroups=False): if group["path"] == path: return group res = self.get_subgroups(group, path) - if res != None: + if res is not None: return res return None @@ -965,20 +862,20 @@ def create_group(self, payload, parent=None, skip_exists=False): :param skip_exists: If true then do not raise an error if it already exists GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation - :return: Group ID for newly created group, otherwise None + :return: Group id for newly created group or None for an existing group """ if parent is None: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( - URL_ADMIN_GROUPS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS.format(**params_path), data=json.dumps(payload) ) else: params_path = {"realm-name": self.realm_name, "id": parent} data_raw = self.raw_post( - URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUP_CHILD.format(**params_path), data=json.dumps(payload) ) raise_error_from_response( @@ -998,13 +895,15 @@ def update_group(self, group_id, payload): :param payload: GroupRepresentation with updated information. GroupRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/#_grouprepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :return: Http response """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_put(URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): @@ -1018,7 +917,7 @@ def group_set_permissions(self, group_id, enabled=True): params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( - URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), + urls_patterns.URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), data=json.dumps({"enabled": enabled}), ) return raise_error_from_response(data_raw, KeycloakPutError) @@ -1033,7 +932,9 @@ def group_user_add(self, user_id, group_id): """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} - data_raw = self.raw_put(URL_ADMIN_USER_GROUP.format(**params_path), data=None) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path), data=None + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): @@ -1046,7 +947,7 @@ def group_user_remove(self, user_id, group_id): """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} - data_raw = self.raw_delete(URL_ADMIN_USER_GROUP.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def delete_group(self, group_id): @@ -1058,7 +959,7 @@ def delete_group(self, group_id): """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_delete(URL_ADMIN_GROUP.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_clients(self): @@ -1066,13 +967,13 @@ def get_clients(self): Returns a list of clients belonging to the realm ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response (ClientRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_CLIENTS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client(self, client_id): @@ -1080,14 +981,14 @@ def get_client(self, client_id): Get representation of the client ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_id(self, client_name): @@ -1096,7 +997,7 @@ def get_client_id(self, client_name): This is required for further actions against this client. :param client_name: name in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ @@ -1113,12 +1014,14 @@ def get_client_authz_settings(self, client_id): Get authorization json from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_resource(self, client_id, payload, skip_exists=False): @@ -1126,9 +1029,9 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): Create resources of client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param payload: ResourceRepresentation - https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_resourcerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_resourcerepresentation :return: Keycloak server response """ @@ -1136,7 +1039,8 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1147,12 +1051,14 @@ def get_client_authz_resources(self, client_id): Get resources from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_role_based_policy(self, client_id, payload, skip_exists=False): @@ -1160,28 +1066,30 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= Create role-based policy of client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param payload: No Document - payload example: - payload={ - "type": "role", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "name": "Policy-1", - "roles": [ - { - "id": id - } - ] - } - :return: Keycloak server response + + Payload example:: + + payload={ + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Policy-1", + "roles": [ + { + "id": id + } + ] + } + """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_ROLE_BASED_POLICY.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response( @@ -1193,29 +1101,31 @@ def create_client_authz_resource_based_permission(self, client_id, payload, skip Create resource-based permission of client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param payload: PolicyRepresentation - https://www.keycloak.org/docs-api/12.0/rest-api/index.html#_policyrepresentation - payload example: - payload={ - "type": "resource", - "logic": "POSITIVE", - "decisionStrategy": "UNANIMOUS", - "name": "Permission-Name", - "resources": [ - resource_id - ], - "policies": [ - policy_id - ] - + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_policyrepresentation :return: Keycloak server response + + Payload example:: + + payload={ + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Permission-Name", + "resources": [ + resource_id + ], + "policies": [ + policy_id + ] + """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response( @@ -1227,12 +1137,12 @@ def get_client_authz_scopes(self, client_id): Get scopes from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_permissions(self, client_id): @@ -1240,12 +1150,14 @@ def get_client_authz_permissions(self, client_id): Get permissions from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policies(self, client_id): @@ -1253,12 +1165,14 @@ def get_client_authz_policies(self, client_id): Get policies from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): @@ -1266,12 +1180,14 @@ def get_client_service_account_user(self, client_id): Get service account user from client. :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: UserRepresentation """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_client(self, payload, skip_exists=False): @@ -1293,7 +1209,9 @@ def create_client(self, payload, skip_exists=False): return client_id params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENTS.format(**params_path), data=json.dumps(payload) + ) raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) @@ -1310,7 +1228,9 @@ def update_client(self, client_id, payload): :return: Http response """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_put(URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client(self, client_id): @@ -1318,14 +1238,14 @@ def delete_client(self, client_id): Get representation of the client ClientRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_clientrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param client_id: keycloak client id (not oauth client-id) :return: Keycloak server response (ClientRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_delete(URL_ADMIN_CLIENT.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_installation_provider(self, client_id, provider_id): @@ -1333,17 +1253,19 @@ def get_client_installation_provider(self, client_id, provider_id): Get content for given installation provider Related documentation: - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_clients_resource + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource Possible provider_id list available in the ServerInfoRepresentation#clientInstallations - https://www.keycloak.org/docs-api/5.0/rest-api/index.html#_serverinforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation :param client_id: Client id :param provider_id: provider id to specify response format """ params_path = {"realm-name": self.realm_name, "id": client_id, "provider-id": provider_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def get_realm_roles(self): @@ -1351,24 +1273,28 @@ def get_realm_roles(self): Get all roles for the realm or client RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_REALM_ROLES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_realm_role_members(self, role_name, **query): + def get_realm_role_members(self, role_name, query=None): """ Get role members of realm by role name. :param role_name: Name of the role. - :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_roles_resource) + :param query: Additional Query parameters + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) :return: Keycloak Server Response (UserRepresentation) """ + query = query or dict() params_path = {"realm-name": self.realm_name, "role-name": role_name} - return self.__fetch_all(URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query) + return self.__fetch_all( + urls_patterns.URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query + ) def get_client_roles(self, client_id): """ @@ -1377,13 +1303,13 @@ def get_client_roles(self, client_id): :param client_id: id of client (not client-id) RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role(self, client_id, role_name): @@ -1395,12 +1321,12 @@ def get_client_role(self, client_id, role_name): :param role_name: role’s name (not id!) RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: role_id """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} - data_raw = self.raw_get(URL_ADMIN_CLIENT_ROLE.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role_id(self, client_id, role_name): @@ -1414,7 +1340,7 @@ def get_client_role_id(self, client_id, role_name): :param role_name: role’s name (not id!) RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: role_id """ @@ -1441,7 +1367,7 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) ) raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1449,24 +1375,6 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): _last_slash_idx = data_raw.headers["Location"].rindex("/") return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 - def update_client_role(self, client_role_id, role_name, payload): - """ - Update a client role - - RoleRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - - :param client_role_id: id of client (not client-id) - :param role_name: role's name (not id!) - :param payload: RoleRepresentation - """ - params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} - data_raw = self.raw_put( - URL_ADMIN_CLIENT_ROLE.format(**params_path), - data=json.dumps(payload), - ) - return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) - def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): """ Add composite roles to client role @@ -1474,29 +1382,46 @@ def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): :param client_role_id: id of client (not client-id) :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be updated - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_post( - URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) + def update_client_role(self, client_role_id, role_name, payload): + """ + Update a client role + + RoleRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + + :param client_role_id: id of client (not client-id) + :param role_name: role's name (not id!) + :param payload: RoleRepresentation + """ + params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + def delete_client_role(self, client_role_id, role_name): """ Delete a client role RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) - :param role_name: role’s name (not id!) + :param role_name: role's name (not id!) """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} - data_raw = self.raw_delete(URL_ADMIN_CLIENT_ROLE.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): @@ -1506,13 +1431,14 @@ def assign_client_role(self, user_id, client_id, roles): :param user_id: id of user :param client_id: id of client (not client-id) :param roles: roles list or role (use RoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_post( - URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1521,11 +1447,14 @@ def get_client_role_members(self, client_id, role_name, **query): Get members by client role . :param client_id: The client id :param role_name: the name of role to be queried. - :param query: Additional query parameters ( see https://www.keycloak.org/docs-api/11.0/rest-api/index.html#_clients_resource) + :param query: Additional query parameters + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} - return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query) + return self.__fetch_all( + urls_patterns.URL_ADMIN_CLIENT_ROLE_MEMBERS.format(**params_path), query + ) def get_client_role_groups(self, client_id, role_name, **query): """ @@ -1537,7 +1466,9 @@ def get_client_role_groups(self, client_id, role_name, **query): :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} - return self.__fetch_all(URL_ADMIN_CLIENT_ROLE_GROUPS.format(**params_path), query) + return self.__fetch_all( + urls_patterns.URL_ADMIN_CLIENT_ROLE_GROUPS.format(**params_path), query + ) def create_realm_role(self, payload, skip_exists=False): """ @@ -1555,7 +1486,7 @@ def create_realm_role(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( - URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), data=json.dumps(payload) ) raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -1569,11 +1500,13 @@ def get_realm_role(self, role_name): :param role_name: role's name, not id! RoleRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_rolerepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: role_id """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def update_realm_role(self, role_name, payload): @@ -1586,7 +1519,8 @@ def update_realm_role(self, role_name, payload): params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_put( - URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -1598,7 +1532,9 @@ def delete_realm_role(self, role_name): """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_delete(URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): @@ -1607,13 +1543,13 @@ def add_composite_realm_roles_to_role(self, role_name, roles): :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be updated - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_post( - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1624,13 +1560,13 @@ def remove_composite_realm_roles_to_role(self, role_name, roles): :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be removed - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( - URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), + urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path), data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1640,11 +1576,13 @@ def get_composite_realm_roles_of_role(self, role_name): Get composite roles of the role :param role_name: The name of the role - :return Keycloak server response (array RoleRepresentation) + :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "role-name": role_name} - data_raw = self.raw_get(URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def assign_realm_roles(self, user_id, roles): @@ -1653,13 +1591,14 @@ def assign_realm_roles(self, user_id, roles): :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post( - URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1669,13 +1608,14 @@ def delete_realm_roles_of_user(self, user_id, roles): :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete( - URL_ADMIN_USER_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1688,7 +1628,7 @@ def get_realm_roles_of_user(self, user_id): """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_available_realm_roles_of_user(self, user_id): @@ -1698,7 +1638,9 @@ def get_available_realm_roles_of_user(self, user_id): :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES_AVAILABLE.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_REALM_ROLES_AVAILABLE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_composite_realm_roles_of_user(self, user_id): @@ -1708,7 +1650,9 @@ def get_composite_realm_roles_of_user(self, user_id): :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_realm_roles(self, group_id, roles): @@ -1717,13 +1661,14 @@ def assign_group_realm_roles(self, group_id, roles): :param group_id: id of groupp :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_post( - URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1733,13 +1678,14 @@ def delete_group_realm_roles(self, group_id, roles): :param group_id: id of group :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete( - URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1751,7 +1697,7 @@ def get_group_realm_roles(self, group_id): :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_get(URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_client_roles(self, group_id, client_id, roles): @@ -1761,13 +1707,14 @@ def assign_group_client_roles(self, group_id, client_id, roles): :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_post( - URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) @@ -1777,11 +1724,11 @@ def get_group_client_roles(self, group_id, client_id): :param group_id: id of group :param client_id: id of client (not client-id) - :return Keycloak server response + :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} - data_raw = self.raw_get(URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_group_client_roles(self, group_id, client_id, roles): @@ -1791,13 +1738,14 @@ def delete_group_client_roles(self, group_id, client_id, roles): :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) - :return Keycloak server response (array RoleRepresentation) + :return: Keycloak server response (array RoleRepresentation) """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_delete( - URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1809,7 +1757,9 @@ def get_client_roles_of_user(self, user_id, client_id): :param client_id: id of client (not client-id) :return: Keycloak server response (array RoleRepresentation) """ - return self._get_client_roles_of_user(URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id) + return self._get_client_roles_of_user( + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id + ) def get_available_client_roles_of_user(self, user_id, client_id): """ @@ -1820,7 +1770,7 @@ def get_available_client_roles_of_user(self, user_id, client_id): :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user( - URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id ) def get_composite_client_roles_of_user(self, user_id, client_id): @@ -1832,7 +1782,7 @@ def get_composite_client_roles_of_user(self, user_id, client_id): :return: Keycloak server response (array RoleRepresentation) """ return self._get_client_roles_of_user( - URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id ) def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): @@ -1852,7 +1802,8 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_delete( - URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) @@ -1861,26 +1812,26 @@ def get_authentication_flows(self): Get authentication flows. Returns all flow details AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :return: Keycloak server response (AuthenticationFlowRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_FLOWS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_authentication_flow_for_id(self, flow_id): """ - Get one authentication flow by it's id/alias. Returns all flow details + Get one authentication flow by it's id. Returns all flow details AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param flow_id: the id of a flow NOT it's alias :return: Keycloak server response (AuthenticationFlowRepresentation) """ params_path = {"realm-name": self.realm_name, "flow-id": flow_id} - data_raw = self.raw_get(URL_ADMIN_FLOWS_ALIAS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_ALIAS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): @@ -1888,22 +1839,25 @@ def create_authentication_flow(self, payload, skip_exists=False): Create a new authentication flow AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation - :param skip_exists: If true then do not raise an error if authentication flow already exists + :param skip_exists: Do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post(URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def copy_authentication_flow(self, payload, flow_alias): """ - Copy existing authentication flow under a new name. The new name is given as 'newName' attribute of the passed payload. + Copy existing authentication flow under a new name. The new name is given as 'newName' + attribute of the passed payload. :param payload: JSON containing 'newName' attribute :param flow_alias: the flow alias @@ -1912,7 +1866,7 @@ def copy_authentication_flow(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( - URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -1921,13 +1875,13 @@ def delete_authentication_flow(self, flow_id): Delete authentication flow AuthenticationInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationinforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationinforepresentation :param flow_id: authentication flow id :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": flow_id} - data_raw = self.raw_delete(URL_ADMIN_FLOW.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_FLOW.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flow_executions(self, flow_alias): @@ -1938,7 +1892,7 @@ def get_authentication_flow_executions(self, flow_alias): :return: Response(json) """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} - data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_authentication_flow_executions(self, payload, flow_alias): @@ -1946,7 +1900,7 @@ def update_authentication_flow_executions(self, payload, flow_alias): Update an authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation :param flow_alias: The flow alias @@ -1955,7 +1909,8 @@ def update_authentication_flow_executions(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put( - URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[202, 204]) @@ -1964,13 +1919,13 @@ def get_authentication_flow_execution(self, execution_id): Get authentication flow execution. AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param execution_id: the execution ID :return: Response(json) """ params_path = {"realm-name": self.realm_name, "id": execution_id} - data_raw = self.raw_get(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow_execution(self, payload, flow_alias): @@ -1978,7 +1933,7 @@ def create_authentication_flow_execution(self, payload, flow_alias): Create an authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation :param flow_alias: The flow alias @@ -1987,7 +1942,8 @@ def create_authentication_flow_execution(self, payload, flow_alias): params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( - URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -1996,13 +1952,13 @@ def delete_authentication_flow_execution(self, execution_id): Delete authentication flow execution AuthenticationExecutionInfoRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationexecutioninforepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param execution_id: keycloak client id (not oauth client-id) :return: Keycloak server response (json) """ params_path = {"realm-name": self.realm_name, "id": execution_id} - data_raw = self.raw_delete(URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): @@ -2010,17 +1966,18 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa Create a new sub authentication flow for a given authentication flow AuthenticationFlowRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticationflowrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation :param flow_alias: The flow alias - :param skip_exists: If true then do not raise an error if authentication flow already exists + :param skip_exists: Do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( - URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -2034,7 +1991,7 @@ def get_authenticator_config(self, config_id): :return: Response(json) """ params_path = {"realm-name": self.realm_name, "id": config_id} - data_raw = self.raw_get(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_authenticator_config(self, payload, config_id): @@ -2042,7 +1999,7 @@ def update_authenticator_config(self, payload, config_id): Update an authenticator configuration. AuthenticatorConfigRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authenticatorconfigrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfigrepresentation :param payload: AuthenticatorConfigRepresentation :param config_id: Authenticator config id @@ -2050,21 +2007,24 @@ def update_authenticator_config(self, payload, config_id): """ params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_put( - URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_authenticator_config(self, config_id): """ Delete a authenticator configuration. - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_authentication_management_resource + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authentication_management_resource :param config_id: Authenticator config id :return: Keycloak server Response """ params_path = {"realm-name": self.realm_name, "id": config_id} - data_raw = self.raw_delete(URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def sync_users(self, storage_id, action): @@ -2080,40 +2040,43 @@ def sync_users(self, storage_id, action): params_path = {"realm-name": self.realm_name, "id": storage_id} data_raw = self.raw_post( - URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), **params_query + urls_patterns.URL_ADMIN_USER_STORAGE.format(**params_path), + data=json.dumps(data), + **params_query ) return raise_error_from_response(data_raw, KeycloakPostError) def get_client_scopes(self): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :return: Keycloak server response Array of (ClientScopeRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPES.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scope(self, client_scope_id): """ Get representation of the client scopes for the realm where we are connected to - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param client_scope_id: The id of the client scope :return: Keycloak server response (ClientScopeRepresentation) """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_SCOPE.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def create_client_scope(self, payload, skip_exists=False): """ Create a client scope - ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientscopes + ClientScopeRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param payload: ClientScopeRepresentation :param skip_exists: If true then do not raise an error if client scope already exists @@ -2122,7 +2085,7 @@ def create_client_scope(self, payload, skip_exists=False): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( - URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists @@ -2132,7 +2095,8 @@ def update_client_scope(self, client_scope_id, payload): """ Update a client scope - ClientScopeRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_client_scopes_resource + ClientScopeRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource :param client_scope_id: The id of the client scope :param payload: ClientScopeRepresentation @@ -2141,14 +2105,14 @@ def update_client_scope(self, client_scope_id, payload): params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_put( - URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_scope_id: The id of the client scope :param payload: ProtocolMapperRepresentation @@ -2158,7 +2122,8 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -2166,7 +2131,7 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): """ Delete a mapper from a client scope - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_delete_mapper + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_delete_mapper :param client_scope_id: The id of the client scope :param payload: ProtocolMapperRepresentation @@ -2179,13 +2144,15 @@ def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): "protocol-mapper-id": protocol_mppaer_id, } - data_raw = self.raw_delete(URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, payload): """ Update an existing protocol mapper in a client scope - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_protocol_mappers_resource + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource :param client_scope_id: The id of the client scope :param protocol_mapper_id: The id of the protocol mapper which exists in the client scope @@ -2201,7 +2168,8 @@ def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, pay } data_raw = self.raw_put( - URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_SCOPES_MAPPERS.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2213,7 +2181,9 @@ def get_default_default_client_scopes(self): :return: Keycloak server response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_default_client_scope(self, scope_id): @@ -2224,7 +2194,9 @@ def delete_default_default_client_scope(self, scope_id): :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": scope_id} - data_raw = self.raw_delete(URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_default_client_scope(self, scope_id): @@ -2237,7 +2209,8 @@ def add_default_default_client_scope(self, scope_id): params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} data_raw = self.raw_put( - URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2248,7 +2221,9 @@ def get_default_optional_client_scopes(self): :return: Keycloak server response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPES.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_optional_client_scope(self, scope_id): @@ -2259,7 +2234,9 @@ def delete_default_optional_client_scope(self, scope_id): :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": scope_id} - data_raw = self.raw_delete(URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_optional_client_scope(self, scope_id): @@ -2272,14 +2249,15 @@ def add_default_optional_client_scope(self, scope_id): params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} data_raw = self.raw_put( - URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_DEFAULT_OPTIONAL_CLIENT_SCOPE.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_client(self, client_id, payload): """ Add a mapper to a client - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_create_mapper + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_id: The id of the client :param payload: ProtocolMapperRepresentation @@ -2289,7 +2267,8 @@ def add_mapper_to_client(self, client_id, payload): params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( - URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -2310,7 +2289,8 @@ def update_client_mapper(self, client_id, mapper_id, payload): } data_raw = self.raw_put( - URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path), + data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2330,35 +2310,39 @@ def remove_client_mapper(self, client_id, client_mapper_id): "protocol-mapper-id": client_mapper_id, } - data_raw = self.raw_delete(URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path)) + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_PROTOCOL_MAPPER.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def generate_client_secrets(self, client_id): """ Generate a new secret for the client - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_regeneratesecret + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_regeneratesecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_post(URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None + ) return raise_error_from_response(data_raw, KeycloakPostError) def get_client_secrets(self, client_id): """ Get representation of the client secrets - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_getclientsecret + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_SECRETS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_components(self, query=None): @@ -2366,13 +2350,15 @@ def get_components(self, query=None): Return a list of components, filtered according to query parameters ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :param query: Query parameters (optional) :return: components list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query + ) return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): @@ -2380,7 +2366,7 @@ def create_component(self, payload): Create a new component. ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :param payload: ComponentRepresentation @@ -2389,7 +2375,7 @@ def create_component(self, payload): params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( - URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -2400,12 +2386,12 @@ def get_component(self, component_id): :param component_id: Component id ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :return: ComponentRepresentation """ params_path = {"realm-name": self.realm_name, "component-id": component_id} - data_raw = self.raw_get(URL_ADMIN_COMPONENT.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_component(self, component_id, payload): @@ -2414,13 +2400,13 @@ def update_component(self, component_id, payload): :param component_id: Component id :param payload: ComponentRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_componentrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :return: Http response """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_put( - URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_COMPONENT.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) @@ -2433,7 +2419,7 @@ def delete_component(self, component_id): :return: Http response """ params_path = {"realm-name": self.realm_name, "component-id": component_id} - data_raw = self.raw_delete(URL_ADMIN_COMPONENT.format(**params_path)) + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_COMPONENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_keys(self): @@ -2441,12 +2427,12 @@ def get_keys(self): Return a list of keys, filtered according to query parameters KeysMetadataRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_key_resource + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_key_resource :return: keys list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_KEYS.format(**params_path), data=None) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_KEYS.format(**params_path), data=None) return raise_error_from_response(data_raw, KeycloakGetError) def get_events(self, query=None): @@ -2454,12 +2440,14 @@ def get_events(self, query=None): Return a list of events, filtered according to query parameters EventRepresentation array - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_eventrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_eventrepresentation :return: events list """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(URL_ADMIN_EVENTS.format(**params_path), data=None, **query) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_EVENTS.format(**params_path), data=None, **query + ) return raise_error_from_response(data_raw, KeycloakGetError) def set_events(self, payload): @@ -2467,12 +2455,14 @@ def set_events(self, payload): Set realm events configuration RealmEventsConfigRepresentation - https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmeventsconfigrepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmeventsconfigrepresentation :return: Http response """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_put(URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload)) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload) + ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def raw_get(self, *args, **kwargs): @@ -2518,8 +2508,8 @@ def raw_delete(self, *args, **kwargs): """ Calls connection.raw_delete. - If auto_refresh is set for *delete* and *access_token* is expired, it will refresh the token - and try *delete* once more. + If auto_refresh is set for *delete* and *access_token* is expired, + it will refresh the token and try *delete* once more. """ r = self.connection.raw_delete(*args, **kwargs) if "delete" in self.auto_refresh_token and r.status_code == 401: @@ -2551,7 +2541,7 @@ def get_token(self): self.realm_name = self.user_realm_name if self.username and self.password: - self._token = self.keycloak_openid.token( + self.token = self.keycloak_openid.token( self.username, self.password, grant_type=grant_type, totp=self.totp ) @@ -2560,14 +2550,14 @@ def get_token(self): "Content-Type": "application/json", } else: - self._token = None + self.token = None headers = {} if self.custom_headers is not None: # merge custom headers to main headers headers.update(self.custom_headers) - self._connection = ConnectionManager( + self.connection = ConnectionManager( base_url=self.server_url, headers=headers, timeout=60, verify=self.verify ) @@ -2600,22 +2590,22 @@ def get_client_all_sessions(self, client_id): :param client_id: id of client UserSessionRepresentation - http://www.keycloak.org/docs-api/3.3/rest-api/index.html#_usersessionrepresentation + http://www.keycloak.org/docs-api/18.0/rest-api/index.html#_usersessionrepresentation :return: UserSessionRepresentation """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_sessions_stats(self): """ Get current session count for all clients with active sessions - https://www.keycloak.org/docs-api/16.1/rest-api/index.html#_getclientsessionstats + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsessionstats :return: Dict of clients and session count """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(self.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 34d85140..7450f2e4 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -124,7 +124,10 @@ URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = ( "admin/realms/{realm-name}/roles/{role-name}/composites" ) -URL_ADMIN_REALM_EXPORT = "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&exportGroupsAndRoles={export-groups-and-roles}" +URL_ADMIN_REALM_EXPORT = ( + "admin/realms/{realm-name}/partial-export?exportClients={export-clients}&" + + "exportGroupsAndRoles={export-groups-and-roles}" +) URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES = URL_ADMIN_REALM + "/default-default-client-scopes" URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPE = URL_ADMIN_DEFAULT_DEFAULT_CLIENT_SCOPES + "/{id}" From 3636de017762b5ca48c5bdb9ccc02894d6bd8014 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:57:49 +0200 Subject: [PATCH 150/566] docs: added autoapi to docs --- .gitignore | 3 +- README.md | 70 +++++----- docs/source/conf.py | 19 ++- docs/source/index.rst | 303 +---------------------------------------- docs/source/readme.rst | 1 + 5 files changed, 58 insertions(+), 338 deletions(-) create mode 100644 docs/source/readme.rst diff --git a/.gitignore b/.gitignore index 4c8d46d3..24f085bc 100644 --- a/.gitignore +++ b/.gitignore @@ -104,4 +104,5 @@ ENV/ main.py main2.py s3air-authz-config.json -.vscode \ No newline at end of file +.vscode +_build \ No newline at end of file diff --git a/README.md b/README.md index 68a2dc5c..01d6e0fd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ -[![CircleCI](https://circleci.com/gh/marcospereirampj/python-keycloak/tree/master.svg?style=svg)](https://circleci.com/gh/marcospereirampj/python-keycloak/tree/master) +[![CircleCI](https://github.com/marcospereirampj/python-keycloak/actions/workflows/daily.yaml/badge.svg)](https://github.com/marcospereirampj/python-keycloak/) [![Documentation Status](https://readthedocs.org/projects/python-keycloak/badge/?version=latest)](http://python-keycloak.readthedocs.io/en/latest/?badge=latest) - -Python Keycloak -==================== +# Python Keycloak For review- see https://github.com/marcospereirampj/python-keycloak @@ -13,24 +11,27 @@ For review- see https://github.com/marcospereirampj/python-keycloak ### Via Pypi Package: -``` $ pip install python-keycloak ``` +`$ pip install python-keycloak` ### Manually -``` $ python setup.py install ``` +`$ python setup.py install` ## Dependencies python-keycloak depends on: -* Python 3 -* [requests](https://requests.readthedocs.io) -* [python-jose](http://python-jose.readthedocs.io/en/latest/) +- Python 3 +- [requests](https://requests.readthedocs.io) +- [python-jose](http://python-jose.readthedocs.io/en/latest/) +- [urllib3](https://urllib3.readthedocs.io/en/stable/) ### Tests Dependencies -* unittest -* [httmock](https://github.com/patrys/httmock) +- [tox](https://tox.readthedocs.io/) +- [pytest](https://docs.pytest.org/en/latest/) +- [pytest-cov](https://github.com/pytest-dev/pytest-cov) +- [wheel](https://github.com/pypa/wheel) ## Bug reports @@ -43,18 +44,19 @@ The documentation for python-keycloak is available on [readthedocs](http://pytho ## Contributors -* [Agriness Team](http://www.agriness.com/pt/) -* [Marcos Pereira](marcospereira.mpj@gmail.com) -* [Martin Devlin](https://bitbucket.org/devlinmpearson/) -* [Shon T. Urbas](https://bitbucket.org/surbas/) -* [Markus Spanier](https://bitbucket.org/spanierm/) -* [Remco Kranenburg](https://bitbucket.org/Remco47/) -* [Armin](https://bitbucket.org/arminfelder/) -* [njordr](https://bitbucket.org/njordr/) -* [Josha Inglis](https://bitbucket.org/joshainglis/) -* [Alex](https://bitbucket.org/alex_zel/) -* [Ewan Jone](https://bitbucket.org/kisamoto/) -* [Lukas Martini](https://github.com/lutoma) +- [Agriness Team](http://www.agriness.com/pt/) +- [Marcos Pereira](marcospereira.mpj@gmail.com) +- [Martin Devlin](https://bitbucket.org/devlinmpearson/) +- [Shon T. Urbas](https://bitbucket.org/surbas/) +- [Markus Spanier](https://bitbucket.org/spanierm/) +- [Remco Kranenburg](https://bitbucket.org/Remco47/) +- [Armin](https://bitbucket.org/arminfelder/) +- [njordr](https://bitbucket.org/njordr/) +- [Josha Inglis](https://bitbucket.org/joshainglis/) +- [Alex](https://bitbucket.org/alex_zel/) +- [Ewan Jone](https://bitbucket.org/kisamoto/) +- [Lukas Martini](https://github.com/lutoma) +- [Adamatics](https://www.adamatics.com) ## Usage @@ -119,13 +121,13 @@ keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", user_realm_name="only_if_other_realm_than_master", client_secret_key="client-secret", verify=True) - -# Add user + +# Add user new_user = keycloak_admin.create_user({"email": "example@example.com", "username": "example@example.com", "enabled": True, "firstName": "Example", - "lastName": "Example"}) + "lastName": "Example"}) # Add user and raise exception if username already exists # exist_ok currently defaults to True for backwards compatibility reasons @@ -135,8 +137,8 @@ new_user = keycloak_admin.create_user({"email": "example@example.com", "firstName": "Example", "lastName": "Example"}, exist_ok=False) - -# Add user and set password + +# Add user and set password new_user = keycloak_admin.create_user({"email": "example@example.com", "username": "example@example.com", "enabled": True, @@ -144,7 +146,7 @@ new_user = keycloak_admin.create_user({"email": "example@example.com", "lastName": "Example", "credentials": [{"value": "secret","type": "password",}]}) -# Add user and specify a locale +# Add user and specify a locale new_user = keycloak_admin.create_user({"email": "example@example.fr", "username": "example@example.fr", "enabled": True, @@ -152,7 +154,7 @@ new_user = keycloak_admin.create_user({"email": "example@example.fr", "lastName": "Example", "attributes": { "locale": ["fr"] - }) + }) # User counter count_users = keycloak_admin.users_count() @@ -167,7 +169,7 @@ user_id_keycloak = keycloak_admin.get_user_id("example@example.com") user = keycloak_admin.get_user("user-id-keycloak") # Update User -response = keycloak_admin.update_user(user_id="user-id-keycloak", +response = keycloak_admin.update_user(user_id="user-id-keycloak", payload={'firstName': 'Example Update'}) # Update User Password @@ -181,7 +183,7 @@ credential = keycloak_admin.get_credential(user_id='user_id', credential_id='cre # Delete User Credential response = keycloak_admin.delete_credential(user_id='user_id', credential_id='credential_id') - + # Delete User response = keycloak_admin.delete_user(user_id="user-id-keycloak") @@ -189,7 +191,7 @@ response = keycloak_admin.delete_user(user_id="user-id-keycloak") consents = keycloak_admin.consents_user(user_id="user-id-keycloak") # Send User Action -response = keycloak_admin.send_update_account(user_id="user-id-keycloak", +response = keycloak_admin.send_update_account(user_id="user-id-keycloak", payload=json.dumps(['UPDATE_PASSWORD'])) # Send Verify Email @@ -260,7 +262,7 @@ group = keycloak_admin.create_group({"name": "Example Group"}) # Get all groups groups = keycloak_admin.get_groups() -# Get group +# Get group group = keycloak_admin.get_group(group_id='group_id') # Get group by name diff --git a/docs/source/conf.py b/docs/source/conf.py index 23e9bbf1..7d0b77d6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,6 +21,7 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) import sphinx_rtd_theme +from keycloak import __version__ # -- General configuration ------------------------------------------------ @@ -36,8 +37,16 @@ "sphinx.ext.intersphinx", "sphinx.ext.todo", "sphinx.ext.viewcode", + "m2r2", + "autoapi.extension", ] +autoapi_type = "python" +autoapi_dirs = ["../../keycloak"] +autoapi_root = "reference" +autoapi_keep_files = False +autoapi_add_toctree_entry = False + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] @@ -60,9 +69,9 @@ # built documents. # # The short X.Y version. -version = "0.27.1" +version = __version__ # The full version, including alpha/beta/rc tags. -release = "0.27.1" +release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -103,7 +112,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] +# html_static_path = ["_static"] html_use_smartypants = False @@ -160,7 +169,7 @@ "python-keycloak Documentation", "Marcos Pereira", "manual", - ) + ), ] @@ -185,5 +194,5 @@ "python-keycloak", "One line description of project.", "Miscellaneous", - ) + ), ] diff --git a/docs/source/index.rst b/docs/source/index.rst index 66753529..6dff08d0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,305 +3,12 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. +.. image:: https://readthedocs.org/projects/adamatics-keycloak/badge/?version=latest + :target: https://adamatics-keycloak.readthedocs.io/en/latest/?badge=latest +.. mdinclude:: ../../README.md .. toctree:: :maxdepth: 2 :caption: Contents: - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - -.. image:: https://readthedocs.org/projects/python-keycloak/badge/?version=latest - :target: http://python-keycloak.readthedocs.io/en/latest/?badge=latest - - -Welcome to python-keycloak's documentation! -=========================================== - -**python-keycloak** is a Python package providing access to the Keycloak API. - -Installation -================== - -Via Pypi Package:: - - $ pip install python-keycloak - -Manually:: - - $ python setup.py install - -Dependencies -================== - -python-keycloak depends on: - -* Python 3 -* `requests `_ -* `python-jose `_ - -Tests Dependencies ------------------- - -* unittest -* `httmock `_ - -Bug reports -================== - -Please report bugs and feature requests at -`https://github.com/marcospereirampj/python-keycloak/issues `_ - -Documentation -================== - -The documentation for python-keycloak is available on `readthedocs `_. - -Contributors -================== - -* `Agriness Team `_ -* `Marcos Pereira `_ -* `Martin Devlin `_ -* `Shon T. Urbas `_ -* `Markus Spanier `_ -* `Remco Kranenburg `_ -* `Armin `_ -* `Njordr `_ -* `Josha Inglis `_ -* `Alex `_ -* `Ewan Jone `_ - -Usage -===== - -Main methods:: - - # KEYCLOAK OPENID - - from keycloak import KeycloakOpenID - - # Configure client - keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - client_id="example_client", - realm_name="example_realm", - client_secret_key="secret", - verify=True) - - # Optionally, you can pass custom headers that will be added to all HTTP calls - # keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - # client_id="example_client", - # realm_name="example_realm", - # client_secret_key="secret", - # verify=True, - # custom_headers={'CustomHeader': 'value'}) - - # Optionally, you can pass proxies as well that will be used in all HTTP calls. See requests documentation for more details_ - # `requests-proxies `_. - # keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - # client_id="example_client", - # realm_name="example_realm", - # client_secret_key="secret", - # verify=True, - # proxies={'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080'}) - - # Get WellKnow - config_well_know = keycloak_openid.well_know() - - # Get Token - token = keycloak_openid.token("user", "password") - token = keycloak_openid.token("user", "password", totp="012345") - - # Get Userinfo - userinfo = keycloak_openid.userinfo(token['access_token']) - - # Refresh token - token = keycloak_openid.refresh_token(token['refresh_token']) - - # Logout - keycloak_openid.logout(token['refresh_token']) - - # Get Certs - certs = keycloak_openid.certs() - - # Get RPT (Entitlement) - token = keycloak_openid.token("user", "password") - rpt = keycloak_openid.entitlement(token['access_token'], "resource_id") - - # Instropect RPT - token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['access_token'], rpt=rpt['rpt'], - token_type_hint="requesting_party_token")) - - # Introspect Token - token_info = keycloak_openid.introspect(token['access_token'])) - - # Decode Token - KEYCLOAK_PUBLIC_KEY = "secret" - options = {"verify_signature": True, "verify_aud": True, "verify_exp": True} - token_info = keycloak_openid.decode_token(token['access_token'], key=KEYCLOAK_PUBLIC_KEY, options=options) - - # Get permissions by token - token = keycloak_openid.token("user", "password") - keycloak_openid.load_authorization_config("example-authz-config.json") - policies = keycloak_openid.get_policies(token['access_token'], method_token_info='decode', key=KEYCLOAK_PUBLIC_KEY) - permissions = keycloak_openid.get_permissions(token['access_token'], method_token_info='introspect') - - # KEYCLOAK ADMIN - - from keycloak import KeycloakAdmin - - keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", - username='example-admin', - password='secret', - realm_name="example_realm", - verify=True) - - # Optionally, you can pass custom headers that will be added to all HTTP calls - #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", - # username='example-admin', - # password='secret', - # realm_name="example_realm", - # verify=True, - # custom_headers={'CustomHeader': 'value'}) - # - # You can also authenticate with client_id and client_secret - #keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", - # client_id="example_client", - # client_secret_key="secret", - # realm_name="example_realm", - # verify=True, - # custom_headers={'CustomHeader': 'value'}) - - # Add user - new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example", - "realmRoles": ["user_default", ], - "attributes": {"example": "1,2,3,3,"}}) - - - # Add user and set password - new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example", - "credentials": [{"value": "secret","type": "password",}], - "realmRoles": ["user_default", ], - "attributes": {"example": "1,2,3,3,"}}) - - # User counter - count_users = keycloak_admin.users_count() - - # Get users Returns a list of users, filtered according to query parameters - users = keycloak_admin.get_users({}) - - # Get user ID from name - user-id-keycloak = keycloak_admin.get_user_id("example@example.com") - - # Get User - user = keycloak_admin.get_user("user-id-keycloak") - - # Update User - response = keycloak_admin.update_user(user_id="user-id-keycloak", - payload={'firstName': 'Example Update'}) - - # Update User Password - response = set_user_password(user_id="user-id-keycloak", password="secret", temporary=True) - - # Delete User - response = keycloak_admin.delete_user(user_id="user-id-keycloak") - - # Get consents granted by the user - consents = keycloak_admin.consents_user(user_id="user-id-keycloak") - - # Send User Action - response = keycloak_admin.send_update_account(user_id="user-id-keycloak", - payload=json.dumps(['UPDATE_PASSWORD'])) - - # Send Verify Email - response = keycloak_admin.send_verify_email(user_id="user-id-keycloak") - - # Get sessions associated with the user - sessions = keycloak_admin.get_sessions(user_id="user-id-keycloak") - - # Get themes, social providers, auth providers, and event listeners available on this server - server_info = keycloak_admin.get_server_info() - - # Get clients belonging to the realm Returns a list of clients belonging to the realm - clients = keycloak_admin.get_clients() - - # Get client - id (not client-id) from client by name - client_id=keycloak_admin.get_client_id("my-client") - - # Get representation of the client - id of client (not client-id) - client = keycloak_admin.get_client(client_id="client_id") - - # Get all roles for the realm or client - realm_roles = keycloak_admin.get_realm_roles() - - # Get all roles for the client - client_roles = keycloak_admin.get_client_roles(client_id="client_id") - - # Get client role - role = keycloak_admin.get_client_role(client_id="client_id", role_name="role_name") - - # Warning: Deprecated - # Get client role id from name - role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test") - - # Create client role - keycloak_admin.create_client_role(client_id="client_id", {'name': 'roleName', 'clientRole': True}) - - # Get client role id from name - role_id = keycloak_admin.get_client_role_id(client_id=client_id, role_name="test") - - # Get all roles for the realm or client - realm_roles = keycloak_admin.get_roles() - - # Assign client role to user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") - - # Assign realm roles to user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.assign_realm_roles(client_id="client_id", user_id="user_id", roles=[{"roles_representation"}]) - - # Delete realm roles of user. Note that BOTH role_name and role_id appear to be required. - keycloak_admin.deletes_realm_roles_of_user(user_id="user_id", roles=[{"roles_representation"}]) - - # Create new group - group = keycloak_admin.create_group(name="Example Group") - - # Get all groups - groups = keycloak_admin.get_groups() - - # Get group - group = keycloak_admin.get_group(group_id='group_id') - - # Get group by path - group = keycloak_admin.get_group_by_path(path='/group/subgroup', search_in_subgroups=True) - - # Function to trigger user sync from provider - sync_users(storage_id="storage_di", action="action") - - # List public RSA keys - components = keycloak_admin.keys - - # List all keys - components = keycloak_admin.get_components(query={"parent":"example_realm", "type":"org.keycloak.keys.KeyProvider"}) - - # Create a new RSA key - component = keycloak_admin.create_component({"name":"rsa-generated","providerId":"rsa-generated","providerType":"org.keycloak.keys.KeyProvider","parentId":"example_realm","config":{"priority":["100"],"enabled":["true"],"active":["true"],"algorithm":["RS256"],"keySize":["2048"]}}) - - # Update the key - component_details['config']['active'] = ["false"] - keycloak_admin.update_component(component['id']) - - # Delete the key - keycloak_admin.delete_component(component['id']) - + readme + reference/keycloak/index diff --git a/docs/source/readme.rst b/docs/source/readme.rst new file mode 100644 index 00000000..3bd447c4 --- /dev/null +++ b/docs/source/readme.rst @@ -0,0 +1 @@ +.. mdinclude:: ../../README.md From 51cb44fe3aca1ed91e4d2da0ecbc2498f8217bc9 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 12:59:25 +0200 Subject: [PATCH 151/566] refactor: isort conf.py --- docs/source/conf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7d0b77d6..403d4657 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -21,6 +21,7 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) import sphinx_rtd_theme + from keycloak import __version__ # -- General configuration ------------------------------------------------ @@ -169,7 +170,7 @@ "python-keycloak Documentation", "Marcos Pereira", "manual", - ), + ) ] @@ -194,5 +195,5 @@ "python-keycloak", "One line description of project.", "Miscellaneous", - ), + ) ] From fa9e56ef42102f34986ead5365cc7854e4c1ca63 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 14:45:06 +0200 Subject: [PATCH 152/566] feat: added authenticator providers getters --- keycloak/keycloak_admin.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/keycloak/keycloak_admin.py b/keycloak/keycloak_admin.py index 7b92d802..df538597 100644 --- a/keycloak/keycloak_admin.py +++ b/keycloak/keycloak_admin.py @@ -1983,6 +1983,34 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + def get_authenticator_providers(self): + """ + Get authenticator providers list. + + :return: Response(json) + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_AUTHENTICATOR_PROVIDERS.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_authenticator_provider_config_description(self, provider_id): + """ + Get authenticator's provider configuration description. + + AuthenticatorConfigInfoRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfiginforepresentation + + :param provider_id: Provider Id + :return: AuthenticatorConfigInfoRepresentation + """ + params_path = {"realm-name": self.realm_name, "provider-id": provider_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG_DESCRIPTION.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_authenticator_config(self, config_id): """ Get authenticator configuration. Returns all configuration details. From 7ae0442370a86c821e1bf8f2e2f4c2f6abe55e16 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 14:45:21 +0200 Subject: [PATCH 153/566] test: test authenticator and configurations --- keycloak/urls_patterns.py | 6 ++++++ tests/test_keycloak_admin.py | 40 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/keycloak/urls_patterns.py b/keycloak/urls_patterns.py index 7450f2e4..071c733a 100644 --- a/keycloak/urls_patterns.py +++ b/keycloak/urls_patterns.py @@ -148,6 +148,12 @@ URL_ADMIN_FLOWS_EXECUTIONS_FLOW = ( "admin/realms/{realm-name}/authentication/flows/{flow-alias}/executions/flow" ) +URL_ADMIN_AUTHENTICATOR_PROVIDERS = ( + "admin/realms/{realm-name}/authentication/authenticator-providers" +) +URL_ADMIN_AUTHENTICATOR_CONFIG_DESCRIPTION = ( + "admin/realms/{realm-name}/authentication/config-description/{provider-id}" +) URL_ADMIN_AUTHENTICATOR_CONFIG = "admin/realms/{realm-name}/authentication/config/{id}" URL_ADMIN_COMPONENTS = "admin/realms/{realm-name}/components" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 29d3b15f..6b04af75 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1199,3 +1199,43 @@ def test_auth_flows(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakDeleteError) as err: admin.delete_authentication_flow(flow_id=flow_id) assert err.match('404: b\'{"error":"Could not find flow with id"}\'') + + +def test_authentication_configs(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test list of auth providers + res = admin.get_authenticator_providers() + assert len(res) == 39 + + res = admin.get_authenticator_provider_config_description(provider_id="auth-cookie") + assert res == { + "helpText": "Validates the SSO cookie set by the auth server.", + "name": "Cookie", + "properties": [], + "providerId": "auth-cookie", + } + + # Test authenticator config + # Currently unable to find a sustainable way to fetch the config id, + # therefore testing only failures + with pytest.raises(KeycloakGetError) as err: + admin.get_authenticator_config(config_id="bad") + assert err.match('404: b\'{"error":"Could not find authenticator config"}\'') + + with pytest.raises(KeycloakPutError) as err: + admin.update_authenticator_config(payload=dict(), config_id="bad") + assert err.match('404: b\'{"error":"Could not find authenticator config"}\'') + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_authenticator_config(config_id="bad") + assert err.match('404: b\'{"error":"Could not find authenticator config"}\'') + + +def test_sync_users(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Only testing the error message + with pytest.raises(KeycloakPostError) as err: + admin.sync_users(storage_id="does-not-exist", action="triggerFullSync") + assert err.match('404: b\'{"error":"Could not find component"}\'') From f5f4de2bfa6a4b89651a762262ea82a16673f7bf Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 16:41:41 +0200 Subject: [PATCH 154/566] chore: fix email format --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 8f3b7fca..7b3903bb 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ url="https://github.com/marcospereirampj/python-keycloak", license="The MIT License", author="Marcos Pereira, Richard Nemeth", - author_email="marcospereira.mpj@gmail.com; ryshoooo@gmail.com", + author_email="marcospereira.mpj@gmail.com, ryshoooo@gmail.com", keywords="keycloak openid oidc", description="python-keycloak is a Python package providing access to the Keycloak API.", long_description=long_description, From 0b35ddc66b974ad34e2a3e8037c3256a9ff7a635 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 17:08:44 +0200 Subject: [PATCH 155/566] docs: fix the docs build --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7aa6ce59..09965df3 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,4 +7,4 @@ build: python: install: - - requirements: docs-requirements.txt + - requirements: .[docs] \ No newline at end of file From 23b943f301cbcb50c8e4aae41ce3be2ef5ba5827 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 17:12:26 +0200 Subject: [PATCH 156/566] docs: fix the docs conf --- .readthedocs.yaml | 2 +- docs/source/conf.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 09965df3..7aa6ce59 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,4 +7,4 @@ build: python: install: - - requirements: .[docs] \ No newline at end of file + - requirements: docs-requirements.txt diff --git a/docs/source/conf.py b/docs/source/conf.py index 403d4657..3a70cb63 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,8 +22,6 @@ # sys.path.insert(0, os.path.abspath('.')) import sphinx_rtd_theme -from keycloak import __version__ - # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -70,9 +68,9 @@ # built documents. # # The short X.Y version. -version = __version__ +version = "0.0.0" # The full version, including alpha/beta/rc tags. -release = __version__ +release = "0.0.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 6154a2d5f75df45eaa5b322d4198e9f9066ae4a5 Mon Sep 17 00:00:00 2001 From: Sam Mesterton-Gibbons Date: Thu, 19 May 2022 17:40:41 +0100 Subject: [PATCH 157/566] fix: Add missing keycloak.authorization package Re-adds `keycloak.authorization` to the list of packages in setup.py so it gets included in wheels. Fixes #311 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7b3903bb..86a6fb2d 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ description="python-keycloak is a Python package providing access to the Keycloak API.", long_description=long_description, long_description_content_type="text/markdown", - packages=["keycloak"], + packages=["keycloak", "keycloak.authorization"], install_requires=reqs, tests_require=dev_reqs, extras_require={"docs": docs_reqs}, From 54beb51fba930314c66b18ee01a328fab544237a Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 19:24:50 +0200 Subject: [PATCH 158/566] chore: move source files into src folder Moved all of the source files into the src folder --- {keycloak => src/keycloak}/__init__.py | 0 {keycloak => src/keycloak}/_version.py | 0 {keycloak => src/keycloak}/authorization/__init__.py | 0 {keycloak => src/keycloak}/authorization/permission.py | 0 {keycloak => src/keycloak}/authorization/policy.py | 0 {keycloak => src/keycloak}/authorization/role.py | 0 {keycloak => src/keycloak}/connection.py | 0 {keycloak => src/keycloak}/exceptions.py | 0 {keycloak => src/keycloak}/keycloak_admin.py | 0 {keycloak => src/keycloak}/keycloak_openid.py | 0 {keycloak => src/keycloak}/urls_patterns.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename {keycloak => src/keycloak}/__init__.py (100%) rename {keycloak => src/keycloak}/_version.py (100%) rename {keycloak => src/keycloak}/authorization/__init__.py (100%) rename {keycloak => src/keycloak}/authorization/permission.py (100%) rename {keycloak => src/keycloak}/authorization/policy.py (100%) rename {keycloak => src/keycloak}/authorization/role.py (100%) rename {keycloak => src/keycloak}/connection.py (100%) rename {keycloak => src/keycloak}/exceptions.py (100%) rename {keycloak => src/keycloak}/keycloak_admin.py (100%) rename {keycloak => src/keycloak}/keycloak_openid.py (100%) rename {keycloak => src/keycloak}/urls_patterns.py (100%) diff --git a/keycloak/__init__.py b/src/keycloak/__init__.py similarity index 100% rename from keycloak/__init__.py rename to src/keycloak/__init__.py diff --git a/keycloak/_version.py b/src/keycloak/_version.py similarity index 100% rename from keycloak/_version.py rename to src/keycloak/_version.py diff --git a/keycloak/authorization/__init__.py b/src/keycloak/authorization/__init__.py similarity index 100% rename from keycloak/authorization/__init__.py rename to src/keycloak/authorization/__init__.py diff --git a/keycloak/authorization/permission.py b/src/keycloak/authorization/permission.py similarity index 100% rename from keycloak/authorization/permission.py rename to src/keycloak/authorization/permission.py diff --git a/keycloak/authorization/policy.py b/src/keycloak/authorization/policy.py similarity index 100% rename from keycloak/authorization/policy.py rename to src/keycloak/authorization/policy.py diff --git a/keycloak/authorization/role.py b/src/keycloak/authorization/role.py similarity index 100% rename from keycloak/authorization/role.py rename to src/keycloak/authorization/role.py diff --git a/keycloak/connection.py b/src/keycloak/connection.py similarity index 100% rename from keycloak/connection.py rename to src/keycloak/connection.py diff --git a/keycloak/exceptions.py b/src/keycloak/exceptions.py similarity index 100% rename from keycloak/exceptions.py rename to src/keycloak/exceptions.py diff --git a/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py similarity index 100% rename from keycloak/keycloak_admin.py rename to src/keycloak/keycloak_admin.py diff --git a/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py similarity index 100% rename from keycloak/keycloak_openid.py rename to src/keycloak/keycloak_openid.py diff --git a/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py similarity index 100% rename from keycloak/urls_patterns.py rename to src/keycloak/urls_patterns.py From 04cc2feeeefe8bb2b83d175c72b79b11c8215ec0 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 19:26:19 +0200 Subject: [PATCH 159/566] chore: update setup py to find source files in src folder --- setup.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 86a6fb2d..27a188db 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import re -from setuptools import setup + +from setuptools import find_packages, setup with open("README.md", "r") as fh: long_description = fh.read() @@ -15,7 +16,7 @@ docs_reqs = fh.read().split("\n") -VERSIONFILE = "keycloak/_version.py" +VERSIONFILE = "src/keycloak/_version.py" verstrline = open(VERSIONFILE, "rt").read() VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" mo = re.search(VSRE, verstrline, re.M) @@ -35,7 +36,8 @@ description="python-keycloak is a Python package providing access to the Keycloak API.", long_description=long_description, long_description_content_type="text/markdown", - packages=["keycloak", "keycloak.authorization"], + packages=find_packages("src"), + package_dir={"": "src"}, install_requires=reqs, tests_require=dev_reqs, extras_require={"docs": docs_reqs}, From bc82de7989efc880cf1946e161a6747a73418e74 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 19:27:08 +0200 Subject: [PATCH 160/566] test: update the test framework Updated the tox framework to operate on files in src for checks, but in toxenv for tests --- docs/source/conf.py | 2 +- tox.ini | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 3a70cb63..2b67d12a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -41,7 +41,7 @@ ] autoapi_type = "python" -autoapi_dirs = ["../../keycloak"] +autoapi_dirs = ["../../src/keycloak"] autoapi_root = "reference" autoapi_keep_files = False autoapi_add_toctree_entry = False diff --git a/tox.ini b/tox.ini index 2726f668..54b2c2b7 100644 --- a/tox.ini +++ b/tox.ini @@ -10,9 +10,9 @@ deps = isort flake8 commands = - black --check --diff keycloak tests docs - isort -c --df keycloak tests docs - flake8 keycloak tests docs + black --check --diff src/keycloak tests docs setup.py + isort -c --df src/keycloak tests docs setup.py + flake8 src/keycloak tests docs setup.py [testenv:apply-check] deps = @@ -20,9 +20,9 @@ deps = isort flake8 commands = - black -C keycloak tests docs - black keycloak tests docs - isort keycloak tests docs + black -C src/keycloak tests docs setup.py + black src/keycloak tests docs setup.py + isort src/keycloak tests docs setup.py [testenv:docs] deps = From 960af199b43efab5f305599892b436ff71c06759 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 22:01:33 +0200 Subject: [PATCH 161/566] fix: escape when get role fails --- src/keycloak/keycloak_admin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index df538597..ffc3cde7 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1361,9 +1361,11 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): """ if skip_exists: - res = self.get_client_role(client_id=client_role_id, role_name=payload["name"]) - if res: + try: + res = self.get_client_role(client_id=client_role_id, role_name=payload["name"]) return res["name"] + except KeycloakGetError: + pass params_path = {"realm-name": self.realm_name, "id": client_role_id} data_raw = self.raw_post( @@ -1480,9 +1482,11 @@ def create_realm_role(self, payload, skip_exists=False): """ if skip_exists: - role = self.get_realm_role(role_name=payload["name"]) - if role is not None: + try: + role = self.get_realm_role(role_name=payload["name"]) return role["name"] + except KeycloakGetError: + pass params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( From b0bc470e33c274ecbe41a774fd37067d146c05ba Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 22:01:49 +0200 Subject: [PATCH 162/566] test: cover the cases with unit tests --- tests/test_keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 6b04af75..29906219 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -718,7 +718,7 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): assert members == list(), members # Test create realm role - role_id = admin.create_realm_role(payload={"name": "test-realm-role"}) + role_id = admin.create_realm_role(payload={"name": "test-realm-role"}, skip_exists=True) assert role_id, role_id with pytest.raises(KeycloakPostError) as err: admin.create_realm_role(payload={"name": "test-realm-role"}) @@ -865,7 +865,7 @@ def test_client_roles(admin: KeycloakAdmin, client: str): # Test create client role client_role_id = admin.create_client_role( - client_role_id=client, payload={"name": "client-role-test"} + client_role_id=client, payload={"name": "client-role-test"}, skip_exists=True ) with pytest.raises(KeycloakPostError) as err: admin.create_client_role(client_role_id=client, payload={"name": "client-role-test"}) From fbbdebb14abf4e8f8a4ac27e48fffd4d3d2c4085 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 22:02:00 +0200 Subject: [PATCH 163/566] docs: fixed the readme examples --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 01d6e0fd..8704e1d4 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ new_user = keycloak_admin.create_user({"email": "example@example.fr", "lastName": "Example", "attributes": { "locale": ["fr"] - }) + }}) # User counter count_users = keycloak_admin.users_count() @@ -226,7 +226,7 @@ role = keycloak_admin.get_client_role(client_id="client_id", role_name="role_nam role_id = keycloak_admin.get_client_role_id(client_id="client_id", role_name="test") # Create client role -keycloak_admin.create_client_role(client_role_id='client_id', {'name': 'roleName', 'clientRole': True}) +keycloak_admin.create_client_role(client_role_id='client_id', payload={'name': 'roleName', 'clientRole': True}) # Assign client role to user. Note that BOTH role_name and role_id appear to be required. keycloak_admin.assign_client_role(client_id="client_id", user_id="user_id", role_id="role_id", role_name="test") From f95a0c055fce0fd20ae52a787dc8239d835ed65e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 19 May 2022 22:15:41 +0200 Subject: [PATCH 164/566] ci: fix tag application --- .github/workflows/publish.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 95198296..531a8e13 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -21,7 +21,7 @@ jobs: - name: Apply the tag version run: | version=${{ github.ref_name }} - sed -i 's/__version__ = .*/__version__ = "'${version:1}'"/' keycloak/_version.py + sed -i 's/__version__ = .*/__version__ = "'${version:1}'"/' src/keycloak/_version.py - name: Run build run: | tox -e build From 8dafb4ec30b7203f2c918a6b52c3e53f19ec72ab Mon Sep 17 00:00:00 2001 From: Merle Nerger Date: Tue, 12 Apr 2022 11:38:16 +0200 Subject: [PATCH 165/566] feat: added UMA-permission request functionality --- README.md | 9 ++ src/keycloak/exceptions.py | 8 ++ src/keycloak/keycloak_openid.py | 76 +++++++++++++++++ src/keycloak/uma_permissions.py | 145 ++++++++++++++++++++++++++++++++ tests/test_uma_permissions.py | 143 +++++++++++++++++++++++++++++++ 5 files changed, 381 insertions(+) create mode 100644 src/keycloak/uma_permissions.py create mode 100644 tests/test_uma_permissions.py diff --git a/README.md b/README.md index 8704e1d4..42a4824c 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,15 @@ keycloak_openid.load_authorization_config("example-authz-config.json") policies = keycloak_openid.get_policies(token['access_token'], method_token_info='decode', key=KEYCLOAK_PUBLIC_KEY) permissions = keycloak_openid.get_permissions(token['access_token'], method_token_info='introspect') +# Get UMA-permissions by token +token = keycloak_openid.token("user", "password") +permissions = keycloak_openid.uma_permissions(token['access_token']) + +# Get auth status for a specific resource and scope by token +token = keycloak_openid.token("user", "password") +auth_status = keycloak_openid.has_uma_access(token['access_token'], "Resource#Scope") + + # KEYCLOAK ADMIN from keycloak import KeycloakAdmin diff --git a/src/keycloak/exceptions.py b/src/keycloak/exceptions.py index a9c1b2b7..925c9379 100644 --- a/src/keycloak/exceptions.py +++ b/src/keycloak/exceptions.py @@ -88,6 +88,14 @@ class KeycloakInvalidTokenError(KeycloakOperationError): pass +class KeycloakPermissionFormatError(KeycloakOperationError): + pass + + +class PermissionDefinitionError(Exception): + pass + + def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): if expected_codes is None: expected_codes = [200, 201, 204] diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 4205b0b5..e4378da4 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -28,6 +28,7 @@ from .authorization import Authorization from .connection import ConnectionManager from .exceptions import ( + KeycloakAuthenticationError, KeycloakAuthorizationConfigError, KeycloakDeprecationError, KeycloakGetError, @@ -35,6 +36,7 @@ KeycloakRPTNotFound, raise_error_from_response, ) +from .uma_permissions import AuthStatus, build_permission_param from .urls_patterns import ( URL_AUTH, URL_CERTS, @@ -47,6 +49,8 @@ URL_WELL_KNOWN, ) +SAME_AS_CLIENT = object() + class KeycloakOpenID: """ @@ -452,3 +456,75 @@ def get_permissions(self, token, method_token_info="introspect", **kwargs): permissions += policy.permissions return list(set(permissions)) + + def uma_permissions( + self, token, permissions, audience=SAME_AS_CLIENT, response_mode="permissions" + ): + """ + The token endpoint is used to retrieve UMA permissions from Keycloak. It can only be + invoked by confidential clients. + + http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint + + + :param token: user token + :param permissions: + :return: permissions list + """ + + if audience is SAME_AS_CLIENT: + audience = self.client_id + + permission = build_permission_param(permissions) + + params_path = {"realm-name": self.realm_name} + payload = { + "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", + "permission": permission, + "response_mode": response_mode, + "audience": audience, + "Authorization": "Bearer " + token, + } + + self.connection.add_param_headers("Authorization", "Bearer " + token) + try: + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) + finally: + self.connection.del_param_headers("Authorization") + + return raise_error_from_response(data_raw, KeycloakGetError) + + def has_uma_access(self, token, permissions): + """ + Get permission by user token + + :param token: user token + :param permissions: dict(resource:scope) + :return: result bool + """ + needed = build_permission_param(permissions) + try: + granted = self.uma_permissions(token, permissions) + except (KeycloakGetError, KeycloakAuthenticationError) as e: + if e.response_code == 403: + return AuthStatus( + is_logged_in=True, is_authorized=False, missing_permissions=needed + ) + elif e.response_code == 401: + return AuthStatus( + is_logged_in=False, is_authorized=False, missing_permissions=needed + ) + raise + + for resource_struct in granted: + resource = resource_struct["rsname"] + scopes = resource_struct.get("scopes", None) + if not scopes: + needed.discard(resource) + continue + for scope in scopes: + needed.discard("{}#{}".format(resource, scope)) + + return AuthStatus( + is_logged_in=True, is_authorized=len(needed) == 0, missing_permissions=needed + ) diff --git a/src/keycloak/uma_permissions.py b/src/keycloak/uma_permissions.py new file mode 100644 index 00000000..5d023dca --- /dev/null +++ b/src/keycloak/uma_permissions.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (C) 2017 Marcos Pereira +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError + + +class UMAPermission: + """A class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + + Usage example: + + >>> r = Resource("Users") + >>> s = Scope("delete") + >>> permission = r(s) + >>> print(permission) + 'Users#delete' + + """ + + def __init__(self, *, resource="", scope=""): + self.resource = resource + self.scope = scope + + def __str__(self): + scope = self.scope + if scope: + scope = "#" + scope + return "{}{}".format(self.resource, scope) + + def __eq__(self, __o: object) -> bool: + return str(self) == str(__o) + + def __repr__(self) -> str: + return self.__str__() + + def __hash__(self) -> int: + return hash(str(self)) + + def __call__(self, *args, resource="", scope="") -> object: + result_resource = self.resource + result_scope = self.scope + + for arg in args: + if not isinstance(arg, UMAPermission): + raise PermissionDefinitionError( + "can't determine if '{}' is a resource or scope".format(arg) + ) + if arg.resource: + result_resource = str(arg.resource) + if arg.scope: + result_scope = str(arg.scope) + + if resource: + result_resource = str(resource) + if scope: + result_scope = str(scope) + + return UMAPermission(resource=result_resource, scope=result_scope) + + +class Resource(UMAPermission): + def __init__(self, resource): + super().__init__(resource=resource) + + +class Scope(UMAPermission): + def __init__(self, scope): + super().__init__(scope=scope) + + +class AuthStatus: + """A class that represents the authorization/login status of a user associated with a token. + This has to evaluate to True if and only if the user is properly authorized for the requested resource.""" + + def __init__(self, is_logged_in, is_authorized, missing_permissions): + self.is_logged_in = is_logged_in + self.is_authorized = is_authorized + self.missing_permissions = missing_permissions + + def __bool__(self): + return self.is_authorized + + def __repr__(self): + return f"AuthStatus(is_authorized={self.is_authorized}, is_logged_in={self.is_logged_in}, missing_permissions={self.missing_permissions})" + + +def build_permission_param(permissions): + """ + Transform permissions to a set, so they are usable for requests + + :param permissions: either str (resource#scope), + iterable[str] (resource#scope), + dict[str,str] (resource: scope), + dict[str,iterable[str]] (resource: scopes) + :return: result bool + """ + if permissions is None or permissions == "": + return set() + if isinstance(permissions, (str, UMAPermission)): + return set((permissions,)) + + try: # treat as dictionary of permissions + result = set() + for resource, scopes in permissions.items(): + if scopes is None: + result.add(resource) + elif isinstance(scopes, (str, UMAPermission)): + result.add("{}#{}".format(resource, scopes)) + else: + for scope in scopes: + if not isinstance(scope, (str, UMAPermission)): + raise KeycloakPermissionFormatError( + "misbuilt permission {}".format(permissions) + ) + result.add("{}#{}".format(resource, scope)) + return result + except AttributeError: + pass + + try: # treat as any other iterable of permissions + return set(permissions) + except TypeError: + pass + raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions)) diff --git a/tests/test_uma_permissions.py b/tests/test_uma_permissions.py new file mode 100644 index 00000000..d26830be --- /dev/null +++ b/tests/test_uma_permissions.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Marcos Pereira +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +import pytest +import re +from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError +from keycloak.uma_permissions import build_permission_param, Resource, Scope + + + +def test_resource_with_scope_obj(): + r = Resource("Resource1") + s = Scope("Scope1") + assert r(s) == "Resource1#Scope1" + + +def test_scope_with_resource_obj(): + r = Resource("Resource1") + s = Scope("Scope1") + assert s(r) == "Resource1#Scope1" + + +def test_resource_scope_str(): + r = Resource("Resource1") + s = "Scope1" + assert r(scope=s) == "Resource1#Scope1" + + +def test_scope_resource_str(): + r = "Resource1" + s = Scope("Scope1") + assert s(resource=r) == "Resource1#Scope1" + + +def test_resource_scope_dict(): + r = Resource("Resource1") + s = {"scope": "Scope1"} + assert r(**s) == "Resource1#Scope1" + + +def test_scope_resource_dict(): + r = {"resource": "Resource1"} + s = Scope("Scope1") + assert s(**r) == "Resource1#Scope1" + + +def test_resource_scope_list(): + r = Resource("Resource1") + s = ["Scope1"] + with pytest.raises(PermissionDefinitionError) as err: + r(*s) + assert err.match("can't determine if 'Scope1' is a resource or scope") + + +def test_build_permission_none(): + assert build_permission_param(None) == set() + + +def test_build_permission_empty_str(): + assert build_permission_param("") == set() + + +def test_build_permission_empty_list(): + assert build_permission_param([]) == set() + + +def test_build_permission_empty_tuple(): + assert build_permission_param(()) == set() + + +def test_build_permission_empty_set(): + assert build_permission_param(set()) == set() + + +def test_build_permission_empty_dict(): + assert build_permission_param({}) == set() + + +def test_build_permission_str(): + assert build_permission_param("resource1") == {"resource1"} + + +def test_build_permission_list_str(): + assert build_permission_param(["res1#scope1", "res1#scope2"]) == {"res1#scope1", "res1#scope2"} + + +def test_build_permission_tuple_str(): + assert build_permission_param(("res1#scope1", "res1#scope2")) == {"res1#scope1", "res1#scope2"} + + +def test_build_permission_set_str(): + assert build_permission_param({"res1#scope1", "res1#scope2"}) == {"res1#scope1", "res1#scope2"} + + +def test_build_permission_tuple_dict_str_str(): + assert build_permission_param({"res1": "scope1"}) == {"res1#scope1"} + + +def test_build_permission_tuple_dict_str_list_str(): + assert build_permission_param({"res1": ["scope1", "scope2"]}) == {"res1#scope1", "res1#scope2"} + + +def test_build_permission_tuple_dict_str_list_str2(): + assert build_permission_param( + {"res1": ["scope1", "scope2"], "res2": ["scope2", "scope3"]} + ) == {"res1#scope1", "res1#scope2", "res2#scope2", "res2#scope3"} + + +def test_build_permission_misbuilt_dict_str_list_list_str(): + with pytest.raises(KeycloakPermissionFormatError) as err: + build_permission_param({"res1": [["scope1", "scope2"]]}) + assert err.match(re.escape("misbuilt permission {'res1': [['scope1', 'scope2']]}")) + + +def test_build_permission_misbuilt_list_list_str(): + with pytest.raises(KeycloakPermissionFormatError) as err: + build_permission_param([["scope1", "scope2"]]) + assert err.match(re.escape("misbuilt permission [['scope1', 'scope2']]")) + + +def test_build_permission_misbuilt_list_set_str(): + with pytest.raises(KeycloakPermissionFormatError) as err: + build_permission_param([{"scope1", "scope2"}]) + assert err.match(re.escape("misbuilt permission [{'scope1', 'scope2'}]")) + + +def test_build_permission_misbuilt_set_set_str(): + with pytest.raises(KeycloakPermissionFormatError) as err: + build_permission_param([{"scope1"}]) + assert err.match(re.escape("misbuilt permission [{'scope1'}]")) From 62e8d667798313a03a811b3d8fb36680de7a247c Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Mon, 23 May 2022 20:39:42 +0200 Subject: [PATCH 166/566] fix: import classes in the base module --- src/keycloak/__init__.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/src/keycloak/__init__.py b/src/keycloak/__init__.py index 62c47a8e..2c7f70f2 100644 --- a/src/keycloak/__init__.py +++ b/src/keycloak/__init__.py @@ -22,7 +22,41 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. from ._version import __version__ +from .connection import ConnectionManager +from .exceptions import ( + KeycloakAuthenticationError, + KeycloakAuthorizationConfigError, + KeycloakConnectionError, + KeycloakDeleteError, + KeycloakDeprecationError, + KeycloakError, + KeycloakGetError, + KeycloakInvalidTokenError, + KeycloakOperationError, + KeycloakPostError, + KeycloakPutError, + KeycloakRPTNotFound, + KeycloakSecretNotFound, +) from .keycloak_admin import KeycloakAdmin from .keycloak_openid import KeycloakOpenID -__all__ = ["KeycloakAdmin", "KeycloakOpenID", "__version__"] +__all__ = [ + "__version__", + "ConnectionManager", + "KeycloakAuthenticationError", + "KeycloakAuthorizationConfigError", + "KeycloakConnectionError", + "KeycloakDeleteError", + "KeycloakDeprecationError", + "KeycloakError", + "KeycloakGetError", + "KeycloakInvalidTokenError", + "KeycloakOperationError", + "KeycloakPostError", + "KeycloakPutError", + "KeycloakRPTNotFound", + "KeycloakSecretNotFound", + "KeycloakAdmin", + "KeycloakOpenID", +] From 8c3b1b62ca4a2feca1a57de461de8a551927f87d Mon Sep 17 00:00:00 2001 From: Jackson Kwok Date: Mon, 23 May 2022 15:14:56 -0400 Subject: [PATCH 167/566] fix: added fixes based on feedback --- README.md | 4 ++ src/keycloak/keycloak_openid.py | 35 ++++++-------- src/keycloak/uma_permissions.py | 83 ++++++++++++++++++++++++--------- tests/test_uma_permissions.py | 43 +++++++++-------- 4 files changed, 101 insertions(+), 64 deletions(-) diff --git a/README.md b/README.md index 42a4824c..692ce483 100644 --- a/README.md +++ b/README.md @@ -114,6 +114,10 @@ permissions = keycloak_openid.get_permissions(token['access_token'], method_toke token = keycloak_openid.token("user", "password") permissions = keycloak_openid.uma_permissions(token['access_token']) +# Get UMA-permissions by token with specific resource and scope requested +token = keycloak_openid.token("user", "password") +permissions = keycloak_openid.uma_permissions(token['access_token'], permissions="Resource#Scope") + # Get auth status for a specific resource and scope by token token = keycloak_openid.token("user", "password") auth_status = keycloak_openid.has_uma_access(token['access_token'], "Resource#Scope") diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index e4378da4..f0b73a6e 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -33,6 +33,7 @@ KeycloakDeprecationError, KeycloakGetError, KeycloakInvalidTokenError, + KeycloakPostError, KeycloakRPTNotFound, raise_error_from_response, ) @@ -49,8 +50,6 @@ URL_WELL_KNOWN, ) -SAME_AS_CLIENT = object() - class KeycloakOpenID: """ @@ -457,55 +456,47 @@ def get_permissions(self, token, method_token_info="introspect", **kwargs): return list(set(permissions)) - def uma_permissions( - self, token, permissions, audience=SAME_AS_CLIENT, response_mode="permissions" - ): + def uma_permissions(self, token, permissions=""): """ + Get UMA permissions by user token with requested permissions + The token endpoint is used to retrieve UMA permissions from Keycloak. It can only be invoked by confidential clients. http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint - :param token: user token - :param permissions: + :param permissions: list of uma permissions list(resource:scope) requested by the user :return: permissions list """ - if audience is SAME_AS_CLIENT: - audience = self.client_id - permission = build_permission_param(permissions) params_path = {"realm-name": self.realm_name} payload = { "grant_type": "urn:ietf:params:oauth:grant-type:uma-ticket", "permission": permission, - "response_mode": response_mode, - "audience": audience, - "Authorization": "Bearer " + token, + "response_mode": "permissions", + "audience": self.client_id, } self.connection.add_param_headers("Authorization", "Bearer " + token) - try: - data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - finally: - self.connection.del_param_headers("Authorization") + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def has_uma_access(self, token, permissions): """ - Get permission by user token + Determine whether user has uma permissions with specified user token :param token: user token - :param permissions: dict(resource:scope) - :return: result bool + :param permissions: list of uma permissions (resource:scope) + :return: auth status """ needed = build_permission_param(permissions) try: granted = self.uma_permissions(token, permissions) - except (KeycloakGetError, KeycloakAuthenticationError) as e: + except (KeycloakPostError, KeycloakAuthenticationError) as e: if e.response_code == 403: return AuthStatus( is_logged_in=True, is_authorized=False, missing_permissions=needed diff --git a/src/keycloak/uma_permissions.py b/src/keycloak/uma_permissions.py index 5d023dca..5653c76a 100644 --- a/src/keycloak/uma_permissions.py +++ b/src/keycloak/uma_permissions.py @@ -38,10 +38,20 @@ class UMAPermission: """ - def __init__(self, *, resource="", scope=""): + def __init__(self, permission=None, resource="", scope=""): self.resource = resource self.scope = scope + if permission: + if not isinstance(permission, UMAPermission): + raise PermissionDefinitionError( + "can't determine if '{}' is a resource or scope".format(permission) + ) + if permission.resource: + self.resource = str(permission.resource) + if permission.scope: + self.scope = str(permission.scope) + def __str__(self): scope = self.scope if scope: @@ -57,41 +67,50 @@ def __repr__(self) -> str: def __hash__(self) -> int: return hash(str(self)) - def __call__(self, *args, resource="", scope="") -> object: + def __call__(self, permission=None, resource="", scope="") -> object: result_resource = self.resource result_scope = self.scope - for arg in args: - if not isinstance(arg, UMAPermission): - raise PermissionDefinitionError( - "can't determine if '{}' is a resource or scope".format(arg) - ) - if arg.resource: - result_resource = str(arg.resource) - if arg.scope: - result_scope = str(arg.scope) - if resource: result_resource = str(resource) if scope: result_scope = str(scope) + if permission: + if not isinstance(permission, UMAPermission): + raise PermissionDefinitionError( + "can't determine if '{}' is a resource or scope".format(permission) + ) + if permission.resource: + result_resource = str(permission.resource) + if permission.scope: + result_scope = str(permission.scope) + return UMAPermission(resource=result_resource, scope=result_scope) class Resource(UMAPermission): + """An UMAPermission Resource class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + """ + def __init__(self, resource): super().__init__(resource=resource) class Scope(UMAPermission): + """An UMAPermission Scope class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + """ + def __init__(self, scope): super().__init__(scope=scope) class AuthStatus: """A class that represents the authorization/login status of a user associated with a token. - This has to evaluate to True if and only if the user is properly authorized for the requested resource.""" + This has to evaluate to True if and only if the user is properly authorized + for the requested resource.""" def __init__(self, is_logged_in, is_authorized, missing_permissions): self.is_logged_in = is_logged_in @@ -102,7 +121,12 @@ def __bool__(self): return self.is_authorized def __repr__(self): - return f"AuthStatus(is_authorized={self.is_authorized}, is_logged_in={self.is_logged_in}, missing_permissions={self.missing_permissions})" + return ( + f"AuthStatus(" + f"is_authorized={self.is_authorized}, " + f"is_logged_in={self.is_logged_in}, " + f"missing_permissions={self.missing_permissions})" + ) def build_permission_param(permissions): @@ -117,29 +141,42 @@ def build_permission_param(permissions): """ if permissions is None or permissions == "": return set() - if isinstance(permissions, (str, UMAPermission)): + if isinstance(permissions, str): return set((permissions,)) + if isinstance(permissions, UMAPermission): + return set((str(permissions),)) try: # treat as dictionary of permissions result = set() for resource, scopes in permissions.items(): + print(f"resource={resource}scopes={scopes}") if scopes is None: result.add(resource) - elif isinstance(scopes, (str, UMAPermission)): + elif isinstance(scopes, str): result.add("{}#{}".format(resource, scopes)) else: - for scope in scopes: - if not isinstance(scope, (str, UMAPermission)): - raise KeycloakPermissionFormatError( - "misbuilt permission {}".format(permissions) - ) - result.add("{}#{}".format(resource, scope)) + try: + for scope in scopes: + if not isinstance(scope, str): + raise KeycloakPermissionFormatError( + "misbuilt permission {}".format(permissions) + ) + result.add("{}#{}".format(resource, scope)) + except TypeError: + raise KeycloakPermissionFormatError( + "misbuilt permission {}".format(permissions) + ) return result except AttributeError: pass try: # treat as any other iterable of permissions - return set(permissions) + result = set() + for permission in permissions: + if not isinstance(permission, (str, UMAPermission)): + raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions)) + result.add(str(permission)) + return result except TypeError: pass raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions)) diff --git a/tests/test_uma_permissions.py b/tests/test_uma_permissions.py index d26830be..09d71472 100644 --- a/tests/test_uma_permissions.py +++ b/tests/test_uma_permissions.py @@ -14,11 +14,12 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . -import pytest import re -from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError -from keycloak.uma_permissions import build_permission_param, Resource, Scope +import pytest + +from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError +from keycloak.uma_permissions import Resource, Scope, build_permission_param def test_resource_with_scope_obj(): @@ -45,24 +46,12 @@ def test_scope_resource_str(): assert s(resource=r) == "Resource1#Scope1" -def test_resource_scope_dict(): - r = Resource("Resource1") - s = {"scope": "Scope1"} - assert r(**s) == "Resource1#Scope1" - - -def test_scope_resource_dict(): - r = {"resource": "Resource1"} - s = Scope("Scope1") - assert s(**r) == "Resource1#Scope1" - - def test_resource_scope_list(): r = Resource("Resource1") s = ["Scope1"] with pytest.raises(PermissionDefinitionError) as err: - r(*s) - assert err.match("can't determine if 'Scope1' is a resource or scope") + r(s) + assert err.match(re.escape("can't determine if '['Scope1']' is a resource or scope")) def test_build_permission_none(): @@ -119,6 +108,16 @@ def test_build_permission_tuple_dict_str_list_str2(): ) == {"res1#scope1", "res1#scope2", "res2#scope2", "res2#scope3"} +def test_build_permission_uma(): + assert build_permission_param(Resource("res1")(Scope("scope1"))) == {"res1#scope1"} + + +def test_build_permission_uma_list(): + assert build_permission_param( + [Resource("res1")(Scope("scope1")), Resource("res1")(Scope("scope2"))] + ) == {"res1#scope1", "res1#scope2"} + + def test_build_permission_misbuilt_dict_str_list_list_str(): with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param({"res1": [["scope1", "scope2"]]}) @@ -127,17 +126,23 @@ def test_build_permission_misbuilt_dict_str_list_list_str(): def test_build_permission_misbuilt_list_list_str(): with pytest.raises(KeycloakPermissionFormatError) as err: - build_permission_param([["scope1", "scope2"]]) + print(build_permission_param([["scope1", "scope2"]])) assert err.match(re.escape("misbuilt permission [['scope1', 'scope2']]")) def test_build_permission_misbuilt_list_set_str(): with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param([{"scope1", "scope2"}]) - assert err.match(re.escape("misbuilt permission [{'scope1', 'scope2'}]")) + assert err.match("misbuilt permission.*") def test_build_permission_misbuilt_set_set_str(): with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param([{"scope1"}]) assert err.match(re.escape("misbuilt permission [{'scope1'}]")) + + +def test_build_permission_misbuilt_dict_non_iterable(): + with pytest.raises(KeycloakPermissionFormatError) as err: + build_permission_param({"res1": 5}) + assert err.match(re.escape("misbuilt permission {'res1': 5}")) From fe160531f478178d159de6daec63966219433438 Mon Sep 17 00:00:00 2001 From: Jackson Kwok Date: Tue, 24 May 2022 16:22:08 -0400 Subject: [PATCH 168/566] fix: allow client_credentials token if username and password not specified --- src/keycloak/keycloak_admin.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index ffc3cde7..b3336e3a 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2566,13 +2566,15 @@ def get_token(self): custom_headers=self.custom_headers, ) - grant_type = ["password"] + grant_type = [] if self.client_secret_key: - grant_type = ["client_credentials"] if self.user_realm_name: self.realm_name = self.user_realm_name + grant_type.append("client_credentials") + elif self.username and self.password: + grant_type.append("password") - if self.username and self.password: + if grant_type: self.token = self.keycloak_openid.token( self.username, self.password, grant_type=grant_type, totp=self.totp ) From 6bfbd0d15fa5981f35e5a6866b3efd62ef0dc968 Mon Sep 17 00:00:00 2001 From: Erik Cederstrand Date: Wed, 25 May 2022 08:32:57 +0200 Subject: [PATCH 169/566] fix: correct spelling of public API method BREAKING CHANGE: Renames `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` --- CHANGELOG.md | 4 ++++ README.md | 2 +- src/keycloak/keycloak_openid.py | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4492c3dc..72b757b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,3 +42,7 @@ All notable changes to this project will be documented in this file. - Add get_idps - Rework group functions + + ## [master] + + * Renamed `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` diff --git a/README.md b/README.md index 692ce483..85e3d341 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", client_secret_key="secret") # Get WellKnow -config_well_know = keycloak_openid.well_know() +config_well_known = keycloak_openid.well_known() # Get Token token = keycloak_openid.token("user", "password") diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index f0b73a6e..e73e9633 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -159,7 +159,7 @@ def _token_info(self, token, method_token_info, **kwargs): return token_info - def well_know(self): + def well_known(self): """The most important endpoint to understand is the well-known configuration endpoint. It lists endpoints and other configuration options relevant to the OpenID Connect implementation in Keycloak. @@ -180,7 +180,7 @@ def auth_url(self, redirect_uri): :return: """ params_path = { - "authorization-endpoint": self.well_know()["authorization_endpoint"], + "authorization-endpoint": self.well_known()["authorization_endpoint"], "client-id": self.client_id, "redirect-uri": redirect_uri, } From 5560799191e3cbcb0a7ec3b549cc1e5e388a7ef5 Mon Sep 17 00:00:00 2001 From: Erik Cederstrand Date: Wed, 25 May 2022 08:36:19 +0200 Subject: [PATCH 170/566] chore: add missing newline --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 24f085bc..0c170799 100644 --- a/.gitignore +++ b/.gitignore @@ -105,4 +105,4 @@ main.py main2.py s3air-authz-config.json .vscode -_build \ No newline at end of file +_build From d9c3326fd1f40c8e453762bf6279f320fffb9304 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 25 May 2022 20:16:18 +0200 Subject: [PATCH 171/566] fix: allow query parameters for users count --- src/keycloak/keycloak_admin.py | 9 +++++++-- tests/test_keycloak_admin.py | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index ffc3cde7..766159ae 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -464,14 +464,19 @@ def create_user(self, payload, exist_ok=False): _last_slash_idx = data_raw.headers["Location"].rindex("/") return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 - def users_count(self): + def users_count(self, query=None): """ User counter + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_users_resource + + :param query: (dict) Query parameters for users count + :return: counter """ + query = query or dict() params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_USERS_COUNT.format(**params_path)) + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USERS_COUNT.format(**params_path), **query) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_id(self, username): diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 29906219..b0f19486 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -184,6 +184,10 @@ def test_users(admin: KeycloakAdmin, realm: str): count = admin.users_count() assert count == 1, count + # Test users count with query + count = admin.users_count(query={"username": "notpresent"}) + assert count == 0 + # Test user groups groups = admin.get_user_groups(user_id=user["id"]) assert len(groups) == 0 From 1029e46a68c5013ae160f69d58f3ae69e371a42f Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 26 May 2022 21:56:55 +0200 Subject: [PATCH 172/566] feat: added new methods for client scopes --- src/keycloak/keycloak_admin.py | 62 +++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 025d9e84..fba28c2f 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2108,6 +2108,22 @@ def get_client_scope(self, client_scope_id): data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_scope_by_name(self, client_scope_name): + """ + Get representation of the client scope identified by the client scope name. + + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes + :param client_scope_name: (str) Name of the client scope + :returns: ClientScopeRepresentation or None + """ + + client_scopes = self.get_client_scopes() + for client_scope in client_scopes: + if client_scope["name"] == client_scope_name: + return client_scope + + return None + def create_client_scope(self, payload, skip_exists=False): """ Create a client scope @@ -2117,16 +2133,24 @@ def create_client_scope(self, payload, skip_exists=False): :param payload: ClientScopeRepresentation :param skip_exists: If true then do not raise an error if client scope already exists - :return: Keycloak server response (ClientScopeRepresentation) + :return: Client scope id """ + if skip_exists: + exists = self.get_client_scope_by_name(client_scope_name=payload["name"]) + + if exists is not None: + return exists["id"] + params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response( + raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client_scope(self, client_scope_id, payload): """ @@ -2146,6 +2170,34 @@ def update_client_scope(self, client_scope_id, payload): ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + def delete_client_scope(self, client_scope_id): + """ + Delete existing client scope. + + ClientScopeRepresentation: + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource + + :param client_scope_id: The id of the client scope + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + + def get_mappers_from_client_scope(self, client_scope_id): + """ + Get a list of all mappers connected to the client scope + + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource + :param client_scope_id: Client scope id + :returns: Keycloak server response (ProtocolMapperRepresentation) + """ + params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def add_mapper_to_client_scope(self, client_scope_id, payload): """ Add a mapper to a client scope @@ -2165,20 +2217,20 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) - def delete_mapper_from_client_scope(self, client_scope_id, protocol_mppaer_id): + def delete_mapper_from_client_scope(self, client_scope_id, protocol_mapper_id): """ Delete a mapper from a client scope https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_delete_mapper :param client_scope_id: The id of the client scope - :param payload: ProtocolMapperRepresentation + :param protocol_mapper_id: Protocol mapper id :return: Keycloak server Response """ params_path = { "realm-name": self.realm_name, "scope-id": client_scope_id, - "protocol-mapper-id": protocol_mppaer_id, + "protocol-mapper-id": protocol_mapper_id, } data_raw = self.raw_delete( From 2e5d75198557b4c6658d1e2ad7e5c23970d9f0d6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 26 May 2022 21:57:08 +0200 Subject: [PATCH 173/566] test: added tests for client scopes --- tests/test_keycloak_admin.py | 139 +++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index b0f19486..58d298ff 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1243,3 +1243,142 @@ def test_sync_users(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakPostError) as err: admin.sync_users(storage_id="does-not-exist", action="triggerFullSync") assert err.match('404: b\'{"error":"Could not find component"}\'') + + +def test_client_scopes(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test get client scopes + res = admin.get_client_scopes() + scope_names = {x["name"] for x in res} + assert len(res) == 10 + assert "email" in scope_names + assert "profile" in scope_names + assert "offline_access" in scope_names + + with pytest.raises(KeycloakGetError) as err: + admin.get_client_scope(client_scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client scope"}\'') + + scope = admin.get_client_scope(client_scope_id=res[0]["id"]) + assert res[0] == scope + + scope = admin.get_client_scope_by_name(client_scope_name=res[0]["name"]) + assert res[0] == scope + + # Test create client scope + res = admin.create_client_scope(payload={"name": "test-scope"}, skip_exists=True) + assert res + res2 = admin.create_client_scope(payload={"name": "test-scope"}, skip_exists=True) + assert res == res2 + with pytest.raises(KeycloakPostError) as err: + admin.create_client_scope(payload={"name": "test-scope"}, skip_exists=False) + assert err.match('409: b\'{"errorMessage":"Client Scope test-scope already exists"}\'') + + # Test update client scope + with pytest.raises(KeycloakPutError) as err: + admin.update_client_scope(client_scope_id="does-not-exist", payload=dict()) + assert err.match('404: b\'{"error":"Could not find client scope"}\'') + + res_update = admin.update_client_scope( + client_scope_id=res, payload={"name": "test-scope-update"} + ) + assert res_update == dict() + admin.get_client_scope(client_scope_id=res)["name"] == "test-scope-update" + + # Test get mappers + mappers = admin.get_mappers_from_client_scope(client_scope_id=res) + assert mappers == list() + + # Test add mapper + with pytest.raises(KeycloakPostError) as err: + admin.add_mapper_to_client_scope(client_scope_id=res, payload=dict()) + assert err.match('404: b\'{"error":"ProtocolMapper provider not found"}\'') + + res_add = admin.add_mapper_to_client_scope( + client_scope_id=res, + payload={ + "name": "test-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + }, + ) + assert res_add == b"" + assert len(admin.get_mappers_from_client_scope(client_scope_id=res)) == 1 + + # Test update mapper + test_mapper = admin.get_mappers_from_client_scope(client_scope_id=res)[0] + with pytest.raises(KeycloakPutError) as err: + admin.update_mapper_in_client_scope( + client_scope_id="does-not-exist", protocol_mapper_id=test_mapper["id"], payload=dict() + ) + assert err.match('404: b\'{"error":"Could not find client scope"}\'') + test_mapper["config"]["user.attribute"] = "test" + res_update = admin.update_mapper_in_client_scope( + client_scope_id=res, + protocol_mapper_id=test_mapper["id"], + payload=test_mapper, + ) + assert res_update == dict() + assert ( + admin.get_mappers_from_client_scope(client_scope_id=res)[0]["config"]["user.attribute"] + == "test" + ) + + # Test delete mapper + res_del = admin.delete_mapper_from_client_scope( + client_scope_id=res, protocol_mapper_id=test_mapper["id"] + ) + assert res_del == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_mapper_from_client_scope( + client_scope_id=res, protocol_mapper_id=test_mapper["id"] + ) + assert err.match('404: b\'{"error":"Model not found"}\'') + + # Test default default scopes + res_defaults = admin.get_default_default_client_scopes() + assert len(res_defaults) == 6 + + with pytest.raises(KeycloakPutError) as err: + admin.add_default_default_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_add = admin.add_default_default_client_scope(scope_id=res) + assert res_add == dict() + assert len(admin.get_default_default_client_scopes()) == 7 + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_default_default_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_del = admin.delete_default_default_client_scope(scope_id=res) + assert res_del == dict() + assert len(admin.get_default_default_client_scopes()) == 6 + + # Test default optional scopes + res_defaults = admin.get_default_optional_client_scopes() + assert len(res_defaults) == 4 + + with pytest.raises(KeycloakPutError) as err: + admin.add_default_optional_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_add = admin.add_default_optional_client_scope(scope_id=res) + assert res_add == dict() + assert len(admin.get_default_optional_client_scopes()) == 5 + + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_default_optional_client_scope(scope_id="does-not-exist") + assert err.match('404: b\'{"error":"Client scope not found"}\'') + + res_del = admin.delete_default_optional_client_scope(scope_id=res) + assert res_del == dict() + assert len(admin.get_default_optional_client_scopes()) == 4 + + # Test client scope delete + res_del = admin.delete_client_scope(client_scope_id=res) + assert res_del == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_scope(client_scope_id=res) + assert err.match('404: b\'{"error":"Could not find client scope"}\'') From 8c8c0e81410977c34bde767d167430ff2bb010ca Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 27 May 2022 08:56:11 +0200 Subject: [PATCH 174/566] fix: use param for update client mapper --- src/keycloak/keycloak_admin.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index fba28c2f..c9712e08 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2343,6 +2343,23 @@ def add_default_optional_client_scope(self, scope_id): ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + def get_mappers_from_client(self, client_id): + """ + List of all client mappers. + + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocolmapperrepresentation + + :param client_id: Client id + :returns: KeycloakServerResponse (list of ProtocolMapperRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_PROTOCOL_MAPPERS.format(**params_path) + ) + + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[200]) + def add_mapper_to_client(self, client_id, payload): """ Add a mapper to a client @@ -2373,7 +2390,7 @@ def update_client_mapper(self, client_id, mapper_id, payload): params_path = { "realm-name": self.realm_name, - "id": self.client_id, + "id": client_id, "protocol-mapper-id": mapper_id, } From dc1c4be115549a026abd562f0019b57483fc0824 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 27 May 2022 08:56:30 +0200 Subject: [PATCH 175/566] test: added missing tests for clients --- tests/test_keycloak_admin.py | 69 ++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 58d298ff..c58de87b 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -573,6 +573,39 @@ def test_clients(admin: KeycloakAdmin, realm: str): admin.update_client(client_id="does-not-exist", payload={"name": "test-client-change"}) assert err.match('404: b\'{"error":"Could not find client"}\'') + # Test client mappers + res = admin.get_mappers_from_client(client_id=client_id) + assert len(res) == 0 + + with pytest.raises(KeycloakPostError) as err: + admin.add_mapper_to_client(client_id="does-not-exist", payload=dict()) + assert err.match('404: b\'{"error":"Could not find client"}\'') + + res = admin.add_mapper_to_client( + client_id=client_id, + payload={ + "name": "test-mapper", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + }, + ) + assert res == b"" + assert len(admin.get_mappers_from_client(client_id=client_id)) == 1 + + mapper = admin.get_mappers_from_client(client_id=client_id)[0] + with pytest.raises(KeycloakPutError) as err: + admin.update_client_mapper(client_id=client_id, mapper_id="does-not-exist", payload=dict()) + assert err.match('404: b\'{"error":"Model not found"}\'') + mapper["config"]["user.attribute"] = "test" + res = admin.update_client_mapper(client_id=client_id, mapper_id=mapper["id"], payload=mapper) + assert res == dict() + + res = admin.remove_client_mapper(client_id=client_id, client_mapper_id=mapper["id"]) + assert res == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.remove_client_mapper(client_id=client_id, client_mapper_id=mapper["id"]) + assert err.match('404: b\'{"error":"Model not found"}\'') + # Test authz auth_client_id = admin.create_client( payload={ @@ -703,6 +736,42 @@ def test_clients(admin: KeycloakAdmin, realm: str): admin.delete_client(client_id=auth_client_id) assert err.match('404: b\'{"error":"Could not find client"}\'') + # Test client credentials + admin.create_client( + payload={ + "name": "test-confidential", + "enabled": True, + "protocol": "openid-connect", + "publicClient": False, + "redirectUris": ["http://localhost/*"], + "webOrigins": ["+"], + "clientId": "test-confidential", + "secret": "test-secret", + "clientAuthenticatorType": "client-secret", + } + ) + with pytest.raises(KeycloakGetError) as err: + admin.get_client_secrets(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + secrets = admin.get_client_secrets( + client_id=admin.get_client_id(client_name="test-confidential") + ) + assert secrets == {"type": "secret", "value": "test-secret"} + + with pytest.raises(KeycloakPostError) as err: + admin.generate_client_secrets(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + res = admin.generate_client_secrets( + client_id=admin.get_client_id(client_name="test-confidential") + ) + assert res + assert ( + admin.get_client_secrets(client_id=admin.get_client_id(client_name="test-confidential")) + == res + ) + def test_realm_roles(admin: KeycloakAdmin, realm: str): admin.realm_name = realm From e56889e5dbbe9208a1ea76d5bb53ee4b039ee8f6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 27 May 2022 14:12:05 +0200 Subject: [PATCH 176/566] fix: fixed components bugs --- src/keycloak/keycloak_admin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index c9712e08..ab0dad64 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2461,6 +2461,7 @@ def get_components(self, query=None): :param query: Query parameters (optional) :return: components list """ + query = query or dict() params_path = {"realm-name": self.realm_name} data_raw = self.raw_get( urls_patterns.URL_ADMIN_COMPONENTS.format(**params_path), data=None, **query @@ -2475,15 +2476,15 @@ def create_component(self, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :param payload: ComponentRepresentation - - :return: UserRepresentation + :return: Component id """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_post( urls_patterns.URL_ADMIN_COMPONENTS.format(**params_path), data=json.dumps(payload) ) - return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + _last_slash_idx = data_raw.headers["Location"].rindex("/") + return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def get_component(self, component_id): """ From 73ff9785a3559d87e8018d446748f2244675874e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 27 May 2022 14:12:18 +0200 Subject: [PATCH 177/566] test: added components tests --- tests/test_keycloak_admin.py | 50 ++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index c58de87b..1c91d949 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1451,3 +1451,53 @@ def test_client_scopes(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakDeleteError) as err: admin.delete_client_scope(client_scope_id=res) assert err.match('404: b\'{"error":"Could not find client scope"}\'') + + +def test_components(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + # Test get components + res = admin.get_components() + assert len(res) == 12 + + with pytest.raises(KeycloakGetError) as err: + admin.get_component(component_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find component"}\'') + + res_get = admin.get_component(component_id=res[0]["id"]) + assert res_get == res[0] + + # Test create component + with pytest.raises(KeycloakPostError) as err: + admin.create_component(payload={"bad": "dict"}) + assert err.match('400: b\'{"error":"Unrecognized field') + + res = admin.create_component( + payload={ + "name": "Test Component", + "providerId": "max-clients", + "providerType": "org.keycloak.services.clientregistration." + + "policy.ClientRegistrationPolicy", + "config": {"max-clients": ["1000"]}, + } + ) + assert res + assert admin.get_component(component_id=res)["name"] == "Test Component" + + # Test update component + component = admin.get_component(component_id=res) + component["name"] = "Test Component Update" + + with pytest.raises(KeycloakPutError) as err: + admin.update_component(component_id="does-not-exist", payload=dict()) + assert err.match('404: b\'{"error":"Could not find component"}\'') + res_upd = admin.update_component(component_id=res, payload=component) + assert res_upd == dict() + assert admin.get_component(component_id=res)["name"] == "Test Component Update" + + # Test delete component + res_del = admin.delete_component(component_id=res) + assert res_del == dict() + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_component(component_id=res) + assert err.match('404: b\'{"error":"Could not find component"}\'') From e95649a93ce7c058ca60b38f41fa65ff441c55ef Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 27 May 2022 22:10:12 +0200 Subject: [PATCH 178/566] fix: fixed bugs in events methods --- src/keycloak/keycloak_admin.py | 3 ++- src/keycloak/urls_patterns.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index ab0dad64..d754cd40 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2551,6 +2551,7 @@ def get_events(self, query=None): :return: events list """ + query = query or dict() params_path = {"realm-name": self.realm_name} data_raw = self.raw_get( urls_patterns.URL_ADMIN_EVENTS.format(**params_path), data=None, **query @@ -2568,7 +2569,7 @@ def set_events(self, payload): """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_put( - urls_patterns.URL_ADMIN_EVENTS.format(**params_path), data=json.dumps(payload) + urls_patterns.URL_ADMIN_EVENTS_CONFIG.format(**params_path), data=json.dumps(payload) ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 071c733a..d836ed4a 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -166,4 +166,5 @@ ) URL_ADMIN_EVENTS = "admin/realms/{realm-name}/events" +URL_ADMIN_EVENTS_CONFIG = URL_ADMIN_EVENTS + "/config" URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" From 02625a8af479168effb537f4b55b0bab8242d04b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 27 May 2022 22:10:33 +0200 Subject: [PATCH 179/566] test: completed admin unit tests --- tests/test_keycloak_admin.py | 175 +++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 1c91d949..d927aae2 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -4,6 +4,7 @@ from keycloak import KeycloakAdmin from keycloak.connection import ConnectionManager from keycloak.exceptions import ( + KeycloakAuthenticationError, KeycloakDeleteError, KeycloakGetError, KeycloakPostError, @@ -54,6 +55,59 @@ def test_keycloak_admin_init(env): assert admin.auto_refresh_token == list(), admin.auto_refresh_token assert admin.user_realm_name is None, admin.user_realm_name assert admin.custom_headers is None, admin.custom_headers + assert admin.token + + admin = KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + realm_name=None, + user_realm_name="master", + ) + assert admin.token + admin = KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=env.KEYCLOAK_ADMIN, + password=env.KEYCLOAK_ADMIN_PASSWORD, + realm_name=None, + user_realm_name=None, + ) + assert admin.token + + admin.create_realm(payload={"realm": "authz", "enabled": True}) + admin.realm_name = "authz" + admin.create_client( + payload={ + "name": "authz-client", + "clientId": "authz-client", + "authorizationServicesEnabled": True, + "serviceAccountsEnabled": True, + "clientAuthenticatorType": "client-secret", + "directAccessGrantsEnabled": False, + "enabled": True, + "implicitFlowEnabled": False, + "publicClient": False, + } + ) + secret = admin.generate_client_secrets(client_id=admin.get_client_id("authz-client")) + assert KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + user_realm_name="authz", + client_id="authz-client", + client_secret_key=secret["value"], + ).token + admin.delete_realm(realm_name="authz") + + assert ( + KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + username=None, + password=None, + client_secret_key=None, + custom_headers={"custom": "header"}, + ).token + is None + ) def test_realms(admin: KeycloakAdmin): @@ -606,6 +660,14 @@ def test_clients(admin: KeycloakAdmin, realm: str): admin.remove_client_mapper(client_id=client_id, client_mapper_id=mapper["id"]) assert err.match('404: b\'{"error":"Model not found"}\'') + # Test client sessions + with pytest.raises(KeycloakGetError) as err: + admin.get_client_all_sessions(client_id="does-not-exist") + assert err.match('404: b\'{"error":"Could not find client"}\'') + + assert admin.get_client_all_sessions(client_id=client_id) == list() + assert admin.get_client_sessions_stats() == list() + # Test authz auth_client_id = admin.create_client( payload={ @@ -1501,3 +1563,116 @@ def test_components(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakDeleteError) as err: admin.delete_component(component_id=res) assert err.match('404: b\'{"error":"Could not find component"}\'') + + +def test_keys(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + assert set(admin.get_keys()["active"].keys()) == {"AES", "HS256", "RS256", "RSA-OAEP"} + assert {k["algorithm"] for k in admin.get_keys()["keys"]} == { + "HS256", + "RSA-OAEP", + "AES", + "RS256", + } + + +def test_events(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + + events = admin.get_events() + assert events == list() + + with pytest.raises(KeycloakPutError) as err: + admin.set_events(payload={"bad": "conf"}) + assert err.match('400: b\'{"error":"Unrecognized field') + + res = admin.set_events(payload={"adminEventsDetailsEnabled": True, "adminEventsEnabled": True}) + assert res == dict() + + admin.create_client(payload={"name": "test", "clientId": "test"}) + + events = admin.get_events() + assert events == list() + + +def test_auto_refresh(admin: KeycloakAdmin, realm: str): + # Test get refresh + admin.auto_refresh_token = list() + admin.connection = ConnectionManager( + base_url=admin.server_url, + headers={"Authorization": "Bearer bad", "Content-Type": "application/json"}, + timeout=60, + verify=admin.verify, + ) + + with pytest.raises(KeycloakAuthenticationError) as err: + admin.get_realm(realm_name=realm) + assert err.match('401: b\'{"error":"HTTP 401 Unauthorized"}\'') + + admin.auto_refresh_token = ["get"] + del admin.token["refresh_token"] + assert admin.get_realm(realm_name=realm) + + # Test bad refresh token + admin.connection = ConnectionManager( + base_url=admin.server_url, + headers={"Authorization": "Bearer bad", "Content-Type": "application/json"}, + timeout=60, + verify=admin.verify, + ) + admin.token["refresh_token"] = "bad" + with pytest.raises(KeycloakGetError) as err: + admin.get_realm(realm_name="test-refresh") + assert err.match( + '400: b\'{"error":"invalid_grant","error_description":"Invalid refresh token"}\'' + ) + admin.realm_name = "master" + admin.get_token() + admin.realm_name = realm + + # Test post refresh + admin.connection = ConnectionManager( + base_url=admin.server_url, + headers={"Authorization": "Bearer bad", "Content-Type": "application/json"}, + timeout=60, + verify=admin.verify, + ) + with pytest.raises(KeycloakAuthenticationError) as err: + admin.create_realm(payload={"realm": "test-refresh"}) + assert err.match('401: b\'{"error":"HTTP 401 Unauthorized"}\'') + + admin.auto_refresh_token = ["get", "post"] + admin.realm_name = "master" + admin.user_logout(user_id=admin.get_user_id(username=admin.username)) + assert admin.create_realm(payload={"realm": "test-refresh"}) == b"" + admin.realm_name = realm + + # Test update refresh + admin.connection = ConnectionManager( + base_url=admin.server_url, + headers={"Authorization": "Bearer bad", "Content-Type": "application/json"}, + timeout=60, + verify=admin.verify, + ) + with pytest.raises(KeycloakAuthenticationError) as err: + admin.update_realm(realm_name="test-refresh", payload={"accountTheme": "test"}) + assert err.match('401: b\'{"error":"HTTP 401 Unauthorized"}\'') + + admin.auto_refresh_token = ["get", "post", "put"] + assert ( + admin.update_realm(realm_name="test-refresh", payload={"accountTheme": "test"}) == dict() + ) + + # Test delete refresh + admin.connection = ConnectionManager( + base_url=admin.server_url, + headers={"Authorization": "Bearer bad", "Content-Type": "application/json"}, + timeout=60, + verify=admin.verify, + ) + with pytest.raises(KeycloakAuthenticationError) as err: + admin.delete_realm(realm_name="test-refresh") + assert err.match('401: b\'{"error":"HTTP 401 Unauthorized"}\'') + + admin.auto_refresh_token = ["get", "post", "put", "delete"] + assert admin.delete_realm(realm_name="test-refresh") == dict() From 94ef46b29b6f6d400bc4dc07702b931284781b0b Mon Sep 17 00:00:00 2001 From: Erik Cederstrand Date: Tue, 31 May 2022 12:53:49 +0200 Subject: [PATCH 180/566] feat: Support Token Exchange. Fixes #305 --- CHANGELOG.md | 1 + README.md | 3 +++ src/keycloak/keycloak_openid.py | 24 ++++++++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72b757b9..4916c273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,3 +46,4 @@ All notable changes to this project will be documented in this file. ## [master] * Renamed `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` + * Add `KeycloakOpenID.token_exchange` to support Token Exchange diff --git a/README.md b/README.md index 85e3d341..d3572f57 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,9 @@ config_well_known = keycloak_openid.well_known() token = keycloak_openid.token("user", "password") token = keycloak_openid.token("user", "password", totp="012345") +# Get token using Token Exchange +token = keycloak_openid.exchange_token(token['access_token'], "my_client", "other_client", "some_user") + # Get Userinfo userinfo = keycloak_openid.userinfo(token['access_token']) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index e73e9633..a1f1f0a2 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -254,6 +254,30 @@ def refresh_token(self, refresh_token, grant_type=["refresh_token"]): data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakGetError) + def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict: + """ + Use a token to obtain an entirely different token. See + https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange + + :param token: + :param client_id: + :param audience: + :param subject: + :return: + """ + params_path = {"realm-name": self.realm_name} + payload = { + "grant_type": ["urn:ietf:params:oauth:grant-type:token-exchange"], + "client_id": client_id, + "subject_token": token, + "requested_token_type": "urn:ietf:params:oauth:token-type:refresh_token", + "audience": audience, + "requested_subject": subject, + } + payload = self._add_secret_key(payload) + data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) + return raise_error_from_response(data_raw, KeycloakGetError) + def userinfo(self, token): """ The userinfo endpoint returns standard claims about the authenticated user, From b6990276c36fab64ba0a5406a6f22e68c51868c2 Mon Sep 17 00:00:00 2001 From: Erik Cederstrand Date: Tue, 31 May 2022 13:02:22 +0200 Subject: [PATCH 181/566] ci: fix docs generation --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2b67d12a..b60a1c9c 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -77,7 +77,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. From 9a13f67fea8b69991ae0cd1ab1cb5dd205290fcc Mon Sep 17 00:00:00 2001 From: Bruno Bonfils Date: Tue, 31 May 2022 13:34:13 +0200 Subject: [PATCH 182/566] feat: Add get_idp_mappers, fix #329 --- src/keycloak/keycloak_admin.py | 14 ++++++++++++++ tests/test_keycloak_admin.py | 6 ++++++ 2 files changed, 20 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index d754cd40..e31102a6 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -413,6 +413,20 @@ def add_mapper_to_idp(self, idp_alias, payload): ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + def get_idp_mappers(self, idp_alias): + """ + Returns a list of ID Providers mappers + + IdentityProviderMapperRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getmappers + + :param: idp_alias: alias for Idp to fetch mappers + :return: array IdentityProviderMapperRepresentation + """ + params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} + data_raw = self.raw_get(urls_patterns.URL_ADMIN_IDP_MAPPERS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + def get_idps(self): """ Returns a list of ID Providers, diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index d927aae2..87c093d2 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -336,6 +336,12 @@ def test_idps(admin: KeycloakAdmin, realm: str): admin.add_mapper_to_idp(idp_alias="does-no-texist", payload=dict()) assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') + # Test IdP mappers listing + idp_mappers = admin.get_idp_mappers( + idp_alias="github", + ) + assert len(idp_mappers) == 1 + # Test delete res = admin.delete_idp(idp_alias="github") assert res == dict(), res From f3de47e1b374a1035d59c1fbcab54af12069982a Mon Sep 17 00:00:00 2001 From: Bruno Bonfils Date: Thu, 2 Jun 2022 11:37:29 +0200 Subject: [PATCH 183/566] feat: Add update_mapper_in_idp --- src/keycloak/keycloak_admin.py | 27 ++++++++++++++++++++++++++- src/keycloak/urls_patterns.py | 1 + tests/test_keycloak_admin.py | 21 ++++++++++++++++----- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index e31102a6..57dfb47b 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -413,6 +413,31 @@ def add_mapper_to_idp(self, idp_alias, payload): ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + def update_mapper_in_idp(self, idp_alias, mapper_id, payload): + """ + Update an IdP mapper + + IdentityProviderMapperRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_update + + :param: idp_alias: alias for Idp to fetch mappers + :param: mapper_id: Mapper Id to update + :param: payload: IdentityProviderMapperRepresentation + :return: Http response + """ + params_path = { + "realm-name": self.realm_name, + "idp-alias": idp_alias, + "mapper-id": mapper_id, + } + + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_IDP_MAPPER_UPDATE.format(**params_path), + data=json.dumps(payload), + ) + + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + def get_idp_mappers(self, idp_alias): """ Returns a list of ID Providers mappers @@ -2208,7 +2233,7 @@ def get_mappers_from_client_scope(self, client_scope_id): """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_get( - urls_patterns.URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_SCOPES_ADD_MAPPER.format(**params_path) ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index d836ed4a..16f348a2 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -119,6 +119,7 @@ URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_IDPS = "admin/realms/{realm-name}/identity-provider/instances" URL_ADMIN_IDP_MAPPERS = "admin/realms/{realm-name}/identity-provider/instances/{idp-alias}/mappers" +URL_ADMIN_IDP_MAPPER_UPDATE = URL_ADMIN_IDP_MAPPERS + "/{mapper-id}" URL_ADMIN_IDP = "admin/realms//{realm-name}/identity-provider/instances/{alias}" URL_ADMIN_REALM_ROLES_ROLE_BY_NAME = "admin/realms/{realm-name}/roles/{role-name}" URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE = ( diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 87c093d2..a2cd5d7b 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -337,10 +337,23 @@ def test_idps(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') # Test IdP mappers listing - idp_mappers = admin.get_idp_mappers( + idp_mappers = admin.get_idp_mappers(idp_alias="github") + assert len(idp_mappers) == 1 + + # Test IdP mapper update + res = admin.update_mapper_in_idp( idp_alias="github", + mapper_id=idp_mappers[0]["id"], + # For an obscure reason, keycloak expect all fields + payload={ + "id": idp_mappers[0]["id"], + "identityProviderAlias": "github-alias", + "identityProviderMapper": "github-user-attribute-mapper", + "name": "test", + "config": idp_mappers[0]["config"], + }, ) - assert len(idp_mappers) == 1 + assert res == dict(), res # Test delete res = admin.delete_idp(idp_alias="github") @@ -1452,9 +1465,7 @@ def test_client_scopes(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"Could not find client scope"}\'') test_mapper["config"]["user.attribute"] = "test" res_update = admin.update_mapper_in_client_scope( - client_scope_id=res, - protocol_mapper_id=test_mapper["id"], - payload=test_mapper, + client_scope_id=res, protocol_mapper_id=test_mapper["id"], payload=test_mapper ) assert res_update == dict() assert ( From 35f1f4630b23a7ddbd3df9cd030ad1e83db1389d Mon Sep 17 00:00:00 2001 From: Bruno Bonfils Date: Thu, 2 Jun 2022 11:47:19 +0200 Subject: [PATCH 184/566] test: Make curl silent --- test_keycloak_init.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index bee88308..bd4c30ac 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -14,7 +14,7 @@ function keycloak_start() { echo "Starting keycloak docker container" docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev SECONDS=0 - until curl localhost:$KEYCLOAK_PORT; do + until curl --silent --output /dev/null localhost:$KEYCLOAK_PORT; do sleep 5; if [ ${SECONDS} -gt 180 ]; then echo "Timeout exceeded"; From bcdf1b825bd1c7e9c0c0cdf91bcb957ac538cf4a Mon Sep 17 00:00:00 2001 From: Bruno Bonfils Date: Fri, 3 Jun 2022 14:28:08 +0200 Subject: [PATCH 185/566] feat: Add update_idp --- src/keycloak/keycloak_admin.py | 16 ++++++++++++++++ tests/test_keycloak_admin.py | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 57dfb47b..7f826792 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -397,6 +397,22 @@ def create_idp(self, payload): ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + def update_idp(self, idp_alias, payload): + """ + Update an ID Provider + + IdentityProviderRepresentation + https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_identity_providers_resource + + :param: alias: alias for IdP to update + :param: payload: The IdentityProviderRepresentation + """ + params_path = {"realm-name": self.realm_name, "alias": idp_alias} + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_IDP.format(**params_path), data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + def add_mapper_to_idp(self, idp_alias, payload): """ Create an ID Provider, diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index a2cd5d7b..ad4e2cea 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -320,6 +320,11 @@ def test_idps(admin: KeycloakAdmin, realm: str): assert len(idps) == 1 assert "github" == idps[0]["alias"] + # Test IdP update + res = admin.update_idp(idp_alias="github", payload=idps[0]) + + assert res == {}, res + # Test adding a mapper res = admin.add_mapper_to_idp( idp_alias="github", From 667d1e088e352f4262cc5cc35d6f69b337d9d513 Mon Sep 17 00:00:00 2001 From: Erik Cederstrand Date: Mon, 13 Jun 2022 12:50:17 +0200 Subject: [PATCH 186/566] feat: support token exchange config via admin API This adds support for the basic endpoints necessary to configure client-to-client token exchange. The /authz API is lacking official documentation. Basic docs added to docstrings instead. --- src/keycloak/keycloak_admin.py | 143 +++++++++++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 11 +++ 2 files changed, 154 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 7f826792..942edc95 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2774,3 +2774,146 @@ def get_client_sessions_stats(self): params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + + def get_client_management_permissions(self, client_id): + """ + Get management permissions for a client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_MANAGEMENT_PERMISSIONS.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_client_management_permissions(self, payload, client_id): + """ + Update management permissions for a client. + + ManagementPermissionReference + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_managementpermissionreference + + :param payload: ManagementPermissionReference + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response + + + Payload example:: + + payload={ + "enabled": true + } + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT_MANAGEMENT_PERMISSIONS.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[200]) + + def get_client_authz_policy_scopes(self, client_id, policy_id): + """ + Get scopes for a given policy. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :param policy_id: No Document + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": client_id, "policy-id": policy_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICY_SCOPES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_client_authz_policy_resources(self, client_id, policy_id): + """ + Get resources for a given policy. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :param policy_id: No Document + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": client_id, "policy-id": policy_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICY_RESOURCES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_client_authz_scope_permission(self, client_id, scope_id): + """ + Get permissions for a given scope. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :param scope_id: No Document + :return: Keycloak server response + """ + params_path = {"realm-name": self.realm_name, "id": client_id, "scope-id": scope_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPE_PERMISSION.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_client_authz_scope_permission(self, payload, client_id, scope_id): + """ + Update permissions for a given scope. + + :param payload: No Document + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :param scope_id: No Document + :return: Keycloak server response + + + Payload example:: + + payload={ + "id": scope_id, + "name": "My Permission Name", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "resources": [some_resource_id], + "scopes": [some_scope_id], + "policies": [some_policy_id], + } + """ + params_path = {"realm-name": self.realm_name, "id": client_id, "scope-id": scope_id} + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPE_PERMISSION.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[201]) + + def create_client_authz_client_policy(self, payload, client_id): + """ + Create a new policy for a given client. + + :param payload: No Document + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response (RoleRepresentation) + + + Payload example:: + + payload={ + "type": "client", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "My Policy", + "clients": [other_client_id], + } + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 16f348a2..3ec134ca 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -87,6 +87,7 @@ URL_ADMIN_CLIENT_ROLES_COMPOSITE_CLIENT_ROLE = URL_ADMIN_CLIENT_ROLE + "/composites" URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_ROLE_GROUPS = URL_ADMIN_CLIENT + "/roles/{role-name}/groups" +URL_ADMIN_CLIENT_MANAGEMENT_PERMISSIONS = URL_ADMIN_CLIENT + "/management/permissions" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" @@ -101,6 +102,16 @@ URL_ADMIN_CLIENT_AUTHZ_RESOURCE_BASED_PERMISSION = ( URL_ADMIN_CLIENT + "/authz/resource-server/permission/resource?max=-1" ) +URL_ADMIN_CLIENT_AUTHZ_POLICY_SCOPES = ( + URL_ADMIN_CLIENT + "/authz/resource-server/policy/{policy-id}/scopes" +) +URL_ADMIN_CLIENT_AUTHZ_POLICY_RESOURCES = ( + URL_ADMIN_CLIENT + "/authz/resource-server/policy/{policy-id}/resources" +) +URL_ADMIN_CLIENT_AUTHZ_SCOPE_PERMISSION = ( + URL_ADMIN_CLIENT + "/authz/resource-server/permission/scope/{scope-id}" +) +URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY = URL_ADMIN_CLIENT + "/authz/resource-server/policy/client" URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER = URL_ADMIN_CLIENT + "/service-account-user" URL_ADMIN_CLIENT_CERTS = URL_ADMIN_CLIENT + "/certificates/{attr}" From 39706bcc68df8ad3c54e4611dfbb95fd80ebb3ed Mon Sep 17 00:00:00 2001 From: Erik Cederstrand Date: Mon, 13 Jun 2022 14:17:38 +0200 Subject: [PATCH 187/566] ci: add test case for token exchange setup --- tests/test_keycloak_admin.py | 80 ++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index ad4e2cea..74cdc14d 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1176,6 +1176,86 @@ def test_client_roles(admin: KeycloakAdmin, client: str): assert err.match('404: b\'{"error":"Could not find role"}\'') +def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): + # Test enabling token exchange between two confidential clients + admin.realm_name = realm + + # Create test clients + source_client_id = admin.create_client( + payload={"name": "Source Client", "clientId": "source-client"} + ) + target_client_id = admin.create_client( + payload={"name": "Target Client", "clientId": "target-client"} + ) + for c in admin.get_clients(): + if c["clientId"] == "realm-management": + realm_management_id = c["id"] + break + else: + raise AssertionError("Missing realm management client") + + # Enable permissions on the Superset client + admin.update_client_management_permissions( + payload={"enabled": True}, client_id=target_client_id + ) + + # Fetch various IDs and strings needed when creating the permission + token_exchange_permission_id = admin.get_client_management_permissions( + client_id=target_client_id + )["scopePermissions"]["token-exchange"] + scopes = admin.get_client_authz_policy_scopes( + client_id=realm_management_id, policy_id=token_exchange_permission_id + ) + + for s in scopes: + if s["name"] == "token-exchange": + token_exchange_scope_id = s["id"] + break + else: + raise AssertionError("Missing token-exchange scope") + + resources = admin.get_client_authz_policy_resources( + client_id=realm_management_id, policy_id=token_exchange_permission_id + ) + for r in resources: + if r["name"] == f"client.resource.{target_client_id}": + token_exchange_resource_id = r["_id"] + break + else: + raise AssertionError("Missing client resource") + + # Create a client policy for source client + client_policy_id = admin.create_client_authz_client_policy( + payload={ + "type": "client", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "name": "Exchange source client token with target client token", + "clients": [source_client_id], + }, + client_id=realm_management_id, + )["id"] + + # Update permissions on the target client to reference this policy + permission_name = admin.get_client_authz_scope_permission( + client_id=realm_management_id, scope_id=token_exchange_permission_id + )["name"] + admin.update_client_authz_scope_permission( + payload={ + "id": token_exchange_permission_id, + "name": permission_name, + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "resources": [token_exchange_resource_id], + "scopes": [token_exchange_scope_id], + "policies": [client_policy_id], + }, + client_id=realm_management_id, + scope_id=token_exchange_permission_id, + ) + + def test_email(admin: KeycloakAdmin, user: str): # Emails will fail as we don't have SMTP test setup with pytest.raises(KeycloakPutError) as err: From 2f212c1350b0e87d34ae3bdd80be599327abb154 Mon Sep 17 00:00:00 2001 From: Erik Cederstrand Date: Thu, 16 Jun 2022 10:43:55 +0200 Subject: [PATCH 188/566] feat: Allow fetching existing policies before calling create_client_authz_client_policy() --- src/keycloak/keycloak_admin.py | 14 ++++++++++++++ tests/test_keycloak_admin.py | 10 +++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 942edc95..099f9fc2 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2891,6 +2891,20 @@ def update_client_authz_scope_permission(self, payload, client_id, scope_id): ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[201]) + def get_client_authz_client_policies(self, client_id): + """ + Get policies for a given client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :return: Keycloak server response (RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY.format(**params_path), + ) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + def create_client_authz_client_policy(self, payload, client_id): """ Create a new policy for a given client. diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 74cdc14d..6f33e03b 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1225,16 +1225,24 @@ def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): raise AssertionError("Missing client resource") # Create a client policy for source client + policy_name = "Exchange source client token with target client token" client_policy_id = admin.create_client_authz_client_policy( payload={ "type": "client", "logic": "POSITIVE", "decisionStrategy": "UNANIMOUS", - "name": "Exchange source client token with target client token", + "name": policy_name, "clients": [source_client_id], }, client_id=realm_management_id, )["id"] + policies = admin.get_client_authz_client_policies(client_id=realm_management_id) + for policy in policies: + if policy["name"] == policy_name: + assert policy["clients"] == [source_client_id] + break + else: + raise AssertionError("Missing client policy") # Update permissions on the target client to reference this policy permission_name = admin.get_client_authz_scope_permission( From d2a6262d6152106b6d3f1a2ba5e8b99322966ffe Mon Sep 17 00:00:00 2001 From: Fredrik Lindner Date: Wed, 22 Jun 2022 11:57:28 +0200 Subject: [PATCH 189/566] feat: Ability to set custom timeout for KCOpenId and KCAdmin --- src/keycloak/keycloak_admin.py | 3 +++ src/keycloak/keycloak_openid.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 099f9fc2..ed868777 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -88,6 +88,7 @@ def __init__( custom_headers=None, user_realm_name=None, auto_refresh_token=None, + timeout=60, ): self.server_url = server_url self.username = username @@ -100,6 +101,7 @@ def __init__( self.auto_refresh_token = auto_refresh_token or [] self.user_realm_name = user_realm_name self.custom_headers = custom_headers + self.timeout = timeout # Get token Admin self.get_token() @@ -2695,6 +2697,7 @@ def get_token(self): verify=self.verify, client_secret_key=self.client_secret_key, custom_headers=self.custom_headers, + timeout=self.timeout, ) grant_type = [] diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index a1f1f0a2..7216b5d1 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -73,13 +73,14 @@ def __init__( verify=True, custom_headers=None, proxies=None, + timeout=60, ): self.client_id = client_id self.client_secret_key = client_secret_key self.realm_name = realm_name headers = custom_headers if custom_headers is not None else dict() self.connection = ConnectionManager( - base_url=server_url, headers=headers, timeout=60, verify=verify, proxies=proxies + base_url=server_url, headers=headers, timeout=timeout, verify=verify, proxies=proxies ) self.authorization = Authorization() From e7152e5c7403c037deec1b8f12e60a2909b70597 Mon Sep 17 00:00:00 2001 From: Chuma Umenze Date: Mon, 27 Jun 2022 14:38:27 +0100 Subject: [PATCH 190/566] build: use poetry for package management --- .github/workflows/publish.yaml | 2 +- CONTRIBUTING.md | 25 +- Pipfile | 15 - Pipfile.lock | 107 --- dev-requirements.txt | 5 - docs-requirements.txt | 9 - poetry.lock | 1433 ++++++++++++++++++++++++++++++++ pyproject.toml | 71 ++ requirements.txt | 3 - setup.cfg | 2 - setup.py | 58 -- src/keycloak/keycloak_admin.py | 2 +- tox.ini | 42 +- 13 files changed, 1542 insertions(+), 232 deletions(-) delete mode 100644 Pipfile delete mode 100644 Pipfile.lock delete mode 100644 dev-requirements.txt delete mode 100644 docs-requirements.txt create mode 100644 poetry.lock delete mode 100644 requirements.txt delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 531a8e13..693f67bd 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -21,7 +21,7 @@ jobs: - name: Apply the tag version run: | version=${{ github.ref_name }} - sed -i 's/__version__ = .*/__version__ = "'${version:1}'"/' src/keycloak/_version.py + sed -Ei '/^version = /s|= "[0-9.]+"$|= "'${version:-1}'"|' pyproject.toml - name: Run build run: | tox -e build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 683f7eec..645b3eab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,11 +10,17 @@ The development environment is mainly up to the developer. Our recommendations a virtual environment and install the necessary requirements. Example ```sh -python -m venv venv -source venv/bin/activate -python -m pip install -U pip -python -m pip install -r requirements.txt -python -m pip install -r dev-requirements.txt +# Install and upgrade pip & poetry +python -m pip install --upgrade pip poetry + +# Create virtualenv +python -m poetry env use + +# install package dependencies including dev dependencies +python -m poetry install --dev + +# Activate virtualenv +python -m poetry shell ``` ## Running checks and tests @@ -67,9 +73,12 @@ After cloning this repository, you must install the pre-commit hook for conventional commits (this is included in the `dev-requirements.txt`) ```sh -python3 -m venv .venv -source .venv/bin/activate -python3 -m pip install pre-commit +# Create virtualenv +python -m poetry env use + +# Activate virtualenv +python -m poetry shell + pre-commit install --install-hooks -t pre-commit -t pre-push -t commit-msg ``` diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 828d2983..00000000 --- a/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -requests = ">=2.20.0" -httmock = ">=1.2.5" -python-jose = ">=1.4.0" -urllib3 = ">=1.26.5" - -[dev-packages] - -[requires] -python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 0430b1ee..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,107 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "8c12705e89c665da92fc69ef0d312a9ca313703c839c15d18fcc833dcb87d7f7" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.7" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:1a4995114262bffbc2413b159f2a1a480c969de6e6eb13ee966d470af86af59c", - "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830" - ], - "version": "==2020.12.5" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "ecdsa": { - "hashes": [ - "sha256:881fa5e12bb992972d3d1b3d4dfbe149ab76a89f13da02daa5ea1ec7dea6e747", - "sha256:cfc046a2ddd425adbd1a78b3c46f0d1325c657811c0f45ecc3a0a6236c1e50ff" - ], - "version": "==0.16.1" - }, - "future": { - "hashes": [ - "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" - ], - "version": "==0.18.2" - }, - "httmock": { - "hashes": [ - "sha256:4696306d1ff835c3ca865fdef2684d7e130b4120cc00126f862ba4797b1602ac" - ], - "index": "pypi", - "version": "==1.2.6" - }, - "idna": { - "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" - ], - "version": "==2.7" - }, - "pyasn1": { - "hashes": [ - "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d", - "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba" - ], - "version": "==0.4.8" - }, - "python-jose": { - "hashes": [ - "sha256:29701d998fe560e52f17246c3213a882a4a39da7e42c7015bcc1f7823ceaff1c", - "sha256:ed7387f0f9af2ea0ddc441d83a6eb47a5909bd0c8a72ac3250e75afec2cc1371" - ], - "index": "pypi", - "version": "==3.0.1" - }, - "requests": { - "hashes": [ - "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", - "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" - ], - "index": "pypi", - "version": "==2.19.1" - }, - "rsa": { - "hashes": [ - "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4", - "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b" - ], - "index": "pypi", - "version": "==4.7" - }, - "six": { - "hashes": [ - "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259", - "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced" - ], - "version": "==1.15.0" - }, - "urllib3": { - "hashes": [ - "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", - "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" - ], - "version": "==1.23" - } - }, - "develop": {} -} diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index d2f6981e..00000000 --- a/dev-requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -tox -pytest -pytest-cov -wheel -pre-commit diff --git a/docs-requirements.txt b/docs-requirements.txt deleted file mode 100644 index 343bf895..00000000 --- a/docs-requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -mock -alabaster -commonmark -recommonmark -sphinx -sphinx-rtd-theme -readthedocs-sphinx-ext -m2r2 -sphinx-autoapi diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..cbad183e --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1433 @@ +[[package]] +name = "alabaster" +version = "0.7.12" +description = "A configurable sidebar-enabled Sphinx theme" +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "astroid" +version = "2.11.6" +description = "An abstract syntax tree for Python with inference support." +category = "main" +optional = true +python-versions = ">=3.6.2" + +[package.dependencies] +lazy-object-proxy = ">=1.4.0" +typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} +typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} +wrapt = ">=1.11,<2" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.4.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "babel" +version = "2.10.3" +description = "Internationalization utilities" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +pytz = ">=2015.7" + +[[package]] +name = "black" +version = "22.3.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.6.15" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "charset-normalizer" +version = "2.0.12" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = false +python-versions = ">=3.5.0" + +[package.extras] +unicode_backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = true +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + +[[package]] +name = "coverage" +version = "6.4.1" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.4" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "docutils" +version = "0.17.1" +description = "Docutils -- Python Documentation Utilities" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "ecdsa" +version = "0.17.0" +description = "ECDSA cryptographic signature library (pure python)" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[package.dependencies] +six = ">=1.9.0" + +[package.extras] +gmpy = ["gmpy"] +gmpy2 = ["gmpy2"] + +[[package]] +name = "filelock" +version = "3.7.1" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + +[[package]] +name = "flake8" +version = "3.9.2" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +mccabe = ">=0.6.0,<0.7.0" +pycodestyle = ">=2.7.0,<2.8.0" +pyflakes = ">=2.3.0,<2.4.0" + +[[package]] +name = "identify" +version = "2.5.1" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "imagesize" +version = "1.3.0" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "importlib-metadata" +version = "4.12.0" +description = "Read metadata from Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} +zipp = ">=0.5" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +perf = ["ipython"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "jinja2" +version = "3.1.2" +description = "A very fast and expressive template engine." +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "lazy-object-proxy" +version = "1.7.1" +description = "A fast and thorough lazy object proxy." +category = "main" +optional = true +python-versions = ">=3.6" + +[[package]] +name = "m2r2" +version = "0.3.2" +description = "Markdown and reStructuredText in a single file." +category = "main" +optional = true +python-versions = "*" + +[package.dependencies] +docutils = "*" +mistune = "0.8.4" + +[[package]] +name = "markupsafe" +version = "2.1.1" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = true +python-versions = ">=3.7" + +[[package]] +name = "mccabe" +version = "0.6.1" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "mistune" +version = "0.8.4" +description = "The fastest markdown parser in pure Python" +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "mock" +version = "4.0.3" +description = "Rolling backport of unittest.mock for all Pythons" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.extras] +build = ["twine", "wheel", "blurb"] +docs = ["sphinx"] +test = ["pytest (<5.4)", "pytest-cov"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.19.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pyasn1" +version = "0.4.8" +description = "ASN.1 types and codecs" +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pycodestyle" +version = "2.7.0" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyflakes" +version = "2.3.1" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pygments" +version = "2.12.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = true +python-versions = ">=3.6" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "main" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.2" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "3.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "python-jose" +version = "3.3.0" +description = "JOSE implementation in Python" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +ecdsa = "!=0.15" +pyasn1 = "*" +rsa = "*" + +[package.extras] +cryptography = ["cryptography (>=3.4.0)"] +pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] +pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] + +[[package]] +name = "pytz" +version = "2022.1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "readthedocs-sphinx-ext" +version = "2.1.8" +description = "Sphinx extension for Read the Docs overrides" +category = "main" +optional = true +python-versions = "*" + +[package.dependencies] +Jinja2 = ">=2.9" +packaging = "*" +requests = "*" + +[[package]] +name = "recommonmark" +version = "0.7.1" +description = "A docutils-compatibility bridge to CommonMark, enabling you to write CommonMark inside of Docutils & Sphinx projects." +category = "main" +optional = true +python-versions = "*" + +[package.dependencies] +commonmark = ">=0.8.1" +docutils = ">=0.11" +sphinx = ">=1.3.1" + +[[package]] +name = "requests" +version = "2.28.0" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=3.7, <4" + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2.0.0,<2.1.0" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] + +[[package]] +name = "rsa" +version = "4.8" +description = "Pure-Python RSA implementation" +category = "main" +optional = false +python-versions = ">=3.6,<4" + +[package.dependencies] +pyasn1 = ">=0.1.3" + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +category = "main" +optional = true +python-versions = "*" + +[[package]] +name = "sphinx" +version = "5.0.2" +description = "Python documentation generator" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +alabaster = ">=0.7,<0.8" +babel = ">=1.3" +colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +docutils = ">=0.14,<0.19" +imagesize = "*" +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.3" +packaging = "*" +Pygments = ">=2.0" +requests = ">=2.5.0" +snowballstemmer = ">=1.1" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.5" + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.950)", "docutils-stubs", "types-typed-ast", "types-requests"] +test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] + +[[package]] +name = "sphinx-autoapi" +version = "1.8.4" +description = "Sphinx API documentation generator" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.dependencies] +astroid = ">=2.7" +Jinja2 = "*" +PyYAML = "*" +sphinx = ">=3.0" +unidecode = "*" + +[package.extras] +docs = ["sphinx", "sphinx-rtd-theme"] +dotnet = ["sphinxcontrib-dotnetdomain"] +go = ["sphinxcontrib-golangdomain"] + +[[package]] +name = "sphinx-rtd-theme" +version = "1.0.0" +description = "Read the Docs theme for Sphinx" +category = "main" +optional = true +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" + +[package.dependencies] +docutils = "<0.18" +sphinx = ">=1.6" + +[package.extras] +dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "1.0.2" +description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books" +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "1.0.2" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document." +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.0.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +category = "main" +optional = true +python-versions = ">=3.6" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest", "html5lib"] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +test = ["pytest", "flake8", "mypy"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "1.0.3" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document." +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "1.1.5" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)." +category = "main" +optional = true +python-versions = ">=3.5" + +[package.extras] +lint = ["flake8", "mypy", "docutils-stubs"] +test = ["pytest"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "tox" +version = "3.25.0" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +filelock = ">=3.0.0" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +toml = ">=0.9.4" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] + +[[package]] +name = "typed-ast" +version = "1.5.4" +description = "a fork of Python 2 and 3 ast modules with type comment support" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "typing-extensions" +version = "4.2.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "unidecode" +version = "1.3.4" +description = "ASCII transliterations of Unicode text" +category = "main" +optional = true +python-versions = ">=3.5" + +[[package]] +name = "urllib3" +version = "1.26.9" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "virtualenv" +version = "20.15.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + +[[package]] +name = "wrapt" +version = "1.14.1" +description = "Module for decorators, wrappers and monkey patching." +category = "main" +optional = true +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "zipp" +version = "3.8.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] + +[extras] +docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.7" +content-hash = "58ad1dfa1c2cdbb232bc53ceb2c1a9d0767a3db7fd8e6d0baae3e753f1c570dc" + +[metadata.files] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +astroid = [ + {file = "astroid-2.11.6-py3-none-any.whl", hash = "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9"}, + {file = "astroid-2.11.6.tar.gz", hash = "sha256:4f933d0bf5e408b03a6feb5d23793740c27e07340605f236496cd6ce552043d6"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, + {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, +] +babel = [ + {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, + {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, +] +black = [ + {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, + {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, + {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, + {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, + {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, + {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, + {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, + {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, + {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, + {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, + {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, + {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, + {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, + {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, + {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, + {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, + {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, + {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, + {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, + {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, + {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, +] +certifi = [ + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, + {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +coverage = [ + {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, + {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, + {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, + {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, + {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, + {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, + {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, + {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, + {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, + {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, + {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, + {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, + {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, + {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, + {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, + {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, + {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, + {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, + {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, + {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, + {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, + {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, + {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, + {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, + {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, + {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, + {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, + {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, + {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, + {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, + {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, + {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, + {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, + {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, + {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, + {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, + {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, + {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, + {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, + {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, + {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, +] +distlib = [ + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, +] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] +ecdsa = [ + {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, + {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, +] +filelock = [ + {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, + {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +identify = [ + {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, + {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +imagesize = [ + {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, + {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, + {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, +] +m2r2 = [ + {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, + {file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"}, +] +markupsafe = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mistune = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] +mock = [ + {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, + {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pre-commit = [ + {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, + {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +pygments = [ + {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, + {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, + {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +python-jose = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] +pytz = [ + {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, + {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, +] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +readthedocs-sphinx-ext = [ + {file = "readthedocs-sphinx-ext-2.1.8.tar.gz", hash = "sha256:a57e3713daf77bf91d1ba19e4b9888a47c0abfeb63ecf02e3ac77fcfd99bfe69"}, + {file = "readthedocs_sphinx_ext-2.1.8-py2.py3-none-any.whl", hash = "sha256:5ab5875993191e5e526ca196a1082b73116b0cefd79073ab25367ba0458fffe9"}, +] +recommonmark = [ + {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, + {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, +] +requests = [ + {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, + {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, +] +rsa = [ + {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, + {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] +sphinx = [ + {file = "Sphinx-5.0.2-py3-none-any.whl", hash = "sha256:d3e57663eed1d7c5c50895d191fdeda0b54ded6f44d5621b50709466c338d1e8"}, + {file = "Sphinx-5.0.2.tar.gz", hash = "sha256:b18e978ea7565720f26019c702cd85c84376e948370f1cd43d60265010e1c7b0"}, +] +sphinx-autoapi = [ + {file = "sphinx-autoapi-1.8.4.tar.gz", hash = "sha256:8c4ec5fbedc1e6e8f4692bcc4fcd1abcfb9e8dfca8a4ded60ad811a743c22ccc"}, + {file = "sphinx_autoapi-1.8.4-py2.py3-none-any.whl", hash = "sha256:007bf9e24cd2aa0ac0561f67e8bcd6a6e2e8911ef4b4fd54aaba799d8832c8d0"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, + {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tox = [ + {file = "tox-3.25.0-py2.py3-none-any.whl", hash = "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a"}, + {file = "tox-3.25.0.tar.gz", hash = "sha256:37888f3092aa4e9f835fc8cc6dadbaaa0782651c41ef359e3a5743fcb0308160"}, +] +typed-ast = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] +typing-extensions = [ + {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, + {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, +] +unidecode = [ + {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, + {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, +] +urllib3 = [ + {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, + {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, +] +virtualenv = [ + {file = "virtualenv-20.15.0-py2.py3-none-any.whl", hash = "sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336"}, + {file = "virtualenv-20.15.0.tar.gz", hash = "sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc"}, +] +wrapt = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] +zipp = [ + {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, + {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, +] diff --git a/pyproject.toml b/pyproject.toml index f9474507..d6012fe4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,74 @@ +[tool.poetry] +name = "python-keycloak" +version = "1.9.0" +description = "python-keycloak is a Python package providing access to the Keycloak API." +license = "MIT" +readme = "README.md" +keywords = [ "keycloak", "openid", "oidc" ] +authors = [ + "Marcos Pereira ", + "Richard Nemeth " +] +classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Operating System :: MacOS", + "Operating System :: Unix", + "Operating System :: Microsoft :: Windows", + "Topic :: Utilities", +] +packages = [ + { include = "keycloak", from = "src/" }, + { include = "keycloak/**/*.py", from = "src/" }, +] + +[tool.poetry.urls] +Documentation = "https://python-keycloak.readthedocs.io/en/latest/" +"Issue tracker" = "https://github.com/marcospereirampj/python-keycloak/issues" + +[tool.poetry.dependencies] +python = "^3.7" +requests = "^2.20.0" +python-jose = "^3.3.0" +urllib3 = "^1.26.0" +mock = {version = "^4.0.3", optional = true} +alabaster = {version = "^0.7.12", optional = true} +commonmark = {version = "^0.9.1", optional = true} +recommonmark = {version = "^0.7.1", optional = true} +Sphinx = {version = "^5.0.2", optional = true} +sphinx-rtd-theme = {version = "^1.0.0", optional = true} +readthedocs-sphinx-ext = {version = "^2.1.8", optional = true} +m2r2 = {version = "^0.3.2", optional = true} +sphinx-autoapi = {version = "^1.8.4", optional = true} + +[tool.poetry.dev-dependencies] +tox = "^3.25.0" +pytest = "^7.1.2" +pytest-cov = "^3.0.0" +wheel = "^0.37.1" +pre-commit = "^2.19.0" +isort = "^5.10.1" +black = "^22.3.0" +flake8 = "^3.5.0" + +[tool.poetry.extras] +docs = [ + "mock", + "alabaster", + "commonmark", + "recommonmark", + "sphinx", + "sphinx-rtd-theme", + "readthedocs-sphinx-ext", + "m2r2", + "sphinx-autoapi", +] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" + [tool.black] line-length = 99 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5474982a..00000000 --- a/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -requests>=2.20.0 -python-jose>=1.4.0 -urllib3>=1.26.0 diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 224a7795..00000000 --- a/setup.cfg +++ /dev/null @@ -1,2 +0,0 @@ -[metadata] -description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 27a188db..00000000 --- a/setup.py +++ /dev/null @@ -1,58 +0,0 @@ -# -*- coding: utf-8 -*- -import re - -from setuptools import find_packages, setup - -with open("README.md", "r") as fh: - long_description = fh.read() - -with open("requirements.txt", "r") as fh: - reqs = fh.read().split("\n") - -with open("dev-requirements.txt", "r") as fh: - dev_reqs = fh.read().split("\n") - -with open("docs-requirements.txt", "r") as fh: - docs_reqs = fh.read().split("\n") - - -VERSIONFILE = "src/keycloak/_version.py" -verstrline = open(VERSIONFILE, "rt").read() -VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" -mo = re.search(VSRE, verstrline, re.M) -if mo: - verstr = mo.group(1) -else: - raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE,)) - -setup( - name="python-keycloak", - version=verstr, - url="https://github.com/marcospereirampj/python-keycloak", - license="The MIT License", - author="Marcos Pereira, Richard Nemeth", - author_email="marcospereira.mpj@gmail.com, ryshoooo@gmail.com", - keywords="keycloak openid oidc", - description="python-keycloak is a Python package providing access to the Keycloak API.", - long_description=long_description, - long_description_content_type="text/markdown", - packages=find_packages("src"), - package_dir={"": "src"}, - install_requires=reqs, - tests_require=dev_reqs, - extras_require={"docs": docs_reqs}, - python_requires=">=3.7", - project_urls={ - "Documentation": "https://python-keycloak.readthedocs.io/en/latest/", - "Issue tracker": "https://github.com/marcospereirampj/python-keycloak/issues", - }, - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Development Status :: 3 - Alpha", - "Operating System :: MacOS", - "Operating System :: Unix", - "Operating System :: Microsoft :: Windows", - "Topic :: Utilities", - ], -) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index ed868777..44e9c3b7 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2904,7 +2904,7 @@ def get_client_authz_client_policies(self, client_id): """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( - urls_patterns.URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY.format(**params_path), + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_CLIENT_POLICY.format(**params_path) ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) diff --git a/tox.ini b/tox.ini index 54b2c2b7..c5c4d5b0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,48 +1,44 @@ [tox] +isolated_build = true envlist = check, apply-check, docs, tests, build [testenv] install_command = pip install {opts} {packages} +deps = + poetry>=1.1.13 +commands_pre = + bash -c "python -m pip install -r <(poetry export --dev --extras=docs --no-interaction)" +whitelist_externals = + bash [testenv:check] -deps = - black - isort - flake8 commands = - black --check --diff src/keycloak tests docs setup.py - isort -c --df src/keycloak tests docs setup.py - flake8 src/keycloak tests docs setup.py + black --check --diff src/keycloak tests docs + isort -c --df src/keycloak tests docs + flake8 src/keycloak tests docs [testenv:apply-check] -deps = - black - isort - flake8 commands = - black -C src/keycloak tests docs setup.py - black src/keycloak tests docs setup.py - isort src/keycloak tests docs setup.py + black -C src/keycloak tests docs + black src/keycloak tests docs + isort src/keycloak tests docs [testenv:docs] -deps = - .[docs] commands = - python -m sphinx -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html + sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html [testenv:tests] setenv = file|tox.env -deps = - -rrequirements.txt - -rdev-requirements.txt commands = ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" [testenv:build] -deps = - -rdev-requirements.txt +commands_pre = +setenv = + POETRY_VIRTUALENVS_CREATE = false commands = - python setup.py sdist bdist_wheel + poetry build --format sdist + poetry build --format wheel [flake8] max-line-length = 99 From 81695472d3237d3f8129256fa432a33e48b53398 Mon Sep 17 00:00:00 2001 From: Chuma Umenze Date: Mon, 27 Jun 2022 19:43:52 +0100 Subject: [PATCH 191/566] chore: tox test install without hashes --- CONTRIBUTING.md | 2 +- pyproject.toml | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 645b3eab..2978d02d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,7 +17,7 @@ python -m pip install --upgrade pip poetry python -m poetry env use # install package dependencies including dev dependencies -python -m poetry install --dev +python -m poetry install # Activate virtualenv python -m poetry shell diff --git a/pyproject.toml b/pyproject.toml index d6012fe4..ae6fc1b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "python-keycloak" -version = "1.9.0" +version = "0.0.0" description = "python-keycloak is a Python package providing access to the Keycloak API." license = "MIT" readme = "README.md" diff --git a/tox.ini b/tox.ini index c5c4d5b0..b88069af 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ install_command = pip install {opts} {packages} deps = poetry>=1.1.13 commands_pre = - bash -c "python -m pip install -r <(poetry export --dev --extras=docs --no-interaction)" + bash -c "python -m pip install -r <(poetry export --dev --extras=docs --without-hashes --no-interaction)" whitelist_externals = bash From b95b1d3505d5fe5d7f82ebb871122ca9afd80d6e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 09:19:12 +0200 Subject: [PATCH 192/566] refactor: slight restructure of the base fixtures --- tests/conftest.py | 73 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b9c266a2..ada9820e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,22 +3,62 @@ import pytest -from keycloak import KeycloakAdmin +from keycloak import KeycloakAdmin, KeycloakOpenID + + +class KeycloakTestEnv(object): + def __init__( + self, + host: str = os.environ["KEYCLOAK_HOST"], + port: str = os.environ["KEYCLOAK_PORT"], + username: str = os.environ["KEYCLOAK_ADMIN"], + password: str = os.environ["KEYCLOAK_ADMIN_PASSWORD"], + ): + self.KEYCLOAK_HOST = host + self.KEYCLOAK_PORT = port + self.KEYCLOAK_ADMIN = username + self.KEYCLOAK_ADMIN_PASSWORD = password + + @property + def KEYCLOAK_HOST(self): + return self._KEYCLOAK_HOST + + @KEYCLOAK_HOST.setter + def KEYCLOAK_HOST(self, value: str): + self._KEYCLOAK_HOST = value + + @property + def KEYCLOAK_PORT(self): + return self._KEYCLOAK_PORT + + @KEYCLOAK_PORT.setter + def KEYCLOAK_PORT(self, value: str): + self._KEYCLOAK_PORT = value + + @property + def KEYCLOAK_ADMIN(self): + return self._KEYCLOAK_ADMIN + + @KEYCLOAK_ADMIN.setter + def KEYCLOAK_ADMIN(self, value: str): + self._KEYCLOAK_ADMIN = value + + @property + def KEYCLOAK_ADMIN_PASSWORD(self): + return self._KEYCLOAK_ADMIN_PASSWORD + + @KEYCLOAK_ADMIN_PASSWORD.setter + def KEYCLOAK_ADMIN_PASSWORD(self, value: str): + self._KEYCLOAK_ADMIN_PASSWORD = value @pytest.fixture def env(): - class KeycloakTestEnv(object): - KEYCLOAK_HOST = os.environ["KEYCLOAK_HOST"] - KEYCLOAK_PORT = os.environ["KEYCLOAK_PORT"] - KEYCLOAK_ADMIN = os.environ["KEYCLOAK_ADMIN"] - KEYCLOAK_ADMIN_PASSWORD = os.environ["KEYCLOAK_ADMIN_PASSWORD"] - return KeycloakTestEnv() @pytest.fixture -def admin(env): +def admin(env: KeycloakTestEnv): return KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -26,6 +66,23 @@ def admin(env): ) +@pytest.fixture +def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + # Set the realm + admin.realm_name = realm + # Create client + client = str(uuid.uuid4()) + client_id = admin.create_client(payload={"name": client, "clientId": client}) + # Return OID + yield KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name=realm, + client_id=client, + ) + # Cleanup + admin.delete_client(client_id=client_id) + + @pytest.fixture def realm(admin: KeycloakAdmin) -> str: realm_name = str(uuid.uuid4()) From 590c7bb582237739582fc58352048ad3ca27be84 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 09:19:38 +0200 Subject: [PATCH 193/566] test: test of init and well_known of oid --- src/keycloak/keycloak_openid.py | 1 - tests/test_keycloak_openid.py | 79 +++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 tests/test_keycloak_openid.py diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 7216b5d1..3e045bcd 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -170,7 +170,6 @@ def well_known(self): params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) def auth_url(self, redirect_uri): diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py new file mode 100644 index 00000000..0ee1bbb6 --- /dev/null +++ b/tests/test_keycloak_openid.py @@ -0,0 +1,79 @@ +from keycloak.keycloak_openid import KeycloakOpenID +from keycloak.connection import ConnectionManager +from keycloak.authorization import Authorization + + +def test_keycloak_openid_init(env): + oid = KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name="master", + client_id="admin-cli", + ) + + assert oid.client_id == "admin-cli" + assert oid.client_secret_key is None + assert oid.realm_name == "master" + assert isinstance(oid.connection, ConnectionManager) + assert isinstance(oid.authorization, Authorization) + + +def test_well_known(oid: KeycloakOpenID): + res = oid.well_known() + assert res is not None + assert res != dict() + for key in [ + "acr_values_supported", + "authorization_encryption_alg_values_supported", + "authorization_encryption_enc_values_supported", + "authorization_endpoint", + "authorization_signing_alg_values_supported", + "backchannel_authentication_endpoint", + "backchannel_authentication_request_signing_alg_values_supported", + "backchannel_logout_session_supported", + "backchannel_logout_supported", + "backchannel_token_delivery_modes_supported", + "check_session_iframe", + "claim_types_supported", + "claims_parameter_supported", + "claims_supported", + "code_challenge_methods_supported", + "device_authorization_endpoint", + "end_session_endpoint", + "frontchannel_logout_session_supported", + "frontchannel_logout_supported", + "grant_types_supported", + "id_token_encryption_alg_values_supported", + "id_token_encryption_enc_values_supported", + "id_token_signing_alg_values_supported", + "introspection_endpoint", + "introspection_endpoint_auth_methods_supported", + "introspection_endpoint_auth_signing_alg_values_supported", + "issuer", + "jwks_uri", + "mtls_endpoint_aliases", + "pushed_authorization_request_endpoint", + "registration_endpoint", + "request_object_encryption_alg_values_supported", + "request_object_encryption_enc_values_supported", + "request_object_signing_alg_values_supported", + "request_parameter_supported", + "request_uri_parameter_supported", + "require_pushed_authorization_requests", + "require_request_uri_registration", + "response_modes_supported", + "response_types_supported", + "revocation_endpoint", + "revocation_endpoint_auth_methods_supported", + "revocation_endpoint_auth_signing_alg_values_supported", + "scopes_supported", + "subject_types_supported", + "tls_client_certificate_bound_access_tokens", + "token_endpoint", + "token_endpoint_auth_methods_supported", + "token_endpoint_auth_signing_alg_values_supported", + "userinfo_encryption_alg_values_supported", + "userinfo_encryption_enc_values_supported", + "userinfo_endpoint", + "userinfo_signing_alg_values_supported", + ]: + assert key in res From d79939535f7da6f8d1db1f37dfc6c3423b108ff4 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 09:20:02 +0200 Subject: [PATCH 194/566] style: applied isort --- tests/test_keycloak_openid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 0ee1bbb6..53a76dd9 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,6 +1,6 @@ -from keycloak.keycloak_openid import KeycloakOpenID -from keycloak.connection import ConnectionManager from keycloak.authorization import Authorization +from keycloak.connection import ConnectionManager +from keycloak.keycloak_openid import KeycloakOpenID def test_keycloak_openid_init(env): From 5cd8fc391398762524e738d434b0bedee1af44cd Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 07:52:23 +0000 Subject: [PATCH 195/566] test: use tox-poetry plugin for tox --- tox.ini | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index b88069af..0458821b 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,10 @@ [tox] -isolated_build = true +requires = + tox-poetry + poetry envlist = check, apply-check, docs, tests, build [testenv] -install_command = pip install {opts} {packages} -deps = - poetry>=1.1.13 -commands_pre = - bash -c "python -m pip install -r <(poetry export --dev --extras=docs --without-hashes --no-interaction)" whitelist_externals = bash @@ -24,16 +21,19 @@ commands = isort src/keycloak tests docs [testenv:docs] +extras = docs commands = sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html [testenv:tests] setenv = file|tox.env +passenv = CONTAINER_HOST commands = ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" [testenv:build] -commands_pre = +deps = + poetry setenv = POETRY_VIRTUALENVS_CREATE = false commands = From db888185c3b82a8e9581fb2ee3351e698636c63b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 19:57:18 +0000 Subject: [PATCH 196/566] test: store keycloak container logs --- .github/workflows/lint.yaml | 3 +++ .gitignore | 1 + test_keycloak_init.sh | 3 +-- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 2cade19a..0f5b1464 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -71,6 +71,9 @@ jobs: - name: Run tests run: | tox -e tests + - name: Keycloak logs + run: | + cat keycloak_test_logs.txt build: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 0c170799..25a3ea48 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ nosetests.xml coverage.xml *.cover .hypothesis/ +keycloak_test_logs.txt # Translations *.mo diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index bd4c30ac..82afabb8 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -3,8 +3,6 @@ CMD_ARGS=$1 KEYCLOAK_DOCKER_IMAGE="quay.io/keycloak/keycloak:latest" -echo "${CMD_ARGS}" - function keycloak_stop() { docker stop unittest_keycloak &> /dev/null docker rm unittest_keycloak &> /dev/null @@ -30,6 +28,7 @@ keycloak_stop # In case it did not shut down correctly last time. keycloak_start eval ${CMD_ARGS} +docker logs unittest_keycloak > keycloak_test_logs.txt RETURN_VALUE=$? exit ${RETURN_VALUE} From 9ab340f4a42ee2ef8886722873c87992376e01c7 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 19:58:20 +0000 Subject: [PATCH 197/566] feat: added flake8-docstrings and upgraded dependencies --- poetry.lock | 256 +++++++++++++++++++++++++++++++++++++++---------- pyproject.toml | 2 + tox.ini | 2 + 3 files changed, 210 insertions(+), 50 deletions(-) diff --git a/poetry.lock b/poetry.lock index cbad183e..c747f94b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -6,6 +6,20 @@ category = "main" optional = true python-versions = "*" +[[package]] +name = "argcomplete" +version = "1.12.3" +description = "Bash tab completion for argparse" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""} + +[package.extras] +test = ["coverage", "flake8", "pexpect", "wheel"] + [[package]] name = "astroid" version = "2.11.6" @@ -55,7 +69,7 @@ pytz = ">=2015.7" [[package]] name = "black" -version = "22.3.0" +version = "22.6.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -66,7 +80,7 @@ click = ">=8.0.0" mypy-extensions = ">=0.4.3" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} @@ -94,11 +108,11 @@ python-versions = ">=3.6.1" [[package]] name = "charset-normalizer" -version = "2.0.12" +version = "2.1.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false -python-versions = ">=3.5.0" +python-versions = ">=3.6.0" [package.extras] unicode_backport = ["unicodedata2"] @@ -123,6 +137,26 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "commitizen" +version = "2.28.0" +description = "Python commitizen client tool" +category = "dev" +optional = false +python-versions = ">=3.6.2,<4.0.0" + +[package.dependencies] +argcomplete = ">=1.12.1,<2.0.0" +colorama = ">=0.4.1,<0.5.0" +decli = ">=0.5.2,<0.6.0" +jinja2 = ">=2.10.3" +packaging = ">=19,<22" +pyyaml = ">=3.08" +questionary = ">=1.4.0,<2.0.0" +termcolor = ">=1.1,<2.0" +tomlkit = ">=0.5.3,<1.0.0" +typing-extensions = ">=4.0.1,<5.0.0" + [[package]] name = "commonmark" version = "0.9.1" @@ -148,6 +182,14 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "decli" +version = "0.5.2" +description = "Minimal, easy-to-use, declarative cli tool" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "distlib" version = "0.3.4" @@ -205,6 +247,18 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "flake8-docstrings" +version = "1.6.0" +description = "Extension for flake8 which uses pydocstyle to check docstrings" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +flake8 = ">=3" +pydocstyle = ">=2.1" + [[package]] name = "identify" version = "2.5.1" @@ -226,7 +280,7 @@ python-versions = ">=3.5" [[package]] name = "imagesize" -version = "1.3.0" +version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "main" optional = true @@ -276,7 +330,7 @@ name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "main" -optional = true +optional = false python-versions = ">=3.7" [package.dependencies] @@ -310,7 +364,7 @@ name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" -optional = true +optional = false python-versions = ">=3.7" [[package]] @@ -421,6 +475,17 @@ pyyaml = ">=5.1" toml = "*" virtualenv = ">=20.0.8" +[[package]] +name = "prompt-toolkit" +version = "3.0.30" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +wcwidth = "*" + [[package]] name = "py" version = "1.11.0" @@ -445,6 +510,20 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pydocstyle" +version = "6.1.1" +description = "Python docstring style checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +snowballstemmer = "*" + +[package.extras] +toml = ["toml"] + [[package]] name = "pyflakes" version = "2.3.1" @@ -543,6 +622,20 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "questionary" +version = "1.10.0" +description = "Python library to build pretty command line user prompts ⭐️" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + +[package.dependencies] +prompt_toolkit = ">=2.0,<4.0" + +[package.extras] +docs = ["Sphinx (>=3.3,<4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)"] + [[package]] name = "readthedocs-sphinx-ext" version = "2.1.8" @@ -571,7 +664,7 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.28.0" +version = "2.28.1" description = "Python HTTP for Humans." category = "main" optional = false @@ -579,13 +672,13 @@ python-versions = ">=3.7, <4" [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2.0.0,<2.1.0" +charset-normalizer = ">=2,<3" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"] +use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rsa" @@ -611,7 +704,7 @@ name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." category = "main" -optional = true +optional = false python-versions = "*" [[package]] @@ -752,6 +845,14 @@ python-versions = ">=3.5" lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +[[package]] +name = "termcolor" +version = "1.1.0" +description = "ANSII Color formatting for output in terminal." +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "toml" version = "0.10.2" @@ -768,9 +869,17 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "tomlkit" +version = "0.11.0" +description = "Style preserving TOML library" +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + [[package]] name = "tox" -version = "3.25.0" +version = "3.25.1" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -801,7 +910,7 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.2.0" +version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -830,7 +939,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.15.0" +version = "20.15.1" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -847,6 +956,14 @@ six = ">=1.9.0,<2" docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +[[package]] +name = "wcwidth" +version = "0.2.5" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "wrapt" version = "1.14.1" @@ -873,13 +990,17 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "58ad1dfa1c2cdbb232bc53ceb2c1a9d0767a3db7fd8e6d0baae3e753f1c570dc" +content-hash = "ed105f41fc20e390af8eeefafd3168bb4b370d3a5135bfdec55aab7fc5d0bb3e" [metadata.files] alabaster = [ {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] +argcomplete = [ + {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, + {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, +] astroid = [ {file = "astroid-2.11.6-py3-none-any.whl", hash = "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9"}, {file = "astroid-2.11.6.tar.gz", hash = "sha256:4f933d0bf5e408b03a6feb5d23793740c27e07340605f236496cd6ce552043d6"}, @@ -897,29 +1018,29 @@ babel = [ {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] black = [ - {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, - {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, - {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, - {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, - {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, - {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, - {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, - {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, - {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, - {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, - {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, - {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, - {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, - {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, - {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, - {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, - {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, - {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, - {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, - {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, - {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, + {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, + {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, + {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, + {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, + {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, + {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, + {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, + {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, + {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, + {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, + {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, + {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, + {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, + {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, + {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, + {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, + {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, ] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, @@ -930,8 +1051,8 @@ cfgv = [ {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"}, - {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"}, + {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, + {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, ] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, @@ -941,6 +1062,10 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] +commitizen = [ + {file = "commitizen-2.28.0-py3-none-any.whl", hash = "sha256:d222f68da12a3ebcaf85c270f19eec7caacbe904349f1823deca6b5e7c2fc0f5"}, + {file = "commitizen-2.28.0.tar.gz", hash = "sha256:8510b67e4c45131ef75114aeca5fe30b4f973b2b943457cf1667177af296192e"}, +] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, @@ -988,6 +1113,10 @@ coverage = [ {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, ] +decli = [ + {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, + {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, +] distlib = [ {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, @@ -1008,6 +1137,10 @@ flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] +flake8-docstrings = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] identify = [ {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, @@ -1017,8 +1150,8 @@ idna = [ {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] imagesize = [ - {file = "imagesize-1.3.0-py2.py3-none-any.whl", hash = "sha256:1db2f82529e53c3e929e8926a1fa9235aa82d0bd0c580359c67ec31b2fddaa8c"}, - {file = "imagesize-1.3.0.tar.gz", hash = "sha256:cd1750d452385ca327479d45b64d9c7729ecf0b3969a58148298c77092261f9d"}, + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] importlib-metadata = [ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, @@ -1161,6 +1294,10 @@ pre-commit = [ {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, ] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, + {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1184,6 +1321,10 @@ pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, @@ -1247,6 +1388,10 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] +questionary = [ + {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, + {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, +] readthedocs-sphinx-ext = [ {file = "readthedocs-sphinx-ext-2.1.8.tar.gz", hash = "sha256:a57e3713daf77bf91d1ba19e4b9888a47c0abfeb63ecf02e3ac77fcfd99bfe69"}, {file = "readthedocs_sphinx_ext-2.1.8-py2.py3-none-any.whl", hash = "sha256:5ab5875993191e5e526ca196a1082b73116b0cefd79073ab25367ba0458fffe9"}, @@ -1256,8 +1401,8 @@ recommonmark = [ {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] requests = [ - {file = "requests-2.28.0-py3-none-any.whl", hash = "sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f"}, - {file = "requests-2.28.0.tar.gz", hash = "sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"}, + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] rsa = [ {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, @@ -1307,6 +1452,9 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] +termcolor = [ + {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1315,9 +1463,13 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +tomlkit = [ + {file = "tomlkit-0.11.0-py3-none-any.whl", hash = "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1"}, + {file = "tomlkit-0.11.0.tar.gz", hash = "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd"}, +] tox = [ - {file = "tox-3.25.0-py2.py3-none-any.whl", hash = "sha256:0805727eb4d6b049de304977dfc9ce315a1938e6619c3ab9f38682bb04662a5a"}, - {file = "tox-3.25.0.tar.gz", hash = "sha256:37888f3092aa4e9f835fc8cc6dadbaaa0782651c41ef359e3a5743fcb0308160"}, + {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, + {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, ] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, @@ -1346,8 +1498,8 @@ typed-ast = [ {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ - {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, - {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, ] unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, @@ -1358,8 +1510,12 @@ urllib3 = [ {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, ] virtualenv = [ - {file = "virtualenv-20.15.0-py2.py3-none-any.whl", hash = "sha256:804cce4de5b8a322f099897e308eecc8f6e2951f1a8e7e2b3598dff865f01336"}, - {file = "virtualenv-20.15.0.tar.gz", hash = "sha256:4c44b1d77ca81f8368e2d7414f9b20c428ad16b343ac6d226206c5b84e2b4fcc"}, + {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, + {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, diff --git a/pyproject.toml b/pyproject.toml index ae6fc1b2..0e54eafc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,8 @@ pre-commit = "^2.19.0" isort = "^5.10.1" black = "^22.3.0" flake8 = "^3.5.0" +flake8-docstrings = "^1.6.0" +commitizen = "^2.28.0" [tool.poetry.extras] docs = [ diff --git a/tox.ini b/tox.ini index 0458821b..3fbdd66a 100644 --- a/tox.ini +++ b/tox.ini @@ -42,3 +42,5 @@ commands = [flake8] max-line-length = 99 +docstring-convention = all +ignore = D203, D213, W503 From 6f839cbc03933a6cac3355993e83bbe9cfeddc23 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 19:59:53 +0000 Subject: [PATCH 198/566] docs: added docstrings to exceptions --- src/keycloak/exceptions.py | 42 +++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/src/keycloak/exceptions.py b/src/keycloak/exceptions.py index 925c9379..8eb69bf9 100644 --- a/src/keycloak/exceptions.py +++ b/src/keycloak/exceptions.py @@ -21,12 +21,22 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak custom exeptions module.""" + import requests class KeycloakError(Exception): - def __init__(self, error_message="", response_code=None, response_body=None): + """Base class for custom Keycloak errors. + + :param error_message: The error message + :type error_message: str + :param response_code: The response status code + :type response_code: int + """ + def __init__(self, error_message="", response_code=None, response_body=None): + """Init method.""" Exception.__init__(self, error_message) self.response_code = response_code @@ -34,6 +44,7 @@ def __init__(self, error_message="", response_code=None, response_body=None): self.error_message = error_message def __str__(self): + """Str method.""" if self.response_code is not None: return "{0}: {1}".format(self.response_code, self.error_message) else: @@ -41,62 +52,91 @@ def __str__(self): class KeycloakAuthenticationError(KeycloakError): + """Keycloak authentication error exception.""" + pass class KeycloakConnectionError(KeycloakError): + """Keycloak connection error exception.""" + pass class KeycloakOperationError(KeycloakError): + """Keycloak operation error exception.""" + pass class KeycloakDeprecationError(KeycloakError): + """Keycloak deprecation error exception.""" + pass class KeycloakGetError(KeycloakOperationError): + """Keycloak request get error exception.""" + pass class KeycloakPostError(KeycloakOperationError): + """Keycloak request post error exception.""" + pass class KeycloakPutError(KeycloakOperationError): + """Keycloak request put error exception.""" + pass class KeycloakDeleteError(KeycloakOperationError): + """Keycloak request delete error exception.""" + pass class KeycloakSecretNotFound(KeycloakOperationError): + """Keycloak secret not found exception.""" + pass class KeycloakRPTNotFound(KeycloakOperationError): + """Keycloak RPT not found exception.""" + pass class KeycloakAuthorizationConfigError(KeycloakOperationError): + """Keycloak authorization config exception.""" + pass class KeycloakInvalidTokenError(KeycloakOperationError): + """Keycloak invalid token exception.""" + pass class KeycloakPermissionFormatError(KeycloakOperationError): + """Keycloak permission format exception.""" + pass class PermissionDefinitionError(Exception): + """Keycloak permission definition exception.""" + pass def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): + """Raise an exception for the response.""" if expected_codes is None: expected_codes = [200, 201, 204] From bead0aff2bf5fdd7c966a0cc28fd1910c2b3e9ff Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 20:00:46 +0000 Subject: [PATCH 199/566] fix: raise correct exceptions --- src/keycloak/keycloak_admin.py | 2 +- src/keycloak/keycloak_openid.py | 27 ++++++++------------------- tests/test_keycloak_admin.py | 2 +- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 44e9c3b7..b2c1de48 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2736,7 +2736,7 @@ def refresh_token(self): else: try: self.token = self.keycloak_openid.refresh_token(refresh_token) - except KeycloakGetError as e: + except KeycloakPostError as e: list_errors = [ b"Refresh token expired", b"Token is not active", diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 3e045bcd..ede9a3c0 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -229,7 +229,7 @@ def token( payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def refresh_token(self, refresh_token, grant_type=["refresh_token"]): """ @@ -252,7 +252,7 @@ def refresh_token(self, refresh_token, grant_type=["refresh_token"]): } payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict: """ @@ -276,7 +276,7 @@ def exchange_token(self, token: str, client_id: str, audience: str, subject: str } payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def userinfo(self, token): """ @@ -288,12 +288,9 @@ def userinfo(self, token): :param token: :return: """ - self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name} - data_raw = self.connection.raw_get(URL_USERINFO.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) def logout(self, refresh_token): @@ -304,11 +301,9 @@ def logout(self, refresh_token): """ params_path = {"realm-name": self.realm_name} payload = {"client_id": self.client_id, "refresh_token": refresh_token} - payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_LOGOUT.format(**params_path), data=payload) - - return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[204]) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def certs(self): """ @@ -367,7 +362,6 @@ def introspect(self, token, rpt=None, token_type_hint=None): :return: """ params_path = {"realm-name": self.realm_name} - payload = {"client_id": self.client_id, "token": token} if token_type_hint == "requesting_party_token": @@ -380,8 +374,7 @@ def introspect(self, token, rpt=None, token_type_hint=None): payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_INTROSPECT.format(**params_path), data=payload) - - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakPostError) def decode_token(self, token, key, algorithms=["RS256"], **kwargs): """ @@ -399,7 +392,6 @@ def decode_token(self, token, key, algorithms=["RS256"], **kwargs): :param algorithms: :return: """ - return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) def load_authorization_config(self, path): @@ -409,10 +401,10 @@ def load_authorization_config(self, path): :param path: settings file (json) :return: """ - authorization_file = open(path, "r") - authorization_json = json.loads(authorization_file.read()) + with open(path, "r") as fp: + authorization_json = json.load(fp) + self.authorization.load_config(authorization_json) - authorization_file.close() def get_policies(self, token, method_token_info="introspect", **kwargs): """ @@ -421,7 +413,6 @@ def get_policies(self, token, method_token_info="introspect", **kwargs): :param token: user token :return: policies list """ - if not self.authorization.policies: raise KeycloakAuthorizationConfigError( "Keycloak settings not found. Load Authorization Keycloak settings." @@ -455,7 +446,6 @@ def get_permissions(self, token, method_token_info="introspect", **kwargs): :param kwargs: parameters for decode :return: permissions list """ - if not self.authorization.policies: raise KeycloakAuthorizationConfigError( "Keycloak settings not found. Load Authorization Keycloak settings." @@ -493,7 +483,6 @@ def uma_permissions(self, token, permissions=""): :param permissions: list of uma permissions list(resource:scope) requested by the user :return: permissions list """ - permission = build_permission_param(permissions) params_path = {"realm-name": self.realm_name} diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 6f33e03b..e62bdda1 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1731,7 +1731,7 @@ def test_auto_refresh(admin: KeycloakAdmin, realm: str): verify=admin.verify, ) admin.token["refresh_token"] = "bad" - with pytest.raises(KeycloakGetError) as err: + with pytest.raises(KeycloakPostError) as err: admin.get_realm(realm_name="test-refresh") assert err.match( '400: b\'{"error":"invalid_grant","error_description":"Invalid refresh token"}\'' From 17bfad5ec0b8a6bad887c3964df3d427f8378b47 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 20:01:00 +0000 Subject: [PATCH 200/566] test: added a license test --- tests/test_license.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/test_license.py diff --git a/tests/test_license.py b/tests/test_license.py new file mode 100644 index 00000000..3c6b10ec --- /dev/null +++ b/tests/test_license.py @@ -0,0 +1,14 @@ +"""Tests for license.""" +import os + + +def test_license_present(): + """Test that the MIT license is present in the header of each module file.""" + for path, _, files in os.walk("src/keycloak"): + for _file in files: + if _file.endswith(".py"): + with open(os.path.join(path, _file), "r") as fp: + content = fp.read() + assert content.startswith( + "# -*- coding: utf-8 -*-\n#\n# The MIT License (MIT)\n#\n#" + ) From 65a4af15503d483b7b315b7219d0ed36c50e74e7 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 3 Jul 2022 20:02:10 +0000 Subject: [PATCH 201/566] test: added auth_url and token tests, more structure to fixtures --- tests/conftest.py | 85 ++++++++++++++++++++++++++++++++++- tests/test_keycloak_openid.py | 58 ++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ada9820e..6023e51d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,5 @@ +"""Fixtures for tests.""" + import os import uuid @@ -7,6 +9,18 @@ class KeycloakTestEnv(object): + """Wrapper for test Keycloak connection configuration. + + :param host: Hostname + :type host: str + :param port: Port + :type port: str + :param username: Admin username + :type username: str + :param password: Admin password + :type password: str + """ + def __init__( self, host: str = os.environ["KEYCLOAK_HOST"], @@ -14,6 +28,7 @@ def __init__( username: str = os.environ["KEYCLOAK_ADMIN"], password: str = os.environ["KEYCLOAK_ADMIN_PASSWORD"], ): + """Init method.""" self.KEYCLOAK_HOST = host self.KEYCLOAK_PORT = port self.KEYCLOAK_ADMIN = username @@ -21,44 +36,54 @@ def __init__( @property def KEYCLOAK_HOST(self): + """Hostname getter.""" return self._KEYCLOAK_HOST @KEYCLOAK_HOST.setter def KEYCLOAK_HOST(self, value: str): + """Hostname setter.""" self._KEYCLOAK_HOST = value @property def KEYCLOAK_PORT(self): + """Port getter.""" return self._KEYCLOAK_PORT @KEYCLOAK_PORT.setter def KEYCLOAK_PORT(self, value: str): + """Port setter.""" self._KEYCLOAK_PORT = value @property def KEYCLOAK_ADMIN(self): + """Admin username getter.""" return self._KEYCLOAK_ADMIN @KEYCLOAK_ADMIN.setter def KEYCLOAK_ADMIN(self, value: str): + """Admin username setter.""" self._KEYCLOAK_ADMIN = value @property def KEYCLOAK_ADMIN_PASSWORD(self): + """Admin password getter.""" return self._KEYCLOAK_ADMIN_PASSWORD @KEYCLOAK_ADMIN_PASSWORD.setter def KEYCLOAK_ADMIN_PASSWORD(self, value: str): + """Admin password setter.""" self._KEYCLOAK_ADMIN_PASSWORD = value @pytest.fixture def env(): + """Fixture for getting the test environment configuration object.""" return KeycloakTestEnv() @pytest.fixture def admin(env: KeycloakTestEnv): + """Fixture for initialized KeycloakAdmin class.""" return KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -68,11 +93,20 @@ def admin(env: KeycloakTestEnv): @pytest.fixture def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + """Fixture for initialized KeycloakOpenID class.""" # Set the realm admin.realm_name = realm # Create client client = str(uuid.uuid4()) - client_id = admin.create_client(payload={"name": client, "clientId": client}) + client_id = admin.create_client( + payload={ + "name": client, + "clientId": client, + "enabled": True, + "publicClient": True, + "protocol": "openid-connect", + } + ) # Return OID yield KeycloakOpenID( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", @@ -83,16 +117,61 @@ def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): admin.delete_client(client_id=client_id) +@pytest.fixture +def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + """Fixture for an initialized KeycloakOpenID class and a random user credentials.""" + # Set the realm + admin.realm_name = realm + # Create client + client = str(uuid.uuid4()) + client_id = admin.create_client( + payload={ + "name": client, + "clientId": client, + "enabled": True, + "publicClient": True, + "protocol": "openid-connect", + } + ) + # Create user + username = str(uuid.uuid4()) + password = str(uuid.uuid4()) + user_id = admin.create_user( + payload={ + "username": username, + "email": f"{username}@test.test", + "enabled": True, + "credentials": [{"type": "password", "value": password}], + } + ) + + yield ( + KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name=realm, + client_id=client, + ), + username, + password, + ) + + # Cleanup + admin.delete_client(client_id=client_id) + admin.delete_user(user_id=user_id) + + @pytest.fixture def realm(admin: KeycloakAdmin) -> str: + """Fixture for a new random realm.""" realm_name = str(uuid.uuid4()) - admin.create_realm(payload={"realm": realm_name}) + admin.create_realm(payload={"realm": realm_name, "enabled": True}) yield realm_name admin.delete_realm(realm_name=realm_name) @pytest.fixture def user(admin: KeycloakAdmin, realm: str) -> str: + """Fixture for a new random user.""" admin.realm_name = realm username = str(uuid.uuid4()) user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"}) @@ -102,6 +181,7 @@ def user(admin: KeycloakAdmin, realm: str) -> str: @pytest.fixture def group(admin: KeycloakAdmin, realm: str) -> str: + """Fixture for a new random group.""" admin.realm_name = realm group_name = str(uuid.uuid4()) group_id = admin.create_group(payload={"name": group_name}) @@ -111,6 +191,7 @@ def group(admin: KeycloakAdmin, realm: str) -> str: @pytest.fixture def client(admin: KeycloakAdmin, realm: str) -> str: + """Fixture for a new random client.""" admin.realm_name = realm client = str(uuid.uuid4()) client_id = admin.create_client(payload={"name": client, "clientId": client}) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 53a76dd9..f01b91c0 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,9 +1,13 @@ +"""Test module for KeycloakOpenID.""" +from unittest import mock + from keycloak.authorization import Authorization from keycloak.connection import ConnectionManager from keycloak.keycloak_openid import KeycloakOpenID def test_keycloak_openid_init(env): + """Test KeycloakOpenId's init method.""" oid = KeycloakOpenID( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", realm_name="master", @@ -18,6 +22,7 @@ def test_keycloak_openid_init(env): def test_well_known(oid: KeycloakOpenID): + """Test the well_known method.""" res = oid.well_known() assert res is not None assert res != dict() @@ -77,3 +82,56 @@ def test_well_known(oid: KeycloakOpenID): "userinfo_signing_alg_values_supported", ]: assert key in res + + +def test_auth_url(env, oid: KeycloakOpenID): + """Test the auth_url method.""" + res = oid.auth_url(redirect_uri="http://test.test/*") + assert ( + res + == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}" + + f"/protocol/openid-connect/auth?client_id={oid.client_id}&response_type=code" + + "&redirect_uri=http://test.test/*" + ) + + +def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): + """Test the token method.""" + oid, username, password = oid_with_credentials + token = oid.token(username=username, password=password) + assert token == { + "access_token": mock.ANY, + "expires_in": 300, + "not-before-policy": 0, + "refresh_expires_in": 1800, + "refresh_token": mock.ANY, + "scope": "profile email", + "session_state": mock.ANY, + "token_type": "Bearer", + } + + # Test with dummy totp + token = oid.token(username=username, password=password, totp="123456") + assert token == { + "access_token": mock.ANY, + "expires_in": 300, + "not-before-policy": 0, + "refresh_expires_in": 1800, + "refresh_token": mock.ANY, + "scope": "profile email", + "session_state": mock.ANY, + "token_type": "Bearer", + } + + # Test with extra param + token = oid.token(username=username, password=password, extra_param="foo") + assert token == { + "access_token": mock.ANY, + "expires_in": 300, + "not-before-policy": 0, + "refresh_expires_in": 1800, + "refresh_token": mock.ANY, + "scope": "profile email", + "session_state": mock.ANY, + "token_type": "Bearer", + } From 81b3cc80db831d9ffb321bd4984fa2c082363fb6 Mon Sep 17 00:00:00 2001 From: Fredrik Lindner Date: Mon, 4 Jul 2022 14:28:22 +0200 Subject: [PATCH 202/566] docs: add timeout to docstring --- src/keycloak/keycloak_admin.py | 1 + src/keycloak/keycloak_openid.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 44e9c3b7..4b85eadf 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -57,6 +57,7 @@ class KeycloakAdmin: :param user_realm_name: The realm name of the user, if different from realm_name :param auto_refresh_token: list of methods that allows automatic token refresh. Ex: ['get', 'put', 'post', 'delete'] + :param timeout: connection timeout in seconds """ PAGE_SIZE = 100 diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 7216b5d1..ad608d0f 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -62,6 +62,7 @@ class KeycloakOpenID: :param verify: True if want check connection SSL :param custom_headers: dict of custom header to pass to each HTML request :param proxies: dict of proxies to sent the request by. + :param timeout: connection timeout in seconds """ def __init__( From b0dcd5f431a953183bbb2dfcc2f1de9f59881550 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 10 Jul 2022 11:31:50 +0200 Subject: [PATCH 203/566] docs: fix readthedocs build --- .readthedocs.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 7aa6ce59..4379fbfa 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,7 +4,11 @@ build: os: "ubuntu-20.04" tools: python: "3.10" - -python: - install: - - requirements: docs-requirements.txt + jobs: + pre_create_environment: + - asdf plugin add poetry + - asdf install poetry latest + - asdf global poetry latest + - poetry config virtualenvs.create false + post_install: + - poetry install -E docs From b10c161ed8f2b786d0bce991748736dd8cf8ad3b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Mon, 11 Jul 2022 13:23:06 +0000 Subject: [PATCH 204/566] test: added more openid tests --- src/keycloak/keycloak_openid.py | 2 +- test_keycloak_init.sh | 4 +- tests/conftest.py | 55 +++++++++++++++- tests/test_keycloak_openid.py | 107 +++++++++++++++++++++++++++++++- 4 files changed, 161 insertions(+), 7 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index ede9a3c0..fa04e4db 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -346,7 +346,7 @@ def entitlement(self, token, resource_server_id): if data_raw.status_code == 404: return raise_error_from_response(data_raw, KeycloakDeprecationError) - return raise_error_from_response(data_raw, KeycloakGetError) + return raise_error_from_response(data_raw, KeycloakGetError) # pragma: no cover def introspect(self, token, rpt=None, token_type_hint=None): """ diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index 82afabb8..e9c6823c 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -10,7 +10,7 @@ function keycloak_stop() { function keycloak_start() { echo "Starting keycloak docker container" - docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev + docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -e KC_FEATURES="token-exchange" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev SECONDS=0 until curl --silent --output /dev/null localhost:$KEYCLOAK_PORT; do sleep 5; @@ -28,7 +28,7 @@ keycloak_stop # In case it did not shut down correctly last time. keycloak_start eval ${CMD_ARGS} -docker logs unittest_keycloak > keycloak_test_logs.txt RETURN_VALUE=$? +docker logs unittest_keycloak > keycloak_test_logs.txt exit ${RETURN_VALUE} diff --git a/tests/conftest.py b/tests/conftest.py index 6023e51d..47c98545 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -124,13 +124,65 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin) admin.realm_name = realm # Create client client = str(uuid.uuid4()) + secret = str(uuid.uuid4()) client_id = admin.create_client( payload={ "name": client, "clientId": client, "enabled": True, - "publicClient": True, + "publicClient": False, + "protocol": "openid-connect", + "secret": secret, + "clientAuthenticatorType": "client-secret", + } + ) + # Create user + username = str(uuid.uuid4()) + password = str(uuid.uuid4()) + user_id = admin.create_user( + payload={ + "username": username, + "email": f"{username}@test.test", + "enabled": True, + "credentials": [{"type": "password", "value": password}], + } + ) + + yield ( + KeycloakOpenID( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + realm_name=realm, + client_id=client, + client_secret_key=secret, + ), + username, + password, + ) + + # Cleanup + admin.delete_client(client_id=client_id) + admin.delete_user(user_id=user_id) + + +@pytest.fixture +def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): + """Fixture for an initialized KeycloakOpenID class and a random user credentials.""" + # Set the realm + admin.realm_name = realm + # Create client + client = str(uuid.uuid4()) + secret = str(uuid.uuid4()) + client_id = admin.create_client( + payload={ + "name": client, + "clientId": client, + "enabled": True, + "publicClient": False, "protocol": "openid-connect", + "secret": secret, + "clientAuthenticatorType": "client-secret", + "authorizationServicesEnabled": True, + "serviceAccountsEnabled": True, } ) # Create user @@ -150,6 +202,7 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin) server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", realm_name=realm, client_id=client, + client_secret_key=secret, ), username, password, diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index f01b91c0..9ed2b882 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,8 +1,12 @@ """Test module for KeycloakOpenID.""" from unittest import mock +import pytest + from keycloak.authorization import Authorization from keycloak.connection import ConnectionManager +from keycloak.exceptions import KeycloakDeprecationError, KeycloakRPTNotFound +from keycloak.keycloak_admin import KeycloakAdmin from keycloak.keycloak_openid import KeycloakOpenID @@ -105,7 +109,7 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, - "scope": "profile email", + "scope": mock.ANY, "session_state": mock.ANY, "token_type": "Bearer", } @@ -118,7 +122,7 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, - "scope": "profile email", + "scope": mock.ANY, "session_state": mock.ANY, "token_type": "Bearer", } @@ -131,7 +135,104 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, - "scope": "profile email", + "scope": mock.ANY, "session_state": mock.ANY, "token_type": "Bearer", } + + +def test_exchange_token( + oid_with_credentials: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test the exchange token method.""" + # Verify existing user + oid, username, password = oid_with_credentials + + # Allow impersonation + admin.realm_name = oid.realm_name + admin.assign_client_role( + user_id=admin.get_user_id(username=username), + client_id=admin.get_client_id(client_name="realm-management"), + roles=[ + admin.get_client_role( + client_id=admin.get_client_id(client_name="realm-management"), + role_name="impersonation", + ) + ], + ) + + token = oid.token(username=username, password=password) + assert oid.userinfo(token=token["access_token"]) == { + "email": f"{username}@test.test", + "email_verified": False, + "preferred_username": username, + "sub": mock.ANY, + } + + # Exchange token with the new user + new_token = oid.exchange_token( + token=token["access_token"], + client_id=oid.client_id, + audience=oid.client_id, + subject=username, + ) + assert oid.userinfo(token=new_token["access_token"]) == { + "email": f"{username}@test.test", + "email_verified": False, + "preferred_username": username, + "sub": mock.ANY, + } + assert token != new_token + + +def test_certs(oid: KeycloakOpenID): + """Test certificates.""" + assert len(oid.certs()["keys"]) == 2 + + +def test_public_key(oid: KeycloakOpenID): + """Test public key.""" + assert oid.public_key() is not None + + +def test_entitlement( + oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test entitlement.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + resource_server_id = admin.get_client_authz_resources( + client_id=admin.get_client_id(oid.client_id) + )[0]["_id"] + + with pytest.raises(KeycloakDeprecationError): + oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id) + + +def test_introspect(oid_with_credentials: tuple[KeycloakOpenID, str, str]): + """Test introspect.""" + oid, username, password = oid_with_credentials + token = oid.token(username=username, password=password) + + assert oid.introspect(token=token["access_token"])["active"] + assert oid.introspect( + token=token["access_token"], rpt="some", token_type_hint="requesting_party_token" + ) == {"active": False} + + with pytest.raises(KeycloakRPTNotFound): + oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token") + + +def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): + """Test decode token.""" + oid, username, password = oid_with_credentials + token = oid.token(username=username, password=password) + + assert ( + oid.decode_token( + token=token["access_token"], + key="-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----", + options={"verify_aud": False}, + )["preferred_username"] + == username + ) From 5e6c775735d9874a3b9b0df89bd30faeb8bc7e3e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 08:00:35 +0000 Subject: [PATCH 205/566] style: fixed docstrings everywhere --- poetry.lock | 44 +- src/keycloak/__init__.py | 2 + src/keycloak/authorization/__init__.py | 10 +- src/keycloak/authorization/permission.py | 14 +- src/keycloak/authorization/policy.py | 20 +- src/keycloak/authorization/role.py | 9 +- src/keycloak/connection.py | 15 +- src/keycloak/keycloak_admin.py | 609 +++++++++-------------- src/keycloak/keycloak_openid.py | 75 +-- src/keycloak/uma_permissions.py | 42 +- src/keycloak/urls_patterns.py | 2 + tests/__init__.py | 1 + tests/test_keycloak_admin.py | 29 ++ tests/test_uma_permissions.py | 28 ++ tests/test_urls_patterns.py | 3 +- 15 files changed, 450 insertions(+), 453 deletions(-) diff --git a/poetry.lock b/poetry.lock index c747f94b..06cdcc12 100644 --- a/poetry.lock +++ b/poetry.lock @@ -22,7 +22,7 @@ test = ["coverage", "flake8", "pexpect", "wheel"] [[package]] name = "astroid" -version = "2.11.6" +version = "2.11.7" description = "An abstract syntax tree for Python with inference support." category = "main" optional = true @@ -36,7 +36,7 @@ wrapt = ">=1.11,<2" [[package]] name = "atomicwrites" -version = "1.4.0" +version = "1.4.1" description = "Atomic file writes." category = "dev" optional = false @@ -208,7 +208,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "ecdsa" -version = "0.17.0" +version = "0.18.0" description = "ECDSA cryptographic signature library (pure python)" category = "main" optional = false @@ -460,7 +460,7 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.19.0" +version = "2.20.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false @@ -871,7 +871,7 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.0" +version = "0.11.1" description = "Style preserving TOML library" category = "dev" optional = false @@ -926,11 +926,11 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.9" +version = "1.26.10" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] @@ -1001,14 +1001,8 @@ argcomplete = [ {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, ] -astroid = [ - {file = "astroid-2.11.6-py3-none-any.whl", hash = "sha256:ba33a82a9a9c06a5ceed98180c5aab16e29c285b828d94696bf32d6015ea82a9"}, - {file = "astroid-2.11.6.tar.gz", hash = "sha256:4f933d0bf5e408b03a6feb5d23793740c27e07340605f236496cd6ce552043d6"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, - {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, -] +astroid = [] +atomicwrites = [] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, @@ -1125,10 +1119,7 @@ docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] -ecdsa = [ - {file = "ecdsa-0.17.0-py2.py3-none-any.whl", hash = "sha256:5cf31d5b33743abe0dfc28999036c849a69d548f994b535e527ee3cb7f3ef676"}, - {file = "ecdsa-0.17.0.tar.gz", hash = "sha256:b9f500bb439e4153d0330610f5d26baaf18d17b8ced1bc54410d189385ea68aa"}, -] +ecdsa = [] filelock = [ {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, @@ -1290,10 +1281,7 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pre-commit = [ - {file = "pre_commit-2.19.0-py2.py3-none-any.whl", hash = "sha256:10c62741aa5704faea2ad69cb550ca78082efe5697d6f04e5710c3c229afdd10"}, - {file = "pre_commit-2.19.0.tar.gz", hash = "sha256:4233a1e38621c87d9dda9808c6606d7e7ba0e087cd56d3fe03202a01d2919615"}, -] +pre-commit = [] prompt-toolkit = [ {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, @@ -1463,10 +1451,7 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tomlkit = [ - {file = "tomlkit-0.11.0-py3-none-any.whl", hash = "sha256:0f4050db66fd445b885778900ce4dd9aea8c90c4721141fde0d6ade893820ef1"}, - {file = "tomlkit-0.11.0.tar.gz", hash = "sha256:71ceb10c0eefd8b8f11fe34e8a51ad07812cb1dc3de23247425fbc9ddc47b9dd"}, -] +tomlkit = [] tox = [ {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, @@ -1505,10 +1490,7 @@ unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] -urllib3 = [ - {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"}, - {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"}, -] +urllib3 = [] virtualenv = [ {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, diff --git a/src/keycloak/__init__.py b/src/keycloak/__init__.py index 2c7f70f2..694e53d8 100644 --- a/src/keycloak/__init__.py +++ b/src/keycloak/__init__.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Python-Keycloak library.""" + from ._version import __version__ from .connection import ConnectionManager from .exceptions import ( diff --git a/src/keycloak/authorization/__init__.py b/src/keycloak/authorization/__init__.py index 789656d8..fddd5513 100644 --- a/src/keycloak/authorization/__init__.py +++ b/src/keycloak/authorization/__init__.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Authorization module.""" + import ast import json @@ -30,18 +32,19 @@ class Authorization: - """ - Keycloak Authorization (policies, roles, scopes and resources). + """Keycloak Authorization (policies, roles, scopes and resources). https://keycloak.gitbooks.io/documentation/authorization_services/index.html """ def __init__(self): + """Init method.""" self.policies = {} @property def policies(self): + """Get policies.""" return self._policies @policies.setter @@ -49,8 +52,7 @@ def policies(self, value): self._policies = value def load_config(self, data): - """ - Load policies, roles and permissions (scope/resources). + """Load policies, roles and permissions (scope/resources). :param data: keycloak authorization data (dict) :returns: None diff --git a/src/keycloak/authorization/permission.py b/src/keycloak/authorization/permission.py index a200afec..a444f836 100644 --- a/src/keycloak/authorization/permission.py +++ b/src/keycloak/authorization/permission.py @@ -21,9 +21,12 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak authorization Permission module.""" + class Permission: - """ + """Base permission class. + Consider this simple and very common permission: A permission associates the object being protected with the policies that must be evaluated to @@ -45,6 +48,7 @@ class Permission: """ def __init__(self, name, type, logic, decision_strategy): + """Init method.""" self._name = name self._type = type self._logic = logic @@ -53,13 +57,16 @@ def __init__(self, name, type, logic, decision_strategy): self._scopes = [] def __repr__(self): + """Repr method.""" return "" % (self.name, self.type) def __str__(self): + """Str method.""" return "Permission: %s (%s)" % (self.name, self.type) @property def name(self): + """Get name.""" return self._name @name.setter @@ -68,6 +75,7 @@ def name(self, value): @property def type(self): + """Get type.""" return self._type @type.setter @@ -76,6 +84,7 @@ def type(self, value): @property def logic(self): + """Get logic.""" return self._logic @logic.setter @@ -84,6 +93,7 @@ def logic(self, value): @property def decision_strategy(self): + """Get decision strategy.""" return self._decision_strategy @decision_strategy.setter @@ -92,6 +102,7 @@ def decision_strategy(self, value): @property def resources(self): + """Get resources.""" return self._resources @resources.setter @@ -100,6 +111,7 @@ def resources(self, value): @property def scopes(self): + """Get scopes.""" return self._scopes @scopes.setter diff --git a/src/keycloak/authorization/policy.py b/src/keycloak/authorization/policy.py index 4014b7a8..6b558d8d 100644 --- a/src/keycloak/authorization/policy.py +++ b/src/keycloak/authorization/policy.py @@ -21,11 +21,14 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak authorization Policy module.""" + from ..exceptions import KeycloakAuthorizationConfigError class Policy: - """ + """Base policy class. + A policy defines the conditions that must be satisfied to grant access to an object. Unlike permissions, you do not specify the object being protected but rather the conditions that must be satisfied for access to a given object (for example, resource, scope, or both). @@ -39,6 +42,7 @@ class Policy: """ def __init__(self, name, type, logic, decision_strategy): + """Init method.""" self._name = name self._type = type self._logic = logic @@ -47,13 +51,16 @@ def __init__(self, name, type, logic, decision_strategy): self._permissions = [] def __repr__(self): + """Repr method.""" return "" % (self.name, self.type) def __str__(self): + """Str method.""" return "Policy: %s (%s)" % (self.name, self.type) @property def name(self): + """Get name.""" return self._name @name.setter @@ -62,6 +69,7 @@ def name(self, value): @property def type(self): + """Get type.""" return self._type @type.setter @@ -70,6 +78,7 @@ def type(self, value): @property def logic(self): + """Get logic.""" return self._logic @logic.setter @@ -78,6 +87,7 @@ def logic(self, value): @property def decision_strategy(self): + """Get decision strategy.""" return self._decision_strategy @decision_strategy.setter @@ -86,15 +96,16 @@ def decision_strategy(self, value): @property def roles(self): + """Get roles.""" return self._roles @property def permissions(self): + """Get permissions.""" return self._permissions def add_role(self, role): - """ - Add keycloak role in policy. + """Add keycloak role in policy. :param role: keycloak role. :return: @@ -106,8 +117,7 @@ def add_role(self, role): self._roles.append(role) def add_permission(self, permission): - """ - Add keycloak permission in policy. + """Add keycloak permission in policy. :param permission: keycloak permission. :return: diff --git a/src/keycloak/authorization/role.py b/src/keycloak/authorization/role.py index 3ff06ddb..05da243b 100644 --- a/src/keycloak/authorization/role.py +++ b/src/keycloak/authorization/role.py @@ -21,25 +21,30 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""The authorization Role module.""" + class Role: - """ + """Authorization Role base class. + Roles identify a type or category of user. Admin, user, manager, and employee are all typical roles that may exist in an organization. https://keycloak.gitbooks.io/documentation/server_admin/topics/roles.html - """ def __init__(self, name, required=False): + """Init method.""" self.name = name self.required = required @property def get_name(self): + """Get name.""" return self.name def __eq__(self, other): + """Eq method.""" if isinstance(other, str): return self.name == other return NotImplemented diff --git a/src/keycloak/connection.py b/src/keycloak/connection.py index 07573778..361d95da 100644 --- a/src/keycloak/connection.py +++ b/src/keycloak/connection.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Connection manager module.""" + try: from urllib.parse import urljoin except ImportError: @@ -33,8 +35,7 @@ class ConnectionManager(object): - """ - Represents a simple server connection. + """Represents a simple server connection. :param base_url: (str) The server URL. :param headers: (dict) The header parameters of the requests to the server. @@ -44,6 +45,7 @@ class ConnectionManager(object): """ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): + """Init method.""" self._base_url = base_url self._headers = headers self._timeout = timeout @@ -66,6 +68,7 @@ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): self._s.proxies.update(proxies) def __del__(self): + """Del method.""" self._s.close() @property @@ -75,7 +78,6 @@ def base_url(self): @base_url.setter def base_url(self, value): - """ """ self._base_url = value @property @@ -85,7 +87,6 @@ def timeout(self): @timeout.setter def timeout(self, value): - """ """ self._timeout = value @property @@ -95,7 +96,6 @@ def verify(self): @verify.setter def verify(self, value): - """ """ self._verify = value @property @@ -105,12 +105,10 @@ def headers(self): @headers.setter def headers(self, value): - """ """ self._headers = value def param_headers(self, key): - """ - Return a specific header parameter. + """Return a specific header parameter. :param key: (str) Header parameters key. :returns: If the header parameters exist, return its value. @@ -151,7 +149,6 @@ def raw_get(self, path, **kwargs): :returns: Response the request. :raises: HttpError Can't connect to server. """ - try: return self._s.get( urljoin(self.base_url, path), diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index b2c1de48..0825730e 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -24,6 +24,8 @@ # Unless otherwise stated in the comments, "id", in e.g. user_id, refers to the # internal Keycloak server ID, usually a uuid string +"""The keycloak admin module.""" + import json from builtins import isinstance from typing import Iterable @@ -41,8 +43,7 @@ class KeycloakAdmin: - """ - Keycloak Admin client. + """Keycloak Admin client. :param server_url: Keycloak server url :param username: admin username @@ -90,6 +91,7 @@ def __init__( auto_refresh_token=None, timeout=60, ): + """Init method.""" self.server_url = server_url self.username = username self.password = password @@ -108,6 +110,7 @@ def __init__( @property def server_url(self): + """Get server url.""" return self._server_url @server_url.setter @@ -116,6 +119,7 @@ def server_url(self, value): @property def realm_name(self): + """Get realm name.""" return self._realm_name @realm_name.setter @@ -124,6 +128,7 @@ def realm_name(self, value): @property def connection(self): + """Get connection.""" return self._connection @connection.setter @@ -132,6 +137,7 @@ def connection(self, value): @property def client_id(self): + """Get client id.""" return self._client_id @client_id.setter @@ -140,6 +146,7 @@ def client_id(self, value): @property def client_secret_key(self): + """Get client secret key.""" return self._client_secret_key @client_secret_key.setter @@ -148,6 +155,7 @@ def client_secret_key(self, value): @property def verify(self): + """Get verify.""" return self._verify @verify.setter @@ -156,6 +164,7 @@ def verify(self, value): @property def username(self): + """Get username.""" return self._username @username.setter @@ -164,6 +173,7 @@ def username(self, value): @property def password(self): + """Get password.""" return self._password @password.setter @@ -172,6 +182,7 @@ def password(self, value): @property def totp(self): + """Get totp.""" return self._totp @totp.setter @@ -180,6 +191,7 @@ def totp(self, value): @property def token(self): + """Get token.""" return self._token @token.setter @@ -188,10 +200,12 @@ def token(self, value): @property def auto_refresh_token(self): + """Get auto refresh token.""" return self._auto_refresh_token @property def user_realm_name(self): + """Get user realm name.""" return self._user_realm_name @user_realm_name.setter @@ -200,6 +214,7 @@ def user_realm_name(self, value): @property def custom_headers(self): + """Get custom headers.""" return self._custom_headers @custom_headers.setter @@ -223,7 +238,9 @@ def auto_refresh_token(self, value): self._auto_refresh_token = value def __fetch_all(self, url, query=None): - """Wrapper function to paginate GET requests + """Paginate over get requests. + + Wrapper function to paginate GET requests. :param url: The url on which the query is executed :param query: Existing query parameters (optional) @@ -258,8 +275,9 @@ def __fetch_paginated(self, url, query=None): return raise_error_from_response(self.raw_get(url, **query), KeycloakGetError) def import_realm(self, payload): - """ - Import a new realm from a RealmRepresentation. Realm name must be unique. + """Import a new realm from a RealmRepresentation. + + Realm name must be unique. RealmRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation @@ -268,13 +286,11 @@ def import_realm(self, payload): :return: RealmRepresentation """ - data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def export_realm(self, export_clients=False, export_groups_and_role=False): - """ - Export the realm configurations in the json format + """Export the realm configurations in the json format. RealmRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_partialexport @@ -295,8 +311,7 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): return raise_error_from_response(data_raw, KeycloakPostError) def get_realms(self): - """ - Lists all realms in Keycloak deployment + """List all realms in Keycloak deployment. :return: realms list """ @@ -304,8 +319,7 @@ def get_realms(self): return raise_error_from_response(data_raw, KeycloakGetError) def get_realm(self, realm_name): - """ - Get a specific realm. + """Get a specific realm. RealmRepresentation: https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation @@ -318,8 +332,7 @@ def get_realm(self, realm_name): return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def create_realm(self, payload, skip_exists=False): - """ - Create a realm + """Create a realm. RealmRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation @@ -328,15 +341,15 @@ def create_realm(self, payload, skip_exists=False): :param skip_exists: Skip if Realm already exist. :return: Keycloak server response (RealmRepresentation) """ - data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response( data_raw, KeycloakPostError, expected_codes=[201], skip_exists=skip_exists ) def update_realm(self, realm_name, payload): - """ - Update a realm. This wil only update top level attributes and will ignore any user, + """Update a realm. + + This wil only update top level attributes and will ignore any user, role, or client information in the payload. RealmRepresentation: @@ -346,7 +359,6 @@ def update_realm(self, realm_name, payload): :param payload: RealmRepresentation :return: Http response """ - params_path = {"realm-name": realm_name} data_raw = self.raw_put( urls_patterns.URL_ADMIN_REALM.format(**params_path), data=json.dumps(payload) @@ -354,19 +366,18 @@ def update_realm(self, realm_name, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm(self, realm_name): - """ - Delete a realm + """Delete a realm. :param realm_name: Realm name (not the realm id) :return: Http response """ - params_path = {"realm-name": realm_name} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_REALM.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_users(self, query=None): - """ + """Get all users. + Return a list of users, filtered according to query parameters UserRepresentation @@ -385,8 +396,7 @@ def get_users(self, query=None): return self.__fetch_all(url, query) def create_idp(self, payload): - """ - Create an ID Provider, + """Create an ID Provider. IdentityProviderRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation @@ -400,8 +410,7 @@ def create_idp(self, payload): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_idp(self, idp_alias, payload): - """ - Update an ID Provider + """Update an ID Provider. IdentityProviderRepresentation https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_identity_providers_resource @@ -416,8 +425,7 @@ def update_idp(self, idp_alias, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def add_mapper_to_idp(self, idp_alias, payload): - """ - Create an ID Provider, + """Create an ID Provider. IdentityProviderRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityprovidermapperrepresentation @@ -432,8 +440,7 @@ def add_mapper_to_idp(self, idp_alias, payload): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_mapper_in_idp(self, idp_alias, mapper_id, payload): - """ - Update an IdP mapper + """Update an IdP mapper. IdentityProviderMapperRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_update @@ -457,7 +464,8 @@ def update_mapper_in_idp(self, idp_alias, mapper_id, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_idp_mappers(self, idp_alias): - """ + """Get IDP mappers. + Returns a list of ID Providers mappers IdentityProviderMapperRepresentation @@ -471,7 +479,8 @@ def get_idp_mappers(self, idp_alias): return raise_error_from_response(data_raw, KeycloakGetError) def get_idps(self): - """ + """Get IDPs. + Returns a list of ID Providers, IdentityProviderRepresentation @@ -484,8 +493,7 @@ def get_idps(self): return raise_error_from_response(data_raw, KeycloakGetError) def delete_idp(self, idp_alias): - """ - Deletes ID Provider, + """Delete an ID Provider. :param: idp_alias: idp alias name """ @@ -494,8 +502,9 @@ def delete_idp(self, idp_alias): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_user(self, payload, exist_ok=False): - """ - Create a new user. Username must be unique + """Create a new user. + + Username must be unique UserRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation @@ -522,8 +531,7 @@ def create_user(self, payload, exist_ok=False): return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def users_count(self, query=None): - """ - User counter + """Count users. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_users_resource @@ -537,8 +545,8 @@ def users_count(self, query=None): return raise_error_from_response(data_raw, KeycloakGetError) def get_user_id(self, username): - """ - Get internal keycloak user id from username + """Get internal keycloak user id from username. + This is required for further actions against this user. UserRepresentation @@ -553,8 +561,7 @@ def get_user_id(self, username): return next((user["id"] for user in users if user["username"] == lower_user_name), None) def get_user(self, user_id): - """ - Get representation of the user + """Get representation of the user. :param user_id: User id @@ -568,7 +575,8 @@ def get_user(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_user_groups(self, user_id): - """ + """Get user groups. + Returns a list of groups of which the user is a member :param user_id: User id @@ -580,8 +588,7 @@ def get_user_groups(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def update_user(self, user_id, payload): - """ - Update the user + """Update the user. :param user_id: User id :param payload: UserRepresentation @@ -595,8 +602,7 @@ def update_user(self, user_id, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_user(self, user_id): - """ - Delete the user + """Delete the user. :param user_id: User id @@ -607,8 +613,9 @@ def delete_user(self, user_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def set_user_password(self, user_id, password, temporary=True): - """ - Set up a password for the user. If temporary is True, the user will have to reset + """Set up a password for the user. + + If temporary is True, the user will have to reset the temporary password next time they log in. https://www.keycloak.org/docs-api/18.0/rest-api/#_users_resource @@ -628,7 +635,8 @@ def set_user_password(self, user_id, password, temporary=True): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_credentials(self, user_id): - """ + """Get user credentials. + Returns a list of credential belonging to the user. CredentialRepresentation @@ -642,8 +650,7 @@ def get_credentials(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def delete_credential(self, user_id, credential_id): - """ - Delete credential of the user. + """Delete credential of the user. CredentialRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation @@ -661,8 +668,7 @@ def delete_credential(self, user_id, credential_id): return raise_error_from_response(data_raw, KeycloakDeleteError) def user_logout(self, user_id): - """ - Logs out user. + """Log out the user. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout @@ -676,8 +682,7 @@ def user_logout(self, user_id): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def user_consents(self, user_id): - """ - Get consents granted by the user + """Get consents granted by the user. UserConsentRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation @@ -690,7 +695,8 @@ def user_consents(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_user_social_logins(self, user_id): - """ + """Get user social logins. + Returns a list of federated identities/social logins of which the user has been associated with :param user_id: User id @@ -703,9 +709,8 @@ def get_user_social_logins(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def add_user_social_login(self, user_id, provider_id, provider_userid, provider_username): + """Add a federated identity / social login provider to the user. - """ - Add a federated identity / social login provider to the user :param user_id: User id :param provider_id: Social login provider id :param provider_userid: userid specified by the provider @@ -725,9 +730,8 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201, 204]) def delete_user_social_login(self, user_id, provider_id): + """Delete a federated identity / social login provider from the user. - """ - Delete a federated identity / social login provider from the user :param user_id: User id :param provider_id: Social login provider id :return: @@ -741,9 +745,9 @@ def delete_user_social_login(self, user_id, provider_id): def send_update_account( self, user_id, payload, client_id=None, lifespan=None, redirect_uri=None ): - """ - Send an update account email to the user. An email contains a - link the user can click to perform a set of required actions. + """Send an update account email to the user. + + An email contains a link the user can click to perform a set of required actions. :param user_id: User id :param payload: A list of actions for the user to complete @@ -763,9 +767,9 @@ def send_update_account( return raise_error_from_response(data_raw, KeycloakPutError) def send_verify_email(self, user_id, client_id=None, redirect_uri=None): - """ - Send a update account email to the user An email contains a - link the user can click to perform a set of required actions. + """Send a update account email to the user. + + An email contains a link the user can click to perform a set of required actions. :param user_id: User id :param client_id: Client id (optional) @@ -783,8 +787,7 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): return raise_error_from_response(data_raw, KeycloakPutError) def get_sessions(self, user_id): - """ - Get sessions associated with the user + """Get sessions associated with the user. :param user_id: id of user @@ -798,8 +801,7 @@ def get_sessions(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_server_info(self): - """ - Get themes, social providers, auth providers, and event listeners available on this server + """Get themes, social providers, auth providers, and event listeners available on this server. ServerInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation @@ -810,7 +812,8 @@ def get_server_info(self): return raise_error_from_response(data_raw, KeycloakGetError) def get_groups(self, query=None): - """ + """Get groups. + Returns a list of groups belonging to the realm GroupRepresentation @@ -828,8 +831,9 @@ def get_groups(self, query=None): return self.__fetch_all(url, query) def get_group(self, group_id): - """ - Get group by id. Returns full group details + """Get group by id. + + Returns full group details GroupRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation @@ -842,7 +846,8 @@ def get_group(self, group_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_subgroups(self, group, path): - """ + """Get subgroups. + Utility function to iterate through nested group structures GroupRepresentation @@ -853,7 +858,6 @@ def get_subgroups(self, group, path): :return: Keycloak server response (GroupRepresentation) """ - for subgroup in group["subGroups"]: if subgroup["path"] == path: return subgroup @@ -866,8 +870,9 @@ def get_subgroups(self, group, path): return None def get_group_members(self, group_id, query=None): - """ - Get members by group id. Returns group members + """Get members by group id. + + Returns group members GroupRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/#_userrepresentation @@ -887,8 +892,8 @@ def get_group_members(self, group_id, query=None): return self.__fetch_all(url, query) def get_group_by_path(self, path, search_in_subgroups=False): - """ - Get group id based on name or path. + """Get group id based on name or path. + A straight name or path match with a top-level group will return first. Subgroups are traversed, the first to match path (or name with path) is returned. @@ -899,7 +904,6 @@ def get_group_by_path(self, path, search_in_subgroups=False): :param search_in_subgroups: True if want search in the subgroups :return: Keycloak server response (GroupRepresentation) """ - groups = self.get_groups() # TODO: Review this code is necessary @@ -916,8 +920,7 @@ def get_group_by_path(self, path, search_in_subgroups=False): return None def create_group(self, payload, parent=None, skip_exists=False): - """ - Creates a group in the Realm + """Create a group in the Realm. :param payload: GroupRepresentation :param parent: parent group's id. Required to create a sub-group. @@ -928,7 +931,6 @@ def create_group(self, payload, parent=None, skip_exists=False): :return: Group id for newly created group or None for an existing group """ - if parent is None: params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( @@ -950,8 +952,7 @@ def create_group(self, payload, parent=None, skip_exists=False): return def update_group(self, group_id, payload): - """ - Update group, ignores subgroups. + """Update group, ignores subgroups. :param group_id: id of group :param payload: GroupRepresentation with updated information. @@ -961,7 +962,6 @@ def update_group(self, group_id, payload): :return: Http response """ - params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_GROUP.format(**params_path), data=json.dumps(payload) @@ -969,14 +969,14 @@ def update_group(self, group_id, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_set_permissions(self, group_id, enabled=True): - """ - Enable/Disable permissions for a group. Cannot delete group if disabled + """Enable/Disable permissions for a group. + + Cannot delete group if disabled :param group_id: id of group :param enabled: boolean :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_GROUP_PERMISSIONS.format(**params_path), @@ -985,14 +985,12 @@ def group_set_permissions(self, group_id, enabled=True): return raise_error_from_response(data_raw, KeycloakPutError) def group_user_add(self, user_id, group_id): - """ - Add user to group (user_id and group_id) + """Add user to group (user_id and group_id). :param user_id: id of user :param group_id: id of group to add to :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path), data=None @@ -1000,32 +998,29 @@ def group_user_add(self, user_id, group_id): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def group_user_remove(self, user_id, group_id): - """ - Remove user from group (user_id and group_id) + """Remove user from group (user_id and group_id). :param user_id: id of user :param group_id: id of group to remove from :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def delete_group(self, group_id): - """ - Deletes a group in the Realm + """Delete a group in the Realm. :param group_id: id of group to delete :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_GROUP.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_clients(self): - """ + """Get clients. + Returns a list of clients belonging to the realm ClientRepresentation @@ -1033,14 +1028,12 @@ def get_clients(self): :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENTS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client(self, client_id): - """ - Get representation of the client + """Get representation of the client. ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1048,21 +1041,19 @@ def get_client(self, client_id): :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_id(self, client_name): - """ - Get internal keycloak client id from client-id. + """Get internal keycloak client id from client-id. + This is required for further actions against this client. :param client_name: name in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ - clients = self.get_clients() for client in clients: @@ -1072,14 +1063,12 @@ def get_client_id(self, client_name): return None def get_client_authz_settings(self, client_id): - """ - Get authorization json from client. + """Get authorization json from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SETTINGS.format(**params_path) @@ -1087,8 +1076,7 @@ def get_client_authz_settings(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_resource(self, client_id, payload, skip_exists=False): - """ - Create resources of client. + """Create resources of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1097,7 +1085,6 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -1109,14 +1096,12 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): ) def get_client_authz_resources(self, client_id): - """ - Get resources from client. + """Get resources from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_RESOURCES.format(**params_path) @@ -1124,8 +1109,7 @@ def get_client_authz_resources(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError) def create_client_authz_role_based_policy(self, client_id, payload, skip_exists=False): - """ - Create role-based policy of client. + """Create role-based policy of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1147,7 +1131,6 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= } """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -1159,8 +1142,7 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= ) def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): - """ - Create resource-based permission of client. + """Create resource-based permission of client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1183,7 +1165,6 @@ def create_client_authz_resource_based_permission(self, client_id, payload, skip ] """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -1195,27 +1176,23 @@ def create_client_authz_resource_based_permission(self, client_id, payload, skip ) def get_client_authz_scopes(self, client_id): - """ - Get scopes from client. + """Get scopes from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_permissions(self, client_id): - """ - Get permissions from client. + """Get permissions from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_PERMISSIONS.format(**params_path) @@ -1223,14 +1200,12 @@ def get_client_authz_permissions(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policies(self, client_id): - """ - Get policies from client. + """Get policies from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_AUTHZ_POLICIES.format(**params_path) @@ -1238,14 +1213,12 @@ def get_client_authz_policies(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_client_service_account_user(self, client_id): - """ - Get service account user from client. + """Get service account user from client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: UserRepresentation """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_SERVICE_ACCOUNT_USER.format(**params_path) @@ -1253,8 +1226,7 @@ def get_client_service_account_user(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError) def create_client(self, payload, skip_exists=False): - """ - Create a client + """Create a client. ClientRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1263,7 +1235,6 @@ def create_client(self, payload, skip_exists=False): :param payload: ClientRepresentation :return: Client ID """ - if skip_exists: client_id = self.get_client_id(client_name=payload["name"]) @@ -1281,8 +1252,7 @@ def create_client(self, payload, skip_exists=False): return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client(self, client_id, payload): - """ - Update a client + """Update a client. :param client_id: Client id :param payload: ClientRepresentation @@ -1296,8 +1266,7 @@ def update_client(self, client_id, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client(self, client_id): - """ - Get representation of the client + """Get representation of the client. ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -1305,14 +1274,12 @@ def delete_client(self, client_id): :param client_id: keycloak client id (not oauth client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_installation_provider(self, client_id, provider_id): - """ - Get content for given installation provider + """Get content for given installation provider. Related documentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource @@ -1323,7 +1290,6 @@ def get_client_installation_provider(self, client_id, provider_id): :param client_id: Client id :param provider_id: provider id to specify response format """ - params_path = {"realm-name": self.realm_name, "id": client_id, "provider-id": provider_id} data_raw = self.raw_get( urls_patterns.URL_ADMIN_CLIENT_INSTALLATION_PROVIDER.format(**params_path) @@ -1331,22 +1297,20 @@ def get_client_installation_provider(self, client_id, provider_id): return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def get_realm_roles(self): - """ - Get all roles for the realm or client + """Get all roles for the realm or client. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_realm_role_members(self, role_name, query=None): - """ - Get role members of realm by role name. + """Get role members of realm by role name. + :param role_name: Name of the role. :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) @@ -1359,8 +1323,7 @@ def get_realm_role_members(self, role_name, query=None): ) def get_client_roles(self, client_id): - """ - Get all roles for the client + """Get all roles for the client. :param client_id: id of client (not client-id) @@ -1369,14 +1332,13 @@ def get_client_roles(self, client_id): :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role(self, client_id, role_name): - """ - Get client role id by name + """Get client role id by name. + This is required for further actions with this role. :param client_id: id of client (not client-id) @@ -1392,10 +1354,8 @@ def get_client_role(self, client_id, role_name): return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role_id(self, client_id, role_name): - """ - Warning: Deprecated + """Get client role id by name. - Get client role id by name This is required for further actions with this role. :param client_id: id of client (not client-id) @@ -1410,8 +1370,7 @@ def get_client_role_id(self, client_id, role_name): return role.get("id") def create_client_role(self, client_role_id, payload, skip_exists=False): - """ - Create a client role + """Create a client role. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation @@ -1421,7 +1380,6 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): :param skip_exists: If true then do not raise an error if client role already exists :return: Client role name """ - if skip_exists: try: res = self.get_client_role(client_id=client_role_id, role_name=payload["name"]) @@ -1440,15 +1398,13 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): - """ - Add composite roles to client role + """Add composite roles to client role. :param client_role_id: id of client (not client-id) :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be updated :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_post( @@ -1458,8 +1414,7 @@ def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def update_client_role(self, client_role_id, role_name, payload): - """ - Update a client role + """Update a client role. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation @@ -1475,8 +1430,7 @@ def update_client_role(self, client_role_id, role_name, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client_role(self, client_role_id, role_name): - """ - Delete a client role + """Delete a client role. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation @@ -1489,15 +1443,13 @@ def delete_client_role(self, client_role_id, role_name): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def assign_client_role(self, user_id, client_id, roles): - """ - Assign a client role to a user + """Assign a client role to a user. :param user_id: id of user :param client_id: id of client (not client-id) :param roles: roles list or role (use RoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_post( @@ -1507,8 +1459,8 @@ def assign_client_role(self, user_id, client_id, roles): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_client_role_members(self, client_id, role_name, **query): - """ - Get members by client role . + """Get members by client role. + :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters @@ -1521,8 +1473,8 @@ def get_client_role_members(self, client_id, role_name, **query): ) def get_client_role_groups(self, client_id, role_name, **query): - """ - Get group members by client role . + """Get group members by client role. + :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters @@ -1535,14 +1487,12 @@ def get_client_role_groups(self, client_id, role_name, **query): ) def create_realm_role(self, payload, skip_exists=False): - """ - Create a new role for the realm or client + """Create a new role for the realm or client. :param payload: The role (use RoleRepresentation) :param skip_exists: If true then do not raise an error if realm role already exists :return: Realm role name """ - if skip_exists: try: role = self.get_realm_role(role_name=payload["name"]) @@ -1561,8 +1511,8 @@ def create_realm_role(self, payload, skip_exists=False): return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def get_realm_role(self, role_name): - """ - Get realm role by role name + """Get realm role by role name. + :param role_name: role's name, not id! RoleRepresentation @@ -1576,13 +1526,12 @@ def get_realm_role(self, role_name): return raise_error_from_response(data_raw, KeycloakGetError) def update_realm_role(self, role_name, payload): - """ - Update a role for the realm by name + """Update a role for the realm by name. + :param role_name: The name of the role to be updated :param payload: The role (use RoleRepresentation) :return Keycloak server response """ - params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_put( urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path), @@ -1591,12 +1540,11 @@ def update_realm_role(self, role_name, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_realm_role(self, role_name): - """ - Delete a role for the realm by name + """Delete a role for the realm by name. + :param payload: The role name {'role-name':'name-of-the-role'} :return Keycloak server response """ - params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( urls_patterns.URL_ADMIN_REALM_ROLES_ROLE_BY_NAME.format(**params_path) @@ -1604,14 +1552,12 @@ def delete_realm_role(self, role_name): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_composite_realm_roles_to_role(self, role_name, roles): - """ - Add composite roles to the role + """Add composite roles to the role. :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be updated :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_post( @@ -1621,14 +1567,12 @@ def add_composite_realm_roles_to_role(self, role_name, roles): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def remove_composite_realm_roles_to_role(self, role_name, roles): - """ - Remove composite roles from the role + """Remove composite roles from the role. :param role_name: The name of the role :param roles: roles list or role (use RoleRepresentation) to be removed :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( @@ -1638,13 +1582,11 @@ def remove_composite_realm_roles_to_role(self, role_name, roles): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_composite_realm_roles_of_role(self, role_name): - """ - Get composite roles of the role + """Get composite roles of the role. :param role_name: The name of the role :return: Keycloak server response (array RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_get( urls_patterns.URL_ADMIN_REALM_ROLES_COMPOSITE_REALM_ROLE.format(**params_path) @@ -1652,14 +1594,12 @@ def get_composite_realm_roles_of_role(self, role_name): return raise_error_from_response(data_raw, KeycloakGetError) def assign_realm_roles(self, user_id, roles): - """ - Assign realm roles to a user + """Assign realm roles to a user. :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post( @@ -1669,14 +1609,12 @@ def assign_realm_roles(self, user_id, roles): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_realm_roles_of_user(self, user_id, roles): - """ - Deletes realm roles of a user + """Delete realm roles of a user. :param user_id: id of user :param roles: roles list or role (use RoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete( @@ -1686,20 +1624,18 @@ def delete_realm_roles_of_user(self, user_id, roles): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_realm_roles_of_user(self, user_id): - """ - Get all realm roles for a user. + """Get all realm roles for a user. :param user_id: id of user :return: Keycloak server response (array RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_available_realm_roles_of_user(self, user_id): - """ - Get all available (i.e. unassigned) realm roles for a user. + """Get all available (i.e. unassigned) realm roles for a user. + :param user_id: id of user :return: Keycloak server response (array RoleRepresentation) """ @@ -1710,8 +1646,8 @@ def get_available_realm_roles_of_user(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_composite_realm_roles_of_user(self, user_id): - """ - Get all composite (i.e. implicit) realm roles for a user. + """Get all composite (i.e. implicit) realm roles for a user. + :param user_id: id of user :return: Keycloak server response (array RoleRepresentation) """ @@ -1722,14 +1658,12 @@ def get_composite_realm_roles_of_user(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_realm_roles(self, group_id, roles): - """ - Assign realm roles to a group + """Assign realm roles to a group. :param group_id: id of groupp :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_post( @@ -1739,14 +1673,12 @@ def assign_group_realm_roles(self, group_id, roles): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def delete_group_realm_roles(self, group_id, roles): - """ - Delete realm roles of a group + """Delete realm roles of a group. :param group_id: id of group :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete( @@ -1756,8 +1688,7 @@ def delete_group_realm_roles(self, group_id, roles): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_group_realm_roles(self, group_id): - """ - Get all realm roles for a group. + """Get all realm roles for a group. :param user_id: id of the group :return: Keycloak server response (array RoleRepresentation) @@ -1767,15 +1698,13 @@ def get_group_realm_roles(self, group_id): return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_client_roles(self, group_id, client_id, roles): - """ - Assign client roles to a group + """Assign client roles to a group. :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_post( @@ -1785,28 +1714,24 @@ def assign_group_client_roles(self, group_id, client_id, roles): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def get_group_client_roles(self, group_id, client_id): - """ - Get client roles of a group + """Get client roles of a group. :param group_id: id of group :param client_id: id of client (not client-id) :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def delete_group_client_roles(self, group_id, client_id, roles): - """ - Delete client roles of a group + """Delete client roles of a group. :param group_id: id of group :param client_id: id of client (not client-id) :param roles: roles list or role (use GroupRoleRepresentation) :return: Keycloak server response (array RoleRepresentation) """ - payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_delete( @@ -1816,8 +1741,7 @@ def delete_group_client_roles(self, group_id, client_id, roles): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_client_roles_of_user(self, user_id, client_id): - """ - Get all client roles for a user. + """Get all client roles for a user. :param user_id: id of user :param client_id: id of client (not client-id) @@ -1828,8 +1752,7 @@ def get_client_roles_of_user(self, user_id, client_id): ) def get_available_client_roles_of_user(self, user_id, client_id): - """ - Get available client role-mappings for a user. + """Get available client role-mappings for a user. :param user_id: id of user :param client_id: id of client (not client-id) @@ -1840,8 +1763,7 @@ def get_available_client_roles_of_user(self, user_id, client_id): ) def get_composite_client_roles_of_user(self, user_id, client_id): - """ - Get composite client role-mappings for a user. + """Get composite client role-mappings for a user. :param user_id: id of user :param client_id: id of client (not client-id) @@ -1857,8 +1779,7 @@ def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, clie return raise_error_from_response(data_raw, KeycloakGetError) def delete_client_roles_of_user(self, user_id, client_id, roles): - """ - Delete client roles from a user. + """Delete client roles from a user. :param user_id: id of user :param client_id: id of client containing role (not client-id) @@ -1874,8 +1795,9 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flows(self): - """ - Get authentication flows. Returns all flow details + """Get authentication flows. + + Returns all flow details AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -1887,8 +1809,9 @@ def get_authentication_flows(self): return raise_error_from_response(data_raw, KeycloakGetError) def get_authentication_flow_for_id(self, flow_id): - """ - Get one authentication flow by it's id. Returns all flow details + """Get one authentication flow by it's id. + + Returns all flow details AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -1901,8 +1824,7 @@ def get_authentication_flow_for_id(self, flow_id): return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow(self, payload, skip_exists=False): - """ - Create a new authentication flow + """Create a new authentication flow. AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -1911,7 +1833,6 @@ def create_authentication_flow(self, payload, skip_exists=False): :param skip_exists: Do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS.format(**params_path), data=json.dumps(payload) @@ -1921,15 +1842,14 @@ def create_authentication_flow(self, payload, skip_exists=False): ) def copy_authentication_flow(self, payload, flow_alias): - """ - Copy existing authentication flow under a new name. The new name is given as 'newName' - attribute of the passed payload. + """Copy existing authentication flow under a new name. + + The new name is given as 'newName' attribute of the passed payload. :param payload: JSON containing 'newName' attribute :param flow_alias: the flow alias :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS_COPY.format(**params_path), data=json.dumps(payload) @@ -1937,8 +1857,7 @@ def copy_authentication_flow(self, payload, flow_alias): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow(self, flow_id): - """ - Delete authentication flow + """Delete authentication flow. AuthenticationInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationinforepresentation @@ -1951,8 +1870,9 @@ def delete_authentication_flow(self, flow_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_authentication_flow_executions(self, flow_alias): - """ - Get authentication flow executions. Returns all execution steps + """Get authentication flow executions. + + Returns all execution steps :param flow_alias: the flow alias :return: Response(json) @@ -1962,8 +1882,7 @@ def get_authentication_flow_executions(self, flow_alias): return raise_error_from_response(data_raw, KeycloakGetError) def update_authentication_flow_executions(self, payload, flow_alias): - """ - Update an authentication flow execution + """Update an authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -1972,7 +1891,6 @@ def update_authentication_flow_executions(self, payload, flow_alias): :param flow_alias: The flow alias :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put( urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path), @@ -1981,8 +1899,7 @@ def update_authentication_flow_executions(self, payload, flow_alias): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[202, 204]) def get_authentication_flow_execution(self, execution_id): - """ - Get authentication flow execution. + """Get authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -1995,8 +1912,7 @@ def get_authentication_flow_execution(self, execution_id): return raise_error_from_response(data_raw, KeycloakGetError) def create_authentication_flow_execution(self, payload, flow_alias): - """ - Create an authentication flow execution + """Create an authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -2005,7 +1921,6 @@ def create_authentication_flow_execution(self, payload, flow_alias): :param flow_alias: The flow alias :return: Keycloak server response """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_EXECUTION.format(**params_path), @@ -2014,8 +1929,7 @@ def create_authentication_flow_execution(self, payload, flow_alias): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_authentication_flow_execution(self, execution_id): - """ - Delete authentication flow execution + """Delete authentication flow execution. AuthenticationExecutionInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation @@ -2028,8 +1942,7 @@ def delete_authentication_flow_execution(self, execution_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=False): - """ - Create a new sub authentication flow for a given authentication flow + """Create a new sub authentication flow for a given authentication flow. AuthenticationFlowRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation @@ -2039,7 +1952,6 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa :param skip_exists: Do not raise an error if authentication flow already exists :return: Keycloak server response (RoleRepresentation) """ - params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS_FLOW.format(**params_path), @@ -2050,8 +1962,7 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa ) def get_authenticator_providers(self): - """ - Get authenticator providers list. + """Get authenticator providers list. :return: Response(json) """ @@ -2062,8 +1973,7 @@ def get_authenticator_providers(self): return raise_error_from_response(data_raw, KeycloakGetError) def get_authenticator_provider_config_description(self, provider_id): - """ - Get authenticator's provider configuration description. + """Get authenticator's provider configuration description. AuthenticatorConfigInfoRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfiginforepresentation @@ -2078,8 +1988,9 @@ def get_authenticator_provider_config_description(self, provider_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_authenticator_config(self, config_id): - """ - Get authenticator configuration. Returns all configuration details. + """Get authenticator configuration. + + Returns all configuration details. :param config_id: Authenticator config id :return: Response(json) @@ -2089,8 +2000,7 @@ def get_authenticator_config(self, config_id): return raise_error_from_response(data_raw, KeycloakGetError) def update_authenticator_config(self, payload, config_id): - """ - Update an authenticator configuration. + """Update an authenticator configuration. AuthenticatorConfigRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfigrepresentation @@ -2107,14 +2017,13 @@ def update_authenticator_config(self, payload, config_id): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_authenticator_config(self, config_id): - """ - Delete a authenticator configuration. + """Delete a authenticator configuration. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authentication_management_resource :param config_id: Authenticator config id :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_delete( urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path) @@ -2122,8 +2031,7 @@ def delete_authenticator_config(self, config_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def sync_users(self, storage_id, action): - """ - Function to trigger user sync from provider + """Trigger user sync from provider. :param storage_id: The id of the user storage provider :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" @@ -2141,39 +2049,39 @@ def sync_users(self, storage_id, action): return raise_error_from_response(data_raw, KeycloakPostError) def get_client_scopes(self): - """ + """Get client scopes. + Get representation of the client scopes for the realm where we are connected to https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :return: Keycloak server response Array of (ClientScopeRepresentation) """ - params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scope(self, client_scope_id): - """ + """Get client scope. + Get representation of the client scopes for the realm where we are connected to https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param client_scope_id: The id of the client scope :return: Keycloak server response (ClientScopeRepresentation) """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_scope_by_name(self, client_scope_name): - """ + """Get client scope by name. + Get representation of the client scope identified by the client scope name. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param client_scope_name: (str) Name of the client scope :returns: ClientScopeRepresentation or None """ - client_scopes = self.get_client_scopes() for client_scope in client_scopes: if client_scope["name"] == client_scope_name: @@ -2182,8 +2090,7 @@ def get_client_scope_by_name(self, client_scope_name): return None def create_client_scope(self, payload, skip_exists=False): - """ - Create a client scope + """Create a client scope. ClientScopeRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes @@ -2192,7 +2099,6 @@ def create_client_scope(self, payload, skip_exists=False): :param skip_exists: If true then do not raise an error if client scope already exists :return: Client scope id """ - if skip_exists: exists = self.get_client_scope_by_name(client_scope_name=payload["name"]) @@ -2210,8 +2116,7 @@ def create_client_scope(self, payload, skip_exists=False): return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def update_client_scope(self, client_scope_id, payload): - """ - Update a client scope + """Update a client scope. ClientScopeRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource @@ -2220,7 +2125,6 @@ def update_client_scope(self, client_scope_id, payload): :param payload: ClientScopeRepresentation :return: Keycloak server response (ClientScopeRepresentation) """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_put( urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path), data=json.dumps(payload) @@ -2228,8 +2132,7 @@ def update_client_scope(self, client_scope_id, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_client_scope(self, client_scope_id): - """ - Delete existing client scope. + """Delete existing client scope. ClientScopeRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource @@ -2242,8 +2145,7 @@ def delete_client_scope(self, client_scope_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_mappers_from_client_scope(self, client_scope_id): - """ - Get a list of all mappers connected to the client scope + """Get a list of all mappers connected to the client scope. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource :param client_scope_id: Client scope id @@ -2256,15 +2158,14 @@ def get_mappers_from_client_scope(self, client_scope_id): return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def add_mapper_to_client_scope(self, client_scope_id, payload): - """ - Add a mapper to a client scope + """Add a mapper to a client scope. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_scope_id: The id of the client scope :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_post( @@ -2275,15 +2176,14 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def delete_mapper_from_client_scope(self, client_scope_id, protocol_mapper_id): - """ - Delete a mapper from a client scope + """Delete a mapper from a client scope. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_delete_mapper :param client_scope_id: The id of the client scope :param protocol_mapper_id: Protocol mapper id :return: Keycloak server Response """ - params_path = { "realm-name": self.realm_name, "scope-id": client_scope_id, @@ -2296,8 +2196,8 @@ def delete_mapper_from_client_scope(self, client_scope_id, protocol_mapper_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, payload): - """ - Update an existing protocol mapper in a client scope + """Update an existing protocol mapper in a client scope. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource :param client_scope_id: The id of the client scope @@ -2306,7 +2206,6 @@ def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, pay :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ - params_path = { "realm-name": self.realm_name, "scope-id": client_scope_id, @@ -2321,7 +2220,8 @@ def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, pay return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_default_client_scopes(self): - """ + """Get default default client scopes. + Return list of default default client scopes :return: Keycloak server response @@ -2333,8 +2233,7 @@ def get_default_default_client_scopes(self): return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_default_client_scope(self, scope_id): - """ - Delete default default client scope + """Delete default default client scope. :param scope_id: default default client scope id :return: Keycloak server response @@ -2346,8 +2245,7 @@ def delete_default_default_client_scope(self, scope_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_default_client_scope(self, scope_id): - """ - Add default default client scope + """Add default default client scope. :param scope_id: default default client scope id :return: Keycloak server response @@ -2361,7 +2259,8 @@ def add_default_default_client_scope(self, scope_id): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_default_optional_client_scopes(self): - """ + """Get default optional client scopes. + Return list of default optional client scopes :return: Keycloak server response @@ -2373,8 +2272,7 @@ def get_default_optional_client_scopes(self): return raise_error_from_response(data_raw, KeycloakGetError) def delete_default_optional_client_scope(self, scope_id): - """ - Delete default optional client scope + """Delete default optional client scope. :param scope_id: default optional client scope id :return: Keycloak server response @@ -2386,8 +2284,7 @@ def delete_default_optional_client_scope(self, scope_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def add_default_optional_client_scope(self, scope_id): - """ - Add default optional client scope + """Add default optional client scope. :param scope_id: default optional client scope id :return: Keycloak server response @@ -2401,8 +2298,7 @@ def add_default_optional_client_scope(self, scope_id): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def get_mappers_from_client(self, client_id): - """ - List of all client mappers. + """List of all client mappers. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocolmapperrepresentation @@ -2418,15 +2314,14 @@ def get_mappers_from_client(self, client_id): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[200]) def add_mapper_to_client(self, client_id, payload): - """ - Add a mapper to a client + """Add a mapper to a client. + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_id: The id of the client :param payload: ProtocolMapperRepresentation :return: Keycloak server Response """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -2437,14 +2332,13 @@ def add_mapper_to_client(self, client_id, payload): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def update_client_mapper(self, client_id, mapper_id, payload): - """ - Update client mapper + """Update client mapper. + :param client_id: The id of the client :param client_mapper_id: The id of the mapper to be deleted :param payload: ProtocolMapperRepresentation :return: Keycloak server response """ - params_path = { "realm-name": self.realm_name, "id": client_id, @@ -2459,14 +2353,13 @@ def update_client_mapper(self, client_id, mapper_id, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def remove_client_mapper(self, client_id, client_mapper_id): - """ - Removes a mapper from the client + """Remove a mapper from the client. + https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_protocol_mappers_resource :param client_id: The id of the client :param client_mapper_id: The id of the mapper to be deleted :return: Keycloak server response """ - params_path = { "realm-name": self.realm_name, "id": client_id, @@ -2479,15 +2372,13 @@ def remove_client_mapper(self, client_id, client_mapper_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def generate_client_secrets(self, client_id): - """ + """Generate a new secret for the client. - Generate a new secret for the client https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_regeneratesecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path), data=None @@ -2495,21 +2386,20 @@ def generate_client_secrets(self, client_id): return raise_error_from_response(data_raw, KeycloakPostError) def get_client_secrets(self, client_id): - """ + """Get representation of the client secrets. - Get representation of the client secrets https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsecret :param client_id: id of client (not client-id) :return: Keycloak server response (ClientRepresentation) """ - params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_components(self, query=None): - """ + """Get components. + Return a list of components, filtered according to query parameters ComponentRepresentation @@ -2526,8 +2416,7 @@ def get_components(self, query=None): return raise_error_from_response(data_raw, KeycloakGetError) def create_component(self, payload): - """ - Create a new component. + """Create a new component. ComponentRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation @@ -2544,8 +2433,7 @@ def create_component(self, payload): return data_raw.headers["Location"][_last_slash_idx + 1 :] # noqa: E203 def get_component(self, component_id): - """ - Get representation of the component + """Get representation of the component. :param component_id: Component id @@ -2559,8 +2447,7 @@ def get_component(self, component_id): return raise_error_from_response(data_raw, KeycloakGetError) def update_component(self, component_id, payload): - """ - Update the component + """Update the component. :param component_id: Component id :param payload: ComponentRepresentation @@ -2575,8 +2462,7 @@ def update_component(self, component_id, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def delete_component(self, component_id): - """ - Delete the component + """Delete the component. :param component_id: Component id @@ -2587,7 +2473,8 @@ def delete_component(self, component_id): return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) def get_keys(self): - """ + """Get keys. + Return a list of keys, filtered according to query parameters KeysMetadataRepresentation @@ -2600,7 +2487,8 @@ def get_keys(self): return raise_error_from_response(data_raw, KeycloakGetError) def get_events(self, query=None): - """ + """Get events. + Return a list of events, filtered according to query parameters EventRepresentation array @@ -2616,8 +2504,7 @@ def get_events(self, query=None): return raise_error_from_response(data_raw, KeycloakGetError) def set_events(self, payload): - """ - Set realm events configuration + """Set realm events configuration. RealmEventsConfigRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmeventsconfigrepresentation @@ -2631,8 +2518,7 @@ def set_events(self, payload): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) def raw_get(self, *args, **kwargs): - """ - Calls connection.raw_get. + """Call connection.raw_get. If auto_refresh is set for *get* and *access_token* is expired, it will refresh the token and try *get* once more. @@ -2644,8 +2530,7 @@ def raw_get(self, *args, **kwargs): return r def raw_post(self, *args, **kwargs): - """ - Calls connection.raw_post. + """Call connection.raw_post. If auto_refresh is set for *post* and *access_token* is expired, it will refresh the token and try *post* once more. @@ -2657,8 +2542,7 @@ def raw_post(self, *args, **kwargs): return r def raw_put(self, *args, **kwargs): - """ - Calls connection.raw_put. + """Call connection.raw_put. If auto_refresh is set for *put* and *access_token* is expired, it will refresh the token and try *put* once more. @@ -2670,8 +2554,7 @@ def raw_put(self, *args, **kwargs): return r def raw_delete(self, *args, **kwargs): - """ - Calls connection.raw_delete. + """Call connection.raw_delete. If auto_refresh is set for *delete* and *access_token* is expired, it will refresh the token and try *delete* once more. @@ -2683,6 +2566,7 @@ def raw_delete(self, *args, **kwargs): return r def get_token(self): + """Get admin token.""" if self.user_realm_name: token_realm_name = self.user_realm_name elif self.realm_name: @@ -2730,6 +2614,7 @@ def get_token(self): ) def refresh_token(self): + """Refresh the token.""" refresh_token = self.token.get("refresh_token", None) if refresh_token is None: self.get_token() @@ -2752,8 +2637,7 @@ def refresh_token(self): ) def get_client_all_sessions(self, client_id): - """ - Get sessions associated with the client + """Get sessions associated with the client. :param client_id: id of client @@ -2767,8 +2651,7 @@ def get_client_all_sessions(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_client_sessions_stats(self): - """ - Get current session count for all clients with active sessions + """Get current session count for all clients with active sessions. https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsessionstats @@ -2779,8 +2662,7 @@ def get_client_sessions_stats(self): return raise_error_from_response(data_raw, KeycloakGetError) def get_client_management_permissions(self, client_id): - """ - Get management permissions for a client. + """Get management permissions for a client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2793,8 +2675,7 @@ def get_client_management_permissions(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError) def update_client_management_permissions(self, payload, client_id): - """ - Update management permissions for a client. + """Update management permissions for a client. ManagementPermissionReference https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_managementpermissionreference @@ -2819,8 +2700,7 @@ def update_client_management_permissions(self, payload, client_id): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[200]) def get_client_authz_policy_scopes(self, client_id, policy_id): - """ - Get scopes for a given policy. + """Get scopes for a given policy. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2834,8 +2714,7 @@ def get_client_authz_policy_scopes(self, client_id, policy_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_policy_resources(self, client_id, policy_id): - """ - Get resources for a given policy. + """Get resources for a given policy. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2849,8 +2728,7 @@ def get_client_authz_policy_resources(self, client_id, policy_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_client_authz_scope_permission(self, client_id, scope_id): - """ - Get permissions for a given scope. + """Get permissions for a given scope. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2864,8 +2742,7 @@ def get_client_authz_scope_permission(self, client_id, scope_id): return raise_error_from_response(data_raw, KeycloakGetError) def update_client_authz_scope_permission(self, payload, client_id, scope_id): - """ - Update permissions for a given scope. + """Update permissions for a given scope. :param payload: No Document :param client_id: id in ClientRepresentation @@ -2895,8 +2772,7 @@ def update_client_authz_scope_permission(self, payload, client_id, scope_id): return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[201]) def get_client_authz_client_policies(self, client_id): - """ - Get policies for a given client. + """Get policies for a given client. :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation @@ -2909,8 +2785,7 @@ def get_client_authz_client_policies(self, client_id): return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) def create_client_authz_client_policy(self, payload, client_id): - """ - Create a new policy for a given client. + """Create a new policy for a given client. :param payload: No Document :param client_id: id in ClientRepresentation diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index fa04e4db..83575b29 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -21,6 +21,12 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak OpenID module. + +The module contains mainly the implementation of KeycloakOpenID class, the main +class to handle authentication and token manipulation. +""" + import json from jose import jwt @@ -52,8 +58,7 @@ class KeycloakOpenID: - """ - Keycloak OpenID client. + """Keycloak OpenID client. :param server_url: Keycloak server url :param client_id: client id @@ -75,6 +80,7 @@ def __init__( proxies=None, timeout=60, ): + """Init method.""" self.client_id = client_id self.client_secret_key = client_secret_key self.realm_name = realm_name @@ -87,6 +93,7 @@ def __init__( @property def client_id(self): + """Get client id.""" return self._client_id @client_id.setter @@ -95,6 +102,7 @@ def client_id(self, value): @property def client_secret_key(self): + """Get the client secret key.""" return self._client_secret_key @client_secret_key.setter @@ -103,6 +111,7 @@ def client_secret_key(self, value): @property def realm_name(self): + """Get the realm name.""" return self._realm_name @realm_name.setter @@ -111,6 +120,7 @@ def realm_name(self, value): @property def connection(self): + """Get connection.""" return self._connection @connection.setter @@ -119,6 +129,7 @@ def connection(self, value): @property def authorization(self): + """Get authorization.""" return self._authorization @authorization.setter @@ -126,8 +137,7 @@ def authorization(self, value): self._authorization = value def _add_secret_key(self, payload): - """ - Add secret key if exist. + """Add secret key if exists. :param payload: :return: @@ -138,7 +148,7 @@ def _add_secret_key(self, payload): return payload def _build_name_role(self, role): - """ + """Build name of a role. :param role: :return: @@ -146,7 +156,7 @@ def _build_name_role(self, role): return self.client_id + "/" + role def _token_info(self, token, method_token_info, **kwargs): - """ + """Getter for the token data. :param token: :param method_token_info: @@ -161,19 +171,20 @@ def _token_info(self, token, method_token_info, **kwargs): return token_info def well_known(self): - """The most important endpoint to understand is the well-known configuration + """Get the well_known object. + + The most important endpoint to understand is the well-known configuration endpoint. It lists endpoints and other configuration options relevant to the OpenID Connect implementation in Keycloak. :return It lists endpoints and other configuration options relevant. """ - params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def auth_url(self, redirect_uri): - """ + """Get the authentication URL endpoint. http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint @@ -196,7 +207,8 @@ def token( totp=None, **extra ): - """ + """Retrieve user token. + The token endpoint is used to obtain tokens. Tokens can either be obtained by exchanging an authorization code or by supplying credentials directly depending on what flow is used. The token endpoint is also used to obtain new access tokens @@ -232,7 +244,8 @@ def token( return raise_error_from_response(data_raw, KeycloakPostError) def refresh_token(self, refresh_token, grant_type=["refresh_token"]): - """ + """Refresh the user token. + The token endpoint is used to obtain tokens. Tokens can either be obtained by exchanging an authorization code or by supplying credentials directly depending on what flow is used. The token endpoint is also used to obtain new access tokens @@ -255,7 +268,8 @@ def refresh_token(self, refresh_token, grant_type=["refresh_token"]): return raise_error_from_response(data_raw, KeycloakPostError) def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict: - """ + """Exchange user token. + Use a token to obtain an entirely different token. See https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange @@ -279,7 +293,8 @@ def exchange_token(self, token: str, client_id: str, audience: str, subject: str return raise_error_from_response(data_raw, KeycloakPostError) def userinfo(self, token): - """ + """Get the user info object. + The userinfo endpoint returns standard claims about the authenticated user, and is protected by a bearer token. @@ -294,8 +309,8 @@ def userinfo(self, token): return raise_error_from_response(data_raw, KeycloakGetError) def logout(self, refresh_token): - """ - The logout endpoint logs out the authenticated user. + """Log out the authenticated user. + :param refresh_token: :return: """ @@ -306,7 +321,8 @@ def logout(self, refresh_token): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) def certs(self): - """ + """Get certificates. + The certificate endpoint returns the public keys enabled by the realm, encoded as a JSON Web Key (JWK). Depending on the realm settings there can be one or more keys enabled for verifying tokens. @@ -320,7 +336,8 @@ def certs(self): return raise_error_from_response(data_raw, KeycloakGetError) def public_key(self): - """ + """Retrieve the public key. + The public key is exposed by the realm page directly. :return: @@ -330,7 +347,8 @@ def public_key(self): return raise_error_from_response(data_raw, KeycloakGetError)["public_key"] def entitlement(self, token, resource_server_id): - """ + """Get entitlements from the token. + Client applications can use a specific endpoint to obtain a special security token called a requesting party token (RPT). This token consists of all the entitlements (or permissions) for a user as a result of the evaluation of the permissions and @@ -349,7 +367,8 @@ def entitlement(self, token, resource_server_id): return raise_error_from_response(data_raw, KeycloakGetError) # pragma: no cover def introspect(self, token, rpt=None, token_type_hint=None): - """ + """Introspect the user token. + The introspection endpoint is used to retrieve the active state of a token. It is can only be invoked by confidential clients. @@ -377,7 +396,8 @@ def introspect(self, token, rpt=None, token_type_hint=None): return raise_error_from_response(data_raw, KeycloakPostError) def decode_token(self, token, key, algorithms=["RS256"], **kwargs): - """ + """Decode user token. + A JSON Web Key (JWK) is a JavaScript Object Notation (JSON) data structure that represents a cryptographic key. This specification also defines a JWK Set JSON data structure that represents a set of @@ -395,8 +415,7 @@ def decode_token(self, token, key, algorithms=["RS256"], **kwargs): return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) def load_authorization_config(self, path): - """ - Load Keycloak settings (authorization) + """Load Keycloak settings (authorization). :param path: settings file (json) :return: @@ -407,8 +426,7 @@ def load_authorization_config(self, path): self.authorization.load_config(authorization_json) def get_policies(self, token, method_token_info="introspect", **kwargs): - """ - Get policies by user token + """Get policies by user token. :param token: user token :return: policies list @@ -438,8 +456,7 @@ def get_policies(self, token, method_token_info="introspect", **kwargs): return list(set(policies)) def get_permissions(self, token, method_token_info="introspect", **kwargs): - """ - Get permission by user token + """Get permission by user token. :param token: user token :param method_token_info: Decode token method @@ -471,8 +488,7 @@ def get_permissions(self, token, method_token_info="introspect", **kwargs): return list(set(permissions)) def uma_permissions(self, token, permissions=""): - """ - Get UMA permissions by user token with requested permissions + """Get UMA permissions by user token with requested permissions. The token endpoint is used to retrieve UMA permissions from Keycloak. It can only be invoked by confidential clients. @@ -499,8 +515,7 @@ def uma_permissions(self, token, permissions=""): return raise_error_from_response(data_raw, KeycloakPostError) def has_uma_access(self, token, permissions): - """ - Determine whether user has uma permissions with specified user token + """Determine whether user has uma permissions with specified user token. :param token: user token :param permissions: list of uma permissions (resource:scope) diff --git a/src/keycloak/uma_permissions.py b/src/keycloak/uma_permissions.py index 5653c76a..1bf21364 100644 --- a/src/keycloak/uma_permissions.py +++ b/src/keycloak/uma_permissions.py @@ -21,11 +21,14 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""User-managed access permissions module.""" + from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError class UMAPermission: """A class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. Usage example: @@ -36,9 +39,16 @@ class UMAPermission: >>> print(permission) 'Users#delete' + :param permission: Permission + :type permission: UMAPermission + :param resource: Resource + :type resource: str + :param scope: Scope + :type scope: str """ def __init__(self, permission=None, resource="", scope=""): + """Init method.""" self.resource = resource self.scope = scope @@ -53,21 +63,26 @@ def __init__(self, permission=None, resource="", scope=""): self.scope = str(permission.scope) def __str__(self): + """Str method.""" scope = self.scope if scope: scope = "#" + scope return "{}{}".format(self.resource, scope) def __eq__(self, __o: object) -> bool: + """Eq method.""" return str(self) == str(__o) def __repr__(self) -> str: + """Repr method.""" return self.__str__() def __hash__(self) -> int: + """Hash method.""" return hash(str(self)) def __call__(self, permission=None, resource="", scope="") -> object: + """Call method.""" result_resource = self.resource result_scope = self.scope @@ -91,36 +106,58 @@ def __call__(self, permission=None, resource="", scope="") -> object: class Resource(UMAPermission): """An UMAPermission Resource class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + + :param resource: Resource + :type resource: str """ def __init__(self, resource): + """Init method.""" super().__init__(resource=resource) class Scope(UMAPermission): """An UMAPermission Scope class to conveniently assembly permissions. + The class itself is callable, and will return the assembled permission. + + :param scope: Scope + :type scope: str """ def __init__(self, scope): + """Init method.""" super().__init__(scope=scope) class AuthStatus: """A class that represents the authorization/login status of a user associated with a token. + This has to evaluate to True if and only if the user is properly authorized - for the requested resource.""" + for the requested resource. + + :param is_logged_in: Is logged in indicator + :type is_logged_in: bool + :param is_authorized: Is authorized indicator + :type is_authorized: bool + :param missing_permissions: Missing permissions + :type missing_permissions: set + """ def __init__(self, is_logged_in, is_authorized, missing_permissions): + """Init method.""" self.is_logged_in = is_logged_in self.is_authorized = is_authorized self.missing_permissions = missing_permissions def __bool__(self): + """Bool method.""" return self.is_authorized def __repr__(self): + """Repr method.""" return ( f"AuthStatus(" f"is_authorized={self.is_authorized}, " @@ -130,8 +167,7 @@ def __repr__(self): def build_permission_param(permissions): - """ - Transform permissions to a set, so they are usable for requests + """Transform permissions to a set, so they are usable for requests. :param permissions: either str (resource#scope), iterable[str] (resource#scope), diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 3ec134ca..b8366923 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -21,6 +21,8 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +"""Keycloak URL patterns.""" + # OPENID URLS URL_REALM = "realms/{realm-name}" URL_WELL_KNOWN = "realms/{realm-name}/.well-known/openid-configuration" diff --git a/tests/__init__.py b/tests/__init__.py index e69de29b..f1b390f8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Tests module.""" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index e62bdda1..069bb61e 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1,3 +1,5 @@ +"""Test the keycloak admin object.""" + import pytest import keycloak @@ -13,10 +15,12 @@ def test_keycloak_version(): + """Test version.""" assert keycloak.__version__, keycloak.__version__ def test_keycloak_admin_bad_init(env): + """Test keycloak admin bad init.""" with pytest.raises(TypeError) as err: KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", @@ -37,6 +41,7 @@ def test_keycloak_admin_bad_init(env): def test_keycloak_admin_init(env): + """Test keycloak admin init.""" admin = KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -111,6 +116,7 @@ def test_keycloak_admin_init(env): def test_realms(admin: KeycloakAdmin): + """Test realms.""" # Get realms realms = admin.get_realms() assert len(realms) == 1, realms @@ -175,6 +181,7 @@ def test_realms(admin: KeycloakAdmin): def test_import_export_realms(admin: KeycloakAdmin, realm: str): + """Test import and export of realms.""" admin.realm_name = realm realm_export = admin.export_realm(export_clients=True, export_groups_and_role=True) @@ -192,6 +199,7 @@ def test_import_export_realms(admin: KeycloakAdmin, realm: str): def test_users(admin: KeycloakAdmin, realm: str): + """Test users.""" admin.realm_name = realm # Check no users present @@ -283,6 +291,7 @@ def test_users(admin: KeycloakAdmin, realm: str): def test_users_pagination(admin: KeycloakAdmin, realm: str): + """Test user pagination.""" admin.realm_name = realm for ind in range(admin.PAGE_SIZE + 50): @@ -300,6 +309,7 @@ def test_users_pagination(admin: KeycloakAdmin, realm: str): def test_idps(admin: KeycloakAdmin, realm: str): + """Test IDPs.""" admin.realm_name = realm # Create IDP @@ -371,6 +381,7 @@ def test_idps(admin: KeycloakAdmin, realm: str): def test_user_credentials(admin: KeycloakAdmin, user: str): + """Test user credentials.""" res = admin.set_user_password(user_id=user, password="booya", temporary=True) assert res == dict(), res @@ -398,6 +409,7 @@ def test_user_credentials(admin: KeycloakAdmin, user: str): def test_social_logins(admin: KeycloakAdmin, user: str): + """Test social logins.""" res = admin.add_user_social_login( user_id=user, provider_id="gitlab", provider_userid="test", provider_username="test" ) @@ -437,6 +449,7 @@ def test_social_logins(admin: KeycloakAdmin, user: str): def test_server_info(admin: KeycloakAdmin): + """Test server info.""" info = admin.get_server_info() assert set(info.keys()) == { "systemInfo", @@ -456,6 +469,7 @@ def test_server_info(admin: KeycloakAdmin): def test_groups(admin: KeycloakAdmin, user: str): + """Test groups.""" # Test get groups groups = admin.get_groups() assert len(groups) == 0 @@ -599,6 +613,7 @@ def test_groups(admin: KeycloakAdmin, user: str): def test_clients(admin: KeycloakAdmin, realm: str): + """Test clients.""" admin.realm_name = realm # Test get clients @@ -860,6 +875,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): def test_realm_roles(admin: KeycloakAdmin, realm: str): + """Test realm roles.""" admin.realm_name = realm # Test get realm roles @@ -1015,6 +1031,7 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): def test_client_roles(admin: KeycloakAdmin, client: str): + """Test client roles.""" # Test get client roles res = admin.get_client_roles(client_id=client) assert len(res) == 0 @@ -1177,6 +1194,7 @@ def test_client_roles(admin: KeycloakAdmin, client: str): def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): + """Test enable token exchange.""" # Test enabling token exchange between two confidential clients admin.realm_name = realm @@ -1265,6 +1283,7 @@ def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): def test_email(admin: KeycloakAdmin, user: str): + """Test email.""" # Emails will fail as we don't have SMTP test setup with pytest.raises(KeycloakPutError) as err: admin.send_update_account(user_id=user, payload=dict()) @@ -1277,6 +1296,7 @@ def test_email(admin: KeycloakAdmin, user: str): def test_get_sessions(admin: KeycloakAdmin): + """Test get sessions.""" sessions = admin.get_sessions(user_id=admin.get_user_id(username=admin.username)) assert len(sessions) >= 1 with pytest.raises(KeycloakGetError) as err: @@ -1285,6 +1305,7 @@ def test_get_sessions(admin: KeycloakAdmin): def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): + """Test get client installation provider.""" with pytest.raises(KeycloakGetError) as err: admin.get_client_installation_provider(client_id=client, provider_id="bad") assert err.match('404: b\'{"error":"Unknown Provider"}\'') @@ -1303,6 +1324,7 @@ def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): def test_auth_flows(admin: KeycloakAdmin, realm: str): + """Test auth flows.""" admin.realm_name = realm res = admin.get_authentication_flows() @@ -1449,6 +1471,7 @@ def test_auth_flows(admin: KeycloakAdmin, realm: str): def test_authentication_configs(admin: KeycloakAdmin, realm: str): + """Test authentication configs.""" admin.realm_name = realm # Test list of auth providers @@ -1480,6 +1503,7 @@ def test_authentication_configs(admin: KeycloakAdmin, realm: str): def test_sync_users(admin: KeycloakAdmin, realm: str): + """Test sync users.""" admin.realm_name = realm # Only testing the error message @@ -1489,6 +1513,7 @@ def test_sync_users(admin: KeycloakAdmin, realm: str): def test_client_scopes(admin: KeycloakAdmin, realm: str): + """Test client scopes.""" admin.realm_name = realm # Test get client scopes @@ -1626,6 +1651,7 @@ def test_client_scopes(admin: KeycloakAdmin, realm: str): def test_components(admin: KeycloakAdmin, realm: str): + """Test components.""" admin.realm_name = realm # Test get components @@ -1676,6 +1702,7 @@ def test_components(admin: KeycloakAdmin, realm: str): def test_keys(admin: KeycloakAdmin, realm: str): + """Test keys.""" admin.realm_name = realm assert set(admin.get_keys()["active"].keys()) == {"AES", "HS256", "RS256", "RSA-OAEP"} assert {k["algorithm"] for k in admin.get_keys()["keys"]} == { @@ -1687,6 +1714,7 @@ def test_keys(admin: KeycloakAdmin, realm: str): def test_events(admin: KeycloakAdmin, realm: str): + """Test events.""" admin.realm_name = realm events = admin.get_events() @@ -1706,6 +1734,7 @@ def test_events(admin: KeycloakAdmin, realm: str): def test_auto_refresh(admin: KeycloakAdmin, realm: str): + """Test auto refresh token.""" # Test get refresh admin.auto_refresh_token = list() admin.connection = ConnectionManager( diff --git a/tests/test_uma_permissions.py b/tests/test_uma_permissions.py index 09d71472..581faf4c 100644 --- a/tests/test_uma_permissions.py +++ b/tests/test_uma_permissions.py @@ -14,6 +14,9 @@ # # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see . + +"""Test uma permissions.""" + import re import pytest @@ -23,30 +26,35 @@ def test_resource_with_scope_obj(): + """Test resource with scope.""" r = Resource("Resource1") s = Scope("Scope1") assert r(s) == "Resource1#Scope1" def test_scope_with_resource_obj(): + """Test scope with resource.""" r = Resource("Resource1") s = Scope("Scope1") assert s(r) == "Resource1#Scope1" def test_resource_scope_str(): + """Test resource scope as string.""" r = Resource("Resource1") s = "Scope1" assert r(scope=s) == "Resource1#Scope1" def test_scope_resource_str(): + """Test scope resource as string.""" r = "Resource1" s = Scope("Scope1") assert s(resource=r) == "Resource1#Scope1" def test_resource_scope_list(): + """Test resource scope as list.""" r = Resource("Resource1") s = ["Scope1"] with pytest.raises(PermissionDefinitionError) as err: @@ -55,94 +63,114 @@ def test_resource_scope_list(): def test_build_permission_none(): + """Test build permission param with None.""" assert build_permission_param(None) == set() def test_build_permission_empty_str(): + """Test build permission param with an empty string.""" assert build_permission_param("") == set() def test_build_permission_empty_list(): + """Test build permission param with an empty list.""" assert build_permission_param([]) == set() def test_build_permission_empty_tuple(): + """Test build permission param with an empty tuple.""" assert build_permission_param(()) == set() def test_build_permission_empty_set(): + """Test build permission param with an empty set.""" assert build_permission_param(set()) == set() def test_build_permission_empty_dict(): + """Test build permission param with an empty dict.""" assert build_permission_param({}) == set() def test_build_permission_str(): + """Test build permission param as string.""" assert build_permission_param("resource1") == {"resource1"} def test_build_permission_list_str(): + """Test build permission param with list of strings.""" assert build_permission_param(["res1#scope1", "res1#scope2"]) == {"res1#scope1", "res1#scope2"} def test_build_permission_tuple_str(): + """Test build permission param with tuple of strings.""" assert build_permission_param(("res1#scope1", "res1#scope2")) == {"res1#scope1", "res1#scope2"} def test_build_permission_set_str(): + """Test build permission param with set of strings.""" assert build_permission_param({"res1#scope1", "res1#scope2"}) == {"res1#scope1", "res1#scope2"} def test_build_permission_tuple_dict_str_str(): + """Test build permission param with dictionary.""" assert build_permission_param({"res1": "scope1"}) == {"res1#scope1"} def test_build_permission_tuple_dict_str_list_str(): + """Test build permission param with dictionary of list.""" assert build_permission_param({"res1": ["scope1", "scope2"]}) == {"res1#scope1", "res1#scope2"} def test_build_permission_tuple_dict_str_list_str2(): + """Test build permission param with mutliple-keyed dictionary.""" assert build_permission_param( {"res1": ["scope1", "scope2"], "res2": ["scope2", "scope3"]} ) == {"res1#scope1", "res1#scope2", "res2#scope2", "res2#scope3"} def test_build_permission_uma(): + """Test build permission param with UMA.""" assert build_permission_param(Resource("res1")(Scope("scope1"))) == {"res1#scope1"} def test_build_permission_uma_list(): + """Test build permission param with list of UMAs.""" assert build_permission_param( [Resource("res1")(Scope("scope1")), Resource("res1")(Scope("scope2"))] ) == {"res1#scope1", "res1#scope2"} def test_build_permission_misbuilt_dict_str_list_list_str(): + """Test bad build of permission param from dictionary.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param({"res1": [["scope1", "scope2"]]}) assert err.match(re.escape("misbuilt permission {'res1': [['scope1', 'scope2']]}")) def test_build_permission_misbuilt_list_list_str(): + """Test bad build of permission param from list.""" with pytest.raises(KeycloakPermissionFormatError) as err: print(build_permission_param([["scope1", "scope2"]])) assert err.match(re.escape("misbuilt permission [['scope1', 'scope2']]")) def test_build_permission_misbuilt_list_set_str(): + """Test bad build of permission param from set.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param([{"scope1", "scope2"}]) assert err.match("misbuilt permission.*") def test_build_permission_misbuilt_set_set_str(): + """Test bad build of permission param from list of set.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param([{"scope1"}]) assert err.match(re.escape("misbuilt permission [{'scope1'}]")) def test_build_permission_misbuilt_dict_non_iterable(): + """Test bad build of permission param from non-iterable.""" with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param({"res1": 5}) assert err.match(re.escape("misbuilt permission {'res1': 5}")) diff --git a/tests/test_urls_patterns.py b/tests/test_urls_patterns.py index 6fa5a871..5fae847e 100644 --- a/tests/test_urls_patterns.py +++ b/tests/test_urls_patterns.py @@ -1,9 +1,10 @@ +"""Test URL patterns.""" + from keycloak import urls_patterns def test_correctness_of_patterns(): """Test that there are no duplicate url patterns.""" - # Test that the patterns are present urls = [x for x in dir(urls_patterns) if not x.startswith("__")] assert len(urls) >= 0 From dfc7c4a2d52ab11b6a11ef1cff4bdd9585b5f473 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 08:00:54 +0000 Subject: [PATCH 206/566] style: added docstring to conf --- docs/source/conf.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index b60a1c9c..9a5d438b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -20,6 +20,9 @@ # import os # import sys # sys.path.insert(0, os.path.abspath('.')) + +"""Sphinx documentation configuration.""" + import sphinx_rtd_theme # -- General configuration ------------------------------------------------ From 3a697caaef50254725d51fb052e999c1e9690a4b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 15:28:12 +0000 Subject: [PATCH 207/566] test: added load authorization config test --- src/keycloak/authorization/permission.py | 12 +++---- src/keycloak/authorization/policy.py | 20 +++++++---- tests/conftest.py | 8 +++++ tests/data/authz_settings.json | 45 ++++++++++++++++++++++++ tests/test_keycloak_openid.py | 38 +++++++++++++++++++- 5 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 tests/data/authz_settings.json diff --git a/src/keycloak/authorization/permission.py b/src/keycloak/authorization/permission.py index a444f836..667f8c3c 100644 --- a/src/keycloak/authorization/permission.py +++ b/src/keycloak/authorization/permission.py @@ -49,12 +49,12 @@ class Permission: def __init__(self, name, type, logic, decision_strategy): """Init method.""" - self._name = name - self._type = type - self._logic = logic - self._decision_strategy = decision_strategy - self._resources = [] - self._scopes = [] + self.name = name + self.type = type + self.logic = logic + self.decision_strategy = decision_strategy + self.resources = [] + self.scopes = [] def __repr__(self): """Repr method.""" diff --git a/src/keycloak/authorization/policy.py b/src/keycloak/authorization/policy.py index 6b558d8d..7e03db0b 100644 --- a/src/keycloak/authorization/policy.py +++ b/src/keycloak/authorization/policy.py @@ -43,12 +43,12 @@ class Policy: def __init__(self, name, type, logic, decision_strategy): """Init method.""" - self._name = name - self._type = type - self._logic = logic - self._decision_strategy = decision_strategy - self._roles = [] - self._permissions = [] + self.name = name + self.type = type + self.logic = logic + self.decision_strategy = decision_strategy + self.roles = [] + self.permissions = [] def __repr__(self): """Repr method.""" @@ -99,11 +99,19 @@ def roles(self): """Get roles.""" return self._roles + @roles.setter + def roles(self, value): + self._roles = value + @property def permissions(self): """Get permissions.""" return self._permissions + @permissions.setter + def permissions(self, value): + self._permissions = value + def add_role(self, role): """Add keycloak role in policy. diff --git a/tests/conftest.py b/tests/conftest.py index 47c98545..632c51bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -185,6 +185,14 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak "serviceAccountsEnabled": True, } ) + admin.create_client_authz_role_based_policy( + client_id=client_id, + payload={ + "name": "test-authz-rb-policy", + "roles": [{"id": admin.get_realm_role(role_name="offline_access")["id"]}], + }, + ) + admin.create_client_authz_resource # Create user username = str(uuid.uuid4()) password = str(uuid.uuid4()) diff --git a/tests/data/authz_settings.json b/tests/data/authz_settings.json new file mode 100644 index 00000000..e0510857 --- /dev/null +++ b/tests/data/authz_settings.json @@ -0,0 +1,45 @@ +{ + "allowRemoteResourceManagement": true, + "policyEnforcementMode": "ENFORCING", + "policies": [ + { + "name": "Default Policy", + "type": "js", + "logic": "POSITIVE", + "decisionStrategy": "AFFIRMATIVE", + "config": { + "code": "// by default, grants any permission associated with this policy\n$evaluation.grant();\n" + } + }, + { + "name": "test-authz-rb-policy", + "type": "role", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "roles": "[{\"id\":\"offline_access\",\"required\":false}]" + } + }, + { + "name": "Default Permission", + "type": "resource", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "applyPolicies": "[\"test-authz-rb-policy\"]" + } + }, + { + "name": "Test scope", + "type": "scope", + "logic": "POSITIVE", + "decisionStrategy": "UNANIMOUS", + "config": { + "scopes": "[]", + "applyPolicies": "[\"test-authz-rb-policy\"]" + } + } + ], + "scopes": [], + "decisionStrategy": "UNANIMOUS" +} \ No newline at end of file diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 9ed2b882..f2c0f7e3 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -4,8 +4,15 @@ import pytest from keycloak.authorization import Authorization +from keycloak.authorization.permission import Permission +from keycloak.authorization.policy import Policy +from keycloak.authorization.role import Role from keycloak.connection import ConnectionManager -from keycloak.exceptions import KeycloakDeprecationError, KeycloakRPTNotFound +from keycloak.exceptions import ( + KeycloakAuthenticationError, + KeycloakDeprecationError, + KeycloakRPTNotFound, +) from keycloak.keycloak_admin import KeycloakAdmin from keycloak.keycloak_openid import KeycloakOpenID @@ -185,6 +192,18 @@ def test_exchange_token( assert token != new_token +def test_logout(oid_with_credentials): + """Test logout.""" + oid, username, password = oid_with_credentials + + token = oid.token(username=username, password=password) + assert oid.userinfo(token=token["access_token"]) != dict() + assert oid.logout(refresh_token=token["refresh_token"]) == dict() + + with pytest.raises(KeycloakAuthenticationError): + oid.userinfo(token=token["access_token"]) + + def test_certs(oid: KeycloakOpenID): """Test certificates.""" assert len(oid.certs()["keys"]) == 2 @@ -236,3 +255,20 @@ def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): )["preferred_username"] == username ) + + +def test_load_authorization_config( + oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test load authorization config.""" + oid, username, password = oid_with_credentials_authz + + oid.load_authorization_config(path="tests/data/authz_settings.json") + assert "test-authz-rb-policy" in oid.authorization.policies + assert isinstance(oid.authorization.policies["test-authz-rb-policy"], Policy) + assert len(oid.authorization.policies["test-authz-rb-policy"].roles) == 1 + assert isinstance(oid.authorization.policies["test-authz-rb-policy"].roles[0], Role) + assert len(oid.authorization.policies["test-authz-rb-policy"].permissions) == 2 + assert isinstance( + oid.authorization.policies["test-authz-rb-policy"].permissions[0], Permission + ) From e4c0ff2c7d865237bbd97720b7ce0383198af319 Mon Sep 17 00:00:00 2001 From: Zerek <16066557+Zerek-Cheng@users.noreply.github.com> Date: Wed, 13 Jul 2022 00:45:09 +0800 Subject: [PATCH 208/566] fix: Support the auth_url method called with scope & state params now --- src/keycloak/keycloak_openid.py | 4 +++- src/keycloak/urls_patterns.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index ad608d0f..b44915f1 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -174,7 +174,7 @@ def well_known(self): return raise_error_from_response(data_raw, KeycloakGetError) - def auth_url(self, redirect_uri): + def auth_url(self, redirect_uri, scope="email", state=""): """ http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint @@ -185,6 +185,8 @@ def auth_url(self, redirect_uri): "authorization-endpoint": self.well_known()["authorization_endpoint"], "client-id": self.client_id, "redirect-uri": redirect_uri, + "scope": scope, + "state": state, } return URL_AUTH.format(**params_path) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 3ec134ca..18b1951a 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -32,6 +32,7 @@ URL_ENTITLEMENT = "realms/{realm-name}/authz/entitlement/{resource-server-id}" URL_AUTH = ( "{authorization-endpoint}?client_id={client-id}&response_type=code&redirect_uri={redirect-uri}" + "&scope={scope}&state={state} " ) # ADMIN URLS From 18ce10c73b9ded0d589098f803504477fad358f6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 20:34:24 +0000 Subject: [PATCH 209/566] test: added authz tests --- src/keycloak/keycloak_openid.py | 1 - tests/test_keycloak_openid.py | 83 +++++++++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 83575b29..0a45dc3f 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -511,7 +511,6 @@ def uma_permissions(self, token, permissions=""): self.connection.add_param_headers("Authorization", "Bearer " + token) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) - return raise_error_from_response(data_raw, KeycloakPostError) def has_uma_access(self, token, permissions): diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index f2c0f7e3..0e94cd3f 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -10,7 +10,9 @@ from keycloak.connection import ConnectionManager from keycloak.exceptions import ( KeycloakAuthenticationError, + KeycloakAuthorizationConfigError, KeycloakDeprecationError, + KeycloakInvalidTokenError, KeycloakRPTNotFound, ) from keycloak.keycloak_admin import KeycloakAdmin @@ -257,9 +259,7 @@ def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): ) -def test_load_authorization_config( - oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin -): +def test_load_authorization_config(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): """Test load authorization config.""" oid, username, password = oid_with_credentials_authz @@ -272,3 +272,80 @@ def test_load_authorization_config( assert isinstance( oid.authorization.policies["test-authz-rb-policy"].permissions[0], Permission ) + + +def test_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): + """Test get policies.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + + with pytest.raises(KeycloakAuthorizationConfigError): + oid.get_policies(token=token["access_token"]) + + oid.load_authorization_config(path="tests/data/authz_settings.json") + assert oid.get_policies(token=token["access_token"]) is None + + key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----" + orig_client_id = oid.client_id + oid.client_id = "account" + assert oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) == [] + policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS") + policy.add_role(role="account/view-profile") + oid.authorization.policies["test"] = policy + assert [ + str(x) + for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) + ] == ["Policy: test (role)"] + assert [ + repr(x) + for x in oid.get_policies(token=token["access_token"], method_token_info="decode", key=key) + ] == [""] + oid.client_id = orig_client_id + + oid.logout(refresh_token=token["refresh_token"]) + with pytest.raises(KeycloakInvalidTokenError): + oid.get_policies(token=token["access_token"]) + + +def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): + """Test get policies.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + + with pytest.raises(KeycloakAuthorizationConfigError): + oid.get_permissions(token=token["access_token"]) + + oid.load_authorization_config(path="tests/data/authz_settings.json") + assert oid.get_permissions(token=token["access_token"]) is None + + key = "-----BEGIN PUBLIC KEY-----\n" + oid.public_key() + "\n-----END PUBLIC KEY-----" + orig_client_id = oid.client_id + oid.client_id = "account" + assert ( + oid.get_permissions(token=token["access_token"], method_token_info="decode", key=key) == [] + ) + policy = Policy(name="test", type="role", logic="POSITIVE", decision_strategy="UNANIMOUS") + policy.add_role(role="account/view-profile") + policy.add_permission( + permission=Permission( + name="test-perm", type="resource", logic="POSITIVE", decision_strategy="UNANIMOUS" + ) + ) + oid.authorization.policies["test"] = policy + assert [ + str(x) + for x in oid.get_permissions( + token=token["access_token"], method_token_info="decode", key=key + ) + ] == ["Permission: test-perm (resource)"] + assert [ + repr(x) + for x in oid.get_permissions( + token=token["access_token"], method_token_info="decode", key=key + ) + ] == [""] + oid.client_id = orig_client_id + + oid.logout(refresh_token=token["refresh_token"]) + with pytest.raises(KeycloakInvalidTokenError): + oid.get_permissions(token=token["access_token"]) From 25f1f687059ee780f9cb99eaf18ce821fb7943ef Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 20:47:18 +0000 Subject: [PATCH 210/566] style: fix docstring for docs pages --- src/keycloak/keycloak_admin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 0825730e..52c56775 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1313,7 +1313,7 @@ def get_realm_role_members(self, role_name, query=None): :param role_name: Name of the role. :param query: Additional Query parameters - (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) :return: Keycloak Server Response (UserRepresentation) """ query = query or dict() @@ -1464,7 +1464,7 @@ def get_client_role_members(self, client_id, role_name, **query): :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters - (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) :return: Keycloak server response (UserRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} @@ -1478,7 +1478,7 @@ def get_client_role_groups(self, client_id, role_name, **query): :param client_id: The client id :param role_name: the name of role to be queried. :param query: Additional query parameters - (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) + (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} @@ -1530,7 +1530,7 @@ def update_realm_role(self, role_name, payload): :param role_name: The name of the role to be updated :param payload: The role (use RoleRepresentation) - :return Keycloak server response + :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_put( @@ -1543,7 +1543,7 @@ def delete_realm_role(self, role_name): """Delete a role for the realm by name. :param payload: The role name {'role-name':'name-of-the-role'} - :return Keycloak server response + :return: Keycloak server response """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( From a07a5eb60f2c3cbb73be966c5141d8c91a7fd85e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 12 Jul 2022 21:08:28 +0000 Subject: [PATCH 211/566] style: removed manifest, applied pre-commit hooks --- LICENSE | 2 +- MANIFEST.in | 4 --- docs/Makefile | 2 +- poetry.lock | 57 ++++------------------------------ pyproject.toml | 1 + tests/data/authz_settings.json | 2 +- tox.ini | 2 +- 7 files changed, 11 insertions(+), 59 deletions(-) delete mode 100644 MANIFEST.in diff --git a/LICENSE b/LICENSE index f193f7d2..781617cd 100644 --- a/LICENSE +++ b/LICENSE @@ -17,4 +17,4 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index acf84afb..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,4 +0,0 @@ -include LICENSE -include requirements.txt -include dev-requirements.txt -include docs-requirements.txt diff --git a/docs/Makefile b/docs/Makefile index 28027de6..c86fc187 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -17,4 +17,4 @@ help: # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile - @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/poetry.lock b/poetry.lock index 06cdcc12..24b870bd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -170,7 +170,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.2" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -974,15 +974,15 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.8.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [extras] docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] @@ -1064,49 +1064,7 @@ commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] -coverage = [ - {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, - {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, - {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, - {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, - {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, - {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, - {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, - {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, - {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, - {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, - {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, - {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, - {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, -] +coverage = [] decli = [ {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, @@ -1565,7 +1523,4 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, -] +zipp = [] diff --git a/pyproject.toml b/pyproject.toml index 0e54eafc..3c320375 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ packages = [ { include = "keycloak", from = "src/" }, { include = "keycloak/**/*.py", from = "src/" }, ] +include = ["LICENSE", "CHANGELOG.md", "CODEOWNERS", "CONTRIBUTING.md"] [tool.poetry.urls] Documentation = "https://python-keycloak.readthedocs.io/en/latest/" diff --git a/tests/data/authz_settings.json b/tests/data/authz_settings.json index e0510857..8f111988 100644 --- a/tests/data/authz_settings.json +++ b/tests/data/authz_settings.json @@ -42,4 +42,4 @@ ], "scopes": [], "decisionStrategy": "UNANIMOUS" -} \ No newline at end of file +} diff --git a/tox.ini b/tox.ini index 3fbdd66a..219a4d55 100644 --- a/tox.ini +++ b/tox.ini @@ -32,7 +32,7 @@ commands = ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" [testenv:build] -deps = +deps = poetry setenv = POETRY_VIRTUALENVS_CREATE = false From 962133ec01d9135acba959b59276683736676464 Mon Sep 17 00:00:00 2001 From: Zerek <16066557+Zerek-Cheng@users.noreply.github.com> Date: Wed, 13 Jul 2022 08:48:44 +0800 Subject: [PATCH 212/566] docs: update auth_url method's docstring and readme file --- README.md | 13 +++++++++++++ src/keycloak/keycloak_openid.py | 14 ++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d3572f57..d300fa9a 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,19 @@ keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", # Get WellKnow config_well_known = keycloak_openid.well_known() +# Get Code With Oauth Authorization Request +auth_url = keycloak_openid.auth_url( + redirect_uri="your_call_back_url", + scope="email", + state="your_state_info") + +# Get Access Token With Code +access_token = keycloak_openid.token( + grant_type='authorization_code', + code='the_code_you_get_from_auth_url_callback', + redirect_uri="your_call_back_url") + + # Get Token token = keycloak_openid.token("user", "password") token = keycloak_openid.token("user", "password", totp="012345") diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index b44915f1..85447b25 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -176,10 +176,16 @@ def well_known(self): def auth_url(self, redirect_uri, scope="email", state=""): """ - - http://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint - - :return: + Get authorization URL endpoint. + + :param redirect_uri: Redirect url to receive oauth code + :type redirect_uri: str + :param scope: Scope of authorization request, split with the blank space + :type: scope: str + :param state: State will be returned to the redirect_uri + :type: str + :returns: Authorization URL Full Build + :rtype: str """ params_path = { "authorization-endpoint": self.well_known()["authorization_endpoint"], From 7031123c1fa3462290c20fa11c4c80b6732a5bf7 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 07:31:04 +0000 Subject: [PATCH 213/566] test: finished off openid tests --- src/keycloak/keycloak_openid.py | 4 ++-- tests/conftest.py | 1 - tests/test_keycloak_openid.py | 41 +++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 0a45dc3f..e2fcca11 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -524,7 +524,7 @@ def has_uma_access(self, token, permissions): try: granted = self.uma_permissions(token, permissions) except (KeycloakPostError, KeycloakAuthenticationError) as e: - if e.response_code == 403: + if e.response_code == 403: # pragma: no cover return AuthStatus( is_logged_in=True, is_authorized=False, missing_permissions=needed ) @@ -540,7 +540,7 @@ def has_uma_access(self, token, permissions): if not scopes: needed.discard(resource) continue - for scope in scopes: + for scope in scopes: # pragma: no cover needed.discard("{}#{}".format(resource, scope)) return AuthStatus( diff --git a/tests/conftest.py b/tests/conftest.py index 632c51bf..5f340ae4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -192,7 +192,6 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak "roles": [{"id": admin.get_realm_role(role_name="offline_access")["id"]}], }, ) - admin.create_client_authz_resource # Create user username = str(uuid.uuid4()) password = str(uuid.uuid4()) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 0e94cd3f..55c9d448 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -13,6 +13,7 @@ KeycloakAuthorizationConfigError, KeycloakDeprecationError, KeycloakInvalidTokenError, + KeycloakPostError, KeycloakRPTNotFound, ) from keycloak.keycloak_admin import KeycloakAdmin @@ -349,3 +350,43 @@ def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, oid.logout(refresh_token=token["refresh_token"]) with pytest.raises(KeycloakInvalidTokenError): oid.get_permissions(token=token["access_token"]) + + +def test_uma_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): + """Test UMA permissions.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + + assert len(oid.uma_permissions(token=token["access_token"])) == 1 + assert oid.uma_permissions(token=token["access_token"])[0]["rsname"] == "Default Resource" + + +def test_has_uma_access( + oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin +): + """Test has UMA access.""" + oid, username, password = oid_with_credentials_authz + token = oid.token(username=username, password=password) + + assert ( + str(oid.has_uma_access(token=token["access_token"], permissions="")) + == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())" + ) + assert ( + str(oid.has_uma_access(token=token["access_token"], permissions="Default Resource")) + == "AuthStatus(is_authorized=True, is_logged_in=True, missing_permissions=set())" + ) + + with pytest.raises(KeycloakPostError): + oid.has_uma_access(token=token["access_token"], permissions="Does not exist") + + oid.logout(refresh_token=token["refresh_token"]) + assert ( + str(oid.has_uma_access(token=token["access_token"], permissions="")) + == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=set())" + ) + assert ( + str(oid.has_uma_access(token=admin.token["access_token"], permissions="Default Resource")) + == "AuthStatus(is_authorized=False, is_logged_in=False, missing_permissions=" + + "{'Default Resource'})" + ) From 81cc71c000d9c9c39e354ef1a683221b7150d8c3 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 07:43:01 +0000 Subject: [PATCH 214/566] test: fix the tests, make them compatible with older python versions --- tests/test_keycloak_openid.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 55c9d448..03240770 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -1,4 +1,5 @@ """Test module for KeycloakOpenID.""" +from typing import Tuple from unittest import mock import pytest @@ -105,11 +106,11 @@ def test_auth_url(env, oid: KeycloakOpenID): res == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}" + f"/protocol/openid-connect/auth?client_id={oid.client_id}&response_type=code" - + "&redirect_uri=http://test.test/*" + + "&redirect_uri=http://test.test/*&scope=email&state= " ) -def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): +def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): """Test the token method.""" oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -152,7 +153,7 @@ def test_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): def test_exchange_token( - oid_with_credentials: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin + oid_with_credentials: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin ): """Test the exchange token method.""" # Verify existing user @@ -218,7 +219,7 @@ def test_public_key(oid: KeycloakOpenID): def test_entitlement( - oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin + oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin ): """Test entitlement.""" oid, username, password = oid_with_credentials_authz @@ -231,7 +232,7 @@ def test_entitlement( oid.entitlement(token=token["access_token"], resource_server_id=resource_server_id) -def test_introspect(oid_with_credentials: tuple[KeycloakOpenID, str, str]): +def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): """Test introspect.""" oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -245,7 +246,7 @@ def test_introspect(oid_with_credentials: tuple[KeycloakOpenID, str, str]): oid.introspect(token=token["access_token"], token_type_hint="requesting_party_token") -def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): +def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): """Test decode token.""" oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -260,7 +261,7 @@ def test_decode_token(oid_with_credentials: tuple[KeycloakOpenID, str, str]): ) -def test_load_authorization_config(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test load authorization config.""" oid, username, password = oid_with_credentials_authz @@ -275,7 +276,7 @@ def test_load_authorization_config(oid_with_credentials_authz: tuple[KeycloakOpe ) -def test_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test get policies.""" oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -308,7 +309,7 @@ def test_get_policies(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str oid.get_policies(token=token["access_token"]) -def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test get policies.""" oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -352,7 +353,7 @@ def test_get_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, oid.get_permissions(token=token["access_token"]) -def test_uma_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, str]): +def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): """Test UMA permissions.""" oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -362,7 +363,7 @@ def test_uma_permissions(oid_with_credentials_authz: tuple[KeycloakOpenID, str, def test_has_uma_access( - oid_with_credentials_authz: tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin + oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin ): """Test has UMA access.""" oid, username, password = oid_with_credentials_authz From 3052f80fd617b90e8190b427f265ac879bee2572 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 17:08:37 +0000 Subject: [PATCH 215/566] refactor: no need to try if the type check is performed --- src/keycloak/uma_permissions.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/keycloak/uma_permissions.py b/src/keycloak/uma_permissions.py index 1bf21364..94779f16 100644 --- a/src/keycloak/uma_permissions.py +++ b/src/keycloak/uma_permissions.py @@ -206,13 +206,9 @@ def build_permission_param(permissions): except AttributeError: pass - try: # treat as any other iterable of permissions - result = set() - for permission in permissions: - if not isinstance(permission, (str, UMAPermission)): - raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions)) - result.add(str(permission)) - return result - except TypeError: - pass - raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions)) + result = set() + for permission in permissions: + if not isinstance(permission, (str, UMAPermission)): + raise KeycloakPermissionFormatError("misbuilt permission {}".format(permissions)) + result.add(str(permission)) + return result From 940f022d921c019fdb5aeb76f547d92dc43bd692 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 17:08:52 +0000 Subject: [PATCH 216/566] test: 100% coverage on UMA permissions --- tests/test_uma_permissions.py | 38 ++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/test_uma_permissions.py b/tests/test_uma_permissions.py index 581faf4c..14f63028 100644 --- a/tests/test_uma_permissions.py +++ b/tests/test_uma_permissions.py @@ -22,7 +22,32 @@ import pytest from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError -from keycloak.uma_permissions import Resource, Scope, build_permission_param +from keycloak.uma_permissions import ( + Resource, + Scope, + build_permission_param, + UMAPermission, + AuthStatus, +) + + +def test_uma_permission_obj(): + """Test generic UMA permission.""" + with pytest.raises(PermissionDefinitionError): + UMAPermission(permission="bad") + + p1 = UMAPermission(permission=Resource("Resource")) + assert p1.resource == "Resource" + assert p1.scope == "" + assert repr(p1) == "Resource" + assert str(p1) == "Resource" + + p2 = UMAPermission(permission=Scope("Scope")) + assert p2.resource == "" + assert p2.scope == "Scope" + assert repr(p2) == "#Scope" + assert str(p2) == "#Scope" + assert {p1, p1} != {p2, p2} def test_resource_with_scope_obj(): @@ -174,3 +199,14 @@ def test_build_permission_misbuilt_dict_non_iterable(): with pytest.raises(KeycloakPermissionFormatError) as err: build_permission_param({"res1": 5}) assert err.match(re.escape("misbuilt permission {'res1': 5}")) + + +def test_auth_status_bool(): + """Test bool method of AuthStatus.""" + assert not bool(AuthStatus(is_logged_in=True, is_authorized=False, missing_permissions="")) + assert bool(AuthStatus(is_logged_in=True, is_authorized=True, missing_permissions="")) + + +def test_build_permission_without_scopes(): + """Test build permission param with scopes.""" + assert build_permission_param(permissions={"Resource": None}) == {"Resource"} From 49ddcdc3a696e9e2f5416d1aa40c7ea5845c6c7b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 22:46:48 +0000 Subject: [PATCH 217/566] fix: turn get_name into a method, use setters in connection manager --- src/keycloak/authorization/role.py | 1 - src/keycloak/connection.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/keycloak/authorization/role.py b/src/keycloak/authorization/role.py index 05da243b..4ee6ec93 100644 --- a/src/keycloak/authorization/role.py +++ b/src/keycloak/authorization/role.py @@ -38,7 +38,6 @@ def __init__(self, name, required=False): self.name = name self.required = required - @property def get_name(self): """Get name.""" return self.name diff --git a/src/keycloak/connection.py b/src/keycloak/connection.py index 361d95da..44978e59 100644 --- a/src/keycloak/connection.py +++ b/src/keycloak/connection.py @@ -25,7 +25,7 @@ try: from urllib.parse import urljoin -except ImportError: +except ImportError: # pragma: no cover from urlparse import urljoin import requests @@ -46,10 +46,10 @@ class ConnectionManager(object): def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): """Init method.""" - self._base_url = base_url - self._headers = headers - self._timeout = timeout - self._verify = verify + self.base_url = base_url + self.headers = headers + self.timeout = timeout + self.verify = verify self._s = requests.Session() self._s.auth = lambda x: x # don't let requests add auth headers From cab018fa5ab289c3d2e329aac00df22dfa8ae1a1 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 13 Jul 2022 22:47:02 +0000 Subject: [PATCH 218/566] test: added all unit tests --- tests/test_authorization.py | 42 +++++++++++++++++++++++++++++++++++ tests/test_connection.py | 41 ++++++++++++++++++++++++++++++++++ tests/test_exceptions.py | 20 +++++++++++++++++ tests/test_uma_permissions.py | 4 ++-- 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 tests/test_authorization.py create mode 100644 tests/test_connection.py create mode 100644 tests/test_exceptions.py diff --git a/tests/test_authorization.py b/tests/test_authorization.py new file mode 100644 index 00000000..a9ffc546 --- /dev/null +++ b/tests/test_authorization.py @@ -0,0 +1,42 @@ +"""Test authorization module.""" +import pytest + +from keycloak.authorization import Permission, Policy, Role +from keycloak.exceptions import KeycloakAuthorizationConfigError + + +def test_authorization_objects(): + """Test authorization objects.""" + # Test permission + p = Permission(name="test", type="test", logic="test", decision_strategy="test") + assert p.name == "test" + assert p.type == "test" + assert p.logic == "test" + assert p.decision_strategy == "test" + p.resources = ["test"] + assert p.resources == ["test"] + p.scopes = ["test"] + assert p.scopes == ["test"] + + # Test policy + p = Policy(name="test", type="test", logic="test", decision_strategy="test") + assert p.name == "test" + assert p.type == "test" + assert p.logic == "test" + assert p.decision_strategy == "test" + p.roles = ["test"] + assert p.roles == ["test"] + p.permissions = ["test"] + assert p.permissions == ["test"] + p.add_permission(permission="test2") + assert p.permissions == ["test", "test2"] + with pytest.raises(KeycloakAuthorizationConfigError): + p.add_role(role="test2") + + # Test role + r = Role(name="test") + assert r.name == "test" + assert not r.required + assert r.get_name() == "test" + assert r == r + assert r == "test" diff --git a/tests/test_connection.py b/tests/test_connection.py new file mode 100644 index 00000000..85730cd4 --- /dev/null +++ b/tests/test_connection.py @@ -0,0 +1,41 @@ +"""Connection test module.""" + +import pytest + +from keycloak.connection import ConnectionManager +from keycloak.exceptions import KeycloakConnectionError + + +def test_connection_proxy(): + """Test proxies of connection manager.""" + cm = ConnectionManager( + base_url="http://test.test", proxies={"http://test.test": "localhost:8080"} + ) + assert cm._s.proxies == {"http://test.test": "localhost:8080"} + + +def test_headers(): + """Test headers manipulation.""" + cm = ConnectionManager(base_url="http://test.test", headers={"H": "A"}) + assert cm.param_headers(key="H") == "A" + assert cm.param_headers(key="A") is None + cm.clean_headers() + assert cm.headers == dict() + cm.add_param_headers(key="H", value="B") + assert cm.exist_param_headers(key="H") + assert not cm.exist_param_headers(key="B") + cm.del_param_headers(key="H") + assert not cm.exist_param_headers(key="H") + + +def test_bad_connection(): + """Test bad connection.""" + cm = ConnectionManager(base_url="http://not.real.domain") + with pytest.raises(KeycloakConnectionError): + cm.raw_get(path="bad") + with pytest.raises(KeycloakConnectionError): + cm.raw_delete(path="bad") + with pytest.raises(KeycloakConnectionError): + cm.raw_post(path="bad", data={}) + with pytest.raises(KeycloakConnectionError): + cm.raw_put(path="bad", data={}) diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py new file mode 100644 index 00000000..72e7161f --- /dev/null +++ b/tests/test_exceptions.py @@ -0,0 +1,20 @@ +"""Test the exceptions module.""" + +from unittest.mock import Mock + +import pytest + +from keycloak.exceptions import KeycloakOperationError, raise_error_from_response + + +def test_raise_error_from_response_from_dict(): + """Test raise error from response using a dictionary.""" + response = Mock() + response.json.return_value = {"key": "value"} + response.status_code = 408 + response.content = "Error" + + with pytest.raises(KeycloakOperationError): + raise_error_from_response( + response=response, error=dict(), expected_codes=[200], skip_exists=False + ) diff --git a/tests/test_uma_permissions.py b/tests/test_uma_permissions.py index 14f63028..8179ff94 100644 --- a/tests/test_uma_permissions.py +++ b/tests/test_uma_permissions.py @@ -23,11 +23,11 @@ from keycloak.exceptions import KeycloakPermissionFormatError, PermissionDefinitionError from keycloak.uma_permissions import ( + AuthStatus, Resource, Scope, - build_permission_param, UMAPermission, - AuthStatus, + build_permission_param, ) From 1c6524e4db5f3e810c7344245400712ee7287f72 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 17 Jul 2022 11:19:45 +0200 Subject: [PATCH 219/566] fix: check client existence based on clientId Remove the necessity for supplying client name for create a new client request, also don't check existing clients based on client name as those can be duplicate BREAKING CHANGE: Renamed parameter client_name to client_id in get_client_id method Closes #351 --- src/keycloak/keycloak_admin.py | 8 ++++---- tests/test_keycloak_admin.py | 10 +++++----- tests/test_keycloak_openid.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 8d65b629..00501ef5 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1046,19 +1046,19 @@ def get_client(self, client_id): data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_client_id(self, client_name): + def get_client_id(self, client_id): """Get internal keycloak client id from client-id. This is required for further actions against this client. - :param client_name: name in ClientRepresentation + :param client_id: clientId in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: client_id (uuid as string) """ clients = self.get_clients() for client in clients: - if client_name == client.get("name") or client_name == client.get("clientId"): + if client_id == client.get("clientId"): return client["id"] return None @@ -1237,7 +1237,7 @@ def create_client(self, payload, skip_exists=False): :return: Client ID """ if skip_exists: - client_id = self.get_client_id(client_name=payload["name"]) + client_id = self.get_client_id(client_id=payload["clientId"]) if client_id is not None: return client_id diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 069bb61e..106e15af 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -655,8 +655,8 @@ def test_clients(admin: KeycloakAdmin, realm: str): assert len(admin.get_clients()) == 7 # Test get client id - assert admin.get_client_id(client_name="test-client") == client_id - assert admin.get_client_id(client_name="does-not-exist") is None + assert admin.get_client_id(client_id="test-client") == client_id + assert admin.get_client_id(client_id="does-not-exist") is None # Test update client res = admin.update_client(client_id=client_id, payload={"name": "test-client-change"}) @@ -856,7 +856,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"Could not find client"}\'') secrets = admin.get_client_secrets( - client_id=admin.get_client_id(client_name="test-confidential") + client_id=admin.get_client_id(client_id="test-confidential") ) assert secrets == {"type": "secret", "value": "test-secret"} @@ -865,11 +865,11 @@ def test_clients(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"Could not find client"}\'') res = admin.generate_client_secrets( - client_id=admin.get_client_id(client_name="test-confidential") + client_id=admin.get_client_id(client_id="test-confidential") ) assert res assert ( - admin.get_client_secrets(client_id=admin.get_client_id(client_name="test-confidential")) + admin.get_client_secrets(client_id=admin.get_client_id(client_id="test-confidential")) == res ) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 03240770..8eccb0f6 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -163,10 +163,10 @@ def test_exchange_token( admin.realm_name = oid.realm_name admin.assign_client_role( user_id=admin.get_user_id(username=username), - client_id=admin.get_client_id(client_name="realm-management"), + client_id=admin.get_client_id(client_id="realm-management"), roles=[ admin.get_client_role( - client_id=admin.get_client_id(client_name="realm-management"), + client_id=admin.get_client_id(client_id="realm-management"), role_name="impersonation", ) ], From 5bc5d4f32152f947e369bcd267b1194815734a51 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Sun, 17 Jul 2022 21:10:41 +0200 Subject: [PATCH 220/566] feat: add functions covering some missing REST API calls --- poetry.lock | 8 ++++++ src/keycloak/keycloak_admin.py | 51 ++++++++++++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 8 ++++++ 3 files changed, 67 insertions(+) diff --git a/poetry.lock b/poetry.lock index 24b870bd..ae1c1f0f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -688,6 +688,14 @@ category = "main" optional = false python-versions = ">=3.6,<4" +[[package]] +name = "requests-toolbelt" +version = "0.9.1" +description = "A utility belt for advanced users of python-requests" +category = "main" +optional = false +python-versions = ">=3.6,<4" + [package.dependencies] pyasn1 = ">=0.1.3" diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 00501ef5..4d2c3688 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -29,6 +29,8 @@ import json from builtins import isinstance from typing import Iterable +import copy +from requests_toolbelt import MultipartEncoder from . import urls_patterns from .connection import ConnectionManager @@ -2810,3 +2812,52 @@ def create_client_authz_client_policy(self, payload, client_id): data=json.dumps(payload), ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + + def get_composite_client_roles_of_group(self, client_id, group_id): + params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_role_client_level_children(self, client_id, role_id): + params_path = {"realm-name": self.realm_name, "role-id": role_id, "client-id": client_id} + data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE_CHILDREN.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def get_user_credentials(self, user_id): + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CREDENTIALS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def upload_certificate(self, client_id, certcont): + params_path = {"realm-name": self.realm_name, "id": client_id, "attr": "jwt.credential"} + m = MultipartEncoder( + fields={ + "keystoreFormat": "Certificate PEM", + "file": certcont + } + ) + new_headers = copy.deepcopy(self.connection.headers) + new_headers["Content-Type"] = m.content_type + self.connection.headers = new_headers + data_raw = self.raw_post(urls_patterns.URL_ADMIN_CLIENT_CERT_UPLOAD.format(**params_path), + data=m, headers=new_headers) + return raise_error_from_response(data_raw, KeycloakPostError) + + def get_required_action_by_alias(self, action_alias): + actions = self.get_required_actions() + for a in actions: + if a['alias'] == action_alias: + break + return a + + def get_required_actions(self): + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_get(urls_patterns.URL_ADMIN_REQUIRED_ACTIONS.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + def update_required_action(self, action_alias, payload): + if not isinstance(payload, str): + payload = json.dumps(payload) + params_path = {"realm-name": self.realm_name, "action-alias": action_alias} + data_raw = self.raw_put(urls_patterns.URL_ADMIN_REQUIRED_ACTIONS_ALIAS.format(**params_path), data=payload) + return raise_error_from_response(data_raw, KeycloakPutError) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 29903627..325d6934 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -183,3 +183,11 @@ URL_ADMIN_EVENTS = "admin/realms/{realm-name}/events" URL_ADMIN_EVENTS_CONFIG = URL_ADMIN_EVENTS + "/config" URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" + +URL_ADMIN_REALM = "admin/realms/{realm-name}" +URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE = URL_ADMIN_GROUPS_CLIENT_ROLES+"/composite" +URL_ADMIN_CLIENT_ROLE_CHILDREN = "admin/realms/{realm-name}/roles-by-id/{role-id}/composites/clients/{client-id}" +URL_ADMIN_USER_CREDENTIALS = URL_ADMIN_USERS+"/{id}/credentials" +URL_ADMIN_CLIENT_CERT_UPLOAD = URL_ADMIN_CLIENT_CERTS + '/upload-certificate' +URL_ADMIN_REQUIRED_ACTIONS = URL_ADMIN_REALM + "/authentication/required-actions" +URL_ADMIN_REQUIRED_ACTIONS_ALIAS = URL_ADMIN_REQUIRED_ACTIONS + "/{action-alias}" From 9bff615fec9e1de56abf610ab44dc3973456d187 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 09:23:14 +0200 Subject: [PATCH 221/566] feat: add docstrings --- poetry.lock | 101 +++++++++++++++++++++++++++------ pyproject.toml | 1 + src/keycloak/keycloak_admin.py | 55 ++++++++++++++++++ 3 files changed, 141 insertions(+), 16 deletions(-) diff --git a/poetry.lock b/poetry.lock index ae1c1f0f..d287c38b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -681,17 +681,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"] use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] [[package]] -name = "rsa" -version = "4.8" -description = "Pure-Python RSA implementation" +name = "requests-toolbelt" +version = "0.9.1" +description = "A utility belt for advanced users of python-requests" category = "main" optional = false -python-versions = ">=3.6,<4" +python-versions = "*" + +[package.dependencies] +requests = ">=2.0.1,<3.0.0" [[package]] -name = "requests-toolbelt" -version = "0.9.1" -description = "A utility belt for advanced users of python-requests" +name = "rsa" +version = "4.8" +description = "Pure-Python RSA implementation" category = "main" optional = false python-versions = ">=3.6,<4" @@ -998,7 +1001,7 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "ed105f41fc20e390af8eeefafd3168bb4b370d3a5135bfdec55aab7fc5d0bb3e" +content-hash = "fb17c4ff35afb569d2bae025578ede6face011dcaa98b1fece090082746d7511" [metadata.files] alabaster = [ @@ -1009,8 +1012,13 @@ argcomplete = [ {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, ] -astroid = [] -atomicwrites = [] +astroid = [ + {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, + {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, @@ -1072,7 +1080,49 @@ commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] -coverage = [] +coverage = [ + {file = "coverage-6.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e"}, + {file = "coverage-6.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39"}, + {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d"}, + {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc"}, + {file = "coverage-6.4.2-cp310-cp310-win32.whl", hash = "sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386"}, + {file = "coverage-6.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0"}, + {file = "coverage-6.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039"}, + {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e"}, + {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083"}, + {file = "coverage-6.4.2-cp37-cp37m-win32.whl", hash = "sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7"}, + {file = "coverage-6.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120"}, + {file = "coverage-6.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452"}, + {file = "coverage-6.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8"}, + {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933"}, + {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de"}, + {file = "coverage-6.4.2-cp38-cp38-win32.whl", hash = "sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783"}, + {file = "coverage-6.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6"}, + {file = "coverage-6.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f"}, + {file = "coverage-6.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29"}, + {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978"}, + {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c"}, + {file = "coverage-6.4.2-cp39-cp39-win32.whl", hash = "sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd"}, + {file = "coverage-6.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf"}, + {file = "coverage-6.4.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97"}, + {file = "coverage-6.4.2.tar.gz", hash = "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe"}, +] decli = [ {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, @@ -1085,7 +1135,10 @@ docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] -ecdsa = [] +ecdsa = [ + {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, + {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, +] filelock = [ {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, @@ -1247,7 +1300,10 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pre-commit = [] +pre-commit = [ + {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, + {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, +] prompt-toolkit = [ {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, @@ -1358,6 +1414,10 @@ requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] rsa = [ {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, @@ -1417,7 +1477,10 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tomlkit = [] +tomlkit = [ + {file = "tomlkit-0.11.1-py3-none-any.whl", hash = "sha256:1c5bebdf19d5051e2e1de6cf70adfc5948d47221f097fcff7a3ffc91e953eaf5"}, + {file = "tomlkit-0.11.1.tar.gz", hash = "sha256:61901f81ff4017951119cd0d1ed9b7af31c821d6845c8c477587bbdcd5e5854e"}, +] tox = [ {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, @@ -1456,7 +1519,10 @@ unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] -urllib3 = [] +urllib3 = [ + {file = "urllib3-1.26.10-py2.py3-none-any.whl", hash = "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec"}, + {file = "urllib3-1.26.10.tar.gz", hash = "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"}, +] virtualenv = [ {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, @@ -1531,4 +1597,7 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -zipp = [] +zipp = [ + {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, + {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, +] diff --git a/pyproject.toml b/pyproject.toml index 3c320375..c9f3b905 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,6 +42,7 @@ sphinx-rtd-theme = {version = "^1.0.0", optional = true} readthedocs-sphinx-ext = {version = "^2.1.8", optional = true} m2r2 = {version = "^0.3.2", optional = true} sphinx-autoapi = {version = "^1.8.4", optional = true} +requests-toolbelt = "^0.9.1" [tool.poetry.dev-dependencies] tox = "^3.25.0" diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 4d2c3688..1123885e 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2814,21 +2814,55 @@ def create_client_authz_client_policy(self, payload, client_id): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def get_composite_client_roles_of_group(self, client_id, group_id): + """Get the composite client roles of the given group for the given client + + :param client_id: id of the client + :type client_id: str + :param group_id: id of the group + :type group_id: str + :return: the composite client roles of the group (list of RoleRepresentation) + :rtype: list + """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_role_client_level_children(self, client_id, role_id): + """Get the child roles of which the given composite client role is composed of + + :param client_id: id of the client + :type client_id: str + :param role_id: id of the role + :type role_id: str + :return: the child roles (list of RoleRepresentation) + :rtype: list + """ params_path = {"realm-name": self.realm_name, "role-id": role_id, "client-id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE_CHILDREN.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def get_user_credentials(self, user_id): + """Get the credentials of the given user + + :param user_id: id of the user + :type user_id: str + :return: the user credentials (list of CredentialsRepresentation) + :rtype: list + """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CREDENTIALS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def upload_certificate(self, client_id, certcont): + """Upload a new certificate for the client + + :param client_id: id of the client + :type client_id: str + :param certcont: the content of the certificate + :type certcont: str + :return: dictionary {"certificate": ""}, where is the content of the uploaded certificate + :rtype: dict + """ params_path = {"realm-name": self.realm_name, "id": client_id, "attr": "jwt.credential"} m = MultipartEncoder( fields={ @@ -2844,6 +2878,13 @@ def upload_certificate(self, client_id, certcont): return raise_error_from_response(data_raw, KeycloakPostError) def get_required_action_by_alias(self, action_alias): + """Get a required action by its alias + + :param action_alias: the alias of the requried action + :type action_alias: str + :return: the required action (RequiredActionProviderRepresentation) + :rtype: dict + """ actions = self.get_required_actions() for a in actions: if a['alias'] == action_alias: @@ -2851,11 +2892,25 @@ def get_required_action_by_alias(self, action_alias): return a def get_required_actions(self): + """Get the required actions for the realms + + :return: the required actions (list of RequiredActionProviderRepresentation) + :rtype: list + """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_REQUIRED_ACTIONS.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) def update_required_action(self, action_alias, payload): + """Update a required action + + :param action_alias: the action alias + :type action_alias: str + :param payload: the new required action (RequiredActionProviderRepresentation) + :type payload: dict + :return: empty dictionary + :rtype: dict + """ if not isinstance(payload, str): payload = json.dumps(payload) params_path = {"realm-name": self.realm_name, "action-alias": action_alias} From fb942c11d8a9418ce52c6482db34a9d8d1fc00d7 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 14:29:25 +0200 Subject: [PATCH 222/566] feat: add unit tests --- poetry.lock | 134 ++++++++++++++++++++++++++++++++- pyproject.toml | 1 + src/keycloak/keycloak_admin.py | 27 ++++--- src/keycloak/urls_patterns.py | 10 ++- tests/conftest.py | 87 +++++++++++++++++++++ tests/test_keycloak_admin.py | 71 +++++++++++++++++ 6 files changed, 313 insertions(+), 17 deletions(-) diff --git a/poetry.lock b/poetry.lock index d287c38b..863296e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -98,6 +98,17 @@ category = "main" optional = false python-versions = ">=3.6" +[[package]] +name = "cffi" +version = "1.15.1" +description = "Foreign Function Interface for Python calling C code." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +pycparser = "*" + [[package]] name = "cfgv" version = "3.3.1" @@ -182,6 +193,25 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1 [package.extras] toml = ["tomli"] +[[package]] +name = "cryptography" +version = "37.0.4" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cffi = ">=1.12" + +[package.extras] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] +sdist = ["setuptools_rust (>=0.11.4)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] + [[package]] name = "decli" version = "0.5.2" @@ -510,6 +540,14 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + [[package]] name = "pydocstyle" version = "6.1.1" @@ -1001,7 +1039,7 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "fb17c4ff35afb569d2bae025578ede6face011dcaa98b1fece090082746d7511" +content-hash = "add6d6f1a2e9ec5cf7aae027fd49d5fcafd212963daa3257d363909aedb380be" [metadata.files] alabaster = [ @@ -1056,6 +1094,72 @@ certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, @@ -1123,6 +1227,30 @@ coverage = [ {file = "coverage-6.4.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97"}, {file = "coverage-6.4.2.tar.gz", hash = "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe"}, ] +cryptography = [ + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, + {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, + {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, + {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, +] decli = [ {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, @@ -1331,6 +1459,10 @@ pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, diff --git a/pyproject.toml b/pyproject.toml index c9f3b905..3534b4c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ readthedocs-sphinx-ext = {version = "^2.1.8", optional = true} m2r2 = {version = "^0.3.2", optional = true} sphinx-autoapi = {version = "^1.8.4", optional = true} requests-toolbelt = "^0.9.1" +cryptography = "^37.0.4" [tool.poetry.dev-dependencies] tox = "^3.25.0" diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 1123885e..58bbaa5a 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -26,10 +26,11 @@ """The keycloak admin module.""" +import copy import json from builtins import isinstance from typing import Iterable -import copy + from requests_toolbelt import MultipartEncoder from . import urls_patterns @@ -2824,7 +2825,9 @@ def get_composite_client_roles_of_group(self, client_id, group_id): :rtype: list """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path) + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_role_client_level_children(self, client_id, role_id): @@ -2864,17 +2867,15 @@ def upload_certificate(self, client_id, certcont): :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": client_id, "attr": "jwt.credential"} - m = MultipartEncoder( - fields={ - "keystoreFormat": "Certificate PEM", - "file": certcont - } - ) + m = MultipartEncoder(fields={"keystoreFormat": "Certificate PEM", "file": certcont}) new_headers = copy.deepcopy(self.connection.headers) new_headers["Content-Type"] = m.content_type self.connection.headers = new_headers - data_raw = self.raw_post(urls_patterns.URL_ADMIN_CLIENT_CERT_UPLOAD.format(**params_path), - data=m, headers=new_headers) + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_CERT_UPLOAD.format(**params_path), + data=m, + headers=new_headers, + ) return raise_error_from_response(data_raw, KeycloakPostError) def get_required_action_by_alias(self, action_alias): @@ -2887,7 +2888,7 @@ def get_required_action_by_alias(self, action_alias): """ actions = self.get_required_actions() for a in actions: - if a['alias'] == action_alias: + if a["alias"] == action_alias: break return a @@ -2914,5 +2915,7 @@ def update_required_action(self, action_alias, payload): if not isinstance(payload, str): payload = json.dumps(payload) params_path = {"realm-name": self.realm_name, "action-alias": action_alias} - data_raw = self.raw_put(urls_patterns.URL_ADMIN_REQUIRED_ACTIONS_ALIAS.format(**params_path), data=payload) + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_REQUIRED_ACTIONS_ALIAS.format(**params_path), data=payload + ) return raise_error_from_response(data_raw, KeycloakPutError) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 325d6934..69a2f1be 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -185,9 +185,11 @@ URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" URL_ADMIN_REALM = "admin/realms/{realm-name}" -URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE = URL_ADMIN_GROUPS_CLIENT_ROLES+"/composite" -URL_ADMIN_CLIENT_ROLE_CHILDREN = "admin/realms/{realm-name}/roles-by-id/{role-id}/composites/clients/{client-id}" -URL_ADMIN_USER_CREDENTIALS = URL_ADMIN_USERS+"/{id}/credentials" -URL_ADMIN_CLIENT_CERT_UPLOAD = URL_ADMIN_CLIENT_CERTS + '/upload-certificate' +URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE = URL_ADMIN_GROUPS_CLIENT_ROLES + "/composite" +URL_ADMIN_CLIENT_ROLE_CHILDREN = ( + "admin/realms/{realm-name}/roles-by-id/{role-id}/composites/clients/{client-id}" +) +URL_ADMIN_USER_CREDENTIALS = URL_ADMIN_USERS + "/{id}/credentials" +URL_ADMIN_CLIENT_CERT_UPLOAD = URL_ADMIN_CLIENT_CERTS + "/upload-certificate" URL_ADMIN_REQUIRED_ACTIONS = URL_ADMIN_REALM + "/authentication/required-actions" URL_ADMIN_REQUIRED_ACTIONS_ALIAS = URL_ADMIN_REQUIRED_ACTIONS + "/{action-alias}" diff --git a/tests/conftest.py b/tests/conftest.py index 5f340ae4..7380999f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -257,3 +257,90 @@ def client(admin: KeycloakAdmin, realm: str) -> str: client_id = admin.create_client(payload={"name": client, "clientId": client}) yield client_id admin.delete_client(client_id=client_id) + + +@pytest.fixture +def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str: + """Fixture for a new random client role.""" + admin.realm_name = realm + role = str(uuid.uuid4()) + admin.create_client_role(client, {"name": role, "composite": False}) + yield role + admin.delete_client_role(client, role) + + +@pytest.fixture +def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_role: str) -> str: + """Fixture for a new random composite client role.""" + admin.realm_name = realm + role = str(uuid.uuid4()) + admin.create_client_role(client, {"name": role, "composite": True}) + role_repr = admin.get_client_role(client, client_role) + admin.add_composite_client_roles_to_role(client, role, roles=[role_repr]) + yield role + admin.delete_client_role(client, role) + + +@pytest.fixture +def selfsigned_cert(): + """Generates self signed certificate for a hostname, and optional IP addresses.""" + from cryptography import x509 + from cryptography.x509.oid import NameOID + from cryptography.hazmat.primitives import hashes + from cryptography.hazmat.backends import default_backend + from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives.asymmetric import rsa + from datetime import datetime, timedelta + import ipaddress + hostname = "testcert" + ip_addresses = None + key = None + # Generate our key + if key is None: + key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend(), + ) + + name = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, hostname) + ]) + + # best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored. + alt_names = [x509.DNSName(hostname)] + + # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios + if ip_addresses: + for addr in ip_addresses: + # openssl wants DNSnames for ips... + alt_names.append(x509.DNSName(addr)) + # ... whereas golang's crypto/tls is stricter, and needs IPAddresses + # note: older versions of cryptography do not understand ip_address objects + alt_names.append(x509.IPAddress(ipaddress.ip_address(addr))) + + san = x509.SubjectAlternativeName(alt_names) + + # path_len=0 means this cert can only sign itself, not other certs. + basic_contraints = x509.BasicConstraints(ca=True, path_length=0) + now = datetime.utcnow() + cert = ( + x509.CertificateBuilder() + .subject_name(name) + .issuer_name(name) + .public_key(key.public_key()) + .serial_number(1000) + .not_valid_before(now) + .not_valid_after(now + timedelta(days=10*365)) + .add_extension(basic_contraints, False) + .add_extension(san, False) + .sign(key, hashes.SHA256(), default_backend()) + ) + cert_pem = cert.public_bytes(encoding=serialization.Encoding.PEM) + key_pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + return cert_pem, key_pem diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 106e15af..20341afa 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -2,6 +2,7 @@ import pytest +import copy import keycloak from keycloak import KeycloakAdmin from keycloak.connection import ConnectionManager @@ -1815,3 +1816,73 @@ def test_auto_refresh(admin: KeycloakAdmin, realm: str): admin.auto_refresh_token = ["get", "post", "put", "delete"] assert admin.delete_realm(realm_name="test-refresh") == dict() + + +def test_get_required_actions(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + ractions = admin.get_required_actions() + assert isinstance(ractions, list) + for ra in ractions: + for key in [ + "alias", + "name", + "providerId", + "enabled", + "defaultAction", + "priority", + "config", + ]: + assert key in ra + + +def test_get_required_action_by_alias(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + ractions = admin.get_required_actions() + ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") + assert ra in ractions + assert ra['alias'] == "UPDATE_PASSWORD" + + +def test_update_required_action(admin: KeycloakAdmin, realm: str): + admin.realm_name = realm + ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") + old = copy.deepcopy(ra) + ra['enabled'] = False + admin.update_required_action("UPDATE_PASSWORD", ra) + newra = admin.get_required_action_by_alias("UPDATE_PASSWORD") + assert old != newra + assert newra['enabled'] is False + + +def test_get_composite_client_roles_of_group(admin: KeycloakAdmin, realm: str, client: str, group: str, composite_client_role: str): + admin.realm_name = realm + role = admin.get_client_role(client, composite_client_role) + admin.assign_group_client_roles(group_id=group, client_id=client, roles=[role]) + result = admin.get_composite_client_roles_of_group(client, group) + assert role['id'] in [x['id'] for x in result] + + +def test_get_role_client_level_children(admin: KeycloakAdmin, realm: str, client: str, composite_client_role: str, client_role: str): + admin.realm_name = realm + child = admin.get_client_role(client, client_role) + parent = admin.get_client_role(client, composite_client_role) + res = admin.get_role_client_level_children(client, parent['id']) + assert child['id'] in [x['id'] for x in res] + + +def test_get_user_credentials(admin: KeycloakAdmin, realm: str, user: str): + admin.realm_name = realm + admin.set_user_password(user, "pwd", temporary=False) + res = admin.get_user_credentials(user) + assert isinstance(res, list) + assert len(res) == 1 + assert res[0]['type'] == "password" + + +def test_upload_certificate(admin: KeycloakAdmin, realm: str, client: str, selfsigned_cert: tuple): + admin.realm_name = realm + cert, _ = selfsigned_cert + cert = cert.decode('utf-8').strip() + admin.upload_certificate(client, cert) + cl = admin.get_client(client) + assert cl['attributes']['jwt.credential.certificate'] == "".join(cert.splitlines()[1:-1]) From 7e50d4313c4d2ead8a2843346a24e8a8a5561703 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 14:49:02 +0200 Subject: [PATCH 223/566] fix: applied tox linting check --- tests/conftest.py | 21 +++++++++------------ tests/test_keycloak_admin.py | 29 +++++++++++++++++------------ 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7380999f..44865e26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -284,28 +284,25 @@ def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_ @pytest.fixture def selfsigned_cert(): """Generates self signed certificate for a hostname, and optional IP addresses.""" + import ipaddress + from datetime import datetime, timedelta + from cryptography import x509 - from cryptography.x509.oid import NameOID - from cryptography.hazmat.primitives import hashes from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import serialization + from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa - from datetime import datetime, timedelta - import ipaddress + from cryptography.x509.oid import NameOID + hostname = "testcert" ip_addresses = None key = None # Generate our key if key is None: key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, - backend=default_backend(), + public_exponent=65537, key_size=2048, backend=default_backend() ) - name = x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, hostname) - ]) + name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)]) # best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored. alt_names = [x509.DNSName(hostname)] @@ -331,7 +328,7 @@ def selfsigned_cert(): .public_key(key.public_key()) .serial_number(1000) .not_valid_before(now) - .not_valid_after(now + timedelta(days=10*365)) + .not_valid_after(now + timedelta(days=10 * 365)) .add_extension(basic_contraints, False) .add_extension(san, False) .sign(key, hashes.SHA256(), default_backend()) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 20341afa..76d0cf49 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1,8 +1,9 @@ """Test the keycloak admin object.""" +import copy + import pytest -import copy import keycloak from keycloak import KeycloakAdmin from keycloak.connection import ConnectionManager @@ -1840,34 +1841,38 @@ def test_get_required_action_by_alias(admin: KeycloakAdmin, realm: str): ractions = admin.get_required_actions() ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") assert ra in ractions - assert ra['alias'] == "UPDATE_PASSWORD" + assert ra["alias"] == "UPDATE_PASSWORD" def test_update_required_action(admin: KeycloakAdmin, realm: str): admin.realm_name = realm ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") old = copy.deepcopy(ra) - ra['enabled'] = False + ra["enabled"] = False admin.update_required_action("UPDATE_PASSWORD", ra) newra = admin.get_required_action_by_alias("UPDATE_PASSWORD") assert old != newra - assert newra['enabled'] is False + assert newra["enabled"] is False -def test_get_composite_client_roles_of_group(admin: KeycloakAdmin, realm: str, client: str, group: str, composite_client_role: str): +def test_get_composite_client_roles_of_group( + admin: KeycloakAdmin, realm: str, client: str, group: str, composite_client_role: str +): admin.realm_name = realm role = admin.get_client_role(client, composite_client_role) admin.assign_group_client_roles(group_id=group, client_id=client, roles=[role]) result = admin.get_composite_client_roles_of_group(client, group) - assert role['id'] in [x['id'] for x in result] + assert role["id"] in [x["id"] for x in result] -def test_get_role_client_level_children(admin: KeycloakAdmin, realm: str, client: str, composite_client_role: str, client_role: str): +def test_get_role_client_level_children( + admin: KeycloakAdmin, realm: str, client: str, composite_client_role: str, client_role: str +): admin.realm_name = realm child = admin.get_client_role(client, client_role) parent = admin.get_client_role(client, composite_client_role) - res = admin.get_role_client_level_children(client, parent['id']) - assert child['id'] in [x['id'] for x in res] + res = admin.get_role_client_level_children(client, parent["id"]) + assert child["id"] in [x["id"] for x in res] def test_get_user_credentials(admin: KeycloakAdmin, realm: str, user: str): @@ -1876,13 +1881,13 @@ def test_get_user_credentials(admin: KeycloakAdmin, realm: str, user: str): res = admin.get_user_credentials(user) assert isinstance(res, list) assert len(res) == 1 - assert res[0]['type'] == "password" + assert res[0]["type"] == "password" def test_upload_certificate(admin: KeycloakAdmin, realm: str, client: str, selfsigned_cert: tuple): admin.realm_name = realm cert, _ = selfsigned_cert - cert = cert.decode('utf-8').strip() + cert = cert.decode("utf-8").strip() admin.upload_certificate(client, cert) cl = admin.get_client(client) - assert cl['attributes']['jwt.credential.certificate'] == "".join(cert.splitlines()[1:-1]) + assert cl["attributes"]["jwt.credential.certificate"] == "".join(cert.splitlines()[1:-1]) From 96085b7b1ddfed2dde43d726da6923ae25ab7871 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 15:14:49 +0200 Subject: [PATCH 224/566] fix: applied flake linting checks --- poetry.lock | 8 +++--- pyproject.toml | 2 +- src/keycloak/keycloak_admin.py | 49 +++++++++++++++++----------------- tests/conftest.py | 4 +-- tests/test_keycloak_admin.py | 7 +++++ 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/poetry.lock b/poetry.lock index 863296e2..e65379e8 100644 --- a/poetry.lock +++ b/poetry.lock @@ -102,7 +102,7 @@ python-versions = ">=3.6" name = "cffi" version = "1.15.1" description = "Foreign Function Interface for Python calling C code." -category = "main" +category = "dev" optional = false python-versions = "*" @@ -197,7 +197,7 @@ toml = ["tomli"] name = "cryptography" version = "37.0.4" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" +category = "dev" optional = false python-versions = ">=3.6" @@ -544,7 +544,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" name = "pycparser" version = "2.21" description = "C parser in Python" -category = "main" +category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -1039,7 +1039,7 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "add6d6f1a2e9ec5cf7aae027fd49d5fcafd212963daa3257d363909aedb380be" +content-hash = "b6851001fb6b3e331a39e6bab1fa2ed99fdc555e9683137c20c9c593c0e1c040" [metadata.files] alabaster = [ diff --git a/pyproject.toml b/pyproject.toml index 3534b4c1..8adfa586 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,6 @@ readthedocs-sphinx-ext = {version = "^2.1.8", optional = true} m2r2 = {version = "^0.3.2", optional = true} sphinx-autoapi = {version = "^1.8.4", optional = true} requests-toolbelt = "^0.9.1" -cryptography = "^37.0.4" [tool.poetry.dev-dependencies] tox = "^3.25.0" @@ -56,6 +55,7 @@ black = "^22.3.0" flake8 = "^3.5.0" flake8-docstrings = "^1.6.0" commitizen = "^2.28.0" +cryptography = "^37.0.4" [tool.poetry.extras] docs = [ diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 58bbaa5a..12350a89 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2815,13 +2815,13 @@ def create_client_authz_client_policy(self, payload, client_id): return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) def get_composite_client_roles_of_group(self, client_id, group_id): - """Get the composite client roles of the given group for the given client + """Get the composite client roles of the given group for the given client. - :param client_id: id of the client + :param client_id: id of the client. :type client_id: str - :param group_id: id of the group + :param group_id: id of the group. :type group_id: str - :return: the composite client roles of the group (list of RoleRepresentation) + :return: the composite client roles of the group (list of RoleRepresentation). :rtype: list """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} @@ -2831,13 +2831,13 @@ def get_composite_client_roles_of_group(self, client_id, group_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_role_client_level_children(self, client_id, role_id): - """Get the child roles of which the given composite client role is composed of + """Get the child roles of which the given composite client role is composed of. - :param client_id: id of the client + :param client_id: id of the client. :type client_id: str - :param role_id: id of the role + :param role_id: id of the role. :type role_id: str - :return: the child roles (list of RoleRepresentation) + :return: the child roles (list of RoleRepresentation). :rtype: list """ params_path = {"realm-name": self.realm_name, "role-id": role_id, "client-id": client_id} @@ -2845,11 +2845,11 @@ def get_role_client_level_children(self, client_id, role_id): return raise_error_from_response(data_raw, KeycloakGetError) def get_user_credentials(self, user_id): - """Get the credentials of the given user + """Get the credentials of the given user. - :param user_id: id of the user + :param user_id: id of the user. :type user_id: str - :return: the user credentials (list of CredentialsRepresentation) + :return: the user credentials (list of CredentialsRepresentation). :rtype: list """ params_path = {"realm-name": self.realm_name, "id": user_id} @@ -2857,13 +2857,14 @@ def get_user_credentials(self, user_id): return raise_error_from_response(data_raw, KeycloakGetError) def upload_certificate(self, client_id, certcont): - """Upload a new certificate for the client + """Upload a new certificate for the client. - :param client_id: id of the client + :param client_id: id of the client. :type client_id: str - :param certcont: the content of the certificate + :param certcont: the content of the certificate. :type certcont: str - :return: dictionary {"certificate": ""}, where is the content of the uploaded certificate + :return: dictionary {"certificate": ""}, + where is the content of the uploaded certificate. :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": client_id, "attr": "jwt.credential"} @@ -2879,11 +2880,11 @@ def upload_certificate(self, client_id, certcont): return raise_error_from_response(data_raw, KeycloakPostError) def get_required_action_by_alias(self, action_alias): - """Get a required action by its alias + """Get a required action by its alias. - :param action_alias: the alias of the requried action + :param action_alias: the alias of the requried action. :type action_alias: str - :return: the required action (RequiredActionProviderRepresentation) + :return: the required action (RequiredActionProviderRepresentation). :rtype: dict """ actions = self.get_required_actions() @@ -2893,9 +2894,9 @@ def get_required_action_by_alias(self, action_alias): return a def get_required_actions(self): - """Get the required actions for the realms + """Get the required actions for the realms. - :return: the required actions (list of RequiredActionProviderRepresentation) + :return: the required actions (list of RequiredActionProviderRepresentation). :rtype: list """ params_path = {"realm-name": self.realm_name} @@ -2903,13 +2904,13 @@ def get_required_actions(self): return raise_error_from_response(data_raw, KeycloakGetError) def update_required_action(self, action_alias, payload): - """Update a required action + """Update a required action. - :param action_alias: the action alias + :param action_alias: the action alias. :type action_alias: str - :param payload: the new required action (RequiredActionProviderRepresentation) + :param payload: the new required action (RequiredActionProviderRepresentation). :type payload: dict - :return: empty dictionary + :return: empty dictionary. :rtype: dict """ if not isinstance(payload, str): diff --git a/tests/conftest.py b/tests/conftest.py index 44865e26..85f6a8f1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -283,7 +283,7 @@ def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_ @pytest.fixture def selfsigned_cert(): - """Generates self signed certificate for a hostname, and optional IP addresses.""" + """Generate self signed certificate for a hostname, and optional IP addresses.""" import ipaddress from datetime import datetime, timedelta @@ -303,8 +303,6 @@ def selfsigned_cert(): ) name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, hostname)]) - - # best practice seem to be to include the hostname in the SAN, which *SHOULD* mean COMMON_NAME is ignored. alt_names = [x509.DNSName(hostname)] # allow addressing by IP, for when you don't have real DNS (common in most testing scenarios diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 76d0cf49..c4ba3d84 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1820,6 +1820,7 @@ def test_auto_refresh(admin: KeycloakAdmin, realm: str): def test_get_required_actions(admin: KeycloakAdmin, realm: str): + """Test requried actions.""" admin.realm_name = realm ractions = admin.get_required_actions() assert isinstance(ractions, list) @@ -1837,6 +1838,7 @@ def test_get_required_actions(admin: KeycloakAdmin, realm: str): def test_get_required_action_by_alias(admin: KeycloakAdmin, realm: str): + """Test get required action by alias.""" admin.realm_name = realm ractions = admin.get_required_actions() ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") @@ -1845,6 +1847,7 @@ def test_get_required_action_by_alias(admin: KeycloakAdmin, realm: str): def test_update_required_action(admin: KeycloakAdmin, realm: str): + """Test update required action.""" admin.realm_name = realm ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") old = copy.deepcopy(ra) @@ -1858,6 +1861,7 @@ def test_update_required_action(admin: KeycloakAdmin, realm: str): def test_get_composite_client_roles_of_group( admin: KeycloakAdmin, realm: str, client: str, group: str, composite_client_role: str ): + """Test get composite client roles of group.""" admin.realm_name = realm role = admin.get_client_role(client, composite_client_role) admin.assign_group_client_roles(group_id=group, client_id=client, roles=[role]) @@ -1868,6 +1872,7 @@ def test_get_composite_client_roles_of_group( def test_get_role_client_level_children( admin: KeycloakAdmin, realm: str, client: str, composite_client_role: str, client_role: str ): + """Test get children of composite client role.""" admin.realm_name = realm child = admin.get_client_role(client, client_role) parent = admin.get_client_role(client, composite_client_role) @@ -1876,6 +1881,7 @@ def test_get_role_client_level_children( def test_get_user_credentials(admin: KeycloakAdmin, realm: str, user: str): + """Test get user credentials.""" admin.realm_name = realm admin.set_user_password(user, "pwd", temporary=False) res = admin.get_user_credentials(user) @@ -1885,6 +1891,7 @@ def test_get_user_credentials(admin: KeycloakAdmin, realm: str, user: str): def test_upload_certificate(admin: KeycloakAdmin, realm: str, client: str, selfsigned_cert: tuple): + """Test upload certificate.""" admin.realm_name = realm cert, _ = selfsigned_cert cert = cert.decode("utf-8").strip() From 7eb56d53888f5af20710726592467d4dd25e67d3 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 15:42:51 +0200 Subject: [PATCH 225/566] fix: applied tox -e docs --- src/keycloak/urls_patterns.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 69a2f1be..10bd99c3 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -184,12 +184,10 @@ URL_ADMIN_EVENTS_CONFIG = URL_ADMIN_EVENTS + "/config" URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" -URL_ADMIN_REALM = "admin/realms/{realm-name}" URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE = URL_ADMIN_GROUPS_CLIENT_ROLES + "/composite" URL_ADMIN_CLIENT_ROLE_CHILDREN = ( "admin/realms/{realm-name}/roles-by-id/{role-id}/composites/clients/{client-id}" ) -URL_ADMIN_USER_CREDENTIALS = URL_ADMIN_USERS + "/{id}/credentials" URL_ADMIN_CLIENT_CERT_UPLOAD = URL_ADMIN_CLIENT_CERTS + "/upload-certificate" URL_ADMIN_REQUIRED_ACTIONS = URL_ADMIN_REALM + "/authentication/required-actions" URL_ADMIN_REQUIRED_ACTIONS_ALIAS = URL_ADMIN_REQUIRED_ACTIONS + "/{action-alias}" From 2d217eca1c30aa48fb02ea3b15900f5bc50e5268 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 16:30:26 +0200 Subject: [PATCH 226/566] fix: remove duplicate function --- src/keycloak/keycloak_admin.py | 12 ------------ tests/test_keycloak_admin.py | 10 ---------- 2 files changed, 22 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 12350a89..66c1fe64 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2844,18 +2844,6 @@ def get_role_client_level_children(self, client_id, role_id): data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE_CHILDREN.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_user_credentials(self, user_id): - """Get the credentials of the given user. - - :param user_id: id of the user. - :type user_id: str - :return: the user credentials (list of CredentialsRepresentation). - :rtype: list - """ - params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CREDENTIALS.format(**params_path)) - return raise_error_from_response(data_raw, KeycloakGetError) - def upload_certificate(self, client_id, certcont): """Upload a new certificate for the client. diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index c4ba3d84..6d62f22c 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1880,16 +1880,6 @@ def test_get_role_client_level_children( assert child["id"] in [x["id"] for x in res] -def test_get_user_credentials(admin: KeycloakAdmin, realm: str, user: str): - """Test get user credentials.""" - admin.realm_name = realm - admin.set_user_password(user, "pwd", temporary=False) - res = admin.get_user_credentials(user) - assert isinstance(res, list) - assert len(res) == 1 - assert res[0]["type"] == "password" - - def test_upload_certificate(admin: KeycloakAdmin, realm: str, client: str, selfsigned_cert: tuple): """Test upload certificate.""" admin.realm_name = realm From 4b95d509e8b14e6afca249b519ecc4ddac06bd34 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 16:37:00 +0200 Subject: [PATCH 227/566] fix: moved imports at the top of the file --- tests/conftest.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 85f6a8f1..1815d392 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,14 @@ import os import uuid - +import ipaddress +from datetime import datetime, timedelta + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID import pytest from keycloak import KeycloakAdmin, KeycloakOpenID @@ -284,15 +291,6 @@ def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_ @pytest.fixture def selfsigned_cert(): """Generate self signed certificate for a hostname, and optional IP addresses.""" - import ipaddress - from datetime import datetime, timedelta - - from cryptography import x509 - from cryptography.hazmat.backends import default_backend - from cryptography.hazmat.primitives import hashes, serialization - from cryptography.hazmat.primitives.asymmetric import rsa - from cryptography.x509.oid import NameOID - hostname = "testcert" ip_addresses = None key = None From 067673f81b2748a55ac02e1739815ba3a80fa76e Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 17:11:04 +0200 Subject: [PATCH 228/566] fix: now get_required_action_by_alias now returns None if action does not exist --- src/keycloak/keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 66c1fe64..7d16ad54 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -2878,8 +2878,8 @@ def get_required_action_by_alias(self, action_alias): actions = self.get_required_actions() for a in actions: if a["alias"] == action_alias: - break - return a + return a + return None def get_required_actions(self): """Get the required actions for the realms. From 83d6cf36f637aeb613656b1897dc73f38a76d044 Mon Sep 17 00:00:00 2001 From: Luca Paganin Date: Mon, 18 Jul 2022 21:41:35 +0200 Subject: [PATCH 229/566] fix: linting --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1815d392..39bd5846 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,16 +1,16 @@ """Fixtures for tests.""" +import ipaddress import os import uuid -import ipaddress from datetime import datetime, timedelta +import pytest from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID -import pytest from keycloak import KeycloakAdmin, KeycloakOpenID From e6c4b2810860efca1f32e2a20afc3a4ed350703d Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 19 Jul 2022 17:43:48 +0200 Subject: [PATCH 230/566] fix: removed whitespace from urls --- src/keycloak/urls_patterns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 10bd99c3..3f4151e1 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -34,7 +34,7 @@ URL_ENTITLEMENT = "realms/{realm-name}/authz/entitlement/{resource-server-id}" URL_AUTH = ( "{authorization-endpoint}?client_id={client-id}&response_type=code&redirect_uri={redirect-uri}" - "&scope={scope}&state={state} " + "&scope={scope}&state={state}" ) # ADMIN URLS From 905e0caa038ed77b3bb8ed9a442ded435c3fb7c4 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 19 Jul 2022 17:44:09 +0200 Subject: [PATCH 231/566] test: improved the tests on urls, back to 100% --- tests/test_keycloak_admin.py | 1 + tests/test_keycloak_openid.py | 2 +- tests/test_urls_patterns.py | 12 ++++++++++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 6d62f22c..2d34a18c 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1844,6 +1844,7 @@ def test_get_required_action_by_alias(admin: KeycloakAdmin, realm: str): ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") assert ra in ractions assert ra["alias"] == "UPDATE_PASSWORD" + assert admin.get_required_action_by_alias("does-not-exist") is None def test_update_required_action(admin: KeycloakAdmin, realm: str): diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 8eccb0f6..6bee648d 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -106,7 +106,7 @@ def test_auth_url(env, oid: KeycloakOpenID): res == f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}/realms/{oid.realm_name}" + f"/protocol/openid-connect/auth?client_id={oid.client_id}&response_type=code" - + "&redirect_uri=http://test.test/*&scope=email&state= " + + "&redirect_uri=http://test.test/*&scope=email&state=" ) diff --git a/tests/test_urls_patterns.py b/tests/test_urls_patterns.py index 5fae847e..5c412b26 100644 --- a/tests/test_urls_patterns.py +++ b/tests/test_urls_patterns.py @@ -1,5 +1,5 @@ """Test URL patterns.""" - +import inspect from keycloak import urls_patterns @@ -15,7 +15,12 @@ def test_correctness_of_patterns(): # Test that the patterns have unique names seen_urls = list() - for url in urls: + urls_from_src = [ + x.split("=")[0].strip() + for x in inspect.getsource(urls_patterns).splitlines() + if x.startswith("URL_") + ] + for url in urls_from_src: assert url not in seen_urls, f"The url pattern {url} is present twice." seen_urls.append(url) @@ -24,4 +29,7 @@ def test_correctness_of_patterns(): for url in urls: url_value = urls_patterns.__dict__[url] assert url_value not in seen_url_values, f"The url {url} has a duplicate value {url_value}" + assert ( + url_value == url_value.strip() + ), f"The url {url} with value '{url_value}' has whitespace values" seen_url_values.append(url_value) From 0a5e39d8a5d69fe4cfaeda8d353a77985eb1b729 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 19 Jul 2022 17:45:08 +0200 Subject: [PATCH 232/566] chore: updated dependencies --- poetry.lock | 263 +++++----------------------------------------------- 1 file changed, 25 insertions(+), 238 deletions(-) diff --git a/poetry.lock b/poetry.lock index e65379e8..c636fa29 100644 --- a/poetry.lock +++ b/poetry.lock @@ -222,7 +222,7 @@ python-versions = ">=3.6" [[package]] name = "distlib" -version = "0.3.4" +version = "0.3.5" description = "Distribution utilities" category = "dev" optional = false @@ -1050,13 +1050,8 @@ argcomplete = [ {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, ] -astroid = [ - {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, - {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, -] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] +astroid = [] +atomicwrites = [] attrs = [ {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, @@ -1065,109 +1060,17 @@ babel = [ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] -black = [ - {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, - {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, - {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, - {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, - {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, - {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, - {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, - {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, - {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, - {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, - {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, - {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, - {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, - {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, - {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, - {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, - {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, - {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, - {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, -] +black = [] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] +cffi = [] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, -] +charset-normalizer = [] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -1176,97 +1079,23 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] -commitizen = [ - {file = "commitizen-2.28.0-py3-none-any.whl", hash = "sha256:d222f68da12a3ebcaf85c270f19eec7caacbe904349f1823deca6b5e7c2fc0f5"}, - {file = "commitizen-2.28.0.tar.gz", hash = "sha256:8510b67e4c45131ef75114aeca5fe30b4f973b2b943457cf1667177af296192e"}, -] +commitizen = [] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] -coverage = [ - {file = "coverage-6.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a9032f9b7d38bdf882ac9f66ebde3afb8145f0d4c24b2e600bc4c6304aafb87e"}, - {file = "coverage-6.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0524adb49c716ca763dbc1d27bedce36b14f33e6b8af6dba56886476b42957c"}, - {file = "coverage-6.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4548be38a1c810d79e097a38107b6bf2ff42151900e47d49635be69943763d8"}, - {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f23876b018dfa5d3e98e96f5644b109090f16a4acb22064e0f06933663005d39"}, - {file = "coverage-6.4.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fe75dcfcb889b6800f072f2af5a331342d63d0c1b3d2bf0f7b4f6c353e8c9c0"}, - {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2f8553878a24b00d5ab04b7a92a2af50409247ca5c4b7a2bf4eabe94ed20d3ee"}, - {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d774d9e97007b018a651eadc1b3970ed20237395527e22cbeb743d8e73e0563d"}, - {file = "coverage-6.4.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d56f105592188ce7a797b2bd94b4a8cb2e36d5d9b0d8a1d2060ff2a71e6b9bbc"}, - {file = "coverage-6.4.2-cp310-cp310-win32.whl", hash = "sha256:d230d333b0be8042ac34808ad722eabba30036232e7a6fb3e317c49f61c93386"}, - {file = "coverage-6.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:5ef42e1db047ca42827a85e34abe973971c635f83aed49611b7f3ab49d0130f0"}, - {file = "coverage-6.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:25b7ec944f114f70803d6529394b64f8749e93cbfac0fe6c5ea1b7e6c14e8a46"}, - {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb00521ab4f99fdce2d5c05a91bddc0280f0afaee0e0a00425e28e209d4af07"}, - {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dff52b3e7f76ada36f82124703f4953186d9029d00d6287f17c68a75e2e6039"}, - {file = "coverage-6.4.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:147605e1702d996279bb3cc3b164f408698850011210d133a2cb96a73a2f7996"}, - {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:422fa44070b42fef9fb8dabd5af03861708cdd6deb69463adc2130b7bf81332f"}, - {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8af6c26ba8df6338e57bedbf916d76bdae6308e57fc8f14397f03b5da8622b4e"}, - {file = "coverage-6.4.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:5336e0352c0b12c7e72727d50ff02557005f79a0b8dcad9219c7c4940a930083"}, - {file = "coverage-6.4.2-cp37-cp37m-win32.whl", hash = "sha256:0f211df2cba951ffcae210ee00e54921ab42e2b64e0bf2c0befc977377fb09b7"}, - {file = "coverage-6.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:a13772c19619118903d65a91f1d5fea84be494d12fd406d06c849b00d31bf120"}, - {file = "coverage-6.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f7bd0ffbcd03dc39490a1f40b2669cc414fae0c4e16b77bb26806a4d0b7d1452"}, - {file = "coverage-6.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0895ea6e6f7f9939166cc835df8fa4599e2d9b759b02d1521b574e13b859ac32"}, - {file = "coverage-6.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e7ced84a11c10160c0697a6cc0b214a5d7ab21dfec1cd46e89fbf77cc66fae"}, - {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80db4a47a199c4563d4a25919ff29c97c87569130375beca3483b41ad5f698e8"}, - {file = "coverage-6.4.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3def6791adf580d66f025223078dc84c64696a26f174131059ce8e91452584e1"}, - {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4f89d8e03c8a3757aae65570d14033e8edf192ee9298303db15955cadcff0c63"}, - {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d0b48aff8e9720bdec315d67723f0babd936a7211dc5df453ddf76f89c59933"}, - {file = "coverage-6.4.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2b20286c2b726f94e766e86a3fddb7b7e37af5d0c635bdfa7e4399bc523563de"}, - {file = "coverage-6.4.2-cp38-cp38-win32.whl", hash = "sha256:d714af0bdba67739598849c9f18efdcc5a0412f4993914a0ec5ce0f1e864d783"}, - {file = "coverage-6.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:5f65e5d3ff2d895dab76b1faca4586b970a99b5d4b24e9aafffc0ce94a6022d6"}, - {file = "coverage-6.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a697977157adc052284a7160569b36a8bbec09db3c3220642e6323b47cec090f"}, - {file = "coverage-6.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c77943ef768276b61c96a3eb854eba55633c7a3fddf0a79f82805f232326d33f"}, - {file = "coverage-6.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54d8d0e073a7f238f0666d3c7c0d37469b2aa43311e4024c925ee14f5d5a1cbe"}, - {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22325010d8824594820d6ce84fa830838f581a7fd86a9235f0d2ed6deb61e29"}, - {file = "coverage-6.4.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24b04d305ea172ccb21bee5bacd559383cba2c6fcdef85b7701cf2de4188aa55"}, - {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:866ebf42b4c5dbafd64455b0a1cd5aa7b4837a894809413b930026c91e18090b"}, - {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:e36750fbbc422c1c46c9d13b937ab437138b998fe74a635ec88989afb57a3978"}, - {file = "coverage-6.4.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:79419370d6a637cb18553ecb25228893966bd7935a9120fa454e7076f13b627c"}, - {file = "coverage-6.4.2-cp39-cp39-win32.whl", hash = "sha256:b5e28db9199dd3833cc8a07fa6cf429a01227b5d429facb56eccd765050c26cd"}, - {file = "coverage-6.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:edfdabe7aa4f97ed2b9dd5dde52d2bb29cb466993bb9d612ddd10d0085a683cf"}, - {file = "coverage-6.4.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:e2618cb2cf5a7cc8d698306e42ebcacd02fb7ef8cfc18485c59394152c70be97"}, - {file = "coverage-6.4.2.tar.gz", hash = "sha256:6c3ccfe89c36f3e5b9837b9ee507472310164f352c9fe332120b764c9d60adbe"}, -] -cryptography = [ - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, - {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, - {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, - {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, -] +coverage = [] +cryptography = [] decli = [ {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, ] -distlib = [ - {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, - {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, -] +distlib = [] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, ] -ecdsa = [ - {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, - {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, -] +ecdsa = [] filelock = [ {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, @@ -1275,10 +1104,7 @@ flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] -flake8-docstrings = [ - {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, - {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, -] +flake8-docstrings = [] identify = [ {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, @@ -1287,10 +1113,7 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -imagesize = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] +imagesize = [] importlib-metadata = [ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, @@ -1428,14 +1251,8 @@ pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] -pre-commit = [ - {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, - {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, -] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, - {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, -] +pre-commit = [] +prompt-toolkit = [] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1459,14 +1276,8 @@ pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, -] +pycparser = [] +pydocstyle = [] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, @@ -1542,14 +1353,8 @@ recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -requests-toolbelt = [ - {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, - {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, -] +requests = [] +requests-toolbelt = [] rsa = [ {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, @@ -1609,14 +1414,8 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tomlkit = [ - {file = "tomlkit-0.11.1-py3-none-any.whl", hash = "sha256:1c5bebdf19d5051e2e1de6cf70adfc5948d47221f097fcff7a3ffc91e953eaf5"}, - {file = "tomlkit-0.11.1.tar.gz", hash = "sha256:61901f81ff4017951119cd0d1ed9b7af31c821d6845c8c477587bbdcd5e5854e"}, -] -tox = [ - {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, - {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, -] +tomlkit = [] +tox = [] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, @@ -1643,22 +1442,13 @@ typed-ast = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] -typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, -] +typing-extensions = [] unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] -urllib3 = [ - {file = "urllib3-1.26.10-py2.py3-none-any.whl", hash = "sha256:8298d6d56d39be0e3bc13c1c97d133f9b45d797169a0e11cdd0e0489d786f7ec"}, - {file = "urllib3-1.26.10.tar.gz", hash = "sha256:879ba4d1e89654d9769ce13121e0f94310ea32e8d2f8cf587b77c08bbcdb30d6"}, -] -virtualenv = [ - {file = "virtualenv-20.15.1-py2.py3-none-any.whl", hash = "sha256:b30aefac647e86af6d82bfc944c556f8f1a9c90427b2fb4e3bfbf338cb82becf"}, - {file = "virtualenv-20.15.1.tar.gz", hash = "sha256:288171134a2ff3bfb1a2f54f119e77cd1b81c29fc1265a2356f3e8d14c7d58c4"}, -] +urllib3 = [] +virtualenv = [] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, @@ -1729,7 +1519,4 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, -] +zipp = [] From 37ef5e9f96c54351a5ef247c2a4ce87ff1f861c6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 19 Jul 2022 17:51:17 +0200 Subject: [PATCH 233/566] refactor: applied linting --- tests/test_urls_patterns.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_urls_patterns.py b/tests/test_urls_patterns.py index 5c412b26..91aaaad3 100644 --- a/tests/test_urls_patterns.py +++ b/tests/test_urls_patterns.py @@ -1,5 +1,6 @@ """Test URL patterns.""" import inspect + from keycloak import urls_patterns From 15fbe3683c5c119c02e664c2359a3d65bbe09ba4 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 19 Jul 2022 21:53:10 +0200 Subject: [PATCH 234/566] docs: added changelog creation with cicd --- .github/workflows/lint.yaml | 4 +- .github/workflows/publish.yaml | 10 ++ CHANGELOG.md | 288 +++++++++++++++++++++++++++++++-- docs/source/changelog.rst | 2 + docs/source/index.rst | 1 + tox.ini | 8 +- 6 files changed, 298 insertions(+), 15 deletions(-) create mode 100644 docs/source/changelog.rst diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 0f5b1464..61534ac6 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -77,7 +77,9 @@ jobs: build: runs-on: ubuntu-latest - needs: test + needs: + - test + - check-docs steps: - uses: actions/checkout@v3 - name: Set up Python 3.10 diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 693f67bd..5c5f97c3 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -10,6 +10,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + with: + fetch-depth: '0' - name: Set up Python 3.10 uses: actions/setup-python@v3 with: @@ -31,3 +33,11 @@ jobs: TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} run: | twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* + - name: Run changelog + run: | + tox -e changelog + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "docs: changelog update" + branch: master + file_pattern: CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 4916c273..251c6d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,49 +1,311 @@ # Changelog -All notable changes to this project will be documented in this file. +## v2.1.1 (2022-07-19) -## [0.5.0] - 2017-08-21 +### Refactor + +- applied linting + +### Fix + +- removed whitespace from urls + +## v2.1.0 (2022-07-18) + +### Feat + +- add functions covering some missing REST API calls +- add unit tests +- add docstrings +- add functions covering some missing REST API calls + +### Fix + +- linting +- now get_required_action_by_alias now returns None if action does not exist +- moved imports at the top of the file +- remove duplicate function +- applied tox -e docs +- applied flake linting checks +- applied tox linting check + +## v2.0.0 (2022-07-17) + +### Fix + +- check client existence based on clientId +- check client existence based on clientId + +### BREAKING CHANGE + +- Renamed parameter client_name to client_id in get_client_id method + +## v1.9.1 (2022-07-13) + +### Fix + +- turn get_name into a method, use setters in connection manager + +### Refactor + +- no need to try if the type check is performed + +## v1.9.0 (2022-07-13) + +### Refactor + +- merge master branch into local + +## v1.8.1 (2022-07-13) + +### Fix + +- Support the auth_url method called with scope & state params now +- Support the auth_url method called with scope & state params now +- raise correct exceptions + +### Feat + +- added flake8-docstrings and upgraded dependencies +- use poetry for package management + +### Refactor + +- slight restructure of the base fixtures + +## v1.8.0 (2022-06-22) + +### Feat + +- Ability to set custom timeout for KeycloakOpenId and KeycloakAdmin +- Ability to set custom timeout for KCOpenId and KCAdmin + +## v1.7.0 (2022-06-16) + +### Feat + +- Allow fetching existing policies before calling create_client_authz_client_policy() + +## v1.6.0 (2022-06-13) + +### Feat + +- support token exchange config via admin API + +## v1.5.0 (2022-06-03) + +### Feat + +- Add update_idp +- Add update_idp + +## v1.4.0 (2022-06-02) + +### Feat + +- Add update_mapper_in_idp +- Add update_mapper_in_idp + +## v1.3.0 (2022-05-31) + +## v1.2.0 (2022-05-31) + +### Feat + +- Add get_idp_mappers, fix #329 +- Support Token Exchange. Fixes #305 + +## v1.1.1 (2022-05-27) + +### Fix + +- fixed bugs in events methods +- fixed components bugs +- use param for update client mapper + +## v1.1.0 (2022-05-26) + +### Feat + +- added new methods for client scopes + +## v1.0.1 (2022-05-25) + +### Fix + +- allow query parameters for users count +- allow query parameters for users count + +## v1.0.0 (2022-05-25) + +### Fix + +- correct spelling of public API method + +### BREAKING CHANGE + +- Renames `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` + +## v0.29.1 (2022-05-24) + +### Fix + +- allow client_credentials token if username and password not spec… +- allow client_credentials token if username and password not specified + +## v0.29.0 (2022-05-23) + +### Fix + +- added fixes based on feedback + +## v0.28.3 (2022-05-23) + +### Fix + +- import classes in the base module +- import classes in the base module + +### Feat + +- added UMA-permission request functionality + +## v0.28.2 (2022-05-19) + +### Fix + +- escape when get role fails + +## v0.28.1 (2022-05-19) + +### Fix + +- Add missing keycloak.authorization package +- Add missing keycloak.authorization package + +## v0.28.0 (2022-05-19) + +## v (2022-05-19) + +### Feat + +- added authenticator providers getters +- fixed admin client to pass the tests +- initial setup of CICD and linting + +### Refactor + +- isort conf.py +- Merge branch 'master' into feature/cicd + +### Fix + +- full tox fix ready +- raise correct errors + +## v0.27.1 (2022-05-18) + +### Fix + +- **release**: version bumps for hotfix release + +## v0.27.0 (2022-02-16) + +### Fix + +- handle refresh_token error "Session not active" + +## v0.26.1 (2021-08-30) + +### Feat + +- add KeycloakAdmin.set_events +- add KeycloakAdmin.set_events + +## v0.25.0 (2021-05-05) + +## v0.24.0 (2020-12-18) + +## 0.23.0 (2020-11-19) + +## v0.22.0 (2020-08-16) + +## v0.21.0 (2020-06-30) + +### Feat + +- add components + +## v0.20.0 (2020-04-11) + +## v0.19.0 (2020-02-18) + +## v0.18.0 (2019-12-10) + +## v0.17.6 (2019-10-10) + +## v0.5.0 (2017-08-21) + +### Feat - Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, entitlement, instropect) -## [0.6.0] - 2017-08-23 +## v0.6.0 (2017-08-23) + +### Feat - Added load authorization settings -## [0.7.0] - 2017-08-23 +## v0.7.0 (2017-08-23) + +### Feat - Added polices -## [0.8.0] - 2017-08-23 +## v0.8.0 (2017-08-23) + +### Feat - Added permissions -## [0.9.0] - 2017-09-05 +## v0.9.0 (2017-09-05) + +### Feat - Added functions for Admin Keycloak API -## [0.10.0] - 2017-10-23 +## v0.10.0 (2017-10-23) + +### Feat - Updated libraries versions - Updated Docs -## [0.11.0] - 2017-12-12 +## v0.11.0 (2017-12-12) + +### Feat - Changed Instropect RPT -## [0.12.0] - 2018-01-25 +## v0.12.0 (2018-01-25) + +### Feat - Add groups functions - Add Admin Tasks for user and client role management - Function to trigger user sync from provider -## [0.12.1] - 2018-08-04 +## v0.12.1 (2018-08-04) + +### Feat - Add get_idps - Rework group functions - ## [master] +## master + +### Feat - * Renamed `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` - * Add `KeycloakOpenID.token_exchange` to support Token Exchange +- Renamed `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` +- Add `KeycloakOpenID.token_exchange` to support Token Exchange diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst new file mode 100644 index 00000000..102c1132 --- /dev/null +++ b/docs/source/changelog.rst @@ -0,0 +1,2 @@ +.. mdinclude:: ../../CHANGELOG.md + \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 6dff08d0..ecf88ea0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -11,4 +11,5 @@ :caption: Contents: readme + changelog reference/keycloak/index diff --git a/tox.ini b/tox.ini index 219a4d55..16768bba 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ requires = tox-poetry poetry -envlist = check, apply-check, docs, tests, build +envlist = check, apply-check, docs, tests, build, changelog [testenv] whitelist_externals = @@ -40,6 +40,12 @@ commands = poetry build --format sdist poetry build --format wheel +[testenv:changelog] +setenv = file|tox.env +passenv = CONTAINER_HOST +commands = + cz changelog --incremental + [flake8] max-line-length = 99 docstring-convention = all From 2bf150f7c11ca8fe77cf5c2df08437c83a3c3f7e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sat, 30 Jul 2022 18:05:07 +0200 Subject: [PATCH 235/566] style: start of more checks --- docs/source/conf.py | 1 + poetry.lock | 38 +++-- pyproject.toml | 5 + src/keycloak/authorization/__init__.py | 8 +- src/keycloak/authorization/permission.py | 69 ++++++-- src/keycloak/authorization/policy.py | 78 +++++++-- src/keycloak/authorization/role.py | 27 +++- src/keycloak/connection.py | 115 ++++++++++---- src/keycloak/exceptions.py | 2 +- src/keycloak/keycloak_admin.py | 194 ++++++++++++++++++++--- tests/test_keycloak_admin.py | 2 +- tox.ini | 5 + 12 files changed, 453 insertions(+), 91 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9a5d438b..c6bcb603 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -86,6 +86,7 @@ # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +suppress_warnings = ["ref.python"] add_function_parentheses = False add_module_names = True diff --git a/poetry.lock b/poetry.lock index c636fa29..9fb39529 100644 --- a/poetry.lock +++ b/poetry.lock @@ -140,6 +140,18 @@ python-versions = ">=3.7" colorama = {version = "*", markers = "platform_system == \"Windows\""} importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +[[package]] +name = "codespell" +version = "2.1.0" +description = "Codespell" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency"] +hard-encoding-detection = ["chardet"] + [[package]] name = "colorama" version = "0.4.5" @@ -212,6 +224,14 @@ sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +[[package]] +name = "darglint" +version = "1.8.1" +description = "A utility for ensuring Google-style docstrings stay up to date with the source code." +category = "dev" +optional = false +python-versions = ">=3.6,<4.0" + [[package]] name = "decli" version = "0.5.2" @@ -291,7 +311,7 @@ pydocstyle = ">=2.1" [[package]] name = "identify" -version = "2.5.1" +version = "2.5.2" description = "File identification library for Python" category = "dev" optional = false @@ -731,7 +751,7 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "rsa" -version = "4.8" +version = "4.9" description = "Pure-Python RSA implementation" category = "main" optional = false @@ -1039,7 +1059,7 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "b6851001fb6b3e331a39e6bab1fa2ed99fdc555e9683137c20c9c593c0e1c040" +content-hash = "5d740e81a3604cb20ca85945c2fc134f6373f599315992afb50284f1f993c1d0" [metadata.files] alabaster = [ @@ -1075,6 +1095,7 @@ click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] +codespell = [] colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, @@ -1086,6 +1107,7 @@ commonmark = [ ] coverage = [] cryptography = [] +darglint = [] decli = [ {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, @@ -1105,10 +1127,7 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-docstrings = [] -identify = [ - {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, - {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, -] +identify = [] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, @@ -1355,10 +1374,7 @@ recommonmark = [ ] requests = [] requests-toolbelt = [] -rsa = [ - {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, - {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, -] +rsa = [] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, diff --git a/pyproject.toml b/pyproject.toml index 8adfa586..7874ca48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,8 @@ flake8 = "^3.5.0" flake8-docstrings = "^1.6.0" commitizen = "^2.28.0" cryptography = "^37.0.4" +codespell = "^2.1.0" +darglint = "^1.8.1" [tool.poetry.extras] docs = [ @@ -80,3 +82,6 @@ line-length = 99 [tool.isort] line_length = 99 profile = "black" + +[tool.darglint] +enable = "DAR104" \ No newline at end of file diff --git a/src/keycloak/authorization/__init__.py b/src/keycloak/authorization/__init__.py index fddd5513..feebe0b7 100644 --- a/src/keycloak/authorization/__init__.py +++ b/src/keycloak/authorization/__init__.py @@ -44,7 +44,11 @@ def __init__(self): @property def policies(self): - """Get policies.""" + """Get policies. + + :returns: Policies + :rtype: dict + """ return self._policies @policies.setter @@ -55,7 +59,7 @@ def load_config(self, data): """Load policies, roles and permissions (scope/resources). :param data: keycloak authorization data (dict) - :returns: None + :type data: dict """ for pol in data["policies"]: if pol["type"] == "role": diff --git a/src/keycloak/authorization/permission.py b/src/keycloak/authorization/permission.py index 667f8c3c..d1b606f5 100644 --- a/src/keycloak/authorization/permission.py +++ b/src/keycloak/authorization/permission.py @@ -45,10 +45,29 @@ class Permission: https://keycloak.gitbooks.io/documentation/authorization_services/topics/permission/overview.html + :param name: Name + :type name: str + :param type: Type + :type type: str + :param logic: Logic + :type logic: str + :param decision_strategy: Decision strategy + :type decision_strategy: str + """ def __init__(self, name, type, logic, decision_strategy): - """Init method.""" + """Init method. + + :param name: Name + :type name: str + :param type: Type + :type type: str + :param logic: Logic + :type logic: str + :param decision_strategy: Decision strategy + :type decision_strategy: str + """ self.name = name self.type = type self.logic = logic @@ -57,16 +76,28 @@ def __init__(self, name, type, logic, decision_strategy): self.scopes = [] def __repr__(self): - """Repr method.""" + """Repr method. + + :returns: Class representation + :rtype: str + """ return "" % (self.name, self.type) def __str__(self): - """Str method.""" + """Str method. + + :returns: Class string representation + :rtype: str + """ return "Permission: %s (%s)" % (self.name, self.type) @property def name(self): - """Get name.""" + """Get name. + + :returns: name + :rtype: str + """ return self._name @name.setter @@ -75,7 +106,11 @@ def name(self, value): @property def type(self): - """Get type.""" + """Get type. + + :returns: type + :rtype: str + """ return self._type @type.setter @@ -84,7 +119,11 @@ def type(self, value): @property def logic(self): - """Get logic.""" + """Get logic. + + :returns: Logic + :rtype: str + """ return self._logic @logic.setter @@ -93,7 +132,11 @@ def logic(self, value): @property def decision_strategy(self): - """Get decision strategy.""" + """Get decision strategy. + + :returns: Decision strategy + :rtype: str + """ return self._decision_strategy @decision_strategy.setter @@ -102,7 +145,11 @@ def decision_strategy(self, value): @property def resources(self): - """Get resources.""" + """Get resources. + + :returns: Resources + :rtype: list + """ return self._resources @resources.setter @@ -111,7 +158,11 @@ def resources(self, value): @property def scopes(self): - """Get scopes.""" + """Get scopes. + + :returns: Scopes + :rtype: list + """ return self._scopes @scopes.setter diff --git a/src/keycloak/authorization/policy.py b/src/keycloak/authorization/policy.py index 7e03db0b..fdf482dd 100644 --- a/src/keycloak/authorization/policy.py +++ b/src/keycloak/authorization/policy.py @@ -39,10 +39,29 @@ class Policy: https://keycloak.gitbooks.io/documentation/authorization_services/topics/policy/overview.html + :param name: Name + :type name: str + :param type: Type + :type type: str + :param logic: Logic + :type logic: str + :param decision_strategy: Decision strategy + :type decision_strategy: str + """ def __init__(self, name, type, logic, decision_strategy): - """Init method.""" + """Init method. + + :param name: Name + :type name: str + :param type: Type + :type type: str + :param logic: Logic + :type logic: str + :param decision_strategy: Decision strategy + :type decision_strategy: str + """ self.name = name self.type = type self.logic = logic @@ -51,16 +70,28 @@ def __init__(self, name, type, logic, decision_strategy): self.permissions = [] def __repr__(self): - """Repr method.""" + """Repr method. + + :returns: Class representation + :rtype: str + """ return "" % (self.name, self.type) def __str__(self): - """Str method.""" + """Str method. + + :returns: Class string representation + :rtype: str + """ return "Policy: %s (%s)" % (self.name, self.type) @property def name(self): - """Get name.""" + """Get name. + + :returns: Name + :rtype: str + """ return self._name @name.setter @@ -69,7 +100,11 @@ def name(self, value): @property def type(self): - """Get type.""" + """Get type. + + :returns: Type + :rtype: str + """ return self._type @type.setter @@ -78,7 +113,11 @@ def type(self, value): @property def logic(self): - """Get logic.""" + """Get logic. + + :returns: Logic + :rtype: str + """ return self._logic @logic.setter @@ -87,7 +126,11 @@ def logic(self, value): @property def decision_strategy(self): - """Get decision strategy.""" + """Get decision strategy. + + :returns: Decision strategy + :rtype: str + """ return self._decision_strategy @decision_strategy.setter @@ -96,7 +139,11 @@ def decision_strategy(self, value): @property def roles(self): - """Get roles.""" + """Get roles. + + :returns: Roles + :rtype: list + """ return self._roles @roles.setter @@ -105,7 +152,11 @@ def roles(self, value): @property def permissions(self): - """Get permissions.""" + """Get permissions. + + :returns: Permissions + :rtype: list + """ return self._permissions @permissions.setter @@ -115,8 +166,9 @@ def permissions(self, value): def add_role(self, role): """Add keycloak role in policy. - :param role: keycloak role. - :return: + :param role: Keycloak role + :type role: keycloak.authorization.Role + :raises KeycloakAuthorizationConfigError: In case of misconfigured policy type """ if self.type != "role": raise KeycloakAuthorizationConfigError( @@ -127,7 +179,7 @@ def add_role(self, role): def add_permission(self, permission): """Add keycloak permission in policy. - :param permission: keycloak permission. - :return: + :param permission: Keycloak permission + :type permission: keycloak.authorization.Permission """ self._permissions.append(permission) diff --git a/src/keycloak/authorization/role.py b/src/keycloak/authorization/role.py index 4ee6ec93..3d4c0008 100644 --- a/src/keycloak/authorization/role.py +++ b/src/keycloak/authorization/role.py @@ -31,19 +31,40 @@ class Role: manager, and employee are all typical roles that may exist in an organization. https://keycloak.gitbooks.io/documentation/server_admin/topics/roles.html + + :param name: Name + :type name: str + :param required: Required role indicator + :type required: bool """ def __init__(self, name, required=False): - """Init method.""" + """Init method. + + :param name: Name + :type name: str + :param required: Required role indicator + :type required: bool + """ self.name = name self.required = required def get_name(self): - """Get name.""" + """Get name. + + :returns: Name + :rtype: str + """ return self.name def __eq__(self, other): - """Eq method.""" + """Eq method. + + :param other: The other object + :type other: str + :returns: Equality bool + :rtype: bool | NotImplemented + """ if isinstance(other, str): return self.name == other return NotImplemented diff --git a/src/keycloak/connection.py b/src/keycloak/connection.py index 44978e59..fcdffb40 100644 --- a/src/keycloak/connection.py +++ b/src/keycloak/connection.py @@ -37,15 +37,32 @@ class ConnectionManager(object): """Represents a simple server connection. - :param base_url: (str) The server URL. - :param headers: (dict) The header parameters of the requests to the server. - :param timeout: (int) Timeout to use for requests to the server. - :param verify: (bool) Verify server SSL. - :param proxies: (dict) The proxies servers requests is sent by. + :param base_url: The server URL. + :type base_url: str + :param headers: The header parameters of the requests to the server. + :type headers: dict + :param timeout: Timeout to use for requests to the server. + :type timeout: int + :param verify: Verify server SSL. + :type verify: bool + :param proxies: The proxies servers requests is sent by. + :type proxies: dict """ def __init__(self, base_url, headers={}, timeout=60, verify=True, proxies=None): - """Init method.""" + """Init method. + + :param base_url: The server URL. + :type base_url: str + :param headers: The header parameters of the requests to the server. + :type headers: dict + :param timeout: Timeout to use for requests to the server. + :type timeout: int + :param verify: Verify server SSL. + :type verify: bool + :param proxies: The proxies servers requests is sent by. + :type proxies: dict + """ self.base_url = base_url self.headers = headers self.timeout = timeout @@ -73,7 +90,11 @@ def __del__(self): @property def base_url(self): - """Return base url in use for requests to the server.""" + """Return base url in use for requests to the server. + + :returns: Base URL + :rtype: str + """ return self._base_url @base_url.setter @@ -82,7 +103,11 @@ def base_url(self, value): @property def timeout(self): - """Return timeout in use for request to the server.""" + """Return timeout in use for request to the server. + + :returns: Timeout + :rtype: int + """ return self._timeout @timeout.setter @@ -91,7 +116,11 @@ def timeout(self, value): @property def verify(self): - """Return verify in use for request to the server.""" + """Return verify in use for request to the server. + + :returns: Verify indicator + :rtype: bool + """ return self._verify @verify.setter @@ -100,7 +129,11 @@ def verify(self, value): @property def headers(self): - """Return header request to the server.""" + """Return header request to the server. + + :returns: Request headers + :rtype: dict + """ return self._headers @headers.setter @@ -110,8 +143,10 @@ def headers(self, value): def param_headers(self, key): """Return a specific header parameter. - :param key: (str) Header parameters key. + :param key: Header parameters key. + :type key: str :returns: If the header parameters exist, return its value. + :rtype: str """ return self.headers.get(key) @@ -122,32 +157,41 @@ def clean_headers(self): def exist_param_headers(self, key): """Check if the parameter exists in the header. - :param key: (str) Header parameters key. + :param key: Header parameters key. + :type key: str :returns: If the header parameters exist, return True. + :rtype: bool """ return self.param_headers(key) is not None def add_param_headers(self, key, value): """Add a single parameter inside the header. - :param key: (str) Header parameters key. - :param value: (str) Value to be added. + :param key: Header parameters key. + :type key: str + :param value: Value to be added. + :type value: str """ self.headers[key] = value def del_param_headers(self, key): """Remove a specific parameter. - :param key: (str) Key of the header parameters. + :param key: Key of the header parameters. + :type key: str """ self.headers.pop(key, None) def raw_get(self, path, **kwargs): """Submit get request to the path. - :param path: (str) Path for request. + :param path: Path for request. + :type path: str + :param kwargs: Additional arguments + :type kwargs: dict :returns: Response the request. - :raises: HttpError Can't connect to server. + :rtype: Response + :raises KeycloakConnectionError: HttpError Can't connect to server. """ try: return self._s.get( @@ -163,10 +207,15 @@ def raw_get(self, path, **kwargs): def raw_post(self, path, data, **kwargs): """Submit post request to the path. - :param path: (str) Path for request. - :param data: (dict) Payload for request. + :param path: Path for request. + :type path: str + :param data: Payload for request. + :type data: dict + :param kwargs: Additional arguments + :type kwargs: dict :returns: Response the request. - :raises: HttpError Can't connect to server. + :rtype: Response + :raises KeycloakConnectionError: HttpError Can't connect to server. """ try: return self._s.post( @@ -183,10 +232,15 @@ def raw_post(self, path, data, **kwargs): def raw_put(self, path, data, **kwargs): """Submit put request to the path. - :param path: (str) Path for request. - :param data: (dict) Payload for request. + :param path: Path for request. + :type path: str + :param data: Payload for request. + :type data: dict + :param kwargs: Additional arguments + :type kwargs: dict :returns: Response the request. - :raises: HttpError Can't connect to server. + :rtype: Response + :raises KeycloakConnectionError: HttpError Can't connect to server. """ try: return self._s.put( @@ -200,19 +254,24 @@ def raw_put(self, path, data, **kwargs): except Exception as e: raise KeycloakConnectionError("Can't connect to server (%s)" % e) - def raw_delete(self, path, data={}, **kwargs): + def raw_delete(self, path, data=None, **kwargs): """Submit delete request to the path. - :param path: (str) Path for request. - :param data: (dict) Payload for request. + :param path: Path for request. + :type path: str + :param data: Payload for request. + :type data: dict | None + :param kwargs: Additional arguments + :type kwargs: dict :returns: Response the request. - :raises: HttpError Can't connect to server. + :rtype: Response + :raises KeycloakConnectionError: HttpError Can't connect to server. """ try: return self._s.delete( urljoin(self.base_url, path), params=kwargs, - data=data, + data=data or dict(), headers=self.headers, timeout=self.timeout, verify=self.verify, diff --git a/src/keycloak/exceptions.py b/src/keycloak/exceptions.py index 8eb69bf9..9d947e3a 100644 --- a/src/keycloak/exceptions.py +++ b/src/keycloak/exceptions.py @@ -21,7 +21,7 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -"""Keycloak custom exeptions module.""" +"""Keycloak custom exceptions module.""" import requests diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 7d16ad54..4d3209b3 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -49,19 +49,31 @@ class KeycloakAdmin: """Keycloak Admin client. :param server_url: Keycloak server url + :type server_url: str :param username: admin username + :type username: str :param password: admin password + :type password: str :param totp: Time based OTP + :type totp: str :param realm_name: realm name + :type realm_name: str :param client_id: client id + :type client_id: str :param verify: True if want check connection SSL + :type verify: bool :param client_secret_key: client secret key (optional, required only for access type confidential) + :type client_secret_key: str :param custom_headers: dict of custom header to pass to each HTML request + :type custom_headers: dict :param user_realm_name: The realm name of the user, if different from realm_name + :type user_realm_name: str :param auto_refresh_token: list of methods that allows automatic token refresh. Ex: ['get', 'put', 'post', 'delete'] + :type auto_refresh_token: list :param timeout: connection timeout in seconds + :type timeout: int """ PAGE_SIZE = 100 @@ -95,7 +107,35 @@ def __init__( auto_refresh_token=None, timeout=60, ): - """Init method.""" + """Init method. + + :param server_url: Keycloak server url + :type server_url: str + :param username: admin username + :type username: str + :param password: admin password + :type password: str + :param totp: Time based OTP + :type totp: str + :param realm_name: realm name + :type realm_name: str + :param client_id: client id + :type client_id: str + :param verify: True if want check connection SSL + :type verify: bool + :param client_secret_key: client secret key + (optional, required only for access type confidential) + :type client_secret_key: str + :param custom_headers: dict of custom header to pass to each HTML request + :type custom_headers: dict + :param user_realm_name: The realm name of the user, if different from realm_name + :type user_realm_name: str + :param auto_refresh_token: list of methods that allows automatic token refresh. + Ex: ['get', 'put', 'post', 'delete'] + :type auto_refresh_token: list + :param timeout: connection timeout in seconds + :type timeout: int + """ self.server_url = server_url self.username = username self.password = password @@ -114,7 +154,11 @@ def __init__( @property def server_url(self): - """Get server url.""" + """Get server url. + + :returns: Keycloak server url + :rtype: str + """ return self._server_url @server_url.setter @@ -123,7 +167,11 @@ def server_url(self, value): @property def realm_name(self): - """Get realm name.""" + """Get realm name. + + :returns: Realm name + :rtype: str + """ return self._realm_name @realm_name.setter @@ -132,7 +180,11 @@ def realm_name(self, value): @property def connection(self): - """Get connection.""" + """Get connection. + + :returns: Connection manager + :rtype: ConnectionManager + """ return self._connection @connection.setter @@ -141,7 +193,11 @@ def connection(self, value): @property def client_id(self): - """Get client id.""" + """Get client id. + + :returns: Client id + :rtype: str + """ return self._client_id @client_id.setter @@ -150,7 +206,11 @@ def client_id(self, value): @property def client_secret_key(self): - """Get client secret key.""" + """Get client secret key. + + :returns: Client secret key + :rtype: str + """ return self._client_secret_key @client_secret_key.setter @@ -159,7 +219,11 @@ def client_secret_key(self, value): @property def verify(self): - """Get verify.""" + """Get verify. + + :returns: Verify indicator + :rtype: bool + """ return self._verify @verify.setter @@ -168,7 +232,11 @@ def verify(self, value): @property def username(self): - """Get username.""" + """Get username. + + :returns: Admin username + :rtype: str + """ return self._username @username.setter @@ -177,7 +245,11 @@ def username(self, value): @property def password(self): - """Get password.""" + """Get password. + + :returns: Admin password + :rtype: str + """ return self._password @password.setter @@ -186,7 +258,11 @@ def password(self, value): @property def totp(self): - """Get totp.""" + """Get totp. + + :returns: TOTP + :rtype: str + """ return self._totp @totp.setter @@ -195,7 +271,11 @@ def totp(self, value): @property def token(self): - """Get token.""" + """Get token. + + :returns: Access and refresh token + :rtype: dict + """ return self._token @token.setter @@ -204,12 +284,20 @@ def token(self, value): @property def auto_refresh_token(self): - """Get auto refresh token.""" + """Get auto refresh token. + + :returns: List of methods for automatic token refresh + :rtype: list + """ return self._auto_refresh_token @property def user_realm_name(self): - """Get user realm name.""" + """Get user realm name. + + :returns: User realm name + :rtype: str + """ return self._user_realm_name @user_realm_name.setter @@ -218,7 +306,11 @@ def user_realm_name(self, value): @property def custom_headers(self): - """Get custom headers.""" + """Get custom headers. + + :returns: Custom headers + :rtype: dict + """ return self._custom_headers @custom_headers.setter @@ -247,13 +339,16 @@ def __fetch_all(self, url, query=None): Wrapper function to paginate GET requests. :param url: The url on which the query is executed + :type url: str :param query: Existing query parameters (optional) + :type query: dict :return: Combined results of paginated queries + :rtype: list """ results = [] - # initalize query if it was called with None + # initialize query if it was called with None if not query: query = {} page = 0 @@ -274,8 +369,16 @@ def __fetch_all(self, url, query=None): return results def __fetch_paginated(self, url, query=None): - query = query or {} + """Make a specific paginated request. + :param url: The url on which the query is executed + :type url: str + :param query: Pagination settings + :type query: dict + :returns: Response + :rtype: dict + """ + query = query or {} return raise_error_from_response(self.raw_get(url, **query), KeycloakGetError) def import_realm(self, payload): @@ -287,8 +390,9 @@ def import_realm(self, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation - + :type payload: dict :return: RealmRepresentation + :rtype: dict """ data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) @@ -299,10 +403,13 @@ def export_realm(self, export_clients=False, export_groups_and_role=False): RealmRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_partialexport - :param export-clients: Skip if not want to export realm clients - :param export-groups-and-roles: Skip if not want to export realm groups and roles + :param export_clients: Skip if not want to export realm clients + :type export_clients: bool + :param export_groups_and_role: Skip if not want to export realm groups and roles + :type export_groups_and_role: bool :return: realm configurations JSON + :rtype: dict """ params_path = { "realm-name": self.realm_name, @@ -318,6 +425,7 @@ def get_realms(self): """List all realms in Keycloak deployment. :return: realms list + :rtype: list """ data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALMS) return raise_error_from_response(data_raw, KeycloakGetError) @@ -329,7 +437,9 @@ def get_realm(self, realm_name): https://www.keycloak.org/docs-api/8.0/rest-api/index.html#_realmrepresentation :param realm_name: Realm name (not the realm id) + :type realm_name: str :return: RealmRepresentation + :rtype: dict """ params_path = {"realm-name": realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM.format(**params_path)) @@ -342,8 +452,11 @@ def create_realm(self, payload, skip_exists=False): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param payload: RealmRepresentation + :type payload: dict :param skip_exists: Skip if Realm already exist. - :return: Keycloak server response (RealmRepresentation) + :type skip_exists: bool + :return: Keycloak server response (RealmRepresentation) + :rtype: dict """ data_raw = self.raw_post(urls_patterns.URL_ADMIN_REALMS, data=json.dumps(payload)) return raise_error_from_response( @@ -353,15 +466,18 @@ def create_realm(self, payload, skip_exists=False): def update_realm(self, realm_name, payload): """Update a realm. - This wil only update top level attributes and will ignore any user, + This will only update top level attributes and will ignore any user, role, or client information in the payload. RealmRepresentation: https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmrepresentation :param realm_name: Realm name (not the realm id) + :type realm_name: str :param payload: RealmRepresentation + :type payload: dict :return: Http response + :rtype: dict """ params_path = {"realm-name": realm_name} data_raw = self.raw_put( @@ -373,7 +489,9 @@ def delete_realm(self, realm_name): """Delete a realm. :param realm_name: Realm name (not the realm id) + :type realm_name: str :return: Http response + :rtype: dict """ params_path = {"realm-name": realm_name} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_REALM.format(**params_path)) @@ -388,7 +506,9 @@ def get_users(self, query=None): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param query: Query parameters (optional) + :type query: dict :return: users list + :rtype: list """ query = query or {} params_path = {"realm-name": self.realm_name} @@ -406,6 +526,9 @@ def create_idp(self, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation :param: payload: IdentityProviderRepresentation + :type payload: dict + :returns: Keycloak server response + :rtype: dict """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( @@ -419,8 +542,12 @@ def update_idp(self, idp_alias, payload): IdentityProviderRepresentation https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_identity_providers_resource - :param: alias: alias for IdP to update + :param: idp_alias: alias for IdP to update + :type idp_alias: str :param: payload: The IdentityProviderRepresentation + :type payload: dict + :returns: Keycloak server response + :rtype: dict """ params_path = {"realm-name": self.realm_name, "alias": idp_alias} data_raw = self.raw_put( @@ -435,7 +562,11 @@ def add_mapper_to_idp(self, idp_alias, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityprovidermapperrepresentation :param: idp_alias: alias for Idp to add mapper in + :type idp_alias: str :param: payload: IdentityProviderMapperRepresentation + :type payload: dict + :returns: Keycloak server response + :rtype: dict """ params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} data_raw = self.raw_post( @@ -450,9 +581,13 @@ def update_mapper_in_idp(self, idp_alias, mapper_id, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_update :param: idp_alias: alias for Idp to fetch mappers + :type idp_alias: str :param: mapper_id: Mapper Id to update + :type mapper_id: str :param: payload: IdentityProviderMapperRepresentation + :type payload: dict :return: Http response + :rtype: dict """ params_path = { "realm-name": self.realm_name, @@ -476,7 +611,9 @@ def get_idp_mappers(self, idp_alias): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getmappers :param: idp_alias: alias for Idp to fetch mappers + :type idp_alias: str :return: array IdentityProviderMapperRepresentation + :rtype: list """ params_path = {"realm-name": self.realm_name, "idp-alias": idp_alias} data_raw = self.raw_get(urls_patterns.URL_ADMIN_IDP_MAPPERS.format(**params_path)) @@ -491,6 +628,7 @@ def get_idps(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_identityproviderrepresentation :return: array IdentityProviderRepresentation + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_IDPS.format(**params_path)) @@ -500,6 +638,9 @@ def delete_idp(self, idp_alias): """Delete an ID Provider. :param: idp_alias: idp alias name + :type idp_alias: str + :returns: Keycloak server response + :rtype: dict """ params_path = {"realm-name": self.realm_name, "alias": idp_alias} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_IDP.format(**params_path)) @@ -514,10 +655,13 @@ def create_user(self, payload, exist_ok=False): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param payload: UserRepresentation + :type payload: dict :param exist_ok: If False, raise KeycloakGetError if username already exists. Otherwise, return existing user ID. + :type exist_ok: bool :return: UserRepresentation + :rtype: dict """ params_path = {"realm-name": self.realm_name} @@ -540,8 +684,10 @@ def users_count(self, query=None): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_users_resource :param query: (dict) Query parameters for users count + :type query: dict :return: counter + :rtype: int """ query = query or dict() params_path = {"realm-name": self.realm_name} @@ -557,8 +703,10 @@ def get_user_id(self, username): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation :param username: id in UserRepresentation + :type username: str :return: user_id + :rtype: str """ lower_user_name = username.lower() users = self.get_users(query={"search": lower_user_name}) @@ -2870,7 +3018,7 @@ def upload_certificate(self, client_id, certcont): def get_required_action_by_alias(self, action_alias): """Get a required action by its alias. - :param action_alias: the alias of the requried action. + :param action_alias: the alias of the required action. :type action_alias: str :return: the required action (RequiredActionProviderRepresentation). :rtype: dict diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 2d34a18c..f2599154 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1820,7 +1820,7 @@ def test_auto_refresh(admin: KeycloakAdmin, realm: str): def test_get_required_actions(admin: KeycloakAdmin, realm: str): - """Test requried actions.""" + """Test required actions.""" admin.realm_name = realm ractions = admin.get_required_actions() assert isinstance(ractions, list) diff --git a/tox.ini b/tox.ini index 16768bba..06a08554 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ commands = black --check --diff src/keycloak tests docs isort -c --df src/keycloak tests docs flake8 src/keycloak tests docs + codespell src tests docs [testenv:apply-check] commands = @@ -50,3 +51,7 @@ commands = max-line-length = 99 docstring-convention = all ignore = D203, D213, W503 +docstring_style = sphinx + +[darglint] +enable = DAR104 \ No newline at end of file From f61b42a6ec45330fd239f323fcb294a5d4efd4bc Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sat, 30 Jul 2022 18:30:18 +0200 Subject: [PATCH 236/566] test: fixed tests, updated dependencies --- poetry.lock | 70 +++++++++++++++++------------------- test_keycloak_init.sh | 8 +++-- tests/test_keycloak_admin.py | 4 +-- 3 files changed, 38 insertions(+), 44 deletions(-) diff --git a/poetry.lock b/poetry.lock index c636fa29..8af9cc01 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,17 +44,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "babel" @@ -117,6 +117,14 @@ category = "dev" optional = false python-versions = ">=3.6.1" +[[package]] +name = "chardet" +version = "5.0.0" +description = "Universal encoding detector for Python 3" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "charset-normalizer" version = "2.1.0" @@ -150,7 +158,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "commitizen" -version = "2.28.0" +version = "2.29.2" description = "Python commitizen client tool" category = "dev" optional = false @@ -158,6 +166,7 @@ python-versions = ">=3.6.2,<4.0.0" [package.dependencies] argcomplete = ">=1.12.1,<2.0.0" +chardet = ">=5.0.0,<6.0.0" colorama = ">=0.4.1,<0.5.0" decli = ">=0.5.2,<0.6.0" jinja2 = ">=2.10.3" @@ -291,7 +300,7 @@ pydocstyle = ">=2.1" [[package]] name = "identify" -version = "2.5.1" +version = "2.5.2" description = "File identification library for Python" category = "dev" optional = false @@ -731,7 +740,7 @@ requests = ">=2.0.1,<3.0.0" [[package]] name = "rsa" -version = "4.8" +version = "4.9" description = "Pure-Python RSA implementation" category = "main" optional = false @@ -758,7 +767,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "5.0.2" +version = "5.1.1" description = "Python documentation generator" category = "main" optional = true @@ -768,7 +777,7 @@ python-versions = ">=3.6" alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.19" +docutils = ">=0.14,<0.20" imagesize = "*" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" @@ -785,16 +794,16 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.950)", "docutils-stubs", "types-typed-ast", "types-requests"] +lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "isort", "mypy (>=0.971)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-autoapi" -version = "1.8.4" +version = "1.9.0" description = "Sphinx API documentation generator" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] astroid = ">=2.7" @@ -975,7 +984,7 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.10" +version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -988,22 +997,21 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.15.1" +version = "20.16.2" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" -six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] [[package]] name = "wcwidth" @@ -1052,10 +1060,7 @@ argcomplete = [ ] astroid = [] atomicwrites = [] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] +attrs = [] babel = [ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, @@ -1070,6 +1075,7 @@ cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] +chardet = [] charset-normalizer = [] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, @@ -1105,10 +1111,7 @@ flake8 = [ {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] flake8-docstrings = [] -identify = [ - {file = "identify-2.5.1-py2.py3-none-any.whl", hash = "sha256:0dca2ea3e4381c435ef9c33ba100a78a9b40c0bab11189c7cf121f75815efeaa"}, - {file = "identify-2.5.1.tar.gz", hash = "sha256:3d11b16f3fe19f52039fb7e39c9c884b21cb1b586988114fbe42671f03de3e82"}, -] +identify = [] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, @@ -1355,10 +1358,7 @@ recommonmark = [ ] requests = [] requests-toolbelt = [] -rsa = [ - {file = "rsa-4.8-py3-none-any.whl", hash = "sha256:95c5d300c4e879ee69708c428ba566c59478fd653cc3a22243eeb8ed846950bb"}, - {file = "rsa-4.8.tar.gz", hash = "sha256:5c6bd9dc7a543b7fe4304a631f8a8a3b674e2bbfc49c2ae96200cdbe55df6b17"}, -] +rsa = [] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1367,14 +1367,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -sphinx = [ - {file = "Sphinx-5.0.2-py3-none-any.whl", hash = "sha256:d3e57663eed1d7c5c50895d191fdeda0b54ded6f44d5621b50709466c338d1e8"}, - {file = "Sphinx-5.0.2.tar.gz", hash = "sha256:b18e978ea7565720f26019c702cd85c84376e948370f1cd43d60265010e1c7b0"}, -] -sphinx-autoapi = [ - {file = "sphinx-autoapi-1.8.4.tar.gz", hash = "sha256:8c4ec5fbedc1e6e8f4692bcc4fcd1abcfb9e8dfca8a4ded60ad811a743c22ccc"}, - {file = "sphinx_autoapi-1.8.4-py2.py3-none-any.whl", hash = "sha256:007bf9e24cd2aa0ac0561f67e8bcd6a6e2e8911ef4b4fd54aaba799d8832c8d0"}, -] +sphinx = [] +sphinx-autoapi = [] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index e9c6823c..5dc5e534 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -4,8 +4,11 @@ CMD_ARGS=$1 KEYCLOAK_DOCKER_IMAGE="quay.io/keycloak/keycloak:latest" function keycloak_stop() { - docker stop unittest_keycloak &> /dev/null - docker rm unittest_keycloak &> /dev/null + if [ "$(docker ps -q -f name=unittest_keycloak)" ]; then + docker logs unittest_keycloak > keycloak_test_logs.txt + docker stop unittest_keycloak &> /dev/null + docker rm unittest_keycloak &> /dev/null + fi } function keycloak_start() { @@ -29,6 +32,5 @@ keycloak_start eval ${CMD_ARGS} RETURN_VALUE=$? -docker logs unittest_keycloak > keycloak_test_logs.txt exit ${RETURN_VALUE} diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 2d34a18c..61685af2 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -759,7 +759,6 @@ def test_clients(admin: KeycloakAdmin, realm: str): res = admin.get_client_authz_policies(client_id=auth_client_id) assert len(res) == 1, res assert res[0]["name"] == "Default Policy" - assert len(admin.get_client_authz_policies(client_id=client_id)) == 1 with pytest.raises(KeycloakGetError) as err: admin.get_client_authz_policies(client_id="does-not-exist") @@ -789,7 +788,6 @@ def test_clients(admin: KeycloakAdmin, realm: str): res = admin.get_client_authz_permissions(client_id=auth_client_id) assert len(res) == 1, res assert res[0]["name"] == "Default Permission" - assert len(admin.get_client_authz_permissions(client_id=client_id)) == 1 with pytest.raises(KeycloakGetError) as err: admin.get_client_authz_permissions(client_id="does-not-exist") @@ -1478,7 +1476,7 @@ def test_authentication_configs(admin: KeycloakAdmin, realm: str): # Test list of auth providers res = admin.get_authenticator_providers() - assert len(res) == 39 + assert len(res) == 38 res = admin.get_authenticator_provider_config_description(provider_id="auth-cookie") assert res == { From c98189ca6951f12f1023ed3370c9aaa0d81e4aa4 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 4 Aug 2022 14:43:56 +0000 Subject: [PATCH 237/566] docs: more docstring linting --- src/keycloak/exceptions.py | 32 +- src/keycloak/keycloak_admin.py | 550 ++++++++++++++++++++++++++++----- 2 files changed, 499 insertions(+), 83 deletions(-) diff --git a/src/keycloak/exceptions.py b/src/keycloak/exceptions.py index 9d947e3a..fe46bf41 100644 --- a/src/keycloak/exceptions.py +++ b/src/keycloak/exceptions.py @@ -36,7 +36,15 @@ class KeycloakError(Exception): """ def __init__(self, error_message="", response_code=None, response_body=None): - """Init method.""" + """Init method. + + :param error_message: The error message + :type error_message: str + :param response_code: The code of the response + :type response_code: int + :param response_body: Body of the response + :type response_body: bytes + """ Exception.__init__(self, error_message) self.response_code = response_code @@ -44,7 +52,11 @@ def __init__(self, error_message="", response_code=None, response_body=None): self.error_message = error_message def __str__(self): - """Str method.""" + """Str method. + + :returns: String representation of the object + :rtype: str + """ if self.response_code is not None: return "{0}: {1}".format(self.response_code, self.error_message) else: @@ -136,7 +148,21 @@ class PermissionDefinitionError(Exception): def raise_error_from_response(response, error, expected_codes=None, skip_exists=False): - """Raise an exception for the response.""" + """Raise an exception for the response. + + :param response: The response object + :type response: Response + :param error: Error object to raise + :type error: dict or Exception + :param expected_codes: Set of expected codes, which should not raise the exception + :type expected_codes: Sequence[int] + :param skip_exists: Indicates whether the response on already existing object should be ignored + :type skip_exists: bool + + :returns: Content of the response message + :type: bytes or dict + :raises KeycloakError: In case of unexpected status codes + """ # noqa: DAR401,DAR402 if expected_codes is None: expected_codes = [200, 201, 204] diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 4d3209b3..7a86a800 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -715,11 +715,11 @@ def get_user_id(self, username): def get_user(self, user_id): """Get representation of the user. - :param user_id: User id - UserRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userrepresentation + :param user_id: User id + :type user_id: str :return: UserRepresentation """ params_path = {"realm-name": self.realm_name, "id": user_id} @@ -732,8 +732,9 @@ def get_user_groups(self, user_id): Returns a list of groups of which the user is a member :param user_id: User id - + :type user_id: str :return: user groups list + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_GROUPS.format(**params_path)) @@ -743,9 +744,12 @@ def update_user(self, user_id, payload): """Update the user. :param user_id: User id + :type user_id: str :param payload: UserRepresentation + :type payload: dict :return: Http response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_put( @@ -757,8 +761,9 @@ def delete_user(self, user_id): """Delete the user. :param user_id: User id - + :type user_id: str :return: Http response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER.format(**params_path)) @@ -774,10 +779,13 @@ def set_user_password(self, user_id, password, temporary=True): https://www.keycloak.org/docs-api/18.0/rest-api/#_credentialrepresentation :param user_id: User id + :type user_id: str :param password: New password + :type password: str :param temporary: True if password is temporary - - :return: + :type temporary: bool + :returns: Response + :rtype: dict """ payload = {"type": "password", "temporary": temporary, "value": password} params_path = {"realm-name": self.realm_name, "id": user_id} @@ -795,7 +803,9 @@ def get_credentials(self, user_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation :param: user_id: user id - :return: Keycloak server response (CredentialRepresentation) + :type user_id: str + :returns: Keycloak server response (CredentialRepresentation) + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CREDENTIALS.format(**params_path)) @@ -808,8 +818,11 @@ def delete_credential(self, user_id, credential_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_credentialrepresentation :param: user_id: user id + :type user_id: str :param: credential_id: credential id + :type credential_id: str :return: Keycloak server response (ClientRepresentation) + :rtype: bytes """ params_path = { "realm-name": self.realm_name, @@ -825,7 +838,9 @@ def user_logout(self, user_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_logout :param user_id: User id - :return: + :type user_id: str + :returns: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_post( @@ -840,7 +855,9 @@ def user_consents(self, user_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_userconsentrepresentation :param user_id: User id - :return: List of UserConsentRepresentations + :type user_id: str + :returns: List of UserConsentRepresentations + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_CONSENTS.format(**params_path)) @@ -852,7 +869,9 @@ def get_user_social_logins(self, user_id): Returns a list of federated identities/social logins of which the user has been associated with :param user_id: User id - :return: federated identities list + :type user_id: str + :returns: Federated identities list + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get( @@ -864,10 +883,15 @@ def add_user_social_login(self, user_id, provider_id, provider_userid, provider_ """Add a federated identity / social login provider to the user. :param user_id: User id + :type user_id: str :param provider_id: Social login provider id + :type provider_id: str :param provider_userid: userid specified by the provider + :type provider_userid: str :param provider_username: username specified by the provider - :return: + :type provider_username: str + :returns: Keycloak server response + :rtype: bytes """ payload = { "identityProvider": provider_id, @@ -885,8 +909,11 @@ def delete_user_social_login(self, user_id, provider_id): """Delete a federated identity / social login provider from the user. :param user_id: User id + :type user_id: str :param provider_id: Social login provider id - :return: + :type provider_id: str + :returns: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id, "provider": provider_id} data_raw = self.raw_delete( @@ -902,12 +929,18 @@ def send_update_account( An email contains a link the user can click to perform a set of required actions. :param user_id: User id + :type user_id: str :param payload: A list of actions for the user to complete + :type payload: list :param client_id: Client id (optional) + :type client_id: str :param lifespan: Number of seconds after which the generated token expires (optional) + :type lifespan: int :param redirect_uri: The redirect uri (optional) + :type redirect_uri: str - :return: + :returns: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "lifespan": lifespan, "redirect_uri": redirect_uri} @@ -924,10 +957,14 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): An email contains a link the user can click to perform a set of required actions. :param user_id: User id + :type user_id: str :param client_id: Client id (optional) + :type client_id: str :param redirect_uri: Redirect uri (optional) + :type redirect_uri: str - :return: + :returns: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id} params_query = {"client_id": client_id, "redirect_uri": redirect_uri} @@ -941,12 +978,13 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): def get_sessions(self, user_id): """Get sessions associated with the user. - :param user_id: id of user - UserSessionRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_usersessionrepresentation + :param user_id: Id of user + :type user_id: str :return: UserSessionRepresentation + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_GET_SESSIONS.format(**params_path)) @@ -959,6 +997,7 @@ def get_server_info(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation :return: ServerInfoRepresentation + :rtype: dict """ data_raw = self.raw_get(urls_patterns.URL_ADMIN_SERVER_INFO) return raise_error_from_response(data_raw, KeycloakGetError) @@ -971,7 +1010,10 @@ def get_groups(self, query=None): GroupRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation + :param query: Additional query options + :type query: dict :return: array GroupRepresentation + :rtype: list """ query = query or {} params_path = {"realm-name": self.realm_name} @@ -991,7 +1033,9 @@ def get_group(self, group_id): https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param group_id: The group id + :type group_id: str :return: Keycloak server response (GroupRepresentation) + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUP.format(**params_path)) @@ -1005,10 +1049,12 @@ def get_subgroups(self, group, path): GroupRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation - :param name: group (GroupRepresentation) + :param group: group (GroupRepresentation) + :type group: dict :param path: group path (string) - + :type path: str :return: Keycloak server response (GroupRepresentation) + :rtype: dict """ for subgroup in group["subGroups"]: if subgroup["path"] == path: @@ -1030,9 +1076,12 @@ def get_group_members(self, group_id, query=None): https://www.keycloak.org/docs-api/18.0/rest-api/#_userrepresentation :param group_id: The group id + :type group_id: str :param query: Additional query parameters (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getmembers) + :type query: dict :return: Keycloak server response (UserRepresentation) + :rtype: list """ query = query or {} params_path = {"realm-name": self.realm_name, "id": group_id} @@ -1053,8 +1102,11 @@ def get_group_by_path(self, path, search_in_subgroups=False): https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation :param path: group path + :type path: str :param search_in_subgroups: True if want search in the subgroups + :type search_in_subgroups: bool :return: Keycloak server response (GroupRepresentation) + :rtype: dict """ groups = self.get_groups() @@ -1074,14 +1126,18 @@ def get_group_by_path(self, path, search_in_subgroups=False): def create_group(self, payload, parent=None, skip_exists=False): """Create a group in the Realm. + GroupRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation + :param payload: GroupRepresentation + :type payload: dict :param parent: parent group's id. Required to create a sub-group. + :type parent: str :param skip_exists: If true then do not raise an error if it already exists - - GroupRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation + :type skip_exists: bool :return: Group id for newly created group or None for an existing group + :rtype: str """ if parent is None: params_path = {"realm-name": self.realm_name} @@ -1106,13 +1162,16 @@ def create_group(self, payload, parent=None, skip_exists=False): def update_group(self, group_id, payload): """Update group, ignores subgroups. - :param group_id: id of group - :param payload: GroupRepresentation with updated information. - GroupRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/#_grouprepresentation + :param group_id: id of group + :type group_id: str + :param payload: GroupRepresentation with updated information. + :type payload: dict + :return: Http response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( @@ -1126,8 +1185,11 @@ def group_set_permissions(self, group_id, enabled=True): Cannot delete group if disabled :param group_id: id of group - :param enabled: boolean + :type group_id: str + :param enabled: Enabled flag + :type enabled: bool :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_put( @@ -1140,8 +1202,11 @@ def group_user_add(self, user_id, group_id): """Add user to group (user_id and group_id). :param user_id: id of user + :type user_id: str :param group_id: id of group to add to + :type group_id: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_put( @@ -1153,8 +1218,11 @@ def group_user_remove(self, user_id, group_id): """Remove user from group (user_id and group_id). :param user_id: id of user + :type user_id: str :param group_id: id of group to remove from + :type group_id: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": user_id, "group-id": group_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_USER_GROUP.format(**params_path)) @@ -1164,7 +1232,9 @@ def delete_group(self, group_id): """Delete a group in the Realm. :param group_id: id of group to delete + :type group_id: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_GROUP.format(**params_path)) @@ -1179,6 +1249,7 @@ def get_clients(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :return: Keycloak server response (ClientRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENTS.format(**params_path)) @@ -1191,7 +1262,9 @@ def get_client(self, client_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response (ClientRepresentation) + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) @@ -1204,7 +1277,9 @@ def get_client_id(self, client_id): :param client_id: clientId in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: client_id (uuid as string) + :rtype: str """ clients = self.get_clients() @@ -1219,7 +1294,9 @@ def get_client_authz_settings(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: Keycloak server response + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( @@ -1232,10 +1309,15 @@ def create_client_authz_resource(self, client_id, payload, skip_exists=False): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :param payload: ResourceRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_resourcerepresentation + :type payload: dict + :param skip_exists: Skip the creation in case the resource exists + :type skip_exists: bool :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id} @@ -1252,7 +1334,9 @@ def get_client_authz_resources(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: Keycloak server response + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( @@ -1263,11 +1347,6 @@ def get_client_authz_resources(self, client_id): def create_client_authz_role_based_policy(self, client_id, payload, skip_exists=False): """Create role-based policy of client. - :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation - :param payload: No Document - :return: Keycloak server response - Payload example:: payload={ @@ -1282,6 +1361,16 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= ] } + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str + :param payload: No Document + :type payload: dict + :param skip_exists: Skip creation in case the object exists + :type skip_exists: bool + :return: Keycloak server response + :rtype: bytes + """ params_path = {"realm-name": self.realm_name, "id": client_id} @@ -1296,12 +1385,6 @@ def create_client_authz_role_based_policy(self, client_id, payload, skip_exists= def create_client_authz_resource_based_permission(self, client_id, payload, skip_exists=False): """Create resource-based permission of client. - :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation - :param payload: PolicyRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_policyrepresentation - :return: Keycloak server response - Payload example:: payload={ @@ -1316,6 +1399,17 @@ def create_client_authz_resource_based_permission(self, client_id, payload, skip policy_id ] + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str + :param payload: PolicyRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_policyrepresentation + :type payload: dict + :param skip_exists: Skip creation in case the object already exists + :type skip_exists: bool + :return: Keycloak server response + :rtype: bytes + """ params_path = {"realm-name": self.realm_name, "id": client_id} @@ -1332,7 +1426,9 @@ def get_client_authz_scopes(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) @@ -1343,7 +1439,9 @@ def get_client_authz_permissions(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( @@ -1356,7 +1454,9 @@ def get_client_authz_policies(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( @@ -1369,7 +1469,9 @@ def get_client_service_account_user(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: UserRepresentation + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( @@ -1384,8 +1486,11 @@ def create_client(self, payload, skip_exists=False): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param skip_exists: If true then do not raise an error if client already exists + :type skip_exists: bool :param payload: ClientRepresentation + :type payload: dict :return: Client ID + :rtype: str """ if skip_exists: client_id = self.get_client_id(client_id=payload["clientId"]) @@ -1407,9 +1512,12 @@ def update_client(self, client_id, payload): """Update a client. :param client_id: Client id + :type client_id: str :param payload: ClientRepresentation + :type payload: dict :return: Http response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_put( @@ -1424,7 +1532,9 @@ def delete_client(self, client_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation :param client_id: keycloak client id (not oauth client-id) + :type client_id: str :return: Keycloak server response (ClientRepresentation) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT.format(**params_path)) @@ -1440,7 +1550,11 @@ def get_client_installation_provider(self, client_id, provider_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_serverinforepresentation :param client_id: Client id + :type client_id: str :param provider_id: provider id to specify response format + :type provider_id: str + :returns: Installation providers + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id, "provider-id": provider_id} data_raw = self.raw_get( @@ -1455,6 +1569,7 @@ def get_realm_roles(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :return: Keycloak server response (RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) @@ -1464,9 +1579,12 @@ def get_realm_role_members(self, role_name, query=None): """Get role members of realm by role name. :param role_name: Name of the role. + :type role_name: str :param query: Additional Query parameters (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_roles_resource) + :type query: dict :return: Keycloak Server Response (UserRepresentation) + :rtype: list """ query = query or dict() params_path = {"realm-name": self.realm_name, "role-name": role_name} @@ -1477,12 +1595,13 @@ def get_realm_role_members(self, role_name, query=None): def get_client_roles(self, client_id): """Get all roles for the client. - :param client_id: id of client (not client-id) - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response (RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) @@ -1493,13 +1612,15 @@ def get_client_role(self, client_id, role_name): This is required for further actions with this role. - :param client_id: id of client (not client-id) - :param role_name: role’s name (not id!) - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + :param client_id: id of client (not client-id) + :type client_id: str + :param role_name: role’s name (not id!) + :type role_name: str :return: role_id + :rtype: str """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) @@ -1510,13 +1631,15 @@ def get_client_role_id(self, client_id, role_name): This is required for further actions with this role. - :param client_id: id of client (not client-id) - :param role_name: role’s name (not id!) - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + :param client_id: id of client (not client-id) + :type client_id: str + :param role_name: role's name (not id!) + :type role_name: str :return: role_id + :rtype: str """ role = self.get_client_role(client_id, role_name) return role.get("id") @@ -1528,9 +1651,13 @@ def create_client_role(self, client_role_id, payload, skip_exists=False): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) + :type client_role_id: str :param payload: RoleRepresentation + :type payload: dict :param skip_exists: If true then do not raise an error if client role already exists + :type skip_exists: bool :return: Client role name + :rtype: str """ if skip_exists: try: @@ -1553,9 +1680,13 @@ def add_composite_client_roles_to_role(self, client_role_id, role_name, roles): """Add composite roles to client role. :param client_role_id: id of client (not client-id) + :type client_role_id: str :param role_name: The name of the role + :type role_name: str :param roles: roles list or role (use RoleRepresentation) to be updated + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} @@ -1572,8 +1703,13 @@ def update_client_role(self, client_role_id, role_name, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) + :type client_role_id: str :param role_name: role's name (not id!) + :type role_name: str :param payload: RoleRepresentation + :type payload: dict + :returns: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_put( @@ -1588,7 +1724,11 @@ def delete_client_role(self, client_role_id, role_name): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_role_id: id of client (not client-id) + :type client_role_id: str :param role_name: role's name (not id!) + :type role_name: str + :returns: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_role_id, "role-name": role_name} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) @@ -1598,9 +1738,13 @@ def assign_client_role(self, user_id, client_id, roles): """Assign a client role to a user. :param user_id: id of user + :type user_id: str :param client_id: id of client (not client-id) + :type client_id: str :param roles: roles list or role (use RoleRepresentation) + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} @@ -1614,10 +1758,14 @@ def get_client_role_members(self, client_id, role_name, **query): """Get members by client role. :param client_id: The client id + :type client_id: str :param role_name: the name of role to be queried. + :type role_name: str :param query: Additional query parameters (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) + :type query: dict :return: Keycloak server response (UserRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} return self.__fetch_all( @@ -1628,10 +1776,14 @@ def get_client_role_groups(self, client_id, role_name, **query): """Get group members by client role. :param client_id: The client id + :type client_id: str :param role_name: the name of role to be queried. + :type role_name: str :param query: Additional query parameters (see https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clients_resource) + :type query: dict :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} return self.__fetch_all( @@ -1642,8 +1794,11 @@ def create_realm_role(self, payload, skip_exists=False): """Create a new role for the realm or client. :param payload: The role (use RoleRepresentation) + :type payload: dict :param skip_exists: If true then do not raise an error if realm role already exists + :type skip_exists: bool :return: Realm role name + :rtype: str """ if skip_exists: try: @@ -1665,11 +1820,13 @@ def create_realm_role(self, payload, skip_exists=False): def get_realm_role(self, role_name): """Get realm role by role name. - :param role_name: role's name, not id! - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: role_id + + :param role_name: role's name, not id! + :type role_name: str + :return: role + :rtype: dict """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_get( @@ -1681,8 +1838,11 @@ def update_realm_role(self, role_name, payload): """Update a role for the realm by name. :param role_name: The name of the role to be updated + :type role_name: str :param payload: The role (use RoleRepresentation) + :type payload: dict :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_put( @@ -1694,8 +1854,10 @@ def update_realm_role(self, role_name, payload): def delete_realm_role(self, role_name): """Delete a role for the realm by name. - :param payload: The role name {'role-name':'name-of-the-role'} + :param role_name: The role name + :type role_name: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_delete( @@ -1707,8 +1869,11 @@ def add_composite_realm_roles_to_role(self, role_name, roles): """Add composite roles to the role. :param role_name: The name of the role + :type role_name: str :param roles: roles list or role (use RoleRepresentation) to be updated + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} @@ -1722,8 +1887,11 @@ def remove_composite_realm_roles_to_role(self, role_name, roles): """Remove composite roles from the role. :param role_name: The name of the role + :type role_name: str :param roles: roles list or role (use RoleRepresentation) to be removed + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "role-name": role_name} @@ -1737,7 +1905,9 @@ def get_composite_realm_roles_of_role(self, role_name): """Get composite roles of the role. :param role_name: The name of the role + :type role_name: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_get( @@ -1749,8 +1919,11 @@ def assign_realm_roles(self, user_id, roles): """Assign realm roles to a user. :param user_id: id of user + :type user_id: str :param roles: roles list or role (use RoleRepresentation) + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} @@ -1764,8 +1937,11 @@ def delete_realm_roles_of_user(self, user_id, roles): """Delete realm roles of a user. :param user_id: id of user + :type user_id: str :param roles: roles list or role (use RoleRepresentation) + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id} @@ -1779,7 +1955,9 @@ def get_realm_roles_of_user(self, user_id): """Get all realm roles for a user. :param user_id: id of user + :type user_id: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_REALM_ROLES.format(**params_path)) @@ -1789,7 +1967,9 @@ def get_available_realm_roles_of_user(self, user_id): """Get all available (i.e. unassigned) realm roles for a user. :param user_id: id of user + :type user_id: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get( @@ -1801,7 +1981,9 @@ def get_composite_realm_roles_of_user(self, user_id): """Get all composite (i.e. implicit) realm roles for a user. :param user_id: id of user + :type user_id: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": user_id} data_raw = self.raw_get( @@ -1812,9 +1994,12 @@ def get_composite_realm_roles_of_user(self, user_id): def assign_group_realm_roles(self, group_id, roles): """Assign realm roles to a group. - :param group_id: id of groupp + :param group_id: id of group + :type group_id: str :param roles: roles list or role (use GroupRoleRepresentation) + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} @@ -1828,8 +2013,11 @@ def delete_group_realm_roles(self, group_id, roles): """Delete realm roles of a group. :param group_id: id of group + :type group_id: str :param roles: roles list or role (use GroupRoleRepresentation) + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id} @@ -1842,8 +2030,10 @@ def delete_group_realm_roles(self, group_id, roles): def get_group_realm_roles(self, group_id): """Get all realm roles for a group. - :param user_id: id of the group + :param group_id: id of the group + :type group_id: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": group_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) @@ -1853,9 +2043,13 @@ def assign_group_client_roles(self, group_id, client_id, roles): """Assign client roles to a group. :param group_id: id of group + :type group_id: str :param client_id: id of client (not client-id) + :type client_id: str :param roles: roles list or role (use GroupRoleRepresentation) + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} @@ -1869,8 +2063,11 @@ def get_group_client_roles(self, group_id, client_id): """Get client roles of a group. :param group_id: id of group + :type group_id: str :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES.format(**params_path)) @@ -1880,9 +2077,13 @@ def delete_group_client_roles(self, group_id, client_id, roles): """Delete client roles of a group. :param group_id: id of group + :type group_id: str :param client_id: id of client (not client-id) + :type client_id: str :param roles: roles list or role (use GroupRoleRepresentation) + :type roles: list :return: Keycloak server response (array RoleRepresentation) + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} @@ -1896,8 +2097,11 @@ def get_client_roles_of_user(self, user_id, client_id): """Get all client roles for a user. :param user_id: id of user + :type user_id: str :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ return self._get_client_roles_of_user( urls_patterns.URL_ADMIN_USER_CLIENT_ROLES, user_id, client_id @@ -1907,8 +2111,11 @@ def get_available_client_roles_of_user(self, user_id, client_id): """Get available client role-mappings for a user. :param user_id: id of user + :type user_id: str :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ return self._get_client_roles_of_user( urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id @@ -1918,14 +2125,28 @@ def get_composite_client_roles_of_user(self, user_id, client_id): """Get composite client role-mappings for a user. :param user_id: id of user + :type user_id: str :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response (array RoleRepresentation) + :rtype: list """ return self._get_client_roles_of_user( urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id ) def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): + """Get client roles of a single user helper. + + :param client_level_role_mapping_url: Url for the client role mapping + :type client_level_role_mapping_url: str + :param user_id: User id + :type user_id: str + :param client_id: Client id + :type client_id: str + :returns: Client roles of a user + :rtype: list + """ params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} data_raw = self.raw_get(client_level_role_mapping_url.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1934,9 +2155,13 @@ def delete_client_roles_of_user(self, user_id, client_id, roles): """Delete client roles from a user. :param user_id: id of user + :type user_id: str :param client_id: id of client containing role (not client-id) + :type client_id: str :param roles: roles list or role to delete (use RoleRepresentation) + :type roles: list :return: Keycloak server response + :rtype: bytes """ payload = roles if isinstance(roles, list) else [roles] params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} @@ -1955,6 +2180,7 @@ def get_authentication_flows(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :return: Keycloak server response (AuthenticationFlowRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS.format(**params_path)) @@ -1969,7 +2195,9 @@ def get_authentication_flow_for_id(self, flow_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param flow_id: the id of a flow NOT it's alias + :type flow_id: str :return: Keycloak server response (AuthenticationFlowRepresentation) + :rtype: dict """ params_path = {"realm-name": self.realm_name, "flow-id": flow_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_ALIAS.format(**params_path)) @@ -1982,8 +2210,11 @@ def create_authentication_flow(self, payload, skip_exists=False): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation + :type payload: dict :param skip_exists: Do not raise an error if authentication flow already exists + :type skip_exists: bool :return: Keycloak server response (RoleRepresentation) + :rtype: bytes """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( @@ -1999,8 +2230,11 @@ def copy_authentication_flow(self, payload, flow_alias): The new name is given as 'newName' attribute of the passed payload. :param payload: JSON containing 'newName' attribute + :type payload: dict :param flow_alias: the flow alias + :type flow_alias: str :return: Keycloak server response (RoleRepresentation) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( @@ -2015,7 +2249,9 @@ def delete_authentication_flow(self, flow_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationinforepresentation :param flow_id: authentication flow id + :type flow_id: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": flow_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_FLOW.format(**params_path)) @@ -2027,7 +2263,9 @@ def get_authentication_flow_executions(self, flow_alias): Returns all execution steps :param flow_alias: the flow alias + :type flow_alias: str :return: Response(json) + :rtype: list """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_EXECUTIONS.format(**params_path)) @@ -2040,8 +2278,11 @@ def update_authentication_flow_executions(self, payload, flow_alias): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation + :type payload: dict :param flow_alias: The flow alias + :type flow_alias: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_put( @@ -2057,7 +2298,9 @@ def get_authentication_flow_execution(self, execution_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param execution_id: the execution ID + :type execution_id: str :return: Response(json) + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": execution_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) @@ -2070,8 +2313,11 @@ def create_authentication_flow_execution(self, payload, flow_alias): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param payload: AuthenticationExecutionInfoRepresentation + :type payload: dict :param flow_alias: The flow alias + :type flow_alias: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( @@ -2087,7 +2333,9 @@ def delete_authentication_flow_execution(self, execution_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationexecutioninforepresentation :param execution_id: keycloak client id (not oauth client-id) + :type execution_id: str :return: Keycloak server response (json) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": execution_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_FLOWS_EXECUTION.format(**params_path)) @@ -2100,9 +2348,13 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticationflowrepresentation :param payload: AuthenticationFlowRepresentation + :type payload: dict :param flow_alias: The flow alias + :type flow_alias: str :param skip_exists: Do not raise an error if authentication flow already exists + :type skip_exists: bool :return: Keycloak server response (RoleRepresentation) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "flow-alias": flow_alias} data_raw = self.raw_post( @@ -2116,7 +2368,8 @@ def create_authentication_flow_subflow(self, payload, flow_alias, skip_exists=Fa def get_authenticator_providers(self): """Get authenticator providers list. - :return: Response(json) + :return: Authenticator providers + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get( @@ -2131,7 +2384,9 @@ def get_authenticator_provider_config_description(self, provider_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfiginforepresentation :param provider_id: Provider Id + :type provider_id: str :return: AuthenticatorConfigInfoRepresentation + :rtype: dict """ params_path = {"realm-name": self.realm_name, "provider-id": provider_id} data_raw = self.raw_get( @@ -2145,7 +2400,9 @@ def get_authenticator_config(self, config_id): Returns all configuration details. :param config_id: Authenticator config id + :type config_id: str :return: Response(json) + :rtype: dict """ params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_AUTHENTICATOR_CONFIG.format(**params_path)) @@ -2158,8 +2415,11 @@ def update_authenticator_config(self, payload, config_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authenticatorconfigrepresentation :param payload: AuthenticatorConfigRepresentation + :type payload: dict :param config_id: Authenticator config id + :type config_id: str :return: Response(json) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_put( @@ -2174,7 +2434,9 @@ def delete_authenticator_config(self, config_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_authentication_management_resource :param config_id: Authenticator config id + :type config_id: str :return: Keycloak server Response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": config_id} data_raw = self.raw_delete( @@ -2186,8 +2448,11 @@ def sync_users(self, storage_id, action): """Trigger user sync from provider. :param storage_id: The id of the user storage provider + :type storage_id: str :param action: Action can be "triggerFullSync" or "triggerChangedUsersSync" - :return: + :type action: str + :return: Keycloak server response + :rtype: bytes """ data = {"action": action} params_query = {"action": action} @@ -2207,6 +2472,7 @@ def get_client_scopes(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :return: Keycloak server response Array of (ClientScopeRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPES.format(**params_path)) @@ -2219,7 +2485,9 @@ def get_client_scope(self, client_scope_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param client_scope_id: The id of the client scope + :type client_scope_id: str :return: Keycloak server response (ClientScopeRepresentation) + :rtype: dict """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) @@ -2232,7 +2500,9 @@ def get_client_scope_by_name(self, client_scope_name): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param client_scope_name: (str) Name of the client scope + :type client_scope_name: str :returns: ClientScopeRepresentation or None + :rtype: dict """ client_scopes = self.get_client_scopes() for client_scope in client_scopes: @@ -2248,8 +2518,11 @@ def create_client_scope(self, payload, skip_exists=False): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientscopes :param payload: ClientScopeRepresentation + :type payload: dict :param skip_exists: If true then do not raise an error if client scope already exists + :type skip_exists: bool :return: Client scope id + :rtype: str """ if skip_exists: exists = self.get_client_scope_by_name(client_scope_name=payload["name"]) @@ -2274,8 +2547,11 @@ def update_client_scope(self, client_scope_id, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource :param client_scope_id: The id of the client scope + :type client_scope_id: str :param payload: ClientScopeRepresentation - :return: Keycloak server response (ClientScopeRepresentation) + :type payload: dict + :return: Keycloak server response (ClientScopeRepresentation) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_put( @@ -2290,7 +2566,9 @@ def delete_client_scope(self, client_scope_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_client_scopes_resource :param client_scope_id: The id of the client scope - :return: Keycloak server response + :type client_scope_id: str + :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_CLIENT_SCOPE.format(**params_path)) @@ -2301,7 +2579,9 @@ def get_mappers_from_client_scope(self, client_scope_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource :param client_scope_id: Client scope id + :type client_scope_id: str :returns: Keycloak server response (ProtocolMapperRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} data_raw = self.raw_get( @@ -2315,8 +2595,11 @@ def add_mapper_to_client_scope(self, client_scope_id, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_scope_id: The id of the client scope + :type client_scope_id: str :param payload: ProtocolMapperRepresentation + :type payload: dict :return: Keycloak server Response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "scope-id": client_scope_id} @@ -2333,8 +2616,11 @@ def delete_mapper_from_client_scope(self, client_scope_id, protocol_mapper_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_delete_mapper :param client_scope_id: The id of the client scope + :type client_scope_id: str :param protocol_mapper_id: Protocol mapper id + :type protocol_mapper_id: str :return: Keycloak server Response + :rtype: bytes """ params_path = { "realm-name": self.realm_name, @@ -2353,10 +2639,14 @@ def update_mapper_in_client_scope(self, client_scope_id, protocol_mapper_id, pay https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocol_mappers_resource :param client_scope_id: The id of the client scope + :type client_scope_id: str :param protocol_mapper_id: The id of the protocol mapper which exists in the client scope and should to be updated + :type protocol_mapper_id: str :param payload: ProtocolMapperRepresentation + :type payload: dict :return: Keycloak server Response + :rtype: bytes """ params_path = { "realm-name": self.realm_name, @@ -2377,6 +2667,7 @@ def get_default_default_client_scopes(self): Return list of default default client scopes :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get( @@ -2388,7 +2679,9 @@ def delete_default_default_client_scope(self, scope_id): """Delete default default client scope. :param scope_id: default default client scope id + :type scope_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": scope_id} data_raw = self.raw_delete( @@ -2400,7 +2693,9 @@ def add_default_default_client_scope(self, scope_id): """Add default default client scope. :param scope_id: default default client scope id + :type scope_id: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} @@ -2416,6 +2711,7 @@ def get_default_optional_client_scopes(self): Return list of default optional client scopes :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get( @@ -2427,7 +2723,9 @@ def delete_default_optional_client_scope(self, scope_id): """Delete default optional client scope. :param scope_id: default optional client scope id + :type scope_id: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": scope_id} data_raw = self.raw_delete( @@ -2439,7 +2737,9 @@ def add_default_optional_client_scope(self, scope_id): """Add default optional client scope. :param scope_id: default optional client scope id + :type scope_id: str :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": scope_id} payload = {"realm": self.realm_name, "clientScopeId": scope_id} @@ -2455,7 +2755,9 @@ def get_mappers_from_client(self, client_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_protocolmapperrepresentation :param client_id: Client id + :type client_id: str :returns: KeycloakServerResponse (list of ProtocolMapperRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} @@ -2471,8 +2773,11 @@ def add_mapper_to_client(self, client_id, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_create_mapper :param client_id: The id of the client + :type client_id: str :param payload: ProtocolMapperRepresentation + :type payload: dict :return: Keycloak server Response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id} @@ -2487,9 +2792,13 @@ def update_client_mapper(self, client_id, mapper_id, payload): """Update client mapper. :param client_id: The id of the client - :param client_mapper_id: The id of the mapper to be deleted + :type client_id: str + :param mapper_id: The id of the mapper to be deleted + :type mapper_id: str :param payload: ProtocolMapperRepresentation + :type payload: dict :return: Keycloak server response + :rtype: bytes """ params_path = { "realm-name": self.realm_name, @@ -2508,9 +2817,13 @@ def remove_client_mapper(self, client_id, client_mapper_id): """Remove a mapper from the client. https://www.keycloak.org/docs-api/15.0/rest-api/index.html#_protocol_mappers_resource + :param client_id: The id of the client + :type client_id: str :param client_mapper_id: The id of the mapper to be deleted + :type client_mapper_id: str :return: Keycloak server response + :rtype: bytes """ params_path = { "realm-name": self.realm_name, @@ -2529,7 +2842,9 @@ def generate_client_secrets(self, client_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_regeneratesecret :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response (ClientRepresentation) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( @@ -2543,7 +2858,9 @@ def get_client_secrets(self, client_id): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsecret :param client_id: id of client (not client-id) + :type client_id: str :return: Keycloak server response (ClientRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SECRETS.format(**params_path)) @@ -2558,7 +2875,9 @@ def get_components(self, query=None): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :param query: Query parameters (optional) + :type query: dict :return: components list + :rtype: list """ query = query or dict() params_path = {"realm-name": self.realm_name} @@ -2574,7 +2893,9 @@ def create_component(self, payload): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation :param payload: ComponentRepresentation + :type payload: dict :return: Component id + :rtype: str """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_post( @@ -2592,7 +2913,10 @@ def get_component(self, component_id): ComponentRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation + :param component_id: Id of the component + :type component_id: str :return: ComponentRepresentation + :rtype: dict """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_COMPONENT.format(**params_path)) @@ -2602,10 +2926,12 @@ def update_component(self, component_id, payload): """Update the component. :param component_id: Component id + :type component_id: str :param payload: ComponentRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_componentrepresentation - + :type payload: dict :return: Http response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_put( @@ -2617,8 +2943,9 @@ def delete_component(self, component_id): """Delete the component. :param component_id: Component id - + :type component_id: str :return: Http response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "component-id": component_id} data_raw = self.raw_delete(urls_patterns.URL_ADMIN_COMPONENT.format(**params_path)) @@ -2633,6 +2960,7 @@ def get_keys(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_key_resource :return: keys list + :rtype: list """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_KEYS.format(**params_path), data=None) @@ -2646,7 +2974,10 @@ def get_events(self, query=None): EventRepresentation array https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_eventrepresentation + :param query: Additional query parameters + :type query: dict :return: events list + :rtype: list """ query = query or dict() params_path = {"realm-name": self.realm_name} @@ -2661,7 +2992,10 @@ def set_events(self, payload): RealmEventsConfigRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_realmeventsconfigrepresentation + :param payload: Payload object for the events configuration + :type payload: dict :return: Http response + :rtype: bytes """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_put( @@ -2674,6 +3008,13 @@ def raw_get(self, *args, **kwargs): If auto_refresh is set for *get* and *access_token* is expired, it will refresh the token and try *get* once more. + + :param args: Additional arguments + :type args: tuple + :param kwargs: Additional keyword arguments + :type kwargs: dict + :returns: Response + :rtype: Response """ r = self.connection.raw_get(*args, **kwargs) if "get" in self.auto_refresh_token and r.status_code == 401: @@ -2686,6 +3027,13 @@ def raw_post(self, *args, **kwargs): If auto_refresh is set for *post* and *access_token* is expired, it will refresh the token and try *post* once more. + + :param args: Additional arguments + :type args: tuple + :param kwargs: Additional keyword arguments + :type kwargs: dict + :returns: Response + :rtype: Response """ r = self.connection.raw_post(*args, **kwargs) if "post" in self.auto_refresh_token and r.status_code == 401: @@ -2698,6 +3046,13 @@ def raw_put(self, *args, **kwargs): If auto_refresh is set for *put* and *access_token* is expired, it will refresh the token and try *put* once more. + + :param args: Additional arguments + :type args: tuple + :param kwargs: Additional keyword arguments + :type kwargs: dict + :returns: Response + :rtype: Response """ r = self.connection.raw_put(*args, **kwargs) if "put" in self.auto_refresh_token and r.status_code == 401: @@ -2710,6 +3065,13 @@ def raw_delete(self, *args, **kwargs): If auto_refresh is set for *delete* and *access_token* is expired, it will refresh the token and try *delete* once more. + + :param args: Additional arguments + :type args: tuple + :param kwargs: Additional keyword arguments + :type kwargs: dict + :returns: Response + :rtype: Response """ r = self.connection.raw_delete(*args, **kwargs) if "delete" in self.auto_refresh_token and r.status_code == 401: @@ -2718,7 +3080,10 @@ def raw_delete(self, *args, **kwargs): return r def get_token(self): - """Get admin token.""" + """Get admin token. + + The admin token is then set in the `token` attribute. + """ if self.user_realm_name: token_realm_name = self.user_realm_name elif self.realm_name: @@ -2766,7 +3131,10 @@ def get_token(self): ) def refresh_token(self): - """Refresh the token.""" + """Refresh the token. + + :raises KeycloakPostError: In case the refresh token request failed. + """ refresh_token = self.token.get("refresh_token", None) if refresh_token is None: self.get_token() @@ -2791,12 +3159,13 @@ def refresh_token(self): def get_client_all_sessions(self, client_id): """Get sessions associated with the client. - :param client_id: id of client - UserSessionRepresentation http://www.keycloak.org/docs-api/18.0/rest-api/index.html#_usersessionrepresentation + :param client_id: id of client + :type client_id: str :return: UserSessionRepresentation + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ALL_SESSIONS.format(**params_path)) @@ -2808,6 +3177,7 @@ def get_client_sessions_stats(self): https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_getclientsessionstats :return: Dict of clients and session count + :rtype: dict """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_SESSION_STATS.format(**params_path)) @@ -2818,7 +3188,9 @@ def get_client_management_permissions(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( @@ -2832,17 +3204,19 @@ def update_client_management_permissions(self, payload, client_id): ManagementPermissionReference https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_managementpermissionreference - :param payload: ManagementPermissionReference - :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation - :return: Keycloak server response - - Payload example:: payload={ "enabled": true } + + :param payload: ManagementPermissionReference + :type payload: dict + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str + :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_put( @@ -2856,8 +3230,11 @@ def get_client_authz_policy_scopes(self, client_id, policy_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :param policy_id: No Document + :type policy_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id, "policy-id": policy_id} data_raw = self.raw_get( @@ -2870,8 +3247,11 @@ def get_client_authz_policy_resources(self, client_id, policy_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :param policy_id: No Document + :type policy_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id, "policy-id": policy_id} data_raw = self.raw_get( @@ -2884,8 +3264,11 @@ def get_client_authz_scope_permission(self, client_id, scope_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :param scope_id: No Document + :type scope_id: str :return: Keycloak server response + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id, "scope-id": scope_id} data_raw = self.raw_get( @@ -2896,13 +3279,6 @@ def get_client_authz_scope_permission(self, client_id, scope_id): def update_client_authz_scope_permission(self, payload, client_id, scope_id): """Update permissions for a given scope. - :param payload: No Document - :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation - :param scope_id: No Document - :return: Keycloak server response - - Payload example:: payload={ @@ -2915,6 +3291,16 @@ def update_client_authz_scope_permission(self, payload, client_id, scope_id): "scopes": [some_scope_id], "policies": [some_policy_id], } + + :param payload: No Document + :type payload: dict + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str + :param scope_id: No Document + :type scope_id: str + :return: Keycloak server response + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id, "scope-id": scope_id} data_raw = self.raw_put( @@ -2928,7 +3314,9 @@ def get_client_authz_client_policies(self, client_id): :param client_id: id in ClientRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str :return: Keycloak server response (RoleRepresentation) + :rtype: list """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get( @@ -2939,12 +3327,6 @@ def get_client_authz_client_policies(self, client_id): def create_client_authz_client_policy(self, payload, client_id): """Create a new policy for a given client. - :param payload: No Document - :param client_id: id in ClientRepresentation - https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation - :return: Keycloak server response (RoleRepresentation) - - Payload example:: payload={ @@ -2954,6 +3336,14 @@ def create_client_authz_client_policy(self, payload, client_id): "name": "My Policy", "clients": [other_client_id], } + + :param payload: No Document + :type payload: dict + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :type client_id: str + :return: Keycloak server response (RoleRepresentation) + :rtype: bytes """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_post( From a14a47dd4005435f6efb57a4769b153887df93d3 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 4 Aug 2022 14:44:10 +0000 Subject: [PATCH 238/566] chore: dependency update --- poetry.lock | 198 +++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 157 insertions(+), 41 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9fb39529..dd69daba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -44,17 +44,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "babel" @@ -117,6 +117,14 @@ category = "dev" optional = false python-versions = ">=3.6.1" +[[package]] +name = "chardet" +version = "5.0.0" +description = "Universal encoding detector for Python 3" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "charset-normalizer" version = "2.1.0" @@ -149,8 +157,8 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency"] hard-encoding-detection = ["chardet"] +dev = ["pytest-dependency", "pytest-cov", "pytest", "flake8", "check-manifest"] [[package]] name = "colorama" @@ -162,7 +170,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "commitizen" -version = "2.28.0" +version = "2.29.3" description = "Python commitizen client tool" category = "dev" optional = false @@ -170,6 +178,7 @@ python-versions = ">=3.6.2,<4.0.0" [package.dependencies] argcomplete = ">=1.12.1,<2.0.0" +chardet = ">=5.0.0,<6.0.0" colorama = ">=0.4.1,<0.5.0" decli = ">=0.5.2,<0.6.0" jinja2 = ">=2.10.3" @@ -311,7 +320,7 @@ pydocstyle = ">=2.1" [[package]] name = "identify" -version = "2.5.2" +version = "2.5.3" description = "File identification library for Python" category = "dev" optional = false @@ -778,7 +787,7 @@ python-versions = "*" [[package]] name = "sphinx" -version = "5.0.2" +version = "5.1.1" description = "Python documentation generator" category = "main" optional = true @@ -788,7 +797,7 @@ python-versions = ">=3.6" alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.19" +docutils = ">=0.14,<0.20" imagesize = "*" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" @@ -805,16 +814,16 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.950)", "docutils-stubs", "types-typed-ast", "types-requests"] +lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "isort", "mypy (>=0.971)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-autoapi" -version = "1.8.4" +version = "1.9.0" description = "Sphinx API documentation generator" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] astroid = ">=2.7" @@ -995,7 +1004,7 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.10" +version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1008,22 +1017,21 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.15.1" +version = "20.16.2" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] distlib = ">=0.3.1,<1" filelock = ">=3.2,<4" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} platformdirs = ">=2,<3" -six = ">=1.9.0,<2" [package.extras] docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] [[package]] name = "wcwidth" @@ -1072,25 +1080,115 @@ argcomplete = [ ] astroid = [] atomicwrites = [] -attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, -] +attrs = [] babel = [ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] -black = [] +black = [ + {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, + {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, + {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, + {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, + {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, + {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, + {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, + {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, + {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, + {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, + {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, + {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, + {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, + {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, + {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, + {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, + {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, + {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, +] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] -cffi = [] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] cfgv = [ {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, ] -charset-normalizer = [] +chardet = [] +charset-normalizer = [ + {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, + {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, +] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -1126,13 +1224,19 @@ flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] -flake8-docstrings = [] +flake8-docstrings = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] identify = [] idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -imagesize = [] +imagesize = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] importlib-metadata = [ {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, @@ -1271,7 +1375,10 @@ pluggy = [ {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, ] pre-commit = [] -prompt-toolkit = [] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, + {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, +] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, @@ -1295,8 +1402,14 @@ pycodestyle = [ {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, ] -pycparser = [] -pydocstyle = [] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, @@ -1372,7 +1485,10 @@ recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, ] -requests = [] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] requests-toolbelt = [] rsa = [] six = [ @@ -1383,14 +1499,8 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -sphinx = [ - {file = "Sphinx-5.0.2-py3-none-any.whl", hash = "sha256:d3e57663eed1d7c5c50895d191fdeda0b54ded6f44d5621b50709466c338d1e8"}, - {file = "Sphinx-5.0.2.tar.gz", hash = "sha256:b18e978ea7565720f26019c702cd85c84376e948370f1cd43d60265010e1c7b0"}, -] -sphinx-autoapi = [ - {file = "sphinx-autoapi-1.8.4.tar.gz", hash = "sha256:8c4ec5fbedc1e6e8f4692bcc4fcd1abcfb9e8dfca8a4ded60ad811a743c22ccc"}, - {file = "sphinx_autoapi-1.8.4-py2.py3-none-any.whl", hash = "sha256:007bf9e24cd2aa0ac0561f67e8bcd6a6e2e8911ef4b4fd54aaba799d8832c8d0"}, -] +sphinx = [] +sphinx-autoapi = [] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, @@ -1431,7 +1541,10 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [] -tox = [] +tox = [ + {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, + {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, +] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, @@ -1458,7 +1571,10 @@ typed-ast = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] -typing-extensions = [] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, From 843113921cfb4e549b927053c8202826ea2b1f06 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 4 Aug 2022 14:44:44 +0000 Subject: [PATCH 239/566] chore: linting on full repo --- docs/source/changelog.rst | 1 - pyproject.toml | 2 +- tox.ini | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 102c1132..ab37940f 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1,2 +1 @@ .. mdinclude:: ../../CHANGELOG.md - \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7874ca48..a115c32d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -84,4 +84,4 @@ line_length = 99 profile = "black" [tool.darglint] -enable = "DAR104" \ No newline at end of file +enable = "DAR104" diff --git a/tox.ini b/tox.ini index 06a08554..044f8b64 100644 --- a/tox.ini +++ b/tox.ini @@ -54,4 +54,4 @@ ignore = D203, D213, W503 docstring_style = sphinx [darglint] -enable = DAR104 \ No newline at end of file +enable = DAR104 From 7ea8ef6cbb314c1112299839c03882fb3a18f2ec Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Thu, 4 Aug 2022 14:58:11 +0000 Subject: [PATCH 240/566] ci: dont do incremental updats to the changelog --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 044f8b64..2b3db365 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,7 @@ commands = setenv = file|tox.env passenv = CONTAINER_HOST commands = - cz changelog --incremental + cz changelog [flake8] max-line-length = 99 From a1591ba50ae3c0cedbec9c1f62cda993ddd1872e Mon Sep 17 00:00:00 2001 From: Romuald OUATTARA Date: Mon, 8 Aug 2022 19:28:53 +0200 Subject: [PATCH 241/566] docs: added change of realm example --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index d300fa9a..99eba7d0 100644 --- a/README.md +++ b/README.md @@ -319,4 +319,10 @@ idps = keycloak_admin.get_idps() # Create a new Realm keycloak_admin.create_realm(payload={"realm": "demo"}, skip_exists=False) +# Changing Realm +keycloak_admin = KeycloakAdmin(realm_name="main", ...) +keycloak_admin.get_users() # Get user in main realm +keycloak_admin.realm_name = "demo" # Change realm to 'demo' +keycloak_admin.get_users() # Get users in realm 'demo' +keycloak_admin.create_user(...) # Creates a new user in 'demo' ``` From 0fb6c2058d96d2c1f194665243eef4e7f10c0ae8 Mon Sep 17 00:00:00 2001 From: Antonio Lucas Neres Date: Thu, 11 Aug 2022 10:35:53 -0300 Subject: [PATCH 242/566] feat: add client scope-mappings realm roles operations --- README.md | 56 ++++++++++++++++++-------------- src/keycloak/keycloak_admin.py | 42 ++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 1 + tests/test_keycloak_admin.py | 58 ++++++++++++++++++++++++++++++++++ 4 files changed, 133 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 99eba7d0..9666ba9d 100644 --- a/README.md +++ b/README.md @@ -65,9 +65,9 @@ from keycloak import KeycloakOpenID # Configure client keycloak_openid = KeycloakOpenID(server_url="http://localhost:8080/auth/", - client_id="example_client", - realm_name="example_realm", - client_secret_key="secret") + client_id="example_client", + realm_name="example_realm", + client_secret_key="secret") # Get WellKnow config_well_known = keycloak_openid.well_known() @@ -110,7 +110,7 @@ rpt = keycloak_openid.entitlement(token['access_token'], "resource_id") # Instropect RPT token_rpt_info = keycloak_openid.introspect(keycloak_openid.introspect(token['access_token'], rpt=rpt['rpt'], - token_type_hint="requesting_party_token")) + token_type_hint="requesting_party_token")) # Introspect Token token_info = keycloak_openid.introspect(token['access_token']) @@ -153,37 +153,37 @@ keycloak_admin = KeycloakAdmin(server_url="http://localhost:8080/auth/", # Add user new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example"}) + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example"}) # Add user and raise exception if username already exists # exist_ok currently defaults to True for backwards compatibility reasons new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example"}, - exist_ok=False) + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example"}, + exist_ok=False) # Add user and set password new_user = keycloak_admin.create_user({"email": "example@example.com", - "username": "example@example.com", - "enabled": True, - "firstName": "Example", - "lastName": "Example", + "username": "example@example.com", + "enabled": True, + "firstName": "Example", + "lastName": "Example", "credentials": [{"value": "secret","type": "password",}]}) # Add user and specify a locale new_user = keycloak_admin.create_user({"email": "example@example.fr", - "username": "example@example.fr", - "enabled": True, - "firstName": "Example", - "lastName": "Example", - "attributes": { - "locale": ["fr"] - }}) + "username": "example@example.fr", + "enabled": True, + "firstName": "Example", + "lastName": "Example", + "attributes": { + "locale": ["fr"] + }}) # User counter count_users = keycloak_admin.users_count() @@ -312,6 +312,14 @@ keycloak_admin.assign_client_role(client_id=client_id, user_id=user_id, role_id= # Assign realm roles to user keycloak_admin.assign_realm_roles(user_id=user_id, roles=realm_roles) +# Assign realm roles to client's scope +keycloak_admin.assign_realm_roles_to_client_scope(client_id=client_id, roles=realm_roles) + +# Get realm roles assigned to client's scope +keycloak_admin.get_realm_roles_of_client_scope(client_id=client_id) + +# Remove realm roles assigned to client's scope +keycloak_admin.delete_realm_roles_of_client_scope(client_id=client_id, roles=realm_roles) # Get all ID Providers idps = keycloak_admin.get_idps() diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 7d16ad54..50299b6b 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1597,6 +1597,48 @@ def get_composite_realm_roles_of_role(self, role_name): ) return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles_to_client_scope(self, client_id, roles): + """Assign realm roles to a client's scope. + + :param client_id: id of client (not client-id) + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) + + def delete_realm_roles_of_client_scope(self, client_id, roles): + """Delete realm roles of a client's scope. + + :param client_id: id of client (not client-id) + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + + def get_realm_roles_of_client_scope(self, client_id): + """Get all realm roles for a client's scope. + + :param client_id: id of client (not client-id) + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles(self, user_id, roles): """Assign realm roles to a user. diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 3f4151e1..f7d4f657 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -91,6 +91,7 @@ URL_ADMIN_CLIENT_ROLE_MEMBERS = URL_ADMIN_CLIENT + "/roles/{role-name}/users" URL_ADMIN_CLIENT_ROLE_GROUPS = URL_ADMIN_CLIENT + "/roles/{role-name}/groups" URL_ADMIN_CLIENT_MANAGEMENT_PERMISSIONS = URL_ADMIN_CLIENT + "/management/permissions" +URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES = URL_ADMIN_CLIENT + "/scope-mappings/realm" URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 61685af2..e9b093ca 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1030,6 +1030,64 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"Could not find role"}\'') +def test_client_scope_realm_roles(admin: KeycloakAdmin, realm: str): + """Test client realm roles.""" + admin.realm_name = realm + + # Test get realm roles + roles = admin.get_realm_roles() + assert len(roles) == 3, roles + role_names = [x["name"] for x in roles] + assert "uma_authorization" in role_names, role_names + assert "offline_access" in role_names, role_names + + # create realm role for test + role_id = admin.create_realm_role(payload={"name": "test-realm-role"}, skip_exists=True) + assert role_id, role_id + + # Test realm role client assignment + client_id = admin.create_client( + payload={"name": "role-testing-client", "clientId": "role-testing-client"} + ) + with pytest.raises(KeycloakPostError) as err: + admin.assign_realm_roles_to_client_scope(client_id=client_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_realm_roles_to_client_scope( + client_id=client_id, + roles=[ + admin.get_realm_role(role_name="offline_access"), + admin.get_realm_role(role_name="test-realm-role"), + ], + ) + assert res == dict(), res + + roles = admin.get_realm_roles_of_client_scope(client_id=client_id) + assert len(roles) == 2 + client_role_names = [x["name"] for x in roles] + assert "offline_access" in client_role_names, client_role_names + assert "test-realm-role" in client_role_names, client_role_names + assert "uma_authorization" not in client_role_names, client_role_names + + # Test remove realm role of client + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_realm_roles_of_client_scope(client_id=client_id, roles=["bad"]) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_realm_roles_of_client_scope( + client_id=client_id, roles=[admin.get_realm_role(role_name="offline_access")] + ) + assert res == dict(), res + roles = admin.get_realm_roles_of_client_scope(client_id=client_id) + assert len(roles) == 1 + assert "test-realm-role" in [x["name"] for x in roles] + + res = admin.delete_realm_roles_of_client_scope( + client_id=client_id, roles=[admin.get_realm_role(role_name="test-realm-role")] + ) + assert res == dict(), res + roles = admin.get_realm_roles_of_client_scope(client_id=client_id) + assert len(roles) == 0 + + def test_client_roles(admin: KeycloakAdmin, client: str): """Test client roles.""" # Test get client roles From 2e2578b1b425b5da34e84a6d6f77cbce6e7fd7e8 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Fri, 12 Aug 2022 06:55:00 +0000 Subject: [PATCH 243/566] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 251c6d59..958d8fad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.2.0 (2022-08-12) + +### Feat + +- add client scope-mappings realm roles operations + ## v2.1.1 (2022-07-19) ### Refactor From d14fbd6b5dff94808b5f92db9539a16a53db6dce Mon Sep 17 00:00:00 2001 From: Subramaniam Ramasubramanian Date: Fri, 12 Aug 2022 10:46:15 +0000 Subject: [PATCH 244/566] feat: Add token_type/scope to token exchange api --- src/keycloak/keycloak_openid.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 82e980ce..055085da 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -275,7 +275,15 @@ def refresh_token(self, refresh_token, grant_type=["refresh_token"]): data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) return raise_error_from_response(data_raw, KeycloakPostError) - def exchange_token(self, token: str, client_id: str, audience: str, subject: str) -> dict: + def exchange_token( + self, + token: str, + client_id: str, + audience: str, + subject: str, + requested_token_type: str = "urn:ietf:params:oauth:token-type:refresh_token", + scope: str = "", + ) -> dict: """Exchange user token. Use a token to obtain an entirely different token. See @@ -285,6 +293,8 @@ def exchange_token(self, token: str, client_id: str, audience: str, subject: str :param client_id: :param audience: :param subject: + :param requested_token_type: + :param scope: :return: """ params_path = {"realm-name": self.realm_name} @@ -292,9 +302,10 @@ def exchange_token(self, token: str, client_id: str, audience: str, subject: str "grant_type": ["urn:ietf:params:oauth:grant-type:token-exchange"], "client_id": client_id, "subject_token": token, - "requested_token_type": "urn:ietf:params:oauth:token-type:refresh_token", + "requested_token_type": requested_token_type, "audience": audience, "requested_subject": subject, + "scope": scope, } payload = self._add_secret_key(payload) data_raw = self.connection.raw_post(URL_TOKEN.format(**params_path), data=payload) From 0b93336f1582e17b105fbcf69899443268f1e11b Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Sat, 13 Aug 2022 00:39:02 +0000 Subject: [PATCH 245/566] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 958d8fad..cf8293b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.3.0 (2022-08-13) + +### Feat + +- Add token_type/scope to token exchange api + ## v2.2.0 (2022-08-12) ### Feat From 7c486ccb4fce28ff7e9fb5a5d4b6b95e94343926 Mon Sep 17 00:00:00 2001 From: Antonio Lucas Neres Date: Fri, 19 Aug 2022 14:13:27 -0300 Subject: [PATCH 246/566] feat: add client scope-mappings client roles operations --- README.md | 11 +++++++ src/keycloak/keycloak_admin.py | 57 ++++++++++++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 3 ++ tests/test_keycloak_admin.py | 56 +++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+) diff --git a/README.md b/README.md index 9666ba9d..3558d309 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,17 @@ keycloak_admin.get_realm_roles_of_client_scope(client_id=client_id) # Remove realm roles assigned to client's scope keycloak_admin.delete_realm_roles_of_client_scope(client_id=client_id, roles=realm_roles) +another_client_id = keycloak_admin.get_client_id("my-client-2") + +# Assign client roles to client's scope +keycloak_admin.assign_client_roles_to_client_scope(client_id=another_client_id, client_roles_owner_id=client_id, roles=client_roles) + +# Get client roles assigned to client's scope +keycloak_admin.get_client_roles_of_client_scope(client_id=another_client_id, client_roles_owner_id=client_id) + +# Remove client roles assigned to client's scope +keycloak_admin.delete_client_roles_of_client_scope(client_id=another_client_id, client_roles_owner_id=client_id, roles=client_roles) + # Get all ID Providers idps = keycloak_admin.get_idps() diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 50299b6b..15423a1a 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1639,6 +1639,63 @@ def get_realm_roles_of_client_scope(self, client_id): ) return raise_error_from_response(data_raw, KeycloakGetError) + def assign_client_roles_to_client_scope(self, client_id, client_roles_owner_id, roles): + """Assign client roles to a client's scope. + + :param client_id: id of client (not client-id) who is assigned the roles + :param client_roles_owner_id: id of client (not client-id) who has the roles + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client": client_roles_owner_id, + } + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[204]) + + def delete_client_roles_of_client_scope(self, client_id, client_roles_owner_id, roles): + """Delete client roles of a client's scope. + + :param client_id: id of client (not client-id) who is assigned the roles + :param client_roles_owner_id: id of client (not client-id) who has the roles + :param roles: roles list or role (use RoleRepresentation) + :return: Keycloak server response + """ + payload = roles if isinstance(roles, list) else [roles] + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client": client_roles_owner_id, + } + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + + def get_client_roles_of_client_scope(self, client_id, client_roles_owner_id): + """Get all client roles for a client's scope. + + :param client_id: id of client (not client-id) + :param client_roles_owner_id: id of client (not client-id) who has the roles + :return: Keycloak server response (array RoleRepresentation) + """ + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client": client_roles_owner_id, + } + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + def assign_realm_roles(self, user_id, roles): """Assign realm roles to a user. diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index f7d4f657..f2a2188d 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -92,6 +92,9 @@ URL_ADMIN_CLIENT_ROLE_GROUPS = URL_ADMIN_CLIENT + "/roles/{role-name}/groups" URL_ADMIN_CLIENT_MANAGEMENT_PERMISSIONS = URL_ADMIN_CLIENT + "/management/permissions" URL_ADMIN_CLIENT_SCOPE_MAPPINGS_REALM_ROLES = URL_ADMIN_CLIENT + "/scope-mappings/realm" +URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES = ( + URL_ADMIN_CLIENT + "/scope-mappings/clients/{client}" +) URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index e9b093ca..0f5af955 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1088,6 +1088,62 @@ def test_client_scope_realm_roles(admin: KeycloakAdmin, realm: str): assert len(roles) == 0 +def test_client_scope_client_roles(admin: KeycloakAdmin, realm: str, client: str): + """Test client assignment of other client roles.""" + admin.realm_name = realm + + client_id = admin.create_client( + payload={"name": "role-testing-client", "clientId": "role-testing-client"} + ) + + # Test get client roles + roles = admin.get_client_roles_of_client_scope(client_id, client) + assert len(roles) == 0, roles + + # create client role for test + client_role_id = admin.create_client_role( + client_role_id=client, payload={"name": "client-role-test"}, skip_exists=True + ) + assert client_role_id, client_role_id + + # Test client role assignment to other client + with pytest.raises(KeycloakPostError) as err: + admin.assign_client_roles_to_client_scope( + client_id=client_id, client_roles_owner_id=client, roles=["bad"] + ) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.assign_client_roles_to_client_scope( + client_id=client_id, + client_roles_owner_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test")], + ) + assert res == dict(), res + + roles = admin.get_client_roles_of_client_scope( + client_id=client_id, client_roles_owner_id=client + ) + assert len(roles) == 1 + client_role_names = [x["name"] for x in roles] + assert "client-role-test" in client_role_names, client_role_names + + # Test remove realm role of client + with pytest.raises(KeycloakDeleteError) as err: + admin.delete_client_roles_of_client_scope( + client_id=client_id, client_roles_owner_id=client, roles=["bad"] + ) + assert err.match('500: b\'{"error":"unknown_error"}\'') + res = admin.delete_client_roles_of_client_scope( + client_id=client_id, + client_roles_owner_id=client, + roles=[admin.get_client_role(client_id=client, role_name="client-role-test")], + ) + assert res == dict(), res + roles = admin.get_client_roles_of_client_scope( + client_id=client_id, client_roles_owner_id=client + ) + assert len(roles) == 0 + + def test_client_roles(admin: KeycloakAdmin, client: str): """Test client roles.""" # Test get client roles From acd457ef39ca04c8a1c7fb4e0dc44642945cc046 Mon Sep 17 00:00:00 2001 From: Merle Nerger Date: Fri, 19 Aug 2022 14:47:57 +0200 Subject: [PATCH 247/566] docs: fixed docstrings stating incorrect return types for get_client_role(s) and get_realm_role(s) --- src/keycloak/keycloak_admin.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 50299b6b..9efa4457 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1306,7 +1306,7 @@ def get_realm_roles(self): RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: Keycloak server response (RoleRepresentation) + :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) @@ -1329,12 +1329,11 @@ def get_realm_role_members(self, role_name, query=None): def get_client_roles(self, client_id): """Get all roles for the client. - :param client_id: id of client (not client-id) - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: Keycloak server response (RoleRepresentation) + :param client_id: id of client (not client-id) + :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) @@ -1345,13 +1344,12 @@ def get_client_role(self, client_id, role_name): This is required for further actions with this role. - :param client_id: id of client (not client-id) - :param role_name: role’s name (not id!) - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: role_id + :param client_id: id of client (not client-id) + :param role_name: role’s name (not id!) + :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id, "role-name": role_name} data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLE.format(**params_path)) @@ -1517,11 +1515,11 @@ def create_realm_role(self, payload, skip_exists=False): def get_realm_role(self, role_name): """Get realm role by role name. - :param role_name: role's name, not id! - RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation - :return: role_id + + :param role_name: role's name, not id! + :return: Keycloak server response (RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "role-name": role_name} data_raw = self.raw_get( From 739e9abfbebaeff42df981b0ebfadd31fe538f61 Mon Sep 17 00:00:00 2001 From: Merle Nerger Date: Thu, 18 Aug 2022 17:04:17 +0200 Subject: [PATCH 248/566] feat: added missing functionality to include attributes when returning realm roles according to specifications --- src/keycloak/keycloak_admin.py | 57 ++++++++++++++++++++++++---------- tests/test_keycloak_admin.py | 57 ++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+), 16 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 9efa4457..770d3ce9 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -578,17 +578,21 @@ def get_user(self, user_id): data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) - def get_user_groups(self, user_id): + def get_user_groups(self, user_id, brief_representation=True): """Get user groups. Returns a list of groups of which the user is a member :param user_id: User id + :param brief_representation: whether to omit attributes in the response :return: user groups list """ + params = {"briefRepresentation": brief_representation} params_path = {"realm-name": self.realm_name, "id": user_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_USER_GROUPS.format(**params_path)) + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_USER_GROUPS.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def update_user(self, user_id, payload): @@ -1300,16 +1304,20 @@ def get_client_installation_provider(self, client_id, provider_id): ) return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) - def get_realm_roles(self): + def get_realm_roles(self, brief_representation=True): """Get all roles for the realm or client. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path)) + params = {"briefRepresentation": brief_representation} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_realm_role_members(self, role_name, query=None): @@ -1326,17 +1334,21 @@ def get_realm_role_members(self, role_name, query=None): urls_patterns.URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query ) - def get_client_roles(self, client_id): + def get_client_roles(self, client_id, brief_representation=True): """Get all roles for the client. RoleRepresentation https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_rolerepresentation :param client_id: id of client (not client-id) + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": client_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path)) + params = {"briefRepresentation": brief_representation} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def get_client_role(self, client_id, role_name): @@ -1689,15 +1701,17 @@ def get_available_realm_roles_of_user(self, user_id): ) return raise_error_from_response(data_raw, KeycloakGetError) - def get_composite_realm_roles_of_user(self, user_id): + def get_composite_realm_roles_of_user(self, user_id, brief_representation=True): """Get all composite (i.e. implicit) realm roles for a user. :param user_id: id of user + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": user_id} + params = {"briefRepresentation": brief_representation} data_raw = self.raw_get( - urls_patterns.URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path) + urls_patterns.URL_ADMIN_USER_REALM_ROLES_COMPOSITE.format(**params_path), **params ) return raise_error_from_response(data_raw, KeycloakGetError) @@ -1731,14 +1745,18 @@ def delete_group_realm_roles(self, group_id, roles): ) return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) - def get_group_realm_roles(self, group_id): + def get_group_realm_roles(self, group_id, brief_representation=True): """Get all realm roles for a group. :param user_id: id of the group + :param brief_representation: whether to omit role attributes in the response :return: Keycloak server response (array RoleRepresentation) """ params_path = {"realm-name": self.realm_name, "id": group_id} - data_raw = self.raw_get(urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path)) + params = {"briefRepresentation": brief_representation} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_GROUPS_REALM_ROLES.format(**params_path), **params + ) return raise_error_from_response(data_raw, KeycloakGetError) def assign_group_client_roles(self, group_id, client_id, roles): @@ -1806,20 +1824,24 @@ def get_available_client_roles_of_user(self, user_id, client_id): urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_AVAILABLE, user_id, client_id ) - def get_composite_client_roles_of_user(self, user_id, client_id): + def get_composite_client_roles_of_user(self, user_id, client_id, brief_representation=False): """Get composite client role-mappings for a user. :param user_id: id of user :param client_id: id of client (not client-id) + :param brief_representation: whether to omit attributes in the response :return: Keycloak server response (array RoleRepresentation) """ + params = {"briefRepresentation": brief_representation} return self._get_client_roles_of_user( - urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id + urls_patterns.URL_ADMIN_USER_CLIENT_ROLES_COMPOSITE, user_id, client_id, **params ) - def _get_client_roles_of_user(self, client_level_role_mapping_url, user_id, client_id): + def _get_client_roles_of_user( + self, client_level_role_mapping_url, user_id, client_id, **params + ): params_path = {"realm-name": self.realm_name, "id": user_id, "client-id": client_id} - data_raw = self.raw_get(client_level_role_mapping_url.format(**params_path)) + data_raw = self.raw_get(client_level_role_mapping_url.format(**params_path), **params) return raise_error_from_response(data_raw, KeycloakGetError) def delete_client_roles_of_user(self, user_id, client_id, roles): @@ -2854,19 +2876,22 @@ def create_client_authz_client_policy(self, payload, client_id): ) return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) - def get_composite_client_roles_of_group(self, client_id, group_id): + def get_composite_client_roles_of_group(self, client_id, group_id, brief_representation=True): """Get the composite client roles of the given group for the given client. :param client_id: id of the client. :type client_id: str :param group_id: id of the group. :type group_id: str + :param brief_representation: whether to omit attributes in the response + :type brief_representation: bool :return: the composite client roles of the group (list of RoleRepresentation). :rtype: list """ params_path = {"realm-name": self.realm_name, "id": group_id, "client-id": client_id} + params = {"briefRepresentation": brief_representation} data_raw = self.raw_get( - urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path) + urls_patterns.URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE.format(**params_path), **params ) return raise_error_from_response(data_raw, KeycloakGetError) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index e9b093ca..cf5d8b26 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1030,6 +1030,63 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): assert err.match('404: b\'{"error":"Could not find role"}\'') +@pytest.mark.parametrize( + "testcase, arg_brief_repr, includes_attributes", + [ + ("brief True", {"brief_representation": True}, False), + ("brief False", {"brief_representation": False}, True), + ("default", {}, False), + ], +) +def test_role_attributes( + admin: KeycloakAdmin, + realm: str, + client: str, + arg_brief_repr: dict, + includes_attributes: bool, + testcase: str, +): + """Test getting role attributes for bulk calls.""" + # setup + attribute_role = "test-realm-role-w-attr" + test_attrs = {"attr1": ["val1"], "attr2": ["val2-1", "val2-2"]} + role_id = admin.create_realm_role( + payload={"name": attribute_role, "attributes": test_attrs}, + skip_exists=True, + ) + assert role_id, role_id + + cli_role_id = admin.create_client_role( + client, + payload={"name": attribute_role, "attributes": test_attrs}, + skip_exists=True, + ) + assert cli_role_id, cli_role_id + + if not includes_attributes: + test_attrs = None + + # tests + roles = admin.get_realm_roles(**arg_brief_repr) + roles_filtered = [role for role in roles if role["name"] == role_id] + assert roles_filtered, roles_filtered + role = roles_filtered[0] + assert role.get("attributes") == test_attrs, testcase + + roles = admin.get_client_roles(client, **arg_brief_repr) + roles_filtered = [role for role in roles if role["name"] == cli_role_id] + assert roles_filtered, roles_filtered + role = roles_filtered[0] + assert role.get("attributes") == test_attrs, testcase + + # cleanup + res = admin.delete_realm_role(role_name=attribute_role) + assert res == dict(), res + + res = admin.delete_client_role(client, role_name=attribute_role) + assert res == dict(), res + + def test_client_scope_realm_roles(admin: KeycloakAdmin, realm: str): """Test client realm roles.""" admin.realm_name = realm From 3c4e0f1443b9215ac360571d4d9c9889ea799328 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Fri, 19 Aug 2022 19:42:03 +0000 Subject: [PATCH 249/566] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf8293b1..ca19f4e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.4.0 (2022-08-19) + +### Feat + +- add client scope-mappings client roles operations + ## v2.3.0 (2022-08-13) ### Feat From dfd5381f6e64fcfa8b0a2d09a55a494ff598de30 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Fri, 19 Aug 2022 19:49:58 +0000 Subject: [PATCH 250/566] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca19f4e9..0f90b910 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## v2.5.0 (2022-08-19) + +### Feat + +- added missing functionality to include attributes when returning realm roles according to specifications + ## v2.4.0 (2022-08-19) ### Feat From 40ac02ae3bf986190304dd8436b470055e490d7e Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sat, 27 Aug 2022 17:56:38 +0000 Subject: [PATCH 251/566] docs: finished off docstrings in the source --- src/keycloak/keycloak_openid.py | 214 ++++++++++++++++++++++++-------- src/keycloak/uma_permissions.py | 96 +++++++++++--- 2 files changed, 240 insertions(+), 70 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 82e980ce..1c81871d 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -81,7 +81,25 @@ def __init__( proxies=None, timeout=60, ): - """Init method.""" + """Init method. + + :param server_url: Keycloak server url + :type server_url: str + :param client_id: client id + :type client_id: str + :param realm_name: realm name + :type realm_name: str + :param client_secret_key: client secret key + :type client_secret_key: str + :param verify: True if want check connection SSL + :type verify: bool + :param custom_headers: dict of custom header to pass to each HTML request + :type custom_headers: dict + :param proxies: dict of proxies to sent the request by. + :type proxies: dict + :param timeout: connection timeout in seconds + :type timeout: int + """ self.client_id = client_id self.client_secret_key = client_secret_key self.realm_name = realm_name @@ -94,7 +112,11 @@ def __init__( @property def client_id(self): - """Get client id.""" + """Get client id. + + :returns: Client id + :rtype: str + """ return self._client_id @client_id.setter @@ -103,7 +125,11 @@ def client_id(self, value): @property def client_secret_key(self): - """Get the client secret key.""" + """Get the client secret key. + + :returns: Client secret key + :rtype: str + """ return self._client_secret_key @client_secret_key.setter @@ -112,7 +138,11 @@ def client_secret_key(self, value): @property def realm_name(self): - """Get the realm name.""" + """Get the realm name. + + :returns: Realm name + :rtype: str + """ return self._realm_name @realm_name.setter @@ -121,7 +151,11 @@ def realm_name(self, value): @property def connection(self): - """Get connection.""" + """Get connection. + + :returns: Connection manager object + :rtype: ConnectionManager + """ return self._connection @connection.setter @@ -130,7 +164,11 @@ def connection(self, value): @property def authorization(self): - """Get authorization.""" + """Get authorization. + + :returns: The authorization manager + :rtype: Authorization + """ return self._authorization @authorization.setter @@ -140,8 +178,10 @@ def authorization(self, value): def _add_secret_key(self, payload): """Add secret key if exists. - :param payload: - :return: + :param payload: Payload + :type payload: dict + :returns: Payload with the secret key + :rtype: dict """ if self.client_secret_key: payload.update({"client_secret": self.client_secret_key}) @@ -151,18 +191,24 @@ def _add_secret_key(self, payload): def _build_name_role(self, role): """Build name of a role. - :param role: - :return: + :param role: Role name + :type role: str + :returns: Role path + :rtype: str """ return self.client_id + "/" + role def _token_info(self, token, method_token_info, **kwargs): """Getter for the token data. - :param token: - :param method_token_info: - :param kwargs: - :return: + :param token: Token + :type token: str + :param method_token_info: Token info method to use + :type method_token_info: str + :param kwargs: Additional keyword arguments + :type kwargs: dict + :returns: Token info + :rtype: dict """ if method_token_info == "introspect": token_info = self.introspect(token) @@ -178,7 +224,8 @@ def well_known(self): endpoint. It lists endpoints and other configuration options relevant to the OpenID Connect implementation in Keycloak. - :return It lists endpoints and other configuration options relevant. + :returns: It lists endpoints and other configuration options relevant + :rtype: dict """ params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_WELL_KNOWN.format(**params_path)) @@ -190,9 +237,9 @@ def auth_url(self, redirect_uri, scope="email", state=""): :param redirect_uri: Redirect url to receive oauth code :type redirect_uri: str :param scope: Scope of authorization request, split with the blank space - :type: scope: str + :type scope: str :param state: State will be returned to the redirect_uri - :type: str + :type state: str :returns: Authorization URL Full Build :rtype: str """ @@ -224,13 +271,22 @@ def token( http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint - :param username: - :param password: - :param grant_type: - :param code: - :param redirect_uri: - :param totp: - :return: + :param username: Username + :type username: str + :param password: Password + :type password: str + :param grant_type: Grant type + :type grant_type: str + :param code: Code + :type code: str + :param redirect_uri: Redirect URI + :type redirect_uri: str + :param totp: Time-based one-time password + :type totp: int + :param extra: Additional extra arguments + :type extra: dict + :returns: Keycloak token + :rtype: dict """ params_path = {"realm-name": self.realm_name} payload = { @@ -261,9 +317,12 @@ def refresh_token(self, refresh_token, grant_type=["refresh_token"]): http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint - :param refresh_token: - :param grant_type: - :return: + :param refresh_token: Refresh token from Keycloak + :type refresh_token: str + :param grant_type: Grant type + :type grant_type: str + :returns: New token + :rtype: dict """ params_path = {"realm-name": self.realm_name} payload = { @@ -281,11 +340,16 @@ def exchange_token(self, token: str, client_id: str, audience: str, subject: str Use a token to obtain an entirely different token. See https://www.keycloak.org/docs/latest/securing_apps/index.html#_token-exchange - :param token: - :param client_id: - :param audience: - :param subject: - :return: + :param token: Access token + :type token: str + :param client_id: Client id + :type client_id: str + :param audience: Audience + :type audience: str + :param subject: Subject + :type subject: str + :returns: Exchanged token + :rtype: dict """ params_path = {"realm-name": self.realm_name} payload = { @@ -308,8 +372,10 @@ def userinfo(self, token): http://openid.net/specs/openid-connect-core-1_0.html#UserInfo - :param token: - :return: + :param token: Access token + :type token: str + :returns: Userinfo object + :rtype: dict """ self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name} @@ -319,8 +385,10 @@ def userinfo(self, token): def logout(self, refresh_token): """Log out the authenticated user. - :param refresh_token: - :return: + :param refresh_token: Refresh token from Keycloak + :type refresh_token: str + :returns: Keycloak server response + :rtype: dict """ params_path = {"realm-name": self.realm_name} payload = {"client_id": self.client_id, "refresh_token": refresh_token} @@ -337,7 +405,8 @@ def certs(self): https://tools.ietf.org/html/rfc7517 - :return: + :returns: Certificates + :rtype: dict """ params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_CERTS.format(**params_path)) @@ -348,7 +417,8 @@ def public_key(self): The public key is exposed by the realm page directly. - :return: + :returns: The public key + :rtype: str """ params_path = {"realm-name": self.realm_name} data_raw = self.connection.raw_get(URL_REALM.format(**params_path)) @@ -363,7 +433,12 @@ def entitlement(self, token, resource_server_id): authorization policies associated with the resources being requested. With an RPT, client applications can gain access to protected resources at the resource server. - :return: + :param token: Access token + :type token: str + :param resource_server_id: Resource server ID + :type resource_server_id: str + :returns: Entitlements + :rtype: dict """ self.connection.add_param_headers("Authorization", "Bearer " + token) params_path = {"realm-name": self.realm_name, "resource-server-id": resource_server_id} @@ -382,11 +457,16 @@ def introspect(self, token, rpt=None, token_type_hint=None): https://tools.ietf.org/html/rfc7662 - :param token: - :param rpt: - :param token_type_hint: + :param token: Access token + :type token: str + :param rpt: Requesting party token + :type rpt: str + :param token_type_hint: Token type hint + :type token_type_hint: str - :return: + :returns: Token info + :rtype: dict + :raises KeycloakRPTNotFound: In case of RPT not specified """ params_path = {"realm-name": self.realm_name} payload = {"client_id": self.client_id, "token": token} @@ -415,10 +495,16 @@ def decode_token(self, token, key, algorithms=["RS256"], **kwargs): https://tools.ietf.org/html/rfc7517 - :param token: - :param key: - :param algorithms: - :return: + :param token: Keycloak token + :type token: str + :param key: Decode key + :type key: str + :param algorithms: Algorithms to use for decoding + :type algorithms: list[str] + :param kwargs: Keyword arguments + :type kwargs: dict + :returns: Decoded token + :rtype: dict """ return jwt.decode(token, key, algorithms=algorithms, audience=self.client_id, **kwargs) @@ -426,7 +512,7 @@ def load_authorization_config(self, path): """Load Keycloak settings (authorization). :param path: settings file (json) - :return: + :type path: str """ with open(path, "r") as fp: authorization_json = json.load(fp) @@ -436,8 +522,16 @@ def load_authorization_config(self, path): def get_policies(self, token, method_token_info="introspect", **kwargs): """Get policies by user token. - :param token: user token - :return: policies list + :param token: User token + :type token: str + :param method_token_info: Method for token info decoding + :type method_token_info: str + :param kwargs: Additional keyword arguments + :type kwargs: dict + :return: Policies + :rtype: dict + :raises KeycloakAuthorizationConfigError: In case of bad authorization configuration + :raises KeycloakInvalidTokenError: In case of bad token """ if not self.authorization.policies: raise KeycloakAuthorizationConfigError( @@ -467,9 +561,15 @@ def get_permissions(self, token, method_token_info="introspect", **kwargs): """Get permission by user token. :param token: user token + :type token: str :param method_token_info: Decode token method + :type method_token_info: str :param kwargs: parameters for decode - :return: permissions list + :type kwargs: dict + :returns: permissions list + :rtype: list + :raises KeycloakAuthorizationConfigError: In case of bad authorization configuration + :raises KeycloakInvalidTokenError: In case of bad token """ if not self.authorization.policies: raise KeycloakAuthorizationConfigError( @@ -504,8 +604,11 @@ def uma_permissions(self, token, permissions=""): http://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint :param token: user token + :type token: str :param permissions: list of uma permissions list(resource:scope) requested by the user - :return: permissions list + :type permissions: str + :returns: Keycloak server response + :rtype: dict """ permission = build_permission_param(permissions) @@ -525,8 +628,13 @@ def has_uma_access(self, token, permissions): """Determine whether user has uma permissions with specified user token. :param token: user token + :type token: str :param permissions: list of uma permissions (resource:scope) - :return: auth status + :type permissions: str + :return: Authentication status + :rtype: AuthStatus + :raises KeycloakAuthenticationError: In case of failed authentication + :raises KeycloakPostError: In case of failed request to Keycloak """ needed = build_permission_param(permissions) try: diff --git a/src/keycloak/uma_permissions.py b/src/keycloak/uma_permissions.py index 94779f16..eadcd722 100644 --- a/src/keycloak/uma_permissions.py +++ b/src/keycloak/uma_permissions.py @@ -48,7 +48,16 @@ class UMAPermission: """ def __init__(self, permission=None, resource="", scope=""): - """Init method.""" + """Init method. + + :param permission: Permission + :type permission: UMAPermission + :param resource: Resource + :type resource: str + :param scope: Scope + :type scope: str + :raises PermissionDefinitionError: In case bad permission definition + """ self.resource = resource self.scope = scope @@ -63,26 +72,55 @@ def __init__(self, permission=None, resource="", scope=""): self.scope = str(permission.scope) def __str__(self): - """Str method.""" + """Str method. + + :returns: String representation + :rtype: str + """ scope = self.scope if scope: scope = "#" + scope return "{}{}".format(self.resource, scope) def __eq__(self, __o: object) -> bool: - """Eq method.""" + """Eq method. + + :param __o: The other object + :type __o: object + :returns: Equality boolean + :rtype: bool + """ return str(self) == str(__o) def __repr__(self) -> str: - """Repr method.""" + """Repr method. + + :returns: The object representation + :rtype: str + """ return self.__str__() def __hash__(self) -> int: - """Hash method.""" + """Hash method. + + :returns: Hash of the object + :rtype: int + """ return hash(str(self)) - def __call__(self, permission=None, resource="", scope="") -> object: - """Call method.""" + def __call__(self, permission=None, resource="", scope="") -> "UMAPermission": + """Call method. + + :param permission: Permission + :type permission: UMAPermission + :param resource: Resource + :type resource: str + :param scope: Scope + :type scope: str + :returns: The combined UMA permission + :rtype: UMAPermission + :raises PermissionDefinitionError: In case bad permission definition + """ result_resource = self.resource result_scope = self.scope @@ -114,7 +152,11 @@ class Resource(UMAPermission): """ def __init__(self, resource): - """Init method.""" + """Init method. + + :param resource: Resource + :type resource: str + """ super().__init__(resource=resource) @@ -128,7 +170,11 @@ class Scope(UMAPermission): """ def __init__(self, scope): - """Init method.""" + """Init method. + + :param scope: Scope + :type scope: str + """ super().__init__(scope=scope) @@ -147,17 +193,33 @@ class AuthStatus: """ def __init__(self, is_logged_in, is_authorized, missing_permissions): - """Init method.""" + """Init method. + + :param is_logged_in: Is logged in indicator + :type is_logged_in: bool + :param is_authorized: Is authorized indicator + :type is_authorized: bool + :param missing_permissions: Missing permissions + :type missing_permissions: set + """ self.is_logged_in = is_logged_in self.is_authorized = is_authorized self.missing_permissions = missing_permissions def __bool__(self): - """Bool method.""" + """Bool method. + + :returns: Boolean representation + :rtype: bool + """ return self.is_authorized def __repr__(self): - """Repr method.""" + """Repr method. + + :returns: The object representation + :rtype: str + """ return ( f"AuthStatus(" f"is_authorized={self.is_authorized}, " @@ -169,11 +231,11 @@ def __repr__(self): def build_permission_param(permissions): """Transform permissions to a set, so they are usable for requests. - :param permissions: either str (resource#scope), - iterable[str] (resource#scope), - dict[str,str] (resource: scope), - dict[str,iterable[str]] (resource: scopes) - :return: result bool + :param permissions: Permissions + :type permissions: str | Iterable[str] | dict[str, str] | dict[str, Iterabble[str]] + :returns: Permission parameters + :rtype: set + :raises KeycloakPermissionFormatError: In case of bad permission format """ if permissions is None or permissions == "": return set() From 0b79b5771ff7accf988e554de4eda195a77d5fba Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sat, 27 Aug 2022 18:28:32 +0000 Subject: [PATCH 252/566] test: updated docstrings to pass checks --- tests/conftest.py | 180 ++++++++++++++++++++--- tests/test_keycloak_admin.py | 263 +++++++++++++++++++++++++++++----- tests/test_keycloak_openid.py | 110 +++++++++++--- 3 files changed, 484 insertions(+), 69 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 39bd5846..e0d93ead 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,7 +35,17 @@ def __init__( username: str = os.environ["KEYCLOAK_ADMIN"], password: str = os.environ["KEYCLOAK_ADMIN_PASSWORD"], ): - """Init method.""" + """Init method. + + :param host: Hostname + :type host: str + :param port: Port + :type port: str + :param username: Admin username + :type username: str + :param password: Admin password + :type password: str + """ self.KEYCLOAK_HOST = host self.KEYCLOAK_PORT = port self.KEYCLOAK_ADMIN = username @@ -43,54 +53,96 @@ def __init__( @property def KEYCLOAK_HOST(self): - """Hostname getter.""" + """Hostname getter. + + :returns: Keycloak host + :rtype: str + """ return self._KEYCLOAK_HOST @KEYCLOAK_HOST.setter def KEYCLOAK_HOST(self, value: str): - """Hostname setter.""" + """Hostname setter. + + :param value: Keycloak host + :type value: str + """ self._KEYCLOAK_HOST = value @property def KEYCLOAK_PORT(self): - """Port getter.""" + """Port getter. + + :returns: Keycloak port + :rtype: str + """ return self._KEYCLOAK_PORT @KEYCLOAK_PORT.setter def KEYCLOAK_PORT(self, value: str): - """Port setter.""" + """Port setter. + + :param value: Keycloak port + :type value: str + """ self._KEYCLOAK_PORT = value @property def KEYCLOAK_ADMIN(self): - """Admin username getter.""" + """Admin username getter. + + :returns: Admin username + :rtype: str + """ return self._KEYCLOAK_ADMIN @KEYCLOAK_ADMIN.setter def KEYCLOAK_ADMIN(self, value: str): - """Admin username setter.""" + """Admin username setter. + + :param value: Admin username + :type value: str + """ self._KEYCLOAK_ADMIN = value @property def KEYCLOAK_ADMIN_PASSWORD(self): - """Admin password getter.""" + """Admin password getter. + + :returns: Admin password + :rtype: str + """ return self._KEYCLOAK_ADMIN_PASSWORD @KEYCLOAK_ADMIN_PASSWORD.setter def KEYCLOAK_ADMIN_PASSWORD(self, value: str): - """Admin password setter.""" + """Admin password setter. + + :param value: Admin password + :type value: str + """ self._KEYCLOAK_ADMIN_PASSWORD = value @pytest.fixture def env(): - """Fixture for getting the test environment configuration object.""" + """Fixture for getting the test environment configuration object. + + :returns: Keycloak test environment object + :rtype: KeycloakTestEnv + """ return KeycloakTestEnv() @pytest.fixture def admin(env: KeycloakTestEnv): - """Fixture for initialized KeycloakAdmin class.""" + """Fixture for initialized KeycloakAdmin class. + + :param env: Keycloak test environment + :type env: KeycloakTestEnv + :returns: Keycloak admin + :rtype: KeycloakAdmin + """ return KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -100,7 +152,17 @@ def admin(env: KeycloakTestEnv): @pytest.fixture def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): - """Fixture for initialized KeycloakOpenID class.""" + """Fixture for initialized KeycloakOpenID class. + + :param env: Keycloak test environment + :type env: KeycloakTestEnv + :param realm: Keycloak realm + :type realm: str + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :yields: Keycloak OpenID client + :rtype: KeycloakOpenID + """ # Set the realm admin.realm_name = realm # Create client @@ -126,7 +188,17 @@ def oid(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): @pytest.fixture def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): - """Fixture for an initialized KeycloakOpenID class and a random user credentials.""" + """Fixture for an initialized KeycloakOpenID class and a random user credentials. + + :param env: Keycloak test environment + :type env: KeycloakTestEnv + :param realm: Keycloak realm + :type realm: str + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :yields: Keycloak OpenID client with user credentials + :rtype: Tuple[KeycloakOpenID, str, str] + """ # Set the realm admin.realm_name = realm # Create client @@ -173,7 +245,17 @@ def oid_with_credentials(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin) @pytest.fixture def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: KeycloakAdmin): - """Fixture for an initialized KeycloakOpenID class and a random user credentials.""" + """Fixture for an initialized KeycloakOpenID class and a random user credentials. + + :param env: Keycloak test environment + :type env: KeycloakTestEnv + :param realm: Keycloak realm + :type realm: str + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :yields: Keycloak OpenID client configured as an authorization server with client credentials + :rtype: Tuple[KeycloakOpenID, str, str] + """ # Set the realm admin.realm_name = realm # Create client @@ -229,7 +311,13 @@ def oid_with_credentials_authz(env: KeycloakTestEnv, realm: str, admin: Keycloak @pytest.fixture def realm(admin: KeycloakAdmin) -> str: - """Fixture for a new random realm.""" + """Fixture for a new random realm. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :yields: Keycloak realm + :rtype: str + """ realm_name = str(uuid.uuid4()) admin.create_realm(payload={"realm": realm_name, "enabled": True}) yield realm_name @@ -238,7 +326,15 @@ def realm(admin: KeycloakAdmin) -> str: @pytest.fixture def user(admin: KeycloakAdmin, realm: str) -> str: - """Fixture for a new random user.""" + """Fixture for a new random user. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :yields: Keycloak user + :rtype: str + """ admin.realm_name = realm username = str(uuid.uuid4()) user_id = admin.create_user(payload={"username": username, "email": f"{username}@test.test"}) @@ -248,7 +344,15 @@ def user(admin: KeycloakAdmin, realm: str) -> str: @pytest.fixture def group(admin: KeycloakAdmin, realm: str) -> str: - """Fixture for a new random group.""" + """Fixture for a new random group. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :yields: Keycloak group + :rtype: str + """ admin.realm_name = realm group_name = str(uuid.uuid4()) group_id = admin.create_group(payload={"name": group_name}) @@ -258,7 +362,15 @@ def group(admin: KeycloakAdmin, realm: str) -> str: @pytest.fixture def client(admin: KeycloakAdmin, realm: str) -> str: - """Fixture for a new random client.""" + """Fixture for a new random client. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :yields: Keycloak client id + :rtype: str + """ admin.realm_name = realm client = str(uuid.uuid4()) client_id = admin.create_client(payload={"name": client, "clientId": client}) @@ -268,7 +380,17 @@ def client(admin: KeycloakAdmin, realm: str) -> str: @pytest.fixture def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str: - """Fixture for a new random client role.""" + """Fixture for a new random client role. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + :yields: Keycloak client role + :rtype: str + """ admin.realm_name = realm role = str(uuid.uuid4()) admin.create_client_role(client, {"name": role, "composite": False}) @@ -278,7 +400,19 @@ def client_role(admin: KeycloakAdmin, realm: str, client: str) -> str: @pytest.fixture def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_role: str) -> str: - """Fixture for a new random composite client role.""" + """Fixture for a new random composite client role. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + :param client_role: Keycloak client role + :type client_role: str + :yields: Composite client role + :rtype: str + """ admin.realm_name = realm role = str(uuid.uuid4()) admin.create_client_role(client, {"name": role, "composite": True}) @@ -290,7 +424,11 @@ def composite_client_role(admin: KeycloakAdmin, realm: str, client: str, client_ @pytest.fixture def selfsigned_cert(): - """Generate self signed certificate for a hostname, and optional IP addresses.""" + """Generate self signed certificate for a hostname, and optional IP addresses. + + :returns: Selfsigned certificate + :rtype: Tuple[str, str] + """ hostname = "testcert" ip_addresses = None key = None diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index b55e1c79..f2865f9d 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -22,7 +22,11 @@ def test_keycloak_version(): def test_keycloak_admin_bad_init(env): - """Test keycloak admin bad init.""" + """Test keycloak admin bad init. + + :param env: Environment fixture + :type env: KeycloakTestEnv + """ with pytest.raises(TypeError) as err: KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", @@ -43,7 +47,11 @@ def test_keycloak_admin_bad_init(env): def test_keycloak_admin_init(env): - """Test keycloak admin init.""" + """Test keycloak admin init. + + :param env: Environment fixture + :type env: KeycloakTestEnv + """ admin = KeycloakAdmin( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", username=env.KEYCLOAK_ADMIN, @@ -118,7 +126,11 @@ def test_keycloak_admin_init(env): def test_realms(admin: KeycloakAdmin): - """Test realms.""" + """Test realms. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + """ # Get realms realms = admin.get_realms() assert len(realms) == 1, realms @@ -183,7 +195,13 @@ def test_realms(admin: KeycloakAdmin): def test_import_export_realms(admin: KeycloakAdmin, realm: str): - """Test import and export of realms.""" + """Test import and export of realms. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm realm_export = admin.export_realm(export_clients=True, export_groups_and_role=True) @@ -201,7 +219,13 @@ def test_import_export_realms(admin: KeycloakAdmin, realm: str): def test_users(admin: KeycloakAdmin, realm: str): - """Test users.""" + """Test users. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Check no users present @@ -293,7 +317,13 @@ def test_users(admin: KeycloakAdmin, realm: str): def test_users_pagination(admin: KeycloakAdmin, realm: str): - """Test user pagination.""" + """Test user pagination. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm for ind in range(admin.PAGE_SIZE + 50): @@ -311,7 +341,13 @@ def test_users_pagination(admin: KeycloakAdmin, realm: str): def test_idps(admin: KeycloakAdmin, realm: str): - """Test IDPs.""" + """Test IDPs. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Create IDP @@ -383,7 +419,13 @@ def test_idps(admin: KeycloakAdmin, realm: str): def test_user_credentials(admin: KeycloakAdmin, user: str): - """Test user credentials.""" + """Test user credentials. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param user: Keycloak user + :type user: str + """ res = admin.set_user_password(user_id=user, password="booya", temporary=True) assert res == dict(), res @@ -411,7 +453,13 @@ def test_user_credentials(admin: KeycloakAdmin, user: str): def test_social_logins(admin: KeycloakAdmin, user: str): - """Test social logins.""" + """Test social logins. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param user: Keycloak user + :type user: str + """ res = admin.add_user_social_login( user_id=user, provider_id="gitlab", provider_userid="test", provider_username="test" ) @@ -451,7 +499,11 @@ def test_social_logins(admin: KeycloakAdmin, user: str): def test_server_info(admin: KeycloakAdmin): - """Test server info.""" + """Test server info. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + """ info = admin.get_server_info() assert set(info.keys()) == { "systemInfo", @@ -471,7 +523,13 @@ def test_server_info(admin: KeycloakAdmin): def test_groups(admin: KeycloakAdmin, user: str): - """Test groups.""" + """Test groups. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param user: Keycloak user + :type user: str + """ # Test get groups groups = admin.get_groups() assert len(groups) == 0 @@ -615,7 +673,13 @@ def test_groups(admin: KeycloakAdmin, user: str): def test_clients(admin: KeycloakAdmin, realm: str): - """Test clients.""" + """Test clients. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Test get clients @@ -875,7 +939,13 @@ def test_clients(admin: KeycloakAdmin, realm: str): def test_realm_roles(admin: KeycloakAdmin, realm: str): - """Test realm roles.""" + """Test realm roles. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Test get realm roles @@ -1031,7 +1101,13 @@ def test_realm_roles(admin: KeycloakAdmin, realm: str): def test_client_roles(admin: KeycloakAdmin, client: str): - """Test client roles.""" + """Test client roles. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param client: Keycloak client + :type client: str + """ # Test get client roles res = admin.get_client_roles(client_id=client) assert len(res) == 0 @@ -1194,7 +1270,14 @@ def test_client_roles(admin: KeycloakAdmin, client: str): def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): - """Test enable token exchange.""" + """Test enable token exchange. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :raises AssertionError: In case of bad configuration + """ # Test enabling token exchange between two confidential clients admin.realm_name = realm @@ -1283,7 +1366,13 @@ def test_enable_token_exchange(admin: KeycloakAdmin, realm: str): def test_email(admin: KeycloakAdmin, user: str): - """Test email.""" + """Test email. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param user: Keycloak user + :type user: str + """ # Emails will fail as we don't have SMTP test setup with pytest.raises(KeycloakPutError) as err: admin.send_update_account(user_id=user, payload=dict()) @@ -1296,7 +1385,11 @@ def test_email(admin: KeycloakAdmin, user: str): def test_get_sessions(admin: KeycloakAdmin): - """Test get sessions.""" + """Test get sessions. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + """ sessions = admin.get_sessions(user_id=admin.get_user_id(username=admin.username)) assert len(sessions) >= 1 with pytest.raises(KeycloakGetError) as err: @@ -1305,7 +1398,13 @@ def test_get_sessions(admin: KeycloakAdmin): def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): - """Test get client installation provider.""" + """Test get client installation provider. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param client: Keycloak client + :type client: str + """ with pytest.raises(KeycloakGetError) as err: admin.get_client_installation_provider(client_id=client, provider_id="bad") assert err.match('404: b\'{"error":"Unknown Provider"}\'') @@ -1324,7 +1423,13 @@ def test_get_client_installation_provider(admin: KeycloakAdmin, client: str): def test_auth_flows(admin: KeycloakAdmin, realm: str): - """Test auth flows.""" + """Test auth flows. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm res = admin.get_authentication_flows() @@ -1471,7 +1576,13 @@ def test_auth_flows(admin: KeycloakAdmin, realm: str): def test_authentication_configs(admin: KeycloakAdmin, realm: str): - """Test authentication configs.""" + """Test authentication configs. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Test list of auth providers @@ -1503,7 +1614,13 @@ def test_authentication_configs(admin: KeycloakAdmin, realm: str): def test_sync_users(admin: KeycloakAdmin, realm: str): - """Test sync users.""" + """Test sync users. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Only testing the error message @@ -1513,7 +1630,13 @@ def test_sync_users(admin: KeycloakAdmin, realm: str): def test_client_scopes(admin: KeycloakAdmin, realm: str): - """Test client scopes.""" + """Test client scopes. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Test get client scopes @@ -1651,7 +1774,13 @@ def test_client_scopes(admin: KeycloakAdmin, realm: str): def test_components(admin: KeycloakAdmin, realm: str): - """Test components.""" + """Test components. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm # Test get components @@ -1702,7 +1831,13 @@ def test_components(admin: KeycloakAdmin, realm: str): def test_keys(admin: KeycloakAdmin, realm: str): - """Test keys.""" + """Test keys. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm assert set(admin.get_keys()["active"].keys()) == {"AES", "HS256", "RS256", "RSA-OAEP"} assert {k["algorithm"] for k in admin.get_keys()["keys"]} == { @@ -1714,7 +1849,13 @@ def test_keys(admin: KeycloakAdmin, realm: str): def test_events(admin: KeycloakAdmin, realm: str): - """Test events.""" + """Test events. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm events = admin.get_events() @@ -1734,7 +1875,13 @@ def test_events(admin: KeycloakAdmin, realm: str): def test_auto_refresh(admin: KeycloakAdmin, realm: str): - """Test auto refresh token.""" + """Test auto refresh token. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ # Test get refresh admin.auto_refresh_token = list() admin.connection = ConnectionManager( @@ -1818,7 +1965,13 @@ def test_auto_refresh(admin: KeycloakAdmin, realm: str): def test_get_required_actions(admin: KeycloakAdmin, realm: str): - """Test required actions.""" + """Test required actions. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm ractions = admin.get_required_actions() assert isinstance(ractions, list) @@ -1836,7 +1989,13 @@ def test_get_required_actions(admin: KeycloakAdmin, realm: str): def test_get_required_action_by_alias(admin: KeycloakAdmin, realm: str): - """Test get required action by alias.""" + """Test get required action by alias. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm ractions = admin.get_required_actions() ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") @@ -1846,7 +2005,13 @@ def test_get_required_action_by_alias(admin: KeycloakAdmin, realm: str): def test_update_required_action(admin: KeycloakAdmin, realm: str): - """Test update required action.""" + """Test update required action. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + """ admin.realm_name = realm ra = admin.get_required_action_by_alias("UPDATE_PASSWORD") old = copy.deepcopy(ra) @@ -1860,7 +2025,19 @@ def test_update_required_action(admin: KeycloakAdmin, realm: str): def test_get_composite_client_roles_of_group( admin: KeycloakAdmin, realm: str, client: str, group: str, composite_client_role: str ): - """Test get composite client roles of group.""" + """Test get composite client roles of group. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + :param group: Keycloak group + :type group: str + :param composite_client_role: Composite client role + :type composite_client_role: str + """ admin.realm_name = realm role = admin.get_client_role(client, composite_client_role) admin.assign_group_client_roles(group_id=group, client_id=client, roles=[role]) @@ -1871,7 +2048,19 @@ def test_get_composite_client_roles_of_group( def test_get_role_client_level_children( admin: KeycloakAdmin, realm: str, client: str, composite_client_role: str, client_role: str ): - """Test get children of composite client role.""" + """Test get children of composite client role. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + :param composite_client_role: Composite client role + :type composite_client_role: str + :param client_role: Client role + :type client_role: str + """ admin.realm_name = realm child = admin.get_client_role(client, client_role) parent = admin.get_client_role(client, composite_client_role) @@ -1880,7 +2069,17 @@ def test_get_role_client_level_children( def test_upload_certificate(admin: KeycloakAdmin, realm: str, client: str, selfsigned_cert: tuple): - """Test upload certificate.""" + """Test upload certificate. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + :param selfsigned_cert: Selfsigned certificates + :type selfsigned_cert: tuple + """ admin.realm_name = realm cert, _ = selfsigned_cert cert = cert.decode("utf-8").strip() diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 6bee648d..5a6a2edb 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -22,7 +22,11 @@ def test_keycloak_openid_init(env): - """Test KeycloakOpenId's init method.""" + """Test KeycloakOpenId's init method. + + :param env: Environment fixture + :type env: KeycloakTestEnv + """ oid = KeycloakOpenID( server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", realm_name="master", @@ -37,7 +41,11 @@ def test_keycloak_openid_init(env): def test_well_known(oid: KeycloakOpenID): - """Test the well_known method.""" + """Test the well_known method. + + :param oid: Keycloak OpenID client + :type oid: KeycloakOpenID + """ res = oid.well_known() assert res is not None assert res != dict() @@ -100,7 +108,13 @@ def test_well_known(oid: KeycloakOpenID): def test_auth_url(env, oid: KeycloakOpenID): - """Test the auth_url method.""" + """Test the auth_url method. + + :param env: Environment fixture + :type env: KeycloakTestEnv + :param oid: Keycloak OpenID client + :type oid: KeycloakOpenID + """ res = oid.auth_url(redirect_uri="http://test.test/*") assert ( res @@ -111,7 +125,11 @@ def test_auth_url(env, oid: KeycloakOpenID): def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): - """Test the token method.""" + """Test the token method. + + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) assert token == { @@ -155,7 +173,13 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): def test_exchange_token( oid_with_credentials: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin ): - """Test the exchange token method.""" + """Test the exchange token method. + + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + """ # Verify existing user oid, username, password = oid_with_credentials @@ -197,7 +221,11 @@ def test_exchange_token( def test_logout(oid_with_credentials): - """Test logout.""" + """Test logout. + + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -209,19 +237,34 @@ def test_logout(oid_with_credentials): def test_certs(oid: KeycloakOpenID): - """Test certificates.""" + """Test certificates. + + :param oid: Keycloak OpenID client + :type oid: KeycloakOpenID + """ assert len(oid.certs()["keys"]) == 2 def test_public_key(oid: KeycloakOpenID): - """Test public key.""" + """Test public key. + + :param oid: Keycloak OpenID client + :type oid: KeycloakOpenID + """ assert oid.public_key() is not None def test_entitlement( oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin ): - """Test entitlement.""" + """Test entitlement. + + :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization + server with client credentials + :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + """ oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) resource_server_id = admin.get_client_authz_resources( @@ -233,7 +276,11 @@ def test_entitlement( def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): - """Test introspect.""" + """Test introspect. + + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -247,7 +294,11 @@ def test_introspect(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): - """Test decode token.""" + """Test decode token. + + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials token = oid.token(username=username, password=password) @@ -262,7 +313,12 @@ def test_decode_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): - """Test load authorization config.""" + """Test load authorization config. + + :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization + server with client credentials + :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials_authz oid.load_authorization_config(path="tests/data/authz_settings.json") @@ -277,7 +333,12 @@ def test_load_authorization_config(oid_with_credentials_authz: Tuple[KeycloakOpe def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): - """Test get policies.""" + """Test get policies. + + :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization + server with client credentials + :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -310,7 +371,12 @@ def test_get_policies(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): - """Test get policies.""" + """Test get policies. + + :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization + server with client credentials + :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -354,7 +420,12 @@ def test_get_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str]): - """Test UMA permissions.""" + """Test UMA permissions. + + :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization + server with client credentials + :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] + """ oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) @@ -365,7 +436,14 @@ def test_uma_permissions(oid_with_credentials_authz: Tuple[KeycloakOpenID, str, def test_has_uma_access( oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str], admin: KeycloakAdmin ): - """Test has UMA access.""" + """Test has UMA access. + + :param oid_with_credentials_authz: Keycloak OpenID client configured as an authorization + server with client credentials + :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + """ oid, username, password = oid_with_credentials_authz token = oid.token(username=username, password=password) From fd1e4b839b0def45874aa7f042b75986ba67abe4 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sat, 27 Aug 2022 18:47:21 +0000 Subject: [PATCH 253/566] chore: dependency updates --- poetry.lock | 713 ++++++++-------------------------------------------- 1 file changed, 108 insertions(+), 605 deletions(-) diff --git a/poetry.lock b/poetry.lock index dd69daba..c94a6797 100644 --- a/poetry.lock +++ b/poetry.lock @@ -117,17 +117,9 @@ category = "dev" optional = false python-versions = ">=3.6.1" -[[package]] -name = "chardet" -version = "5.0.0" -description = "Universal encoding detector for Python 3" -category = "dev" -optional = false -python-versions = ">=3.6" - [[package]] name = "charset-normalizer" -version = "2.1.0" +version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -150,15 +142,15 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "codespell" -version = "2.1.0" +version = "2.2.1" description = "Codespell" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" [package.extras] +dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency"] hard-encoding-detection = ["chardet"] -dev = ["pytest-dependency", "pytest-cov", "pytest", "flake8", "check-manifest"] [[package]] name = "colorama" @@ -170,7 +162,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "commitizen" -version = "2.29.3" +version = "2.32.2" description = "Python commitizen client tool" category = "dev" optional = false @@ -178,7 +170,7 @@ python-versions = ">=3.6.2,<4.0.0" [package.dependencies] argcomplete = ">=1.12.1,<2.0.0" -chardet = ">=5.0.0,<6.0.0" +charset-normalizer = ">=2.1.0,<3.0.0" colorama = ">=0.4.1,<0.5.0" decli = ">=0.5.2,<0.6.0" jinja2 = ">=2.10.3" @@ -198,11 +190,11 @@ optional = true python-versions = "*" [package.extras] -test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] +test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"] [[package]] name = "coverage" -version = "6.4.2" +version = "6.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -251,7 +243,7 @@ python-versions = ">=3.6" [[package]] name = "distlib" -version = "0.3.5" +version = "0.3.6" description = "Distribution utilities" category = "dev" optional = false @@ -282,15 +274,15 @@ gmpy2 = ["gmpy2"] [[package]] name = "filelock" -version = "3.7.1" +version = "3.8.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -514,8 +506,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "pre-commit" @@ -601,12 +593,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.12.0" +version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = true python-versions = ">=3.6" +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "pyparsing" version = "3.0.9" @@ -653,7 +648,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "python-jose" @@ -675,7 +670,7 @@ pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] [[package]] name = "pytz" -version = "2022.1" +version = "2022.2.1" description = "World timezone definitions, modern and historical" category = "main" optional = true @@ -850,7 +845,7 @@ docutils = "<0.18" sphinx = ">=1.6" [package.extras] -dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] [[package]] name = "sphinxcontrib-applehelp" @@ -861,8 +856,8 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -873,8 +868,8 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -885,8 +880,8 @@ optional = true python-versions = ">=3.6" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] +test = ["html5lib", "pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -897,7 +892,7 @@ optional = true python-versions = ">=3.5" [package.extras] -test = ["pytest", "flake8", "mypy"] +test = ["mypy", "flake8", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -908,8 +903,8 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -920,8 +915,8 @@ optional = true python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "termcolor" @@ -949,7 +944,7 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.1" +version = "0.11.4" description = "Style preserving TOML library" category = "dev" optional = false @@ -1004,7 +999,7 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.11" +version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false @@ -1012,26 +1007,26 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.2" +version = "20.16.3" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.1,<1" -filelock = ">=3.2,<4" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -platformdirs = ">=2,<3" +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} +platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] name = "wcwidth" @@ -1070,585 +1065,93 @@ python-versions = "^3.7" content-hash = "5d740e81a3604cb20ca85945c2fc134f6373f599315992afb50284f1f993c1d0" [metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] -argcomplete = [ - {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, - {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, -] +alabaster = [] +argcomplete = [] astroid = [] atomicwrites = [] attrs = [] -babel = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, -] -black = [ - {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, - {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, - {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, - {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, - {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, - {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, - {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, - {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, - {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, - {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, - {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, - {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, - {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, - {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, - {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, - {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, - {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, - {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, - {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, -] -certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -chardet = [] -charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] +babel = [] +black = [] +certifi = [] +cffi = [] +cfgv = [] +charset-normalizer = [] +click = [] codespell = [] -colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, -] +colorama = [] commitizen = [] -commonmark = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] +commonmark = [] coverage = [] cryptography = [] darglint = [] -decli = [ - {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, - {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, -] +decli = [] distlib = [] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] +docutils = [] ecdsa = [] -filelock = [ - {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, - {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, -] -flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] -flake8-docstrings = [ - {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, - {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, -] +filelock = [] +flake8 = [] +flake8-docstrings = [] identify = [] -idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, -] -imagesize = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, - {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, - {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, -] -m2r2 = [ - {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, - {file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mistune = [ - {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, - {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, -] -mock = [ - {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, - {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -nodeenv = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, -] -platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] +idna = [] +imagesize = [] +importlib-metadata = [] +iniconfig = [] +isort = [] +jinja2 = [] +lazy-object-proxy = [] +m2r2 = [] +markupsafe = [] +mccabe = [] +mistune = [] +mock = [] +mypy-extensions = [] +nodeenv = [] +packaging = [] +pathspec = [] +platformdirs = [] +pluggy = [] pre-commit = [] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.30-py3-none-any.whl", hash = "sha256:d8916d3f62a7b67ab353a952ce4ced6a1d2587dfe9ef8ebc30dd7c386751f289"}, - {file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, -] -pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, -] -pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] -pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -python-jose = [ - {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, - {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, -] -pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -questionary = [ - {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, - {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, -] -readthedocs-sphinx-ext = [ - {file = "readthedocs-sphinx-ext-2.1.8.tar.gz", hash = "sha256:a57e3713daf77bf91d1ba19e4b9888a47c0abfeb63ecf02e3ac77fcfd99bfe69"}, - {file = "readthedocs_sphinx_ext-2.1.8-py2.py3-none-any.whl", hash = "sha256:5ab5875993191e5e526ca196a1082b73116b0cefd79073ab25367ba0458fffe9"}, -] -recommonmark = [ - {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, - {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] +prompt-toolkit = [] +py = [] +pyasn1 = [] +pycodestyle = [] +pycparser = [] +pydocstyle = [] +pyflakes = [] +pygments = [] +pyparsing = [] +pytest = [] +pytest-cov = [] +python-jose = [] +pytz = [] +pyyaml = [] +questionary = [] +readthedocs-sphinx-ext = [] +recommonmark = [] +requests = [] requests-toolbelt = [] rsa = [] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] +six = [] +snowballstemmer = [] sphinx = [] sphinx-autoapi = [] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, - {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] -termcolor = [ - {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] +sphinx-rtd-theme = [] +sphinxcontrib-applehelp = [] +sphinxcontrib-devhelp = [] +sphinxcontrib-htmlhelp = [] +sphinxcontrib-jsmath = [] +sphinxcontrib-qthelp = [] +sphinxcontrib-serializinghtml = [] +termcolor = [] +toml = [] +tomli = [] tomlkit = [] -tox = [ - {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, - {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, -] -unidecode = [ - {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, - {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, -] +tox = [] +typed-ast = [] +typing-extensions = [] +unidecode = [] urllib3 = [] virtualenv = [] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -wrapt = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, -] +wcwidth = [] +wrapt = [] zipp = [] From ea8a0b441659b423491358cbe2e071da5d154020 Mon Sep 17 00:00:00 2001 From: Ice Lake <30789544+istiak101@users.noreply.github.com> Date: Fri, 2 Sep 2022 15:16:14 +0600 Subject: [PATCH 254/566] docs: change to username --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3558d309..117dd972 100644 --- a/README.md +++ b/README.md @@ -191,8 +191,8 @@ count_users = keycloak_admin.users_count() # Get users Returns a list of users, filtered according to query parameters users = keycloak_admin.get_users({}) -# Get user ID from name -user_id_keycloak = keycloak_admin.get_user_id("example@example.com") +# Get user ID from username +user_id_keycloak = keycloak_admin.get_user_id("username-keycloak") # Get User user = keycloak_admin.get_user("user-id-keycloak") From 31e51e394f26b254fa8f24524a284ed1e07bc42c Mon Sep 17 00:00:00 2001 From: Ice Lake <30789544+istiak101@users.noreply.github.com> Date: Fri, 2 Sep 2022 18:43:55 +0600 Subject: [PATCH 255/566] docs: fix send_update_account example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3558d309..1d4a55e8 100644 --- a/README.md +++ b/README.md @@ -221,7 +221,7 @@ consents = keycloak_admin.consents_user(user_id="user-id-keycloak") # Send User Action response = keycloak_admin.send_update_account(user_id="user-id-keycloak", - payload=json.dumps(['UPDATE_PASSWORD'])) + payload=['UPDATE_PASSWORD']) # Send Verify Email response = keycloak_admin.send_verify_email(user_id="user-id-keycloak") From 754cd042748d57c71f23d14c3d21d9c4ab774c7a Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 2 Sep 2022 14:41:51 +0000 Subject: [PATCH 256/566] chore: update dependencies --- poetry.lock | 936 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 789 insertions(+), 147 deletions(-) diff --git a/poetry.lock b/poetry.lock index c94a6797..caf1ad09 100644 --- a/poetry.lock +++ b/poetry.lock @@ -30,18 +30,11 @@ python-versions = ">=3.6.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" +setuptools = ">=20.0" typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<2" -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "attrs" version = "22.1.0" @@ -51,13 +44,13 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] -name = "babel" +name = "Babel" version = "2.10.3" description = "Internationalization utilities" category = "main" @@ -69,7 +62,7 @@ pytz = ">=2015.7" [[package]] name = "black" -version = "22.6.0" +version = "22.8.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -190,7 +183,7 @@ optional = true python-versions = "*" [package.extras] -test = ["hypothesis (==3.55.3)", "flake8 (==3.7.8)"] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" @@ -218,12 +211,12 @@ python-versions = ">=3.6" cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"] +docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] +test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] [[package]] name = "darglint" @@ -350,9 +343,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -371,13 +364,13 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +requirements_deprecated_finder = ["pip-api", "pipreqs"] [[package]] -name = "jinja2" +name = "Jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "main" @@ -411,7 +404,7 @@ docutils = "*" mistune = "0.8.4" [[package]] -name = "markupsafe" +name = "MarkupSafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" @@ -443,7 +436,7 @@ optional = true python-versions = ">=3.6" [package.extras] -build = ["twine", "wheel", "blurb"] +build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest (<5.4)", "pytest-cov"] @@ -463,6 +456,9 @@ category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +[package.dependencies] +setuptools = "*" + [[package]] name = "packaging" version = "21.3" @@ -476,11 +472,11 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.9.0" +version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" [[package]] name = "platformdirs" @@ -491,8 +487,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -506,8 +502,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" @@ -528,7 +524,7 @@ virtualenv = ">=20.0.8" [[package]] name = "prompt-toolkit" -version = "3.0.30" +version = "3.0.31" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false @@ -592,7 +588,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "pygments" +name = "Pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" @@ -611,18 +607,17 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.2" +version = "7.1.3" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -648,7 +643,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "python-jose" @@ -665,8 +660,8 @@ rsa = "*" [package.extras] cryptography = ["cryptography (>=3.4.0)"] -pycrypto = ["pycrypto (>=2.6.0,<2.7.0)", "pyasn1"] -pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)", "pyasn1"] +pycrypto = ["pyasn1", "pycrypto (>=2.6.0,<2.7.0)"] +pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] [[package]] name = "pytz" @@ -677,7 +672,7 @@ optional = true python-versions = "*" [[package]] -name = "pyyaml" +name = "PyYAML" version = "6.0" description = "YAML parser and emitter for Python" category = "main" @@ -696,7 +691,7 @@ python-versions = ">=3.6,<4.0" prompt_toolkit = ">=2.0,<4.0" [package.extras] -docs = ["Sphinx (>=3.3,<4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)"] +docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] [[package]] name = "readthedocs-sphinx-ext" @@ -764,6 +759,19 @@ python-versions = ">=3.6,<4" [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "setuptools" +version = "65.3.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -781,7 +789,7 @@ optional = false python-versions = "*" [[package]] -name = "sphinx" +name = "Sphinx" version = "5.1.1" description = "Python documentation generator" category = "main" @@ -809,8 +817,8 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "isort", "mypy (>=0.971)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] -test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "isort", "mypy (>=0.971)", "sphinx-lint", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest (>=4.6)", "typed-ast"] [[package]] name = "sphinx-autoapi" @@ -856,8 +864,8 @@ optional = true python-versions = ">=3.5" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -868,8 +876,8 @@ optional = true python-versions = ">=3.5" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -880,8 +888,8 @@ optional = true python-versions = ">=3.6" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -892,7 +900,7 @@ optional = true python-versions = ">=3.5" [package.extras] -test = ["mypy", "flake8", "pytest"] +test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -903,8 +911,8 @@ optional = true python-versions = ">=3.5" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -915,8 +923,8 @@ optional = true python-versions = ">=3.5" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "termcolor" @@ -971,7 +979,7 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "typed-ast" @@ -990,7 +998,7 @@ optional = false python-versions = ">=3.7" [[package]] -name = "unidecode" +name = "Unidecode" version = "1.3.4" description = "ASCII transliterations of Unicode text" category = "main" @@ -1006,13 +1014,13 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.3" +version = "20.16.4" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1036,6 +1044,17 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "wheel" +version = "0.37.1" +description = "A built-package format for Python" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.extras] +test = ["pytest (>=3.0.0)", "pytest-cov"] + [[package]] name = "wrapt" version = "1.14.1" @@ -1053,8 +1072,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] +testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] @@ -1065,93 +1084,716 @@ python-versions = "^3.7" content-hash = "5d740e81a3604cb20ca85945c2fc134f6373f599315992afb50284f1f993c1d0" [metadata.files] -alabaster = [] -argcomplete = [] -astroid = [] -atomicwrites = [] -attrs = [] -babel = [] -black = [] -certifi = [] -cffi = [] -cfgv = [] -charset-normalizer = [] -click = [] -codespell = [] -colorama = [] -commitizen = [] -commonmark = [] -coverage = [] -cryptography = [] -darglint = [] -decli = [] -distlib = [] -docutils = [] -ecdsa = [] -filelock = [] -flake8 = [] -flake8-docstrings = [] -identify = [] -idna = [] -imagesize = [] -importlib-metadata = [] -iniconfig = [] -isort = [] -jinja2 = [] -lazy-object-proxy = [] -m2r2 = [] -markupsafe = [] -mccabe = [] -mistune = [] -mock = [] -mypy-extensions = [] -nodeenv = [] -packaging = [] -pathspec = [] -platformdirs = [] -pluggy = [] -pre-commit = [] -prompt-toolkit = [] -py = [] -pyasn1 = [] -pycodestyle = [] -pycparser = [] -pydocstyle = [] -pyflakes = [] -pygments = [] -pyparsing = [] -pytest = [] -pytest-cov = [] -python-jose = [] -pytz = [] -pyyaml = [] -questionary = [] -readthedocs-sphinx-ext = [] -recommonmark = [] -requests = [] -requests-toolbelt = [] -rsa = [] -six = [] -snowballstemmer = [] -sphinx = [] -sphinx-autoapi = [] -sphinx-rtd-theme = [] -sphinxcontrib-applehelp = [] -sphinxcontrib-devhelp = [] -sphinxcontrib-htmlhelp = [] -sphinxcontrib-jsmath = [] -sphinxcontrib-qthelp = [] -sphinxcontrib-serializinghtml = [] -termcolor = [] -toml = [] -tomli = [] -tomlkit = [] -tox = [] -typed-ast = [] -typing-extensions = [] -unidecode = [] -urllib3 = [] -virtualenv = [] -wcwidth = [] -wrapt = [] -zipp = [] +alabaster = [ + {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, + {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, +] +argcomplete = [ + {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, + {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, +] +astroid = [ + {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, + {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, +] +attrs = [ + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, +] +Babel = [ + {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, + {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, +] +black = [ + {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, + {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, + {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, + {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, + {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, + {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, + {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, + {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, + {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, + {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, + {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, + {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, + {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, + {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, + {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, + {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, + {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, + {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, + {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, +] +certifi = [ + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +] +cffi = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +charset-normalizer = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +codespell = [ + {file = "codespell-2.2.1-py3-none-any.whl", hash = "sha256:0c53a70f466952706407383d87142a78f319a0e18602802c4aadd3d93158bfc6"}, + {file = "codespell-2.2.1.tar.gz", hash = "sha256:569b67e5e5c3ade02a1e23f6bbc56c64b608a3ab48ddd943ece0a03e6c346ed1"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +commitizen = [ + {file = "commitizen-2.32.2-py3-none-any.whl", hash = "sha256:44a07dc8bb5c6fb737471c1e92985b604ba5cad826ea928936537ff75c7b4a0c"}, + {file = "commitizen-2.32.2.tar.gz", hash = "sha256:44be55e35e727fdcbbb35fbe62e4cafedd8844393461906d753f7afc8ab62b0d"}, +] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] +coverage = [ + {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, + {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, + {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, + {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, + {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, + {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, + {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, + {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, + {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, + {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, + {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, + {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, + {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, + {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, + {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, + {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, +] +cryptography = [ + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, + {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, + {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, + {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, +] +darglint = [ + {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, + {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, +] +decli = [ + {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, + {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, +] +distlib = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] +docutils = [ + {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, + {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, +] +ecdsa = [ + {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, + {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, +] +filelock = [ + {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, + {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +flake8-docstrings = [ + {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, + {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, +] +identify = [ + {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, + {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, +] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +imagesize = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] +importlib-metadata = [ + {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, + {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +Jinja2 = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] +lazy-object-proxy = [ + {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, + {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, + {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, + {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, + {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, + {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, + {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, +] +m2r2 = [ + {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, + {file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"}, +] +MarkupSafe = [ + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, + {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, + {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, + {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, + {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, + {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, +] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] +mistune = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] +mock = [ + {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, + {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, + {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, +] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pre-commit = [ + {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, + {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, +] +prompt-toolkit = [ + {file = "prompt_toolkit-3.0.31-py3-none-any.whl", hash = "sha256:9696f386133df0fc8ca5af4895afe5d78f5fcfe5258111c2a79a1c3e41ffa96d"}, + {file = "prompt_toolkit-3.0.31.tar.gz", hash = "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148"}, +] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] +pycparser = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] +pydocstyle = [ + {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, + {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, +] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] +Pygments = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [ + {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, + {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, +] +pytest-cov = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] +python-jose = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] +pytz = [ + {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, + {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, +] +PyYAML = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +questionary = [ + {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, + {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, +] +readthedocs-sphinx-ext = [ + {file = "readthedocs-sphinx-ext-2.1.8.tar.gz", hash = "sha256:a57e3713daf77bf91d1ba19e4b9888a47c0abfeb63ecf02e3ac77fcfd99bfe69"}, + {file = "readthedocs_sphinx_ext-2.1.8-py2.py3-none-any.whl", hash = "sha256:5ab5875993191e5e526ca196a1082b73116b0cefd79073ab25367ba0458fffe9"}, +] +recommonmark = [ + {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, + {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, +] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] +requests-toolbelt = [ + {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, + {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, +] +rsa = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] +setuptools = [ + {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, + {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +snowballstemmer = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] +Sphinx = [ + {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, + {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, +] +sphinx-autoapi = [ + {file = "sphinx-autoapi-1.9.0.tar.gz", hash = "sha256:c897ea337df16ad0cde307cbdfe2bece207788dde1587fa4fc8b857d1fc5dcba"}, + {file = "sphinx_autoapi-1.9.0-py2.py3-none-any.whl", hash = "sha256:d217953273b359b699d8cb81a5a72985a3e6e15cfe3f703d9a3c201ffc30849b"}, +] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, + {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, +] +sphinxcontrib-applehelp = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] +sphinxcontrib-devhelp = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] +sphinxcontrib-htmlhelp = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] +sphinxcontrib-jsmath = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] +sphinxcontrib-qthelp = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] +sphinxcontrib-serializinghtml = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] +termcolor = [ + {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +tomlkit = [ + {file = "tomlkit-0.11.4-py3-none-any.whl", hash = "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c"}, + {file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"}, +] +tox = [ + {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, + {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, +] +typed-ast = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] +Unidecode = [ + {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, + {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, +] +urllib3 = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, +] +virtualenv = [ + {file = "virtualenv-20.16.4-py3-none-any.whl", hash = "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22"}, + {file = "virtualenv-20.16.4.tar.gz", hash = "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782"}, +] +wcwidth = [ + {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, + {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, +] +wheel = [ + {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, + {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, +] +wrapt = [ + {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, + {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, + {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, + {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, + {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, + {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, + {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, + {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, + {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, + {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, + {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, + {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, + {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, + {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, + {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, + {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, + {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, + {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, + {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, + {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, + {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, + {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, + {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, + {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, + {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, + {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, + {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, + {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, + {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, + {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, + {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, + {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, +] +zipp = [ + {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, + {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, +] From d307825885f81d890f6697736a030dda4280c373 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 2 Sep 2022 14:49:54 +0000 Subject: [PATCH 257/566] docs: debug --- .readthedocs.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4379fbfa..0c9a2ba1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,3 +12,5 @@ build: - poetry config virtualenvs.create false post_install: - poetry install -E docs + - pip list + - poetry show From e04a99de4a7730740bb3a574c13cae601a9566f6 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 2 Sep 2022 14:56:46 +0000 Subject: [PATCH 258/566] docs: debug --- .readthedocs.yaml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0c9a2ba1..fd6a1201 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,9 +6,7 @@ build: python: "3.10" jobs: pre_create_environment: - - asdf plugin add poetry - - asdf install poetry latest - - asdf global poetry latest + - which python - poetry config virtualenvs.create false post_install: - poetry install -E docs From 3391871980c3b92b0aef2b79e2501e885135a3bf Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 2 Sep 2022 14:58:48 +0000 Subject: [PATCH 259/566] docs: debug --- .readthedocs.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index fd6a1201..62fc38c0 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,10 +5,9 @@ build: tools: python: "3.10" jobs: - pre_create_environment: - - which python - - poetry config virtualenvs.create false post_install: - - poetry install -E docs + - python -m pip install poetry + - python -m poetry config virtualenvs.create false + - python -m poetry install -E docs - pip list - poetry show From f4eda508304f251df02b08b85c32bf154dcc29b8 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 2 Sep 2022 15:02:40 +0000 Subject: [PATCH 260/566] docs: fix docs build --- .readthedocs.yaml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 62fc38c0..960276c7 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,8 +6,6 @@ build: python: "3.10" jobs: post_install: - - python -m pip install poetry - - python -m poetry config virtualenvs.create false - - python -m poetry install -E docs - - pip list - - poetry show + - /home/docs/checkouts/readthedocs.org/user_builds/python-keycloak/envs/latest/bin/python -m pip install poetry + - /home/docs/checkouts/readthedocs.org/user_builds/python-keycloak/envs/latest/bin/python -m poetry config virtualenvs.create false + - /home/docs/checkouts/readthedocs.org/user_builds/python-keycloak/envs/latest/bin/python -m poetry install -E docs From fc6a70f459f3caafbd85bc29fa423890cd68caf0 Mon Sep 17 00:00:00 2001 From: Fredrik Lindner Date: Mon, 3 Oct 2022 11:36:40 +0200 Subject: [PATCH 261/566] feat: attack detection API implementation --- src/keycloak/keycloak_admin.py | 38 ++++++++++ src/keycloak/urls_patterns.py | 5 ++ tests/test_keycloak_admin.py | 127 +++++++++++++++++++++++++++++++-- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 798ba72c..994b97da 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -3597,3 +3597,41 @@ def update_required_action(self, action_alias, payload): urls_patterns.URL_ADMIN_REQUIRED_ACTIONS_ALIAS.format(**params_path), data=payload ) return raise_error_from_response(data_raw, KeycloakPutError) + + def get_bruteforce_detection_status(self, user_id): + """Get bruteforce detection status for user. + + :param user_id: User id + :type user_id: str + :return: Bruteforce status. + :rtype: dict + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_ATTACK_DETECTION_USER.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def clear_bruteforce_attempts_for_user(self, user_id): + """Clear bruteforce attempts for user. + + :param user_id: User id + :type user_id: str + :return: empty dictionary. + :rtype: dict + """ + params_path = {"realm-name": self.realm_name, "id": user_id} + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_ATTACK_DETECTION_USER.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakDeleteError) + + def clear_all_bruteforce_attempts(self): + """Clear bruteforce attempts for all users in realm. + + :return: empty dictionary. + :rtype: dict + """ + params_path = {"realm-name": self.realm_name} + data_raw = self.raw_delete(urls_patterns.URL_ADMIN_ATTACK_DETECTION.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakDeleteError) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index f2a2188d..b5f32778 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -195,3 +195,8 @@ URL_ADMIN_CLIENT_CERT_UPLOAD = URL_ADMIN_CLIENT_CERTS + "/upload-certificate" URL_ADMIN_REQUIRED_ACTIONS = URL_ADMIN_REALM + "/authentication/required-actions" URL_ADMIN_REQUIRED_ACTIONS_ALIAS = URL_ADMIN_REQUIRED_ACTIONS + "/{action-alias}" + +URL_ADMIN_ATTACK_DETECTION = "admin/realms/{realm-name}/attack-detection/brute-force/users" +URL_ADMIN_ATTACK_DETECTION_USER = ( + "admin/realms/{realm-name}/attack-detection/brute-force/users/{id}" +) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index b3ad951e..b1625f01 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1,11 +1,12 @@ """Test the keycloak admin object.""" import copy +from typing import Tuple import pytest import keycloak -from keycloak import KeycloakAdmin +from keycloak import KeycloakAdmin, KeycloakOpenID from keycloak.connection import ConnectionManager from keycloak.exceptions import ( KeycloakAuthenticationError, @@ -1135,15 +1136,12 @@ def test_role_attributes( attribute_role = "test-realm-role-w-attr" test_attrs = {"attr1": ["val1"], "attr2": ["val2-1", "val2-2"]} role_id = admin.create_realm_role( - payload={"name": attribute_role, "attributes": test_attrs}, - skip_exists=True, + payload={"name": attribute_role, "attributes": test_attrs}, skip_exists=True ) assert role_id, role_id cli_role_id = admin.create_client_role( - client, - payload={"name": attribute_role, "attributes": test_attrs}, - skip_exists=True, + client, payload={"name": attribute_role, "attributes": test_attrs}, skip_exists=True ) assert cli_role_id, cli_role_id @@ -2285,3 +2283,120 @@ def test_upload_certificate(admin: KeycloakAdmin, realm: str, client: str, selfs admin.upload_certificate(client, cert) cl = admin.get_client(client) assert cl["attributes"]["jwt.credential.certificate"] == "".join(cert.splitlines()[1:-1]) + + +def test_get_bruteforce_status_for_user( + admin: KeycloakAdmin, oid_with_credentials: Tuple[KeycloakOpenID, str, str], realm: str +): + """Test users. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + :param realm: Keycloak realm + :type realm: str + """ + oid, username, password = oid_with_credentials + admin.realm_name = realm + + # Turn on bruteforce protection + res = admin.update_realm(realm_name=realm, payload={"bruteForceProtected": True}) + res = admin.get_realm(realm_name=realm) + assert res["bruteForceProtected"] is True + + # Test login user with wrong credentials + try: + oid.token(username=username, password="wrongpassword") + except KeycloakAuthenticationError: + pass + + user_id = admin.get_user_id(username) + bruteforce_status = admin.get_bruteforce_detection_status(user_id) + + assert bruteforce_status["numFailures"] == 1 + + # Cleanup + res = admin.update_realm(realm_name=realm, payload={"bruteForceProtected": False}) + res = admin.get_realm(realm_name=realm) + assert res["bruteForceProtected"] is False + + +def test_clear_bruteforce_attempts_for_user( + admin: KeycloakAdmin, oid_with_credentials: Tuple[KeycloakOpenID, str, str], realm: str +): + """Test users. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + :param realm: Keycloak realm + :type realm: str + """ + oid, username, password = oid_with_credentials + admin.realm_name = realm + + # Turn on bruteforce protection + res = admin.update_realm(realm_name=realm, payload={"bruteForceProtected": True}) + res = admin.get_realm(realm_name=realm) + assert res["bruteForceProtected"] is True + + # Test login user with wrong credentials + try: + oid.token(username=username, password="wrongpassword") + except KeycloakAuthenticationError: + pass + + user_id = admin.get_user_id(username) + bruteforce_status = admin.get_bruteforce_detection_status(user_id) + assert bruteforce_status["numFailures"] == 1 + + res = admin.clear_bruteforce_attempts_for_user(user_id) + bruteforce_status = admin.get_bruteforce_detection_status(user_id) + assert bruteforce_status["numFailures"] == 0 + + # Cleanup + res = admin.update_realm(realm_name=realm, payload={"bruteForceProtected": False}) + res = admin.get_realm(realm_name=realm) + assert res["bruteForceProtected"] is False + + +def test_clear_bruteforce_attempts_for_all_users( + admin: KeycloakAdmin, oid_with_credentials: Tuple[KeycloakOpenID, str, str], realm: str +): + """Test users. + + :param admin: Keycloak Admin client + :type admin: KeycloakAdmin + :param oid_with_credentials: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials: Tuple[KeycloakOpenID, str, str] + :param realm: Keycloak realm + :type realm: str + """ + oid, username, password = oid_with_credentials + admin.realm_name = realm + + # Turn on bruteforce protection + res = admin.update_realm(realm_name=realm, payload={"bruteForceProtected": True}) + res = admin.get_realm(realm_name=realm) + assert res["bruteForceProtected"] is True + + # Test login user with wrong credentials + try: + oid.token(username=username, password="wrongpassword") + except KeycloakAuthenticationError: + pass + + user_id = admin.get_user_id(username) + bruteforce_status = admin.get_bruteforce_detection_status(user_id) + assert bruteforce_status["numFailures"] == 1 + + res = admin.clear_all_bruteforce_attempts() + bruteforce_status = admin.get_bruteforce_detection_status(user_id) + assert bruteforce_status["numFailures"] == 0 + + # Cleanup + res = admin.update_realm(realm_name=realm, payload={"bruteForceProtected": False}) + res = admin.get_realm(realm_name=realm) + assert res["bruteForceProtected"] is False From a63982188ae14e1a0948f324bb3e9a8387650e65 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Mon, 3 Oct 2022 17:07:54 +0000 Subject: [PATCH 262/566] docs: changelog update --- CHANGELOG.md | 137 +++++++++++---------------------------------------- 1 file changed, 30 insertions(+), 107 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f90b910..8674555c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,17 @@ -# Changelog - -## v2.5.0 (2022-08-19) +## v2.6.0 (2022-10-03) ### Feat -- added missing functionality to include attributes when returning realm roles according to specifications +- attack detection API implementation + +## v2.5.0 (2022-08-19) ## v2.4.0 (2022-08-19) ### Feat - add client scope-mappings client roles operations +- added missing functionality to include attributes when returning realm roles according to specifications ## v2.3.0 (2022-08-13) @@ -26,19 +27,18 @@ ## v2.1.1 (2022-07-19) -### Refactor - -- applied linting - ### Fix - removed whitespace from urls +### Refactor + +- applied linting + ## v2.1.0 (2022-07-18) ### Feat -- add functions covering some missing REST API calls - add unit tests - add docstrings - add functions covering some missing REST API calls @@ -55,15 +55,14 @@ ## v2.0.0 (2022-07-17) -### Fix - -- check client existence based on clientId -- check client existence based on clientId - ### BREAKING CHANGE - Renamed parameter client_name to client_id in get_client_id method +### Fix + +- check client existence based on clientId + ## v1.9.1 (2022-07-13) ### Fix @@ -82,17 +81,15 @@ ## v1.8.1 (2022-07-13) +### Feat + +- added flake8-docstrings and upgraded dependencies + ### Fix -- Support the auth_url method called with scope & state params now - Support the auth_url method called with scope & state params now - raise correct exceptions -### Feat - -- added flake8-docstrings and upgraded dependencies -- use poetry for package management - ### Refactor - slight restructure of the base fixtures @@ -101,7 +98,6 @@ ### Feat -- Ability to set custom timeout for KeycloakOpenId and KeycloakAdmin - Ability to set custom timeout for KCOpenId and KCAdmin ## v1.7.0 (2022-06-16) @@ -120,14 +116,12 @@ ### Feat -- Add update_idp - Add update_idp ## v1.4.0 (2022-06-02) ### Feat -- Add update_mapper_in_idp - Add update_mapper_in_idp ## v1.3.0 (2022-05-31) @@ -157,24 +151,22 @@ ### Fix -- allow query parameters for users count - allow query parameters for users count ## v1.0.0 (2022-05-25) -### Fix - -- correct spelling of public API method - ### BREAKING CHANGE - Renames `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` +### Fix + +- correct spelling of public API method + ## v0.29.1 (2022-05-24) ### Fix -- allow client_credentials token if username and password not spec… - allow client_credentials token if username and password not specified ## v0.29.0 (2022-05-23) @@ -185,15 +177,14 @@ ## v0.28.3 (2022-05-23) -### Fix - -- import classes in the base module -- import classes in the base module - ### Feat - added UMA-permission request functionality +### Fix + +- import classes in the base module + ## v0.28.2 (2022-05-19) ### Fix @@ -204,7 +195,6 @@ ### Fix -- Add missing keycloak.authorization package - Add missing keycloak.authorization package ## v0.28.0 (2022-05-19) @@ -217,16 +207,16 @@ - fixed admin client to pass the tests - initial setup of CICD and linting -### Refactor - -- isort conf.py -- Merge branch 'master' into feature/cicd - ### Fix - full tox fix ready - raise correct errors +### Refactor + +- isort conf.py +- Merge branch 'master' into feature/cicd + ## v0.27.1 (2022-05-18) ### Fix @@ -243,7 +233,6 @@ ### Feat -- add KeycloakAdmin.set_events - add KeycloakAdmin.set_events ## v0.25.0 (2021-05-05) @@ -267,69 +256,3 @@ ## v0.18.0 (2019-12-10) ## v0.17.6 (2019-10-10) - -## v0.5.0 (2017-08-21) - -### Feat - -- Basic functions for Keycloak API (well_know, token, userinfo, logout, certs, - entitlement, instropect) - -## v0.6.0 (2017-08-23) - -### Feat - -- Added load authorization settings - -## v0.7.0 (2017-08-23) - -### Feat - -- Added polices - -## v0.8.0 (2017-08-23) - -### Feat - -- Added permissions - -## v0.9.0 (2017-09-05) - -### Feat - -- Added functions for Admin Keycloak API - -## v0.10.0 (2017-10-23) - -### Feat - -- Updated libraries versions -- Updated Docs - -## v0.11.0 (2017-12-12) - -### Feat - -- Changed Instropect RPT - -## v0.12.0 (2018-01-25) - -### Feat - -- Add groups functions -- Add Admin Tasks for user and client role management -- Function to trigger user sync from provider - -## v0.12.1 (2018-08-04) - -### Feat - -- Add get_idps -- Rework group functions - -## master - -### Feat - -- Renamed `KeycloakOpenID.well_know` to `KeycloakOpenID.well_known` -- Add `KeycloakOpenID.token_exchange` to support Token Exchange From 6b30631378ad4d8d7b64d219d313be4e10429ba2 Mon Sep 17 00:00:00 2001 From: Igli Manaj Date: Fri, 14 Oct 2022 02:34:31 +0200 Subject: [PATCH 263/566] feat: helping functions for disabling users --- src/keycloak/keycloak_admin.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 994b97da..1b8426cc 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -762,6 +762,28 @@ def update_user(self, user_id, payload): ) return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + def disable_user(self, user_id): + """Disable the user from the realm. Disabled users can not log in. + + :param user_id: User id + :type user_id: str + + :return: Http response + :rtype: bytes + """ + return self.update_user(user_id=user_id, payload={"enabled": False}) + + + def disable_all_users(self): + """Disable all existing users. + """ + users = self.get_users() + for user in users: + user_id = user["id"] + print(f"Disabling user with id: {user_id}") + self.disable_user(user_id=user_id) + + def delete_user(self, user_id): """Delete the user. From 477e0c5a3c712b74c29953f82b8c925e8ea567df Mon Sep 17 00:00:00 2001 From: Igli Manaj Date: Fri, 14 Oct 2022 02:44:02 +0200 Subject: [PATCH 264/566] feat: option for enabling users --- src/keycloak/keycloak_admin.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 1b8426cc..55bb6d66 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -772,7 +772,17 @@ def disable_user(self, user_id): :rtype: bytes """ return self.update_user(user_id=user_id, payload={"enabled": False}) + + def enable_user(self, user_id): + """Enable the user from the realm. + + :param user_id: User id + :type user_id: str + :return: Http response + :rtype: bytes + """ + return self.update_user(user_id=user_id, payload={"enabled": True}) def disable_all_users(self): """Disable all existing users. @@ -783,6 +793,14 @@ def disable_all_users(self): print(f"Disabling user with id: {user_id}") self.disable_user(user_id=user_id) + def enable_all_users(self): + """Disable all existing users. + """ + users = self.get_users() + for user in users: + user_id = user["id"] + print(f"Enabling user with id: {user_id}") + self.enable_user(user_id=user_id) def delete_user(self, user_id): """Delete the user. From 1eaa4afc484caff8629463d69d5fb2ff1e890c48 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 25 Oct 2022 06:03:00 +0000 Subject: [PATCH 265/566] chore: dependency update --- poetry.lock | 353 ++++++++++++++++++++++++++-------------------------- 1 file changed, 178 insertions(+), 175 deletions(-) diff --git a/poetry.lock b/poetry.lock index caf1ad09..893fa2ca 100644 --- a/poetry.lock +++ b/poetry.lock @@ -8,11 +8,11 @@ python-versions = "*" [[package]] name = "argcomplete" -version = "1.12.3" +version = "2.0.0" description = "Bash tab completion for argparse" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.6" [package.dependencies] importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""} @@ -47,10 +47,10 @@ python-versions = ">=3.5" dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] -name = "Babel" +name = "babel" version = "2.10.3" description = "Internationalization utilities" category = "main" @@ -62,11 +62,11 @@ pytz = ">=2015.7" [[package]] name = "black" -version = "22.8.0" +version = "22.10.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" @@ -85,7 +85,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.6.15" +version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -119,7 +119,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" @@ -135,34 +135,35 @@ importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "codespell" -version = "2.2.1" +version = "2.2.2" description = "Codespell" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.extras] -dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency"] +dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency", "tomli"] hard-encoding-detection = ["chardet"] +toml = ["tomli"] [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "commitizen" -version = "2.32.2" +version = "2.35.0" description = "Python commitizen client tool" category = "dev" optional = false python-versions = ">=3.6.2,<4.0.0" [package.dependencies] -argcomplete = ">=1.12.1,<2.0.0" +argcomplete = ">=1.12.1,<2.1" charset-normalizer = ">=2.1.0,<3.0.0" colorama = ">=0.4.1,<0.5.0" decli = ">=0.5.2,<0.6.0" @@ -170,7 +171,7 @@ jinja2 = ">=2.10.3" packaging = ">=19,<22" pyyaml = ">=3.08" questionary = ">=1.4.0,<2.0.0" -termcolor = ">=1.1,<2.0" +termcolor = {version = ">=1.1,<3", markers = "python_version >= \"3.7\""} tomlkit = ">=0.5.3,<1.0.0" typing-extensions = ">=4.0.1,<5.0.0" @@ -187,7 +188,7 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "6.4.4" +version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -305,7 +306,7 @@ pydocstyle = ">=2.1" [[package]] name = "identify" -version = "2.5.3" +version = "2.5.7" description = "File identification library for Python" category = "dev" optional = false @@ -316,7 +317,7 @@ license = ["ukkonen"] [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false @@ -332,7 +333,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.12.0" +version = "4.13.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -343,9 +344,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["jaraco.packaging (>=9)", "rst.linker (>=1.9)", "sphinx"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] perf = ["ipython"] -testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -365,12 +366,12 @@ python-versions = ">=3.6.1,<4.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] -requirements_deprecated_finder = ["pip-api", "pipreqs"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] -name = "Jinja2" +name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." category = "main" @@ -404,7 +405,7 @@ docutils = "*" mistune = "0.8.4" [[package]] -name = "MarkupSafe" +name = "markupsafe" version = "2.1.1" description = "Safely add untrusted strings to HTML/XML markup." category = "main" @@ -588,7 +589,7 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] -name = "Pygments" +name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" @@ -665,14 +666,14 @@ pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] [[package]] name = "pytz" -version = "2022.2.1" +version = "2022.5" description = "World timezone definitions, modern and historical" category = "main" optional = true python-versions = "*" [[package]] -name = "PyYAML" +name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" category = "main" @@ -695,7 +696,7 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphin [[package]] name = "readthedocs-sphinx-ext" -version = "2.1.8" +version = "2.1.9" description = "Sphinx extension for Read the Docs overrides" category = "main" optional = true @@ -735,7 +736,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" @@ -761,14 +762,14 @@ pyasn1 = ">=0.1.3" [[package]] name = "setuptools" -version = "65.3.0" +version = "65.5.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] @@ -789,8 +790,8 @@ optional = false python-versions = "*" [[package]] -name = "Sphinx" -version = "5.1.1" +name = "sphinx" +version = "5.3.0" description = "Python documentation generator" category = "main" optional = true @@ -798,16 +799,16 @@ python-versions = ">=3.6" [package.dependencies] alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.20" -imagesize = "*" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.3" -packaging = "*" -Pygments = ">=2.0" +imagesize = ">=1.3" +importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.12" requests = ">=2.5.0" -snowballstemmer = ">=1.1" +snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" @@ -817,8 +818,8 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "isort", "mypy (>=0.971)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed-ast"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" @@ -928,11 +929,14 @@ test = ["pytest"] [[package]] name = "termcolor" -version = "1.1.0" -description = "ANSII Color formatting for output in terminal." +version = "2.0.1" +description = "ANSI color formatting for output in terminal" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" + +[package.extras] +tests = ["pytest", "pytest-cov"] [[package]] name = "toml" @@ -952,7 +956,7 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.4" +version = "0.11.5" description = "Style preserving TOML library" category = "dev" optional = false @@ -960,7 +964,7 @@ python-versions = ">=3.6,<4.0" [[package]] name = "tox" -version = "3.25.1" +version = "3.26.0" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -974,7 +978,7 @@ packaging = ">=14" pluggy = ">=0.12.0" py = ">=1.4.17" six = ">=1.14.0" -toml = ">=0.9.4" +tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" [package.extras] @@ -991,15 +995,15 @@ python-versions = ">=3.6" [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" [[package]] -name = "Unidecode" -version = "1.3.4" +name = "unidecode" +version = "1.3.6" description = "ASCII transliterations of Unicode text" category = "main" optional = true @@ -1020,7 +1024,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.4" +version = "20.16.5" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1065,15 +1069,15 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.8.1" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx"] -testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] @@ -1089,8 +1093,8 @@ alabaster = [ {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] argcomplete = [ - {file = "argcomplete-1.12.3-py2.py3-none-any.whl", hash = "sha256:291f0beca7fd49ce285d2f10e4c1c77e9460cf823eef2de54df0c0fec88b0d81"}, - {file = "argcomplete-1.12.3.tar.gz", hash = "sha256:2c7dbffd8c045ea534921e63b0be6fe65e88599990d8dc408ac8c542b72a5445"}, + {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, + {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, ] astroid = [ {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, @@ -1100,38 +1104,36 @@ attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] -Babel = [ +babel = [ {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] black = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, @@ -1212,72 +1214,72 @@ click = [ {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, ] codespell = [ - {file = "codespell-2.2.1-py3-none-any.whl", hash = "sha256:0c53a70f466952706407383d87142a78f319a0e18602802c4aadd3d93158bfc6"}, - {file = "codespell-2.2.1.tar.gz", hash = "sha256:569b67e5e5c3ade02a1e23f6bbc56c64b608a3ab48ddd943ece0a03e6c346ed1"}, + {file = "codespell-2.2.2-py3-none-any.whl", hash = "sha256:87dfcd9bdc9b3cb8b067b37f0af22044d7a84e28174adfc8eaa203056b7f9ecc"}, + {file = "codespell-2.2.2.tar.gz", hash = "sha256:c4d00c02b5a2a55661f00d5b4b3b5a710fa803ced9a9d7e45438268b099c319c"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] commitizen = [ - {file = "commitizen-2.32.2-py3-none-any.whl", hash = "sha256:44a07dc8bb5c6fb737471c1e92985b604ba5cad826ea928936537ff75c7b4a0c"}, - {file = "commitizen-2.32.2.tar.gz", hash = "sha256:44be55e35e727fdcbbb35fbe62e4cafedd8844393461906d753f7afc8ab62b0d"}, + {file = "commitizen-2.35.0-py3-none-any.whl", hash = "sha256:ced3e161decf290c5263373dda440040405ed7f8b701b463d81e2ecc1e31d92c"}, + {file = "commitizen-2.35.0.tar.gz", hash = "sha256:34a7462c2279fc4e22929c03a9bb89242ab45dc501c0f17d1174e65c7fb9d793"}, ] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, ] coverage = [ - {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, - {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, - {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, - {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, - {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, - {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, - {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, - {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, - {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, - {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, - {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, - {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, - {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, - {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, - {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, - {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] cryptography = [ {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, @@ -1336,20 +1338,20 @@ flake8-docstrings = [ {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, ] identify = [ - {file = "identify-2.5.3-py2.py3-none-any.whl", hash = "sha256:25851c8c1370effb22aaa3c987b30449e9ff0cece408f810ae6ce408fdd20893"}, - {file = "identify-2.5.3.tar.gz", hash = "sha256:887e7b91a1be152b0d46bbf072130235a8117392b9f1828446079a816a05ef44"}, + {file = "identify-2.5.7-py2.py3-none-any.whl", hash = "sha256:7a67b2a6208d390fd86fd04fb3def94a3a8b7f0bcbd1d1fcd6736f4defe26390"}, + {file = "identify-2.5.7.tar.gz", hash = "sha256:5b8fd1e843a6d4bf10685dd31f4520a7f1c7d0e14e9bc5d34b1d6f111cabc011"}, ] idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] imagesize = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] importlib-metadata = [ - {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, - {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, + {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, + {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, ] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, @@ -1359,7 +1361,7 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] -Jinja2 = [ +jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] @@ -1406,7 +1408,7 @@ m2r2 = [ {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, {file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"}, ] -MarkupSafe = [ +markupsafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, @@ -1527,7 +1529,7 @@ pyflakes = [ {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, ] -Pygments = [ +pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] @@ -1548,10 +1550,10 @@ python-jose = [ {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, ] pytz = [ - {file = "pytz-2022.2.1-py2.py3-none-any.whl", hash = "sha256:220f481bdafa09c3955dfbdddb7b57780e9a94f5127e35456a48589b9e0c0197"}, - {file = "pytz-2022.2.1.tar.gz", hash = "sha256:cea221417204f2d1a2aa03ddae3e867921971d0d76f14d87abb4414415bbdcf5"}, + {file = "pytz-2022.5-py2.py3-none-any.whl", hash = "sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22"}, + {file = "pytz-2022.5.tar.gz", hash = "sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914"}, ] -PyYAML = [ +pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, @@ -1591,8 +1593,8 @@ questionary = [ {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, ] readthedocs-sphinx-ext = [ - {file = "readthedocs-sphinx-ext-2.1.8.tar.gz", hash = "sha256:a57e3713daf77bf91d1ba19e4b9888a47c0abfeb63ecf02e3ac77fcfd99bfe69"}, - {file = "readthedocs_sphinx_ext-2.1.8-py2.py3-none-any.whl", hash = "sha256:5ab5875993191e5e526ca196a1082b73116b0cefd79073ab25367ba0458fffe9"}, + {file = "readthedocs-sphinx-ext-2.1.9.tar.gz", hash = "sha256:470aadad72a6ccdc803466e45381a5b5d33ac5389553c6efee17ec0268a6986c"}, + {file = "readthedocs_sphinx_ext-2.1.9-py2.py3-none-any.whl", hash = "sha256:1a98ad8f054e331fe6691f2e62a466acf1753a2d747042001125c8b90a13db9b"}, ] recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, @@ -1611,8 +1613,8 @@ rsa = [ {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, ] setuptools = [ - {file = "setuptools-65.3.0-py3-none-any.whl", hash = "sha256:2e24e0bec025f035a2e72cdd1961119f557d78ad331bb00ff82efb2ab8da8e82"}, - {file = "setuptools-65.3.0.tar.gz", hash = "sha256:7732871f4f7fa58fb6bdcaeadb0161b2bd046c85905dbaa066bdcbcc81953b57"}, + {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, + {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1622,9 +1624,9 @@ snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] -Sphinx = [ - {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, - {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, +sphinx = [ + {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, + {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, ] sphinx-autoapi = [ {file = "sphinx-autoapi-1.9.0.tar.gz", hash = "sha256:c897ea337df16ad0cde307cbdfe2bece207788dde1587fa4fc8b857d1fc5dcba"}, @@ -1659,7 +1661,8 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] termcolor = [ - {file = "termcolor-1.1.0.tar.gz", hash = "sha256:1d6d69ce66211143803fbc56652b41d73b4a400a2891d7bf7a1cdf4c02de613b"}, + {file = "termcolor-2.0.1-py3-none-any.whl", hash = "sha256:7e597f9de8e001a3208c4132938597413b9da45382b6f1d150cff8d062b7aaa3"}, + {file = "termcolor-2.0.1.tar.gz", hash = "sha256:6b2cf769e93364a2676e1de56a7c0cff2cf5bd07f37e9cc80b0dd6320ebfe388"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1670,12 +1673,12 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ - {file = "tomlkit-0.11.4-py3-none-any.whl", hash = "sha256:25d4e2e446c453be6360c67ddfb88838cfc42026322770ba13d1fbd403a93a5c"}, - {file = "tomlkit-0.11.4.tar.gz", hash = "sha256:3235a9010fae54323e727c3ac06fb720752fe6635b3426e379daec60fbd44a83"}, + {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, + {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, ] tox = [ - {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, - {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, + {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, + {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, ] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, @@ -1704,20 +1707,20 @@ typed-ast = [ {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] -Unidecode = [ - {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, - {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, +unidecode = [ + {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, + {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, ] urllib3 = [ {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] virtualenv = [ - {file = "virtualenv-20.16.4-py3-none-any.whl", hash = "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22"}, - {file = "virtualenv-20.16.4.tar.gz", hash = "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782"}, + {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, + {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1794,6 +1797,6 @@ wrapt = [ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] From b1d682623392859e5494bde52f234599b22af993 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 25 Oct 2022 06:03:11 +0000 Subject: [PATCH 266/566] ci: fix readthedocs build --- .readthedocs.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 960276c7..9347d5d8 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -6,6 +6,6 @@ build: python: "3.10" jobs: post_install: - - /home/docs/checkouts/readthedocs.org/user_builds/python-keycloak/envs/latest/bin/python -m pip install poetry - - /home/docs/checkouts/readthedocs.org/user_builds/python-keycloak/envs/latest/bin/python -m poetry config virtualenvs.create false - - /home/docs/checkouts/readthedocs.org/user_builds/python-keycloak/envs/latest/bin/python -m poetry install -E docs + - pip install -U poetry + - poetry config virtualenvs.create false + - poetry install -E docs From eda6a2762ca36d933c5656f25c8d013d8e62d93b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 25 Oct 2022 06:38:59 +0000 Subject: [PATCH 267/566] ci: fix docs build --- docs/source/conf.py | 10 +++++++++- poetry.lock | 10 +++++----- pyproject.toml | 6 +++--- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index c6bcb603..6216cce1 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -180,7 +180,15 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [(master_doc, "python-keycloak", "python-keycloak Documentation", [author], 1)] +man_pages = [ + ( + master_doc, + "python-keycloak", + "python-keycloak Documentation", + [author], + 1, + ) +] # -- Options for Texinfo output ------------------------------------------- diff --git a/poetry.lock b/poetry.lock index 893fa2ca..6c67e946 100644 --- a/poetry.lock +++ b/poetry.lock @@ -823,7 +823,7 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" -version = "1.9.0" +version = "2.0.0" description = "Sphinx API documentation generator" category = "main" optional = true @@ -833,7 +833,7 @@ python-versions = ">=3.7" astroid = ">=2.7" Jinja2 = "*" PyYAML = "*" -sphinx = ">=3.0" +sphinx = ">=4.0" unidecode = "*" [package.extras] @@ -1085,7 +1085,7 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "5d740e81a3604cb20ca85945c2fc134f6373f599315992afb50284f1f993c1d0" +content-hash = "da04d73122fef0b5938fc53dccdc95dbdd245f983ccd02a570c5569d945e89c1" [metadata.files] alabaster = [ @@ -1629,8 +1629,8 @@ sphinx = [ {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, ] sphinx-autoapi = [ - {file = "sphinx-autoapi-1.9.0.tar.gz", hash = "sha256:c897ea337df16ad0cde307cbdfe2bece207788dde1587fa4fc8b857d1fc5dcba"}, - {file = "sphinx_autoapi-1.9.0-py2.py3-none-any.whl", hash = "sha256:d217953273b359b699d8cb81a5a72985a3e6e15cfe3f703d9a3c201ffc30849b"}, + {file = "sphinx-autoapi-2.0.0.tar.gz", hash = "sha256:97dcf1b5b54cd0d8efef867594e4a4f3e2d3a2c0ec1e5a891e0a61bc77046006"}, + {file = "sphinx_autoapi-2.0.0-py2.py3-none-any.whl", hash = "sha256:dab2753a38cad907bf4e61473c0da365a26bfbe69fbf5aa6e4f7d48e1cf8a148"}, ] sphinx-rtd-theme = [ {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, diff --git a/pyproject.toml b/pyproject.toml index a115c32d..f8636431 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,11 +37,11 @@ mock = {version = "^4.0.3", optional = true} alabaster = {version = "^0.7.12", optional = true} commonmark = {version = "^0.9.1", optional = true} recommonmark = {version = "^0.7.1", optional = true} -Sphinx = {version = "^5.0.2", optional = true} +Sphinx = {version = "^5.3.0", optional = true} sphinx-rtd-theme = {version = "^1.0.0", optional = true} -readthedocs-sphinx-ext = {version = "^2.1.8", optional = true} +readthedocs-sphinx-ext = {version = "^2.1.9", optional = true} m2r2 = {version = "^0.3.2", optional = true} -sphinx-autoapi = {version = "^1.8.4", optional = true} +sphinx-autoapi = {version = "^2.0.0", optional = true} requests-toolbelt = "^0.9.1" [tool.poetry.dev-dependencies] From 7bb9d643aac714f5710128842d522e4be48ca72d Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 25 Oct 2022 07:10:31 +0000 Subject: [PATCH 268/566] style: lint --- .github/workflows/bump.yaml | 42 +++++------ .github/workflows/daily.yaml | 28 ++++---- .github/workflows/lint.yaml | 124 ++++++++++++++++----------------- .github/workflows/publish.yaml | 66 +++++++++--------- 4 files changed, 130 insertions(+), 130 deletions(-) diff --git a/.github/workflows/bump.yaml b/.github/workflows/bump.yaml index e5623469..c623d74a 100644 --- a/.github/workflows/bump.yaml +++ b/.github/workflows/bump.yaml @@ -2,8 +2,8 @@ name: Bump version on: workflow_run: - workflows: [ "Lint" ] - branches: [ master ] + workflows: ["Lint"] + branches: [master] types: - completed @@ -11,22 +11,22 @@ jobs: tag-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT_TOKEN }} - - uses: actions/setup-node@v3 - with: - node-version: 18 - - name: determine-version - run: | - VERSION=$(npx semantic-release --branches master --dry-run | { grep -i 'the next release version is' || test $? = 1; } | sed -E 's/.* ([[:digit:].]+)$/\1/') - echo "VERSION=$VERSION" >> $GITHUB_ENV - id: version - - uses: rickstaa/action-create-tag@v1 - continue-on-error: true - env: - GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} - with: - tag: v${{ env.VERSION }} - message: "Releasing v${{ env.VERSION }}" - github_token: ${{ secrets.PAT_TOKEN }} + - uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT_TOKEN }} + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: determine-version + run: | + VERSION=$(npx semantic-release --branches master --dry-run | { grep -i 'the next release version is' || test $? = 1; } | sed -E 's/.* ([[:digit:].]+)$/\1/') + echo "VERSION=$VERSION" >> $GITHUB_ENV + id: version + - uses: rickstaa/action-create-tag@v1 + continue-on-error: true + env: + GITHUB_TOKEN: ${{ secrets.PAT_TOKEN }} + with: + tag: v${{ env.VERSION }} + message: "Releasing v${{ env.VERSION }}" + github_token: ${{ secrets.PAT_TOKEN }} diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml index 7ddc6227..d53caf05 100644 --- a/.github/workflows/daily.yaml +++ b/.github/workflows/daily.yaml @@ -2,7 +2,7 @@ name: Daily check on: schedule: - - cron: '0 4 * * *' + - cron: "0 4 * * *" jobs: test: @@ -12,16 +12,16 @@ jobs: matrix: python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - uses: docker-practice/actions-setup-docker@master - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Run tests - run: | - tox -e tests + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - uses: docker-practice/actions-setup-docker@master + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run tests + run: | + tox -e tests diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 61534ac6..e156b1b0 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -2,51 +2,51 @@ name: Lint on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: check-commits: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: webiny/action-conventional-commits@v1.0.3 + - uses: actions/checkout@v3 + - uses: webiny/action-conventional-commits@v1.0.3 check-linting: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Check linting, formatting - run: | - tox -e check + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Check linting, formatting + run: | + tox -e check check-docs: runs-on: ubuntu-latest needs: - - check-commits - - check-linting + - check-commits + - check-linting steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Check documentation build - run: | - tox -e docs + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Check documentation build + run: | + tox -e docs test: runs-on: ubuntu-latest @@ -55,41 +55,41 @@ jobs: matrix: python-version: ["3.7", "3.8", "3.9", "3.10"] needs: - - check-commits - - check-linting + - check-commits + - check-linting steps: - - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v3 - with: - python-version: ${{ matrix.python-version }} - - uses: docker-practice/actions-setup-docker@master - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Run tests - run: | - tox -e tests - - name: Keycloak logs - run: | - cat keycloak_test_logs.txt + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - uses: docker-practice/actions-setup-docker@master + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run tests + run: | + tox -e tests + - name: Keycloak logs + run: | + cat keycloak_test_logs.txt build: runs-on: ubuntu-latest needs: - - test - - check-docs + - test + - check-docs steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox - - name: Run build - run: | - tox -e build + - uses: actions/checkout@v3 + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox + - name: Run build + run: | + tox -e build diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 5c5f97c3..c073b31b 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -3,41 +3,41 @@ name: Publish on: push: tags: - - 'v*' + - "v*" jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - with: - fetch-depth: '0' - - name: Set up Python 3.10 - uses: actions/setup-python@v3 - with: - python-version: "3.10" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install tox wheel twine - - name: Apply the tag version - run: | - version=${{ github.ref_name }} - sed -Ei '/^version = /s|= "[0-9.]+"$|= "'${version:-1}'"|' pyproject.toml - - name: Run build - run: | - tox -e build - - name: Publish to PyPi - env: - TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} - run: | - twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* - - name: Run changelog - run: | - tox -e changelog - - uses: stefanzweifel/git-auto-commit-action@v4 - with: - commit_message: "docs: changelog update" - branch: master - file_pattern: CHANGELOG.md + - uses: actions/checkout@v3 + with: + fetch-depth: "0" + - name: Set up Python 3.10 + uses: actions/setup-python@v3 + with: + python-version: "3.10" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox wheel twine + - name: Apply the tag version + run: | + version=${{ github.ref_name }} + sed -Ei '/^version = /s|= "[0-9.]+"$|= "'${version:-1}'"|' pyproject.toml + - name: Run build + run: | + tox -e build + - name: Publish to PyPi + env: + TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + run: | + twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* + - name: Run changelog + run: | + tox -e changelog + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: "docs: changelog update" + branch: master + file_pattern: CHANGELOG.md From 6554235b505cf1b23c79ec53ef3e8f66826297c8 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 25 Oct 2022 07:10:46 +0000 Subject: [PATCH 269/566] ci: added python 3.11 --- .github/workflows/lint.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index e156b1b0..8a740af3 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -53,7 +53,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] needs: - check-commits - check-linting From 9ae8efd5ebb06a74ef0f2111ff2565096f4c266a Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 13 Dec 2022 21:44:17 +0000 Subject: [PATCH 270/566] chore: dependency update --- poetry.lock | 290 +++++++++++++++++++++++----------------------------- 1 file changed, 130 insertions(+), 160 deletions(-) diff --git a/poetry.lock b/poetry.lock index 6c67e946..a31d3c62 100644 --- a/poetry.lock +++ b/poetry.lock @@ -51,7 +51,7 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy [[package]] name = "babel" -version = "2.10.3" +version = "2.11.0" description = "Internationalization utilities" category = "main" optional = true @@ -62,7 +62,7 @@ pytz = ">=2015.7" [[package]] name = "black" -version = "22.10.0" +version = "22.12.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -85,7 +85,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2022.12.7" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -156,7 +156,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7 [[package]] name = "commitizen" -version = "2.35.0" +version = "2.38.0" description = "Python commitizen client tool" category = "dev" optional = false @@ -168,7 +168,7 @@ charset-normalizer = ">=2.1.0,<3.0.0" colorama = ">=0.4.1,<0.5.0" decli = ">=0.5.2,<0.6.0" jinja2 = ">=2.10.3" -packaging = ">=19,<22" +packaging = ">=19" pyyaml = ">=3.08" questionary = ">=1.4.0,<2.0.0" termcolor = {version = ">=1.1,<3", markers = "python_version >= \"3.7\""} @@ -266,17 +266,28 @@ six = ">=1.9.0" gmpy = ["gmpy"] gmpy2 = ["gmpy2"] +[[package]] +name = "exceptiongroup" +version = "1.0.4" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "filelock" -version = "3.8.0" +version = "3.8.2" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] -testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=6.5)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -306,7 +317,7 @@ pydocstyle = ">=2.1" [[package]] name = "identify" -version = "2.5.7" +version = "2.5.9" description = "File identification library for Python" category = "dev" optional = false @@ -358,11 +369,11 @@ python-versions = "*" [[package]] name = "isort" -version = "5.10.1" +version = "5.11.2" description = "A Python utility / library to sort Python imports." category = "dev" optional = false -python-versions = ">=3.6.1,<4.0" +python-versions = ">=3.7.0" [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] @@ -386,11 +397,11 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "lazy-object-proxy" -version = "1.7.1" +version = "1.8.0" description = "A fast and thorough lazy object proxy." category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "m2r2" @@ -462,18 +473,15 @@ setuptools = "*" [[package]] name = "packaging" -version = "21.3" +version = "22.0" description = "Core utilities for Python packages" category = "main" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.7" [[package]] name = "pathspec" -version = "0.10.1" +version = "0.10.3" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false @@ -481,15 +489,15 @@ python-versions = ">=3.7" [[package]] name = "platformdirs" -version = "2.5.2" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "2.6.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] -test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] +docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] +test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -525,7 +533,7 @@ virtualenv = ">=20.0.8" [[package]] name = "prompt-toolkit" -version = "3.0.31" +version = "3.0.36" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false @@ -599,20 +607,9 @@ python-versions = ">=3.6" [package.extras] plugins = ["importlib-metadata"] -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "main" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -621,12 +618,12 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -666,7 +663,7 @@ pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] [[package]] name = "pytz" -version = "2022.5" +version = "2022.6" description = "World timezone definitions, modern and historical" category = "main" optional = true @@ -696,7 +693,7 @@ docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphin [[package]] name = "readthedocs-sphinx-ext" -version = "2.1.9" +version = "2.2.0" description = "Sphinx extension for Read the Docs overrides" category = "main" optional = true @@ -762,7 +759,7 @@ pyasn1 = ">=0.1.3" [[package]] name = "setuptools" -version = "65.5.0" +version = "65.6.3" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false @@ -770,7 +767,7 @@ python-versions = ">=3.7" [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] [[package]] @@ -843,18 +840,18 @@ go = ["sphinxcontrib-golangdomain"] [[package]] name = "sphinx-rtd-theme" -version = "1.0.0" +version = "1.1.1" description = "Read the Docs theme for Sphinx" category = "main" optional = true -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" [package.dependencies] docutils = "<0.18" -sphinx = ">=1.6" +sphinx = ">=1.6,<6" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] [[package]] name = "sphinxcontrib-applehelp" @@ -929,7 +926,7 @@ test = ["pytest"] [[package]] name = "termcolor" -version = "2.0.1" +version = "2.1.1" description = "ANSI color formatting for output in terminal" category = "dev" optional = false @@ -956,15 +953,15 @@ python-versions = ">=3.7" [[package]] name = "tomlkit" -version = "0.11.5" +version = "0.11.6" description = "Style preserving TOML library" category = "dev" optional = false -python-versions = ">=3.6,<4.0" +python-versions = ">=3.6" [[package]] name = "tox" -version = "3.26.0" +version = "3.27.1" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -1011,11 +1008,11 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.12" +version = "1.26.13" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -1024,20 +1021,20 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.17.1" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] @@ -1069,7 +1066,7 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "zipp" -version = "3.10.0" +version = "3.11.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false @@ -1105,35 +1102,26 @@ attrs = [ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] babel = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, + {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, + {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, ] black = [ - {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, - {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, - {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, - {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, - {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, - {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, - {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, - {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, - {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, - {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, - {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, - {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, - {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, - {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, - {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, - {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, - {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, - {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, - {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, - {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, - {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, ] certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] cffi = [ {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, @@ -1222,8 +1210,8 @@ colorama = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] commitizen = [ - {file = "commitizen-2.35.0-py3-none-any.whl", hash = "sha256:ced3e161decf290c5263373dda440040405ed7f8b701b463d81e2ecc1e31d92c"}, - {file = "commitizen-2.35.0.tar.gz", hash = "sha256:34a7462c2279fc4e22929c03a9bb89242ab45dc501c0f17d1174e65c7fb9d793"}, + {file = "commitizen-2.38.0-py3-none-any.whl", hash = "sha256:401e1d6907d752dbb00fd5a8b0d0201ff36bc110870168776d49de20bf5b8b61"}, + {file = "commitizen-2.38.0.tar.gz", hash = "sha256:7daa217f703f330c18548304400d133a834840fd01bc79ef2966426c74bdbf1f"}, ] commonmark = [ {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, @@ -1325,9 +1313,13 @@ ecdsa = [ {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, ] +exceptiongroup = [ + {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, + {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, +] filelock = [ - {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, - {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, + {file = "filelock-3.8.2-py3-none-any.whl", hash = "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c"}, + {file = "filelock-3.8.2.tar.gz", hash = "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2"}, ] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, @@ -1338,8 +1330,8 @@ flake8-docstrings = [ {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, ] identify = [ - {file = "identify-2.5.7-py2.py3-none-any.whl", hash = "sha256:7a67b2a6208d390fd86fd04fb3def94a3a8b7f0bcbd1d1fcd6736f4defe26390"}, - {file = "identify-2.5.7.tar.gz", hash = "sha256:5b8fd1e843a6d4bf10685dd31f4520a7f1c7d0e14e9bc5d34b1d6f111cabc011"}, + {file = "identify-2.5.9-py2.py3-none-any.whl", hash = "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d"}, + {file = "identify-2.5.9.tar.gz", hash = "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -1358,51 +1350,33 @@ iniconfig = [ {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, ] isort = [ - {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, - {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, + {file = "isort-5.11.2-py3-none-any.whl", hash = "sha256:e486966fba83f25b8045f8dd7455b0a0d1e4de481e1d7ce4669902d9fb85e622"}, + {file = "isort-5.11.2.tar.gz", hash = "sha256:dd8bbc5c0990f2a095d754e50360915f73b4c26fc82733eb5bfc6b48396af4d2"}, ] jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] lazy-object-proxy = [ - {file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb8c5fd1684d60a9902c60ebe276da1f2281a318ca16c1d0a96db28f62e9166b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a57d51ed2997e97f3b8e3500c984db50a554bb5db56c50b5dab1b41339b37e36"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd45683c3caddf83abbb1249b653a266e7069a09f486daa8863fb0e7496a9fdb"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8561da8b3dd22d696244d6d0d5330618c993a215070f473b699e00cf1f3f6443"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fccdf7c2c5821a8cbd0a9440a456f5050492f2270bd54e94360cac663398739b"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win32.whl", hash = "sha256:898322f8d078f2654d275124a8dd19b079080ae977033b713f677afcfc88e2b9"}, - {file = "lazy_object_proxy-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:85b232e791f2229a4f55840ed54706110c80c0a210d076eee093f2b2e33e1bfd"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:46ff647e76f106bb444b4533bb4153c7370cdf52efc62ccfc1a28bdb3cc95442"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12f3bb77efe1367b2515f8cb4790a11cffae889148ad33adad07b9b55e0ab22c"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c19814163728941bb871240d45c4c30d33b8a2e85972c44d4e63dd7107faba44"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:e40f2013d96d30217a51eeb1db28c9ac41e9d0ee915ef9d00da639c5b63f01a1"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2052837718516a94940867e16b1bb10edb069ab475c3ad84fd1e1a6dd2c0fcfc"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win32.whl", hash = "sha256:6a24357267aa976abab660b1d47a34aaf07259a0c3859a34e536f1ee6e76b5bb"}, - {file = "lazy_object_proxy-1.7.1-cp36-cp36m-win_amd64.whl", hash = "sha256:6aff3fe5de0831867092e017cf67e2750c6a1c7d88d84d2481bd84a2e019ec35"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6a6e94c7b02641d1311228a102607ecd576f70734dc3d5e22610111aeacba8a0"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ce15276a1a14549d7e81c243b887293904ad2d94ad767f42df91e75fd7b5b6"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e368b7f7eac182a59ff1f81d5f3802161932a41dc1b1cc45c1f757dc876b5d2c"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6ecbb350991d6434e1388bee761ece3260e5228952b1f0c46ffc800eb313ff42"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:553b0f0d8dbf21890dd66edd771f9b1b5f51bd912fa5f26de4449bfc5af5e029"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win32.whl", hash = "sha256:c7a683c37a8a24f6428c28c561c80d5f4fd316ddcf0c7cab999b15ab3f5c5c69"}, - {file = "lazy_object_proxy-1.7.1-cp37-cp37m-win_amd64.whl", hash = "sha256:df2631f9d67259dc9620d831384ed7732a198eb434eadf69aea95ad18c587a28"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07fa44286cda977bd4803b656ffc1c9b7e3bc7dff7d34263446aec8f8c96f88a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dca6244e4121c74cc20542c2ca39e5c4a5027c81d112bfb893cf0790f96f57e"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91ba172fc5b03978764d1df5144b4ba4ab13290d7bab7a50f12d8117f8630c38"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:043651b6cb706eee4f91854da4a089816a6606c1428fd391573ef8cb642ae4f7"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b9e89b87c707dd769c4ea91f7a31538888aad05c116a59820f28d59b3ebfe25a"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win32.whl", hash = "sha256:9d166602b525bf54ac994cf833c385bfcc341b364e3ee71e3bf5a1336e677b55"}, - {file = "lazy_object_proxy-1.7.1-cp38-cp38-win_amd64.whl", hash = "sha256:8f3953eb575b45480db6568306893f0bd9d8dfeeebd46812aa09ca9579595148"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dd7ed7429dbb6c494aa9bc4e09d94b778a3579be699f9d67da7e6804c422d3de"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70ed0c2b380eb6248abdef3cd425fc52f0abd92d2b07ce26359fcbc399f636ad"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7096a5e0c1115ec82641afbdd70451a144558ea5cf564a896294e346eb611be1"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f769457a639403073968d118bc70110e7dce294688009f5c24ab78800ae56dc8"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:39b0e26725c5023757fc1ab2a89ef9d7ab23b84f9251e28f9cc114d5b59c1b09"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win32.whl", hash = "sha256:2130db8ed69a48a3440103d4a520b89d8a9405f1b06e2cc81640509e8bf6548f"}, - {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, - {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, + {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, + {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, + {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, + {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, + {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, + {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, + {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, + {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, + {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, ] m2r2 = [ {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, @@ -1471,16 +1445,16 @@ nodeenv = [ {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, ] packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, + {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, + {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, ] pathspec = [ - {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, - {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, + {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, + {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, ] platformdirs = [ - {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, - {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, + {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, + {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, ] pluggy = [ {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, @@ -1491,8 +1465,8 @@ pre-commit = [ {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, ] prompt-toolkit = [ - {file = "prompt_toolkit-3.0.31-py3-none-any.whl", hash = "sha256:9696f386133df0fc8ca5af4895afe5d78f5fcfe5258111c2a79a1c3e41ffa96d"}, - {file = "prompt_toolkit-3.0.31.tar.gz", hash = "sha256:9ada952c9d1787f52ff6d5f3484d0b4df8952787c087edf6a1f7c2cb1ea88148"}, + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, ] py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, @@ -1533,13 +1507,9 @@ pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, @@ -1550,8 +1520,8 @@ python-jose = [ {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, ] pytz = [ - {file = "pytz-2022.5-py2.py3-none-any.whl", hash = "sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22"}, - {file = "pytz-2022.5.tar.gz", hash = "sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914"}, + {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, + {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -1593,8 +1563,8 @@ questionary = [ {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, ] readthedocs-sphinx-ext = [ - {file = "readthedocs-sphinx-ext-2.1.9.tar.gz", hash = "sha256:470aadad72a6ccdc803466e45381a5b5d33ac5389553c6efee17ec0268a6986c"}, - {file = "readthedocs_sphinx_ext-2.1.9-py2.py3-none-any.whl", hash = "sha256:1a98ad8f054e331fe6691f2e62a466acf1753a2d747042001125c8b90a13db9b"}, + {file = "readthedocs-sphinx-ext-2.2.0.tar.gz", hash = "sha256:e5effcd825816111a377ab7a897b819215138f8e5e8acc86f99218328f957240"}, + {file = "readthedocs_sphinx_ext-2.2.0-py2.py3-none-any.whl", hash = "sha256:d801f0bfb125d2837f18f40451462528d4a97eefd8de8a12ad526b4f1ce14205"}, ] recommonmark = [ {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, @@ -1613,8 +1583,8 @@ rsa = [ {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, ] setuptools = [ - {file = "setuptools-65.5.0-py3-none-any.whl", hash = "sha256:f62ea9da9ed6289bfe868cd6845968a2c854d1427f8548d52cae02a42b4f0356"}, - {file = "setuptools-65.5.0.tar.gz", hash = "sha256:512e5536220e38146176efb833d4a62aa726b7bbff82cfbc8ba9eaa3996e0b17"}, + {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, + {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, @@ -1633,8 +1603,8 @@ sphinx-autoapi = [ {file = "sphinx_autoapi-2.0.0-py2.py3-none-any.whl", hash = "sha256:dab2753a38cad907bf4e61473c0da365a26bfbe69fbf5aa6e4f7d48e1cf8a148"}, ] sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.0.0-py2.py3-none-any.whl", hash = "sha256:4d35a56f4508cfee4c4fb604373ede6feae2a306731d533f409ef5c3496fdbd8"}, - {file = "sphinx_rtd_theme-1.0.0.tar.gz", hash = "sha256:eec6d497e4c2195fa0e8b2016b337532b8a699a68bcb22a512870e16925c6a5c"}, + {file = "sphinx_rtd_theme-1.1.1-py2.py3-none-any.whl", hash = "sha256:31faa07d3e97c8955637fc3f1423a5ab2c44b74b8cc558a51498c202ce5cbda7"}, + {file = "sphinx_rtd_theme-1.1.1.tar.gz", hash = "sha256:6146c845f1e1947b3c3dd4432c28998a1693ccc742b4f9ad7c63129f0757c103"}, ] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, @@ -1661,8 +1631,8 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] termcolor = [ - {file = "termcolor-2.0.1-py3-none-any.whl", hash = "sha256:7e597f9de8e001a3208c4132938597413b9da45382b6f1d150cff8d062b7aaa3"}, - {file = "termcolor-2.0.1.tar.gz", hash = "sha256:6b2cf769e93364a2676e1de56a7c0cff2cf5bd07f37e9cc80b0dd6320ebfe388"}, + {file = "termcolor-2.1.1-py3-none-any.whl", hash = "sha256:fa852e957f97252205e105dd55bbc23b419a70fec0085708fc0515e399f304fd"}, + {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"}, ] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, @@ -1673,12 +1643,12 @@ tomli = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tomlkit = [ - {file = "tomlkit-0.11.5-py3-none-any.whl", hash = "sha256:f2ef9da9cef846ee027947dc99a45d6b68a63b0ebc21944649505bf2e8bc5fe7"}, - {file = "tomlkit-0.11.5.tar.gz", hash = "sha256:571854ebbb5eac89abcb4a2e47d7ea27b89bf29e09c35395da6f03dd4ae23d1c"}, + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, ] tox = [ - {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, - {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, + {file = "tox-3.27.1-py2.py3-none-any.whl", hash = "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84"}, + {file = "tox-3.27.1.tar.gz", hash = "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04"}, ] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, @@ -1715,12 +1685,12 @@ unidecode = [ {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, ] urllib3 = [ - {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, - {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, + {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, + {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, ] virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, + {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, + {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, ] wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, @@ -1797,6 +1767,6 @@ wrapt = [ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] zipp = [ - {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, - {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, + {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, + {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, ] From 90298f7af65af376c27ea374b6d3349b7a3a88f1 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 13 Dec 2022 21:45:00 +0000 Subject: [PATCH 271/566] style: formatting applied --- docs/source/conf.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6216cce1..c6bcb603 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -180,15 +180,7 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ( - master_doc, - "python-keycloak", - "python-keycloak Documentation", - [author], - 1, - ) -] +man_pages = [(master_doc, "python-keycloak", "python-keycloak Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- From f1809970bcc49930fc7711294a6d04d8f8e4cb86 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 13 Dec 2022 21:45:39 +0000 Subject: [PATCH 272/566] test: fix the tests for latest keycloak --- test_keycloak_init.sh | 2 +- tests/test_keycloak_admin.py | 39 +++++++++++++++++++---------------- tests/test_keycloak_openid.py | 3 +++ tox.ini | 1 + 4 files changed, 26 insertions(+), 19 deletions(-) diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index 5dc5e534..07df97d3 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -13,7 +13,7 @@ function keycloak_stop() { function keycloak_start() { echo "Starting keycloak docker container" - docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -e KC_FEATURES="token-exchange" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev + docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -e KC_FEATURES="token-exchange,admin-fine-grained-authz" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev SECONDS=0 until curl --silent --output /dev/null localhost:$KEYCLOAK_PORT; do sleep 5; diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index b1625f01..1420c565 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -506,21 +506,24 @@ def test_server_info(admin: KeycloakAdmin): :type admin: KeycloakAdmin """ info = admin.get_server_info() - assert set(info.keys()) == { - "systemInfo", - "memoryInfo", - "profileInfo", - "themes", - "socialProviders", - "identityProviders", - "providers", - "protocolMapperTypes", - "builtinProtocolMappers", - "clientInstallations", - "componentTypes", - "passwordPolicies", - "enums", - }, info.keys() + assert set(info.keys()).issubset( + { + "systemInfo", + "memoryInfo", + "profileInfo", + "themes", + "socialProviders", + "identityProviders", + "providers", + "protocolMapperTypes", + "builtinProtocolMappers", + "clientInstallations", + "componentTypes", + "passwordPolicies", + "enums", + "cryptoInfo", + } + ), info.keys() def test_groups(admin: KeycloakAdmin, user: str): @@ -790,7 +793,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakGetError) as err: admin.get_client_authz_settings(client_id=client_id) - assert err.match('500: b\'{"error":"HTTP 500 Internal Server Error"}\'') + assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') # Authz resources res = admin.get_client_authz_resources(client_id=auth_client_id) @@ -799,7 +802,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakGetError) as err: admin.get_client_authz_resources(client_id=client_id) - assert err.match('500: b\'{"error":"unknown_error"}\'') + assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') res = admin.create_client_authz_resource( client_id=auth_client_id, payload={"name": "test-resource"} @@ -885,7 +888,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakGetError) as err: admin.get_client_authz_scopes(client_id=client_id) - assert err.match('500: b\'{"error":"unknown_error"}\'') + assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') # Test service account user res = admin.get_client_service_account_user(client_id=auth_client_id) diff --git a/tests/test_keycloak_openid.py b/tests/test_keycloak_openid.py index 5a6a2edb..e1a14214 100644 --- a/tests/test_keycloak_openid.py +++ b/tests/test_keycloak_openid.py @@ -135,6 +135,7 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): assert token == { "access_token": mock.ANY, "expires_in": 300, + "id_token": mock.ANY, "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, @@ -148,6 +149,7 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): assert token == { "access_token": mock.ANY, "expires_in": 300, + "id_token": mock.ANY, "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, @@ -161,6 +163,7 @@ def test_token(oid_with_credentials: Tuple[KeycloakOpenID, str, str]): assert token == { "access_token": mock.ANY, "expires_in": 300, + "id_token": mock.ANY, "not-before-policy": 0, "refresh_expires_in": 1800, "refresh_token": mock.ANY, diff --git a/tox.ini b/tox.ini index 2b3db365..174d0748 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ requires = tox-poetry poetry + tox<4.0.0 envlist = check, apply-check, docs, tests, build, changelog [testenv] From 451a22a1030bb7266b6501c743a66436eee072b4 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 13 Dec 2022 21:45:54 +0000 Subject: [PATCH 273/566] fix: default scope to openid --- src/keycloak/keycloak_openid.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/keycloak/keycloak_openid.py b/src/keycloak/keycloak_openid.py index 9a794745..56e0315d 100644 --- a/src/keycloak/keycloak_openid.py +++ b/src/keycloak/keycloak_openid.py @@ -260,6 +260,7 @@ def token( code="", redirect_uri="", totp=None, + scope="openid", **extra ): """Retrieve user token. @@ -283,6 +284,8 @@ def token( :type redirect_uri: str :param totp: Time-based one-time password :type totp: int + :param scope: Scope, defaults to openid + :type scope: str :param extra: Additional extra arguments :type extra: dict :returns: Keycloak token @@ -296,6 +299,7 @@ def token( "grant_type": grant_type, "code": code, "redirect_uri": redirect_uri, + "scope": scope, } if extra: payload.update(extra) @@ -341,7 +345,7 @@ def exchange_token( audience: str, subject: str, requested_token_type: str = "urn:ietf:params:oauth:token-type:refresh_token", - scope: str = "", + scope: str = "openid", ) -> dict: """Exchange user token. @@ -358,7 +362,7 @@ def exchange_token( :type subject: str :param requested_token_type: Token type specification :type requested_token_type: str - :param scope: Scope + :param scope: Scope, defaults to openid :type scope: str :returns: Exchanged token :rtype: dict From fa955b759f6d7ce6e19ee833db0ce3cf961c114b Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 13 Dec 2022 21:46:07 +0000 Subject: [PATCH 274/566] fix: use version from the package --- src/keycloak/_version.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/keycloak/_version.py b/src/keycloak/_version.py index f3403b2c..d6030e78 100644 --- a/src/keycloak/_version.py +++ b/src/keycloak/_version.py @@ -21,4 +21,6 @@ # IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -__version__ = "0.0.0" +import pkg_resources + +__version__ = pkg_resources.get_distribution("python-keycloak").version From a86ae023f72ec4c21db220c05883b84a66d46db0 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 13 Dec 2022 21:54:02 +0000 Subject: [PATCH 275/566] chore: added twine to poetry --- poetry.lock | 231 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 29 ++++--- 2 files changed, 242 insertions(+), 18 deletions(-) diff --git a/poetry.lock b/poetry.lock index a31d3c62..72a50654 100644 --- a/poetry.lock +++ b/poetry.lock @@ -83,6 +83,22 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bleach" +version = "5.0.1" +description = "An easy safelist-based HTML-sanitizing tool." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +six = ">=1.9.0" +webencodings = "*" + +[package.extras] +css = ["tinycss2 (>=1.1.0,<1.2)"] +dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] + [[package]] name = "certifi" version = "2022.12.7" @@ -180,7 +196,7 @@ name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" category = "main" -optional = true +optional = false python-versions = "*" [package.extras] @@ -248,7 +264,7 @@ name = "docutils" version = "0.17.1" description = "Docutils -- Python Documentation Utilities" category = "main" -optional = true +optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] @@ -381,6 +397,33 @@ pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] +[[package]] +name = "jaraco-classes" +version = "3.2.3" +description = "Utility functions for Python class constructs" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +more-itertools = "*" + +[package.extras] +docs = ["jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[[package]] +name = "jeepney" +version = "0.8.0" +description = "Low-level, pure Python DBus protocol wrapper." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] +trio = ["async_generator", "trio"] + [[package]] name = "jinja2" version = "3.1.2" @@ -395,6 +438,25 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "keyring" +version = "23.11.0" +description = "Store and access your passwords safely." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +"jaraco.classes" = "*" +jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} +pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + [[package]] name = "lazy-object-proxy" version = "1.8.0" @@ -452,6 +514,14 @@ build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest (<5.4)", "pytest-cov"] +[[package]] +name = "more-itertools" +version = "9.0.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = ">=3.7" + [[package]] name = "mypy-extensions" version = "0.4.3" @@ -487,6 +557,17 @@ category = "dev" optional = false python-versions = ">=3.7" +[[package]] +name = "pkginfo" +version = "1.9.2" +description = "Query metadatdata from sdists / bdists / installed packages." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +testing = ["pytest", "pytest-cov"] + [[package]] name = "platformdirs" version = "2.6.0" @@ -601,7 +682,7 @@ name = "pygments" version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" -optional = true +optional = false python-versions = ">=3.6" [package.extras] @@ -669,6 +750,14 @@ category = "main" optional = true python-versions = "*" +[[package]] +name = "pywin32-ctypes" +version = "0.2.0" +description = "" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "pyyaml" version = "6.0" @@ -691,6 +780,22 @@ prompt_toolkit = ">=2.0,<4.0" [package.extras] docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] +[[package]] +name = "readme-renderer" +version = "37.3" +description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +bleach = ">=2.1.0" +docutils = ">=0.13.1" +Pygments = ">=2.5.1" + +[package.extras] +md = ["cmarkgfm (>=0.8.0)"] + [[package]] name = "readthedocs-sphinx-ext" version = "2.2.0" @@ -746,6 +851,33 @@ python-versions = "*" [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "rfc3986" +version = "2.0.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "rich" +version = "12.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "dev" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" +typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + [[package]] name = "rsa" version = "4.9" @@ -757,6 +889,18 @@ python-versions = ">=3.6,<4" [package.dependencies] pyasn1 = ">=0.1.3" +[[package]] +name = "secretstorage" +version = "3.3.3" +description = "Python bindings to FreeDesktop.org Secret Service API" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +cryptography = ">=2.0" +jeepney = ">=0.6" + [[package]] name = "setuptools" version = "65.6.3" @@ -982,6 +1126,25 @@ virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2, docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] +[[package]] +name = "twine" +version = "4.0.2" +description = "Collection of utilities for publishing packages on PyPI" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +importlib-metadata = ">=3.6" +keyring = ">=15.1" +pkginfo = ">=1.8.1" +readme-renderer = ">=35.0" +requests = ">=2.20" +requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" +rfc3986 = ">=1.4.0" +rich = ">=12.0.0" +urllib3 = ">=1.26.0" + [[package]] name = "typed-ast" version = "1.5.4" @@ -1045,6 +1208,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "webencodings" +version = "0.5.1" +description = "Character encoding aliases for legacy web content" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "wheel" version = "0.37.1" @@ -1082,7 +1253,7 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "da04d73122fef0b5938fc53dccdc95dbdd245f983ccd02a570c5569d945e89c1" +content-hash = "48a656650bc98b40759fa591d4878a407f5af1fe65d1035ab7bb6430bf9fae29" [metadata.files] alabaster = [ @@ -1119,6 +1290,10 @@ black = [ {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, ] +bleach = [ + {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, + {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, +] certifi = [ {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, @@ -1353,10 +1528,22 @@ isort = [ {file = "isort-5.11.2-py3-none-any.whl", hash = "sha256:e486966fba83f25b8045f8dd7455b0a0d1e4de481e1d7ce4669902d9fb85e622"}, {file = "isort-5.11.2.tar.gz", hash = "sha256:dd8bbc5c0990f2a095d754e50360915f73b4c26fc82733eb5bfc6b48396af4d2"}, ] +jaraco-classes = [ + {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, + {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, +] +jeepney = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] jinja2 = [ {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, ] +keyring = [ + {file = "keyring-23.11.0-py3-none-any.whl", hash = "sha256:3dd30011d555f1345dec2c262f0153f2f0ca6bca041fb1dc4588349bb4c0ac1e"}, + {file = "keyring-23.11.0.tar.gz", hash = "sha256:ad192263e2cdd5f12875dedc2da13534359a7e760e77f8d04b50968a821c2361"}, +] lazy-object-proxy = [ {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, @@ -1436,6 +1623,10 @@ mock = [ {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, ] +more-itertools = [ + {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, + {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, +] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, @@ -1452,6 +1643,10 @@ pathspec = [ {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, ] +pkginfo = [ + {file = "pkginfo-1.9.2-py3-none-any.whl", hash = "sha256:d580059503f2f4549ad6e4c106d7437356dbd430e2c7df99ee1efe03d75f691e"}, + {file = "pkginfo-1.9.2.tar.gz", hash = "sha256:ac03e37e4d601aaee40f8087f63fc4a2a6c9814dda2c8fa6aab1b1829653bdfa"}, +] platformdirs = [ {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, @@ -1523,6 +1718,10 @@ pytz = [ {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, ] +pywin32-ctypes = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, @@ -1562,6 +1761,10 @@ questionary = [ {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, ] +readme-renderer = [ + {file = "readme_renderer-37.3-py3-none-any.whl", hash = "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343"}, + {file = "readme_renderer-37.3.tar.gz", hash = "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273"}, +] readthedocs-sphinx-ext = [ {file = "readthedocs-sphinx-ext-2.2.0.tar.gz", hash = "sha256:e5effcd825816111a377ab7a897b819215138f8e5e8acc86f99218328f957240"}, {file = "readthedocs_sphinx_ext-2.2.0-py2.py3-none-any.whl", hash = "sha256:d801f0bfb125d2837f18f40451462528d4a97eefd8de8a12ad526b4f1ce14205"}, @@ -1578,10 +1781,22 @@ requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] +rfc3986 = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] +rich = [ + {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, + {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, +] rsa = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, ] +secretstorage = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] setuptools = [ {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, @@ -1650,6 +1865,10 @@ tox = [ {file = "tox-3.27.1-py2.py3-none-any.whl", hash = "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84"}, {file = "tox-3.27.1.tar.gz", hash = "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04"}, ] +twine = [ + {file = "twine-4.0.2-py3-none-any.whl", hash = "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8"}, + {file = "twine-4.0.2.tar.gz", hash = "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8"}, +] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, @@ -1696,6 +1915,10 @@ wcwidth = [ {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] +webencodings = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] wheel = [ {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, diff --git a/pyproject.toml b/pyproject.toml index f8636431..685d8daf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,20 @@ m2r2 = {version = "^0.3.2", optional = true} sphinx-autoapi = {version = "^2.0.0", optional = true} requests-toolbelt = "^0.9.1" -[tool.poetry.dev-dependencies] +[tool.poetry.extras] +docs = [ + "mock", + "alabaster", + "commonmark", + "recommonmark", + "sphinx", + "sphinx-rtd-theme", + "readthedocs-sphinx-ext", + "m2r2", + "sphinx-autoapi", +] + +[tool.poetry.group.dev.dependencies] tox = "^3.25.0" pytest = "^7.1.2" pytest-cov = "^3.0.0" @@ -58,19 +71,7 @@ commitizen = "^2.28.0" cryptography = "^37.0.4" codespell = "^2.1.0" darglint = "^1.8.1" - -[tool.poetry.extras] -docs = [ - "mock", - "alabaster", - "commonmark", - "recommonmark", - "sphinx", - "sphinx-rtd-theme", - "readthedocs-sphinx-ext", - "m2r2", - "sphinx-autoapi", -] +twine = "^4.0.2" [build-system] requires = ["poetry-core>=1.0.0"] From 8496356103be92768921b500f646579c044593d7 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 13 Dec 2022 21:55:00 +0000 Subject: [PATCH 276/566] ci: use tox from poetry --- .github/workflows/daily.yaml | 5 +++-- .github/workflows/lint.yaml | 20 ++++++++++++-------- .github/workflows/publish.yaml | 9 +++++---- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml index d53caf05..3b3fb0f8 100644 --- a/.github/workflows/daily.yaml +++ b/.github/workflows/daily.yaml @@ -21,7 +21,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox + python -m pip install poetry + poetry install - name: Run tests run: | - tox -e tests + poetry run tox -e tests diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 8a740af3..9f3458c8 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -24,10 +24,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox + python -m pip install poetry + poetry install - name: Check linting, formatting run: | - tox -e check + poetry run tox -e check check-docs: runs-on: ubuntu-latest @@ -43,10 +44,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox + python -m pip install poetry + poetry install - name: Check documentation build run: | - tox -e docs + poetry run tox -e docs test: runs-on: ubuntu-latest @@ -67,10 +69,11 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox + python -m pip install poetry + poetry install - name: Run tests run: | - tox -e tests + poetry run tox -e tests - name: Keycloak logs run: | cat keycloak_test_logs.txt @@ -89,7 +92,8 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox + python -m pip install poetry + poetry install - name: Run build run: | - tox -e build + poetry run tox -e build diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index c073b31b..a84cced2 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -19,23 +19,24 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install tox wheel twine + python -m pip install poetry + poetry install - name: Apply the tag version run: | version=${{ github.ref_name }} sed -Ei '/^version = /s|= "[0-9.]+"$|= "'${version:-1}'"|' pyproject.toml - name: Run build run: | - tox -e build + poetry run tox -e build - name: Publish to PyPi env: TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} run: | - twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* + poetry run twine upload -u $TWINE_USERNAME -p $TWINE_PASSWORD dist/* - name: Run changelog run: | - tox -e changelog + poetry run tox -e changelog - uses: stefanzweifel/git-auto-commit-action@v4 with: commit_message: "docs: changelog update" From aca13dec9a7fadaea39f686304120cb648e84ad0 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Tue, 13 Dec 2022 22:20:46 +0000 Subject: [PATCH 277/566] docs: changelog update --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8674555c..f2472908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v2.6.1 (2022-12-13) + +### Fix + +- use version from the package +- default scope to openid + ## v2.6.0 (2022-10-03) ### Feat From e9b173024bdcfc30c824d0e03f0636c3fd877ebe Mon Sep 17 00:00:00 2001 From: Igli Manaj Date: Tue, 20 Dec 2022 02:17:21 +0100 Subject: [PATCH 278/566] refactor: remove print statements --- src/keycloak/keycloak_admin.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 55bb6d66..0e0cca09 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -790,7 +790,6 @@ def disable_all_users(self): users = self.get_users() for user in users: user_id = user["id"] - print(f"Disabling user with id: {user_id}") self.disable_user(user_id=user_id) def enable_all_users(self): @@ -799,7 +798,6 @@ def enable_all_users(self): users = self.get_users() for user in users: user_id = user["id"] - print(f"Enabling user with id: {user_id}") self.enable_user(user_id=user_id) def delete_user(self, user_id): From 4fe06af67745d0322ba9214fd9fc91b4bcde3f23 Mon Sep 17 00:00:00 2001 From: iglimanaj Date: Sat, 24 Dec 2022 00:47:53 +0100 Subject: [PATCH 279/566] refactor: code formatting after tox checks --- src/keycloak/keycloak_admin.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 0e0cca09..ac1f46ef 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -772,7 +772,7 @@ def disable_user(self, user_id): :rtype: bytes """ return self.update_user(user_id=user_id, payload={"enabled": False}) - + def enable_user(self, user_id): """Enable the user from the realm. @@ -783,18 +783,16 @@ def enable_user(self, user_id): :rtype: bytes """ return self.update_user(user_id=user_id, payload={"enabled": True}) - + def disable_all_users(self): - """Disable all existing users. - """ + """Disable all existing users.""" users = self.get_users() for user in users: user_id = user["id"] self.disable_user(user_id=user_id) - + def enable_all_users(self): - """Disable all existing users. - """ + """Disable all existing users.""" users = self.get_users() for user in users: user_id = user["id"] From f9ecd865230398228cbf0b99a84340e86f4f4ac1 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Sat, 24 Dec 2022 08:19:24 +0000 Subject: [PATCH 280/566] docs: changelog update --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2472908..8be645dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ +## v2.7.0 (2022-12-24) + +### Refactor + +- code formatting after tox checks +- remove print statements + ## v2.6.1 (2022-12-13) +### Feat + +- option for enabling users +- helping functions for disabling users + ### Fix - use version from the package From 35af02af95a56799bcea03b9f2a67dd0156877a1 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Tue, 27 Dec 2022 21:39:00 +0100 Subject: [PATCH 281/566] ci: added python3.11 to daily check --- .github/workflows/daily.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily.yaml b/.github/workflows/daily.yaml index 3b3fb0f8..6f4168aa 100644 --- a/.github/workflows/daily.yaml +++ b/.github/workflows/daily.yaml @@ -10,7 +10,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} From eeb2fbb6281b626db5c9741bc4d6856f0ab19b38 Mon Sep 17 00:00:00 2001 From: hadeer_e Date: Wed, 28 Dec 2022 21:15:16 +0200 Subject: [PATCH 282/566] feat(api): add tests for create_authz_scopes --- src/keycloak/keycloak_admin.py | 19 +++++++++++++++++++ tests/test_keycloak_admin.py | 18 ++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index ac1f46ef..6d74a8c5 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1475,6 +1475,25 @@ def get_client_authz_scopes(self, client_id): data_raw = self.raw_get(urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path)) return raise_error_from_response(data_raw, KeycloakGetError) + def create_client_authz_scopes(self, client_id, payload): + """Create scopes for client. + + :param client_id: id in ClientRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_clientrepresentation + :param payload: ScopeRepresentation + https://www.keycloak.org/docs-api/18.0/rest-api/index.html#_ScopeRepresentation + :type payload: dict + :type client_id: str + :return: Keycloak server response + :rtype: bytes + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_CLIENT_AUTHZ_SCOPES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + def get_client_authz_permissions(self, client_id): """Get permissions from client. diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 1420c565..29d9e136 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -890,6 +890,24 @@ def test_clients(admin: KeycloakAdmin, realm: str): admin.get_client_authz_scopes(client_id=client_id) assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') + res = admin.create_client_authz_scopes( + client_id=auth_client_id, payload={"name": "test-authz-scope"} + ) + assert res["name"] == "test-authz-scope", res + + with pytest.raises(KeycloakPostError) as err: + admin.create_client_authz_scopes( + client_id=auth_client_id, payload={"name": "test-authz-scope"} + ) + assert err.match('409: b\'{"error":"invalid_request"') + assert admin.create_client_authz_scopes( + client_id=auth_client_id, payload={"name": "test-authz-scope"}, skip_exists=True + ) == {"msg": "Already exists"} + + res = admin.get_client_authz_scopes(client_id=auth_client_id) + assert len(res) == 2 + assert {x["name"] for x in res} == {"Default Scope", "test-authz-scope"} + # Test service account user res = admin.get_client_service_account_user(client_id=auth_client_id) assert res["username"] == "service-account-authz-client", res From 5855ec867ec92757dc4d9012e4e49235ef21f14a Mon Sep 17 00:00:00 2001 From: hadeer_e Date: Wed, 28 Dec 2022 21:27:55 +0200 Subject: [PATCH 283/566] fix: create authz clients test case --- tests/test_keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 29d9e136..5be7dfbc 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -887,7 +887,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): assert len(res) == 0, res with pytest.raises(KeycloakGetError) as err: - admin.get_client_authz_scopes(client_id=client_id) + admin.get_client_authz_scopes(client_id=auth_client_id) assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') res = admin.create_client_authz_scopes( From 1da9de39b6266d4e577a08b31127845c67ed4bc9 Mon Sep 17 00:00:00 2001 From: hadeer_e Date: Wed, 28 Dec 2022 21:47:01 +0200 Subject: [PATCH 284/566] fix: create authz clients test case --- tests/test_keycloak_admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 5be7dfbc..1afeda97 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -887,7 +887,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): assert len(res) == 0, res with pytest.raises(KeycloakGetError) as err: - admin.get_client_authz_scopes(client_id=auth_client_id) + admin.get_client_authz_scopes(client_id=client_id) assert err.match('404: b\'{"error":"HTTP 404 Not Found"}\'') res = admin.create_client_authz_scopes( @@ -905,8 +905,8 @@ def test_clients(admin: KeycloakAdmin, realm: str): ) == {"msg": "Already exists"} res = admin.get_client_authz_scopes(client_id=auth_client_id) - assert len(res) == 2 - assert {x["name"] for x in res} == {"Default Scope", "test-authz-scope"} + assert len(res) == 1 + assert {x["name"] for x in res} == {"test-authz-scope"} # Test service account user res = admin.get_client_service_account_user(client_id=auth_client_id) From 936ed549779b66bcda482c9a329db9a47d297b6c Mon Sep 17 00:00:00 2001 From: hadeer_e Date: Wed, 28 Dec 2022 22:14:22 +0200 Subject: [PATCH 285/566] fix: add testcase for invalid client id --- tests/test_keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 1afeda97..128699aa 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -897,9 +897,9 @@ def test_clients(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakPostError) as err: admin.create_client_authz_scopes( - client_id=auth_client_id, payload={"name": "test-authz-scope"} + client_id='invalid_client_id', payload={"name": "test-authz-scope"} ) - assert err.match('409: b\'{"error":"invalid_request"') + assert err.match('404: b\'{"error":"Could not find client"') assert admin.create_client_authz_scopes( client_id=auth_client_id, payload={"name": "test-authz-scope"}, skip_exists=True ) == {"msg": "Already exists"} From 15a325382fe2f969d997008e371e233154590682 Mon Sep 17 00:00:00 2001 From: hadeer_e Date: Wed, 28 Dec 2022 22:27:01 +0200 Subject: [PATCH 286/566] fix: fix linting --- tests/test_keycloak_admin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 128699aa..1dcf6350 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -897,7 +897,7 @@ def test_clients(admin: KeycloakAdmin, realm: str): with pytest.raises(KeycloakPostError) as err: admin.create_client_authz_scopes( - client_id='invalid_client_id', payload={"name": "test-authz-scope"} + client_id="invalid_client_id", payload={"name": "test-authz-scope"} ) assert err.match('404: b\'{"error":"Could not find client"') assert admin.create_client_authz_scopes( From 1d864703a3de98b0fdaa6ad46c1db0f9083b0f65 Mon Sep 17 00:00:00 2001 From: hadeer_e Date: Wed, 28 Dec 2022 22:39:08 +0200 Subject: [PATCH 287/566] fix: fix testing create_client_authz_scopes parameters --- tests/test_keycloak_admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 1dcf6350..9dd6b511 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -901,8 +901,8 @@ def test_clients(admin: KeycloakAdmin, realm: str): ) assert err.match('404: b\'{"error":"Could not find client"') assert admin.create_client_authz_scopes( - client_id=auth_client_id, payload={"name": "test-authz-scope"}, skip_exists=True - ) == {"msg": "Already exists"} + client_id=auth_client_id, payload={"name": "test-authz-scope"} + ) res = admin.get_client_authz_scopes(client_id=auth_client_id) assert len(res) == 1 From f53f59cc0b659ca286d69de987c9cfd3b6a847a4 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Thu, 29 Dec 2022 06:39:09 +0000 Subject: [PATCH 288/566] docs: changelog update --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8be645dc..1b521124 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## v2.8.0 (2022-12-29) + +### Feat + +- **api**: add tests for create_authz_scopes + +### Fix + +- fix testing create_client_authz_scopes parameters +- fix linting +- add testcase for invalid client id +- create authz clients test case +- create authz clients test case + ## v2.7.0 (2022-12-24) ### Refactor From aa207286f05372e2d59aafe72db4b4b6aee57c70 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Wed, 11 Jan 2023 10:56:44 +0000 Subject: [PATCH 289/566] feat: added default realm roles handlers --- src/keycloak/keycloak_admin.py | 62 +++++++++++++++++++++++++++-- src/keycloak/urls_patterns.py | 6 +-- tests/test_keycloak_admin.py | 73 ++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 6d74a8c5..26792f51 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -988,7 +988,7 @@ def send_update_account( data_raw = self.raw_put( urls_patterns.URL_ADMIN_SEND_UPDATE_ACCOUNT.format(**params_path), data=json.dumps(payload), - **params_query + **params_query, ) return raise_error_from_response(data_raw, KeycloakPutError) @@ -1012,7 +1012,7 @@ def send_verify_email(self, user_id, client_id=None, redirect_uri=None): data_raw = self.raw_put( urls_patterns.URL_ADMIN_SEND_VERIFY_EMAIL.format(**params_path), data={}, - **params_query + **params_query, ) return raise_error_from_response(data_raw, KeycloakPutError) @@ -1657,6 +1657,62 @@ def get_realm_role_members(self, role_name, query=None): urls_patterns.URL_ADMIN_REALM_ROLES_MEMBERS.format(**params_path), query ) + def get_default_realm_role_id(self): + """Get the ID of the default realm role. + + :return: Realm role ID + :rtype: str + """ + all_realm_roles = self.get_realm_roles() + default_realm_roles = [ + realm_role + for realm_role in all_realm_roles + if realm_role["name"] == f"default-roles-{self.realm_name}" + ] + return default_realm_roles[0]["id"] + + def get_realm_default_roles(self): + """Get all the default realm roles. + + :return: Keycloak Server Response (UserRepresentation) + :rtype: list + """ + params_path = {"realm-name": self.realm_name, "role-id": self.get_default_realm_role_id()} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_REALM_ROLE_COMPOSITES_REALM.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def remove_realm_default_roles(self, payload): + """Remove a set of default realm roles. + + :param payload: List of RoleRepresentations + :type payload: list + :return: Keycloak Server Response + :rtype: dict + """ + params_path = {"realm-name": self.realm_name, "role-id": self.get_default_realm_role_id()} + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_REALM_ROLE_COMPOSITES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakDeleteError) + + def add_realm_default_roles(self, payload): + """Add a set of default realm roles. + + :param payload: List of RoleRepresentations + :type payload: list + :return: Keycloak Server Response + :rtype: dict + """ + params_path = {"realm-name": self.realm_name, "role-id": self.get_default_realm_role_id()} + data_raw = self.raw_post( + urls_patterns.URL_ADMIN_REALM_ROLE_COMPOSITES.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPostError) + def get_client_roles(self, client_id, brief_representation=True): """Get all roles for the client. @@ -2664,7 +2720,7 @@ def sync_users(self, storage_id, action): data_raw = self.raw_post( urls_patterns.URL_ADMIN_USER_STORAGE.format(**params_path), data=json.dumps(data), - **params_query + **params_query, ) return raise_error_from_response(data_raw, KeycloakPostError) diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index b5f32778..4ea04493 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -189,9 +189,9 @@ URL_ADMIN_CLIENT_SESSION_STATS = "admin/realms/{realm-name}/client-session-stats" URL_ADMIN_GROUPS_CLIENT_ROLES_COMPOSITE = URL_ADMIN_GROUPS_CLIENT_ROLES + "/composite" -URL_ADMIN_CLIENT_ROLE_CHILDREN = ( - "admin/realms/{realm-name}/roles-by-id/{role-id}/composites/clients/{client-id}" -) +URL_ADMIN_REALM_ROLE_COMPOSITES = "admin/realms/{realm-name}/roles-by-id/{role-id}/composites" +URL_ADMIN_REALM_ROLE_COMPOSITES_REALM = URL_ADMIN_REALM_ROLE_COMPOSITES + "/realm" +URL_ADMIN_CLIENT_ROLE_CHILDREN = URL_ADMIN_REALM_ROLE_COMPOSITES + "/clients/{client-id}" URL_ADMIN_CLIENT_CERT_UPLOAD = URL_ADMIN_CLIENT_CERTS + "/upload-certificate" URL_ADMIN_REQUIRED_ACTIONS = URL_ADMIN_REALM + "/authentication/required-actions" URL_ADMIN_REQUIRED_ACTIONS_ALIAS = URL_ADMIN_REQUIRED_ACTIONS + "/{action-alias}" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index 9dd6b511..f6d34d0a 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -2421,3 +2421,76 @@ def test_clear_bruteforce_attempts_for_all_users( res = admin.update_realm(realm_name=realm, payload={"bruteForceProtected": False}) res = admin.get_realm(realm_name=realm) assert res["bruteForceProtected"] is False + + +def test_default_realm_role_present(realm: str, admin: KeycloakAdmin) -> None: + """Test that the default realm role is present in a brand new realm. + + :param realm: Realm name + :type realm: str + :param admin: Keycloak admin + :type admin: KeycloakAdmin + """ + admin.realm_name = realm + assert f"default-roles-{realm}" in [x["name"] for x in admin.get_realm_roles()] + assert ( + len([x["name"] for x in admin.get_realm_roles() if x["name"] == f"default-roles-{realm}"]) + == 1 + ) + + +def test_get_default_realm_role_id(realm: str, admin: KeycloakAdmin) -> None: + """Test getter for the ID of the default realm role. + + :param realm: Realm name + :type realm: str + :param admin: Keycloak admin + :type admin: KeycloakAdmin + """ + admin.realm_name = realm + assert ( + admin.get_default_realm_role_id() + == [x["id"] for x in admin.get_realm_roles() if x["name"] == f"default-roles-{realm}"][0] + ) + + +def test_realm_default_roles(admin: KeycloakAdmin, realm: str) -> None: + """Test getting, adding and deleting default realm roles. + + :param realm: Realm name + :type realm: str + :param admin: Keycloak admin + :type admin: KeycloakAdmin + """ + admin.realm_name = realm + + # Test listing all default realm roles + roles = admin.get_realm_default_roles() + assert len(roles) == 2 + assert {x["name"] for x in roles} == {"offline_access", "uma_authorization"} + + with pytest.raises(KeycloakGetError) as err: + admin.realm_name = "doesnotexist" + admin.get_realm_default_roles() + assert err.match('404: b\'{"error":"Realm not found."}\'') + admin.realm_name = realm + + # Test removing a default realm role + res = admin.remove_realm_default_roles(payload=[roles[0]]) + assert res == {} + assert roles[0] not in admin.get_realm_default_roles() + assert len(admin.get_realm_default_roles()) == 1 + + with pytest.raises(KeycloakDeleteError) as err: + admin.remove_realm_default_roles(payload=[{"id": "bad id"}]) + assert err.match('404: b\'{"error":"Could not find composite role"}\'') + + # Test adding a default realm role + res = admin.add_realm_default_roles(payload=[roles[0]]) + assert res == {} + assert roles[0] in admin.get_realm_default_roles() + assert len(admin.get_realm_default_roles()) == 2 + + with pytest.raises(KeycloakPostError) as err: + admin.add_realm_default_roles(payload=[{"id": "bad id"}]) + assert err.match('404: b\'{"error":"Could not find composite role"}\'') From 503df44452784d7bbfc30e043d20b89952a80cec Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Wed, 11 Jan 2023 11:18:26 +0000 Subject: [PATCH 290/566] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b521124..3c186745 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.9.0 (2023-01-11) + +### Feat + +- added default realm roles handlers + ## v2.8.0 (2022-12-29) ### Feat From fb0445c0c7f24d116e440a31ed5f924908038940 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Mendes?= Date: Thu, 2 Feb 2023 16:27:16 +0000 Subject: [PATCH 291/566] feat: init KeycloakAdmin with token --- src/keycloak/keycloak_admin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 26792f51..d985c1d2 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -54,6 +54,8 @@ class KeycloakAdmin: :type username: str :param password: admin password :type password: str + :param token: access and refresh tokens + :type token: dict :param totp: Time based OTP :type totp: str :param realm_name: realm name @@ -88,7 +90,6 @@ class KeycloakAdmin: _client_secret_key = None _auto_refresh_token = None _connection = None - _token = None _custom_headers = None _user_realm_name = None @@ -97,6 +98,7 @@ def __init__( server_url, username=None, password=None, + token=None, totp=None, realm_name="master", client_id="admin-cli", @@ -115,6 +117,8 @@ def __init__( :type username: str :param password: admin password :type password: str + :param token: access and refresh tokens + :type token: dict :param totp: Time based OTP :type totp: str :param realm_name: realm name @@ -139,6 +143,7 @@ def __init__( self.server_url = server_url self.username = username self.password = password + self.token = token self.totp = totp self.realm_name = realm_name self.client_id = client_id @@ -150,7 +155,8 @@ def __init__( self.timeout = timeout # Get token Admin - self.get_token() + if not self.token: + self.get_token() @property def server_url(self): From 9c51d02a63e367cacef5e463f47ca696be3ea2e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Mendes?= Date: Fri, 3 Feb 2023 22:07:47 +0000 Subject: [PATCH 292/566] feat: update header if token is given --- src/keycloak/keycloak_admin.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index d985c1d2..2a17da50 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -154,10 +154,22 @@ def __init__( self.custom_headers = custom_headers self.timeout = timeout - # Get token Admin - if not self.token: + if self.token is None: self.get_token() + headers = { + "Authorization": "Bearer " + self.token.get("access_token"), + "Content-Type": "application/json", + } if self.token is not None else {} + + if self.custom_headers is not None: + # merge custom headers to main headers + headers.update(self.custom_headers) + + self.connection = ConnectionManager( + base_url=self.server_url, headers=headers, timeout=60, verify=self.verify + ) + @property def server_url(self): """Get server url. @@ -3378,22 +3390,8 @@ def get_token(self): self.token = self.keycloak_openid.token( self.username, self.password, grant_type=grant_type, totp=self.totp ) - - headers = { - "Authorization": "Bearer " + self.token.get("access_token"), - "Content-Type": "application/json", - } else: self.token = None - headers = {} - - if self.custom_headers is not None: - # merge custom headers to main headers - headers.update(self.custom_headers) - - self.connection = ConnectionManager( - base_url=self.server_url, headers=headers, timeout=60, verify=self.verify - ) def refresh_token(self): """Refresh the token. From 23ecba1e6fcfb5f96bb27c6298396d190c8b3659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Mendes?= Date: Sat, 4 Feb 2023 13:30:37 +0000 Subject: [PATCH 293/566] style: fix formatting --- src/keycloak/keycloak_admin.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 2a17da50..9a60e07a 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -157,10 +157,14 @@ def __init__( if self.token is None: self.get_token() - headers = { - "Authorization": "Bearer " + self.token.get("access_token"), - "Content-Type": "application/json", - } if self.token is not None else {} + headers = ( + { + "Authorization": "Bearer " + self.token.get("access_token"), + "Content-Type": "application/json", + } + if self.token is not None + else {} + ) if self.custom_headers is not None: # merge custom headers to main headers From f39fc53858b8a7e2a43f14c29f9d591b55b4e071 Mon Sep 17 00:00:00 2001 From: Philippe Moll Date: Mon, 5 Dec 2022 16:33:44 +0100 Subject: [PATCH 294/566] feat: Add Client Scopes of Client --- src/keycloak/keycloak_admin.py | 136 +++++++++++++++++++++++++++++++++ src/keycloak/urls_patterns.py | 8 ++ tests/test_keycloak_admin.py | 92 ++++++++++++++++++++++ 3 files changed, 236 insertions(+) diff --git a/src/keycloak/keycloak_admin.py b/src/keycloak/keycloak_admin.py index 26792f51..d9742cf1 100644 --- a/src/keycloak/keycloak_admin.py +++ b/src/keycloak/keycloak_admin.py @@ -1539,6 +1539,142 @@ def get_client_service_account_user(self, client_id): ) return raise_error_from_response(data_raw, KeycloakGetError) + def get_client_default_client_scopes(self, client_id): + """Get all default client scopes from client. + + :param client_id: id of the client in which the new default client scope should be added + :type client_id: str + + :return: list of client scopes with id and name + :rtype: list + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_DEFAULT_CLIENT_SCOPES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def add_client_default_client_scope(self, client_id, client_scope_id, payload): + """Add a client scope to the default client scopes from client. + + Payload example:: + + payload={ + "realm":"testrealm", + "client":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "clientScopeId":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + + :param client_id: id of the client in which the new default client scope should be added + :type client_id: str + :param client_scope_id: id of the new client scope that should be added + :type client_scope_id: str + :param payload: dictionary with realm, client and clientScopeId + :type payload: dict + + :return: Http response + :rtype: bytes + """ + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client_scope_id": client_scope_id, + } + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT_DEFAULT_CLIENT_SCOPE.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError) + + def delete_client_default_client_scope(self, client_id, client_scope_id): + """Delete a client scope from the default client scopes of the client. + + :param client_id: id of the client in which the default client scope should be deleted + :type client_id: str + :param client_scope_id: id of the client scope that should be deleted + :type client_scope_id: str + + :return: list of client scopes with id and name + :rtype: list + """ + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client_scope_id": client_scope_id, + } + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_DEFAULT_CLIENT_SCOPE.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakDeleteError) + + def get_client_optional_client_scopes(self, client_id): + """Get all optional client scopes from client. + + :param client_id: id of the client in which the new optional client scope should be added + :type client_id: str + + :return: list of client scopes with id and name + :rtype: list + """ + params_path = {"realm-name": self.realm_name, "id": client_id} + data_raw = self.raw_get( + urls_patterns.URL_ADMIN_CLIENT_OPTIONAL_CLIENT_SCOPES.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakGetError) + + def add_client_optional_client_scope(self, client_id, client_scope_id, payload): + """Add a client scope to the optional client scopes from client. + + Payload example:: + + payload={ + "realm":"testrealm", + "client":"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", + "clientScopeId":"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" + } + + :param client_id: id of the client in which the new optional client scope should be added + :type client_id: str + :param client_scope_id: id of the new client scope that should be added + :type client_scope_id: str + :param payload: dictionary with realm, client and clientScopeId + :type payload: dict + + :return: Http response + :rtype: bytes + """ + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client_scope_id": client_scope_id, + } + data_raw = self.raw_put( + urls_patterns.URL_ADMIN_CLIENT_OPTIONAL_CLIENT_SCOPE.format(**params_path), + data=json.dumps(payload), + ) + return raise_error_from_response(data_raw, KeycloakPutError) + + def delete_client_optional_client_scope(self, client_id, client_scope_id): + """Delete a client scope from the optional client scopes of the client. + + :param client_id: id of the client in which the optional client scope should be deleted + :type client_id: str + :param client_scope_id: id of the client scope that should be deleted + :type client_scope_id: str + + :return: list of client scopes with id and name + :rtype: list + """ + params_path = { + "realm-name": self.realm_name, + "id": client_id, + "client_scope_id": client_scope_id, + } + data_raw = self.raw_delete( + urls_patterns.URL_ADMIN_CLIENT_OPTIONAL_CLIENT_SCOPE.format(**params_path) + ) + return raise_error_from_response(data_raw, KeycloakDeleteError) + def create_client(self, payload, skip_exists=False): """Create a client. diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index 4ea04493..d8c5f0f3 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -95,6 +95,14 @@ URL_ADMIN_CLIENT_SCOPE_MAPPINGS_CLIENT_ROLES = ( URL_ADMIN_CLIENT + "/scope-mappings/clients/{client}" ) +URL_ADMIN_CLIENT_OPTIONAL_CLIENT_SCOPES = URL_ADMIN_CLIENT + "/optional-client-scopes" +URL_ADMIN_CLIENT_OPTIONAL_CLIENT_SCOPE = ( + URL_ADMIN_CLIENT_OPTIONAL_CLIENT_SCOPES + "/{client_scope_id}" +) +URL_ADMIN_CLIENT_DEFAULT_CLIENT_SCOPES = URL_ADMIN_CLIENT + "/default-client-scopes" +URL_ADMIN_CLIENT_DEFAULT_CLIENT_SCOPE = ( + URL_ADMIN_CLIENT_DEFAULT_CLIENT_SCOPES + "/{client_scope_id}" +) URL_ADMIN_CLIENT_AUTHZ_SETTINGS = URL_ADMIN_CLIENT + "/authz/resource-server/settings" URL_ADMIN_CLIENT_AUTHZ_RESOURCES = URL_ADMIN_CLIENT + "/authz/resource-server/resource?max=-1" diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index f6d34d0a..c59bb8cd 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -1318,6 +1318,98 @@ def test_client_scope_client_roles(admin: KeycloakAdmin, realm: str, client: str assert len(roles) == 0 +def test_client_default_client_scopes(admin: KeycloakAdmin, realm: str, client: str): + """Test client assignment of default client scopes. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + """ + admin.realm_name = realm + + client_id = admin.create_client( + payload={"name": "role-testing-client", "clientId": "role-testing-client"} + ) + # Test get client default scopes + # keycloak default roles: web-origins, acr, profile, roles, email + default_client_scopes = admin.get_client_default_client_scopes(client_id) + assert len(default_client_scopes) == 5, default_client_scopes + + # Test add a client scope to client default scopes + default_client_scope = "test-client-default-scope" + new_client_scope = { + "name": default_client_scope, + "description": f"Test Client Scope: {default_client_scope}", + "protocol": "openid-connect", + "attributes": {}, + } + new_client_scope_id = admin.create_client_scope(new_client_scope, skip_exists=False) + new_default_client_scope_data = { + "realm": realm, + "client": client_id, + "clientScopeId": new_client_scope_id, + } + admin.add_client_default_client_scope( + client_id, new_client_scope_id, new_default_client_scope_data + ) + default_client_scopes = admin.get_client_default_client_scopes(client_id) + assert len(default_client_scopes) == 6, default_client_scopes + + # Test remove a client default scope + admin.delete_client_default_client_scope(client_id, new_client_scope_id) + default_client_scopes = admin.get_client_default_client_scopes(client_id) + assert len(default_client_scopes) == 5, default_client_scopes + + +def test_client_optional_client_scopes(admin: KeycloakAdmin, realm: str, client: str): + """Test client assignment of optional client scopes. + + :param admin: Keycloak admin + :type admin: KeycloakAdmin + :param realm: Keycloak realm + :type realm: str + :param client: Keycloak client + :type client: str + """ + admin.realm_name = realm + + client_id = admin.create_client( + payload={"name": "role-testing-client", "clientId": "role-testing-client"} + ) + # Test get client optional scopes + # keycloak optional roles: microprofile-jwt, offline_access, address, phone + optional_client_scopes = admin.get_client_optional_client_scopes(client_id) + assert len(optional_client_scopes) == 4, optional_client_scopes + + # Test add a client scope to client optional scopes + optional_client_scope = "test-client-optional-scope" + new_client_scope = { + "name": optional_client_scope, + "description": f"Test Client Scope: {optional_client_scope}", + "protocol": "openid-connect", + "attributes": {}, + } + new_client_scope_id = admin.create_client_scope(new_client_scope, skip_exists=False) + new_optional_client_scope_data = { + "realm": realm, + "client": client_id, + "clientScopeId": new_client_scope_id, + } + admin.add_client_optional_client_scope( + client_id, new_client_scope_id, new_optional_client_scope_data + ) + optional_client_scopes = admin.get_client_optional_client_scopes(client_id) + assert len(optional_client_scopes) == 5, optional_client_scopes + + # Test remove a client optional scope + admin.delete_client_optional_client_scope(client_id, new_client_scope_id) + optional_client_scopes = admin.get_client_optional_client_scopes(client_id) + assert len(optional_client_scopes) == 4, optional_client_scopes + + def test_client_roles(admin: KeycloakAdmin, client: str): """Test client roles. From ea624540c6f95bab12aa5680f600f859934dd189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jer=C3=B3nimo=20Mendes?= Date: Wed, 8 Feb 2023 15:41:05 +0000 Subject: [PATCH 295/566] test: test admin init with token --- tests/test_keycloak_admin.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_keycloak_admin.py b/tests/test_keycloak_admin.py index f6d34d0a..2ec7f8b7 100644 --- a/tests/test_keycloak_admin.py +++ b/tests/test_keycloak_admin.py @@ -90,6 +90,15 @@ def test_keycloak_admin_init(env): ) assert admin.token + token = admin.token + admin = KeycloakAdmin( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", + token=token, + realm_name=None, + user_realm_name=None, + ) + assert admin.token == token + admin.create_realm(payload={"realm": "authz", "enabled": True}) admin.realm_name = "authz" admin.create_client( From 811bcfef819f92e992f9c7ed29b3698af57546c3 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Wed, 8 Feb 2023 20:02:05 +0000 Subject: [PATCH 296/566] docs: changelog update --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c186745..e23cfb6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## v2.10.0 (2023-02-08) + +### Feat + +- update header if token is given +- init KeycloakAdmin with token + ## v2.9.0 (2023-01-11) ### Feat From b693d6e396860a233728d8c5794aede01999dbec Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Wed, 8 Feb 2023 20:10:36 +0000 Subject: [PATCH 297/566] docs: changelog update --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e23cfb6f..080c8bb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,12 @@ +## v2.11.0 (2023-02-08) + ## v2.10.0 (2023-02-08) ### Feat - update header if token is given - init KeycloakAdmin with token +- Add Client Scopes of Client ## v2.9.0 (2023-01-11) From 12392fe9854c082904c3d1031c09cc9a95bc2354 Mon Sep 17 00:00:00 2001 From: Will Earp Date: Wed, 8 Feb 2023 21:11:14 +0100 Subject: [PATCH 298/566] fix: do not include CODEOWNERS (#407) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 685d8daf..14e02858 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ packages = [ { include = "keycloak", from = "src/" }, { include = "keycloak/**/*.py", from = "src/" }, ] -include = ["LICENSE", "CHANGELOG.md", "CODEOWNERS", "CONTRIBUTING.md"] +include = ["LICENSE", "CHANGELOG.md", "CONTRIBUTING.md"] [tool.poetry.urls] Documentation = "https://python-keycloak.readthedocs.io/en/latest/" From 75f4571fa9c72a679954984dd97e7e424b4952d7 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Wed, 8 Feb 2023 20:15:06 +0000 Subject: [PATCH 299/566] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 080c8bb0..f1695ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.11.1 (2023-02-08) + +### Fix + +- do not include CODEOWNERS (#407) + ## v2.11.0 (2023-02-08) ## v2.10.0 (2023-02-08) From fb84c3b67b8c97836fc9d46d9764583c24ec4e78 Mon Sep 17 00:00:00 2001 From: Nuwan Goonasekera <2070605+nuwang@users.noreply.github.com> Date: Fri, 10 Feb 2023 13:15:54 +0530 Subject: [PATCH 300/566] feat: add Keycloak UMA client (#403) --- src/keycloak/__init__.py | 2 + src/keycloak/keycloak_uma.py | 241 ++++++++++++++++++++++++++++++++ src/keycloak/uma_permissions.py | 6 +- src/keycloak/urls_patterns.py | 7 +- tests/conftest.py | 19 ++- tests/test_keycloak_uma.py | 120 ++++++++++++++++ 6 files changed, 390 insertions(+), 5 deletions(-) create mode 100644 src/keycloak/keycloak_uma.py create mode 100644 tests/test_keycloak_uma.py diff --git a/src/keycloak/__init__.py b/src/keycloak/__init__.py index 694e53d8..7e49c8fa 100644 --- a/src/keycloak/__init__.py +++ b/src/keycloak/__init__.py @@ -42,6 +42,7 @@ ) from .keycloak_admin import KeycloakAdmin from .keycloak_openid import KeycloakOpenID +from .keycloak_uma import KeycloakUMA __all__ = [ "__version__", @@ -61,4 +62,5 @@ "KeycloakSecretNotFound", "KeycloakAdmin", "KeycloakOpenID", + "KeycloakUMA", ] diff --git a/src/keycloak/keycloak_uma.py b/src/keycloak/keycloak_uma.py new file mode 100644 index 00000000..a0665676 --- /dev/null +++ b/src/keycloak/keycloak_uma.py @@ -0,0 +1,241 @@ +# -*- coding: utf-8 -*- +# +# The MIT License (MIT) +# +# Copyright (C) 2017 Marcos Pereira +# +# Permission is hereby granted, free of charge, to any person obtaining a copy of +# this software and associated documentation files (the "Software"), to deal in +# the Software without restriction, including without limitation the rights to +# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do so, +# subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +"""Keycloak UMA module. + +The module contains a UMA compatible client for keycloak: +https://docs.kantarainitiative.org/uma/wg/rec-oauth-uma-federated-authz-2.0.html +""" +import json +from urllib.parse import quote_plus + +from .connection import ConnectionManager +from .exceptions import ( + KeycloakDeleteError, + KeycloakGetError, + KeycloakPostError, + KeycloakPutError, + raise_error_from_response, +) +from .urls_patterns import URL_UMA_WELL_KNOWN + + +class KeycloakUMA: + """Keycloak UMA client. + + :param server_url: Keycloak server url + :param client_id: client id + :param realm_name: realm name + :param client_secret_key: client secret key + :param verify: True if want check connection SSL + :param custom_headers: dict of custom header to pass to each HTML request + :param proxies: dict of proxies to sent the request by. + :param timeout: connection timeout in seconds + """ + + def __init__( + self, server_url, realm_name, verify=True, custom_headers=None, proxies=None, timeout=60 + ): + """Init method. + + :param server_url: Keycloak server url + :type server_url: str + :param realm_name: realm name + :type realm_name: str + :param verify: True if want check connection SSL + :type verify: bool + :param custom_headers: dict of custom header to pass to each HTML request + :type custom_headers: dict + :param proxies: dict of proxies to sent the request by. + :type proxies: dict + :param timeout: connection timeout in seconds + :type timeout: int + """ + self.realm_name = realm_name + headers = custom_headers if custom_headers is not None else dict() + headers.update({"Content-Type": "application/json"}) + self.connection = ConnectionManager( + base_url=server_url, headers=headers, timeout=timeout, verify=verify, proxies=proxies + ) + self._well_known = None + + def _fetch_well_known(self): + params_path = {"realm-name": self.realm_name} + data_raw = self.connection.raw_get(URL_UMA_WELL_KNOWN.format(**params_path)) + return raise_error_from_response(data_raw, KeycloakGetError) + + @staticmethod + def format_url(url, **kwargs): + """Substitute url path parameters. + + Given a parameterized url string, returns the string after url encoding and substituting + the given params. For example, + `format_url("https://myserver/{my_resource}/{id}", my_resource="hello world", id="myid")` + would produce `https://myserver/hello+world/myid`. + + :param url: url string to format + :type url: str + :param kwargs: dict containing kwargs to substitute + :type kwargs: dict + :return: formatted string + :rtype: str + """ + return url.format(**{k: quote_plus(v) for k, v in kwargs.items()}) + + def _add_bearer_token_header(self, token): + self.connection.add_param_headers("Authorization", "Bearer " + token) + + @property + def uma_well_known(self): + """Get the well_known UMA2 config. + + :returns: It lists endpoints and other configuration options relevant + :rtype: dict + """ + # per instance cache + if not self._well_known: + self._well_known = self._fetch_well_known() + return self._well_known + + def resource_set_create(self, token, payload): + """Create a resource set. + + Spec + https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#rfc.section.2.2.1 + + ResourceRepresentation + https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation + + :param token: client access token + :type token: str + :param payload: ResourceRepresentation + :type payload: dict + :return: ResourceRepresentation with the _id property assigned + :rtype: dict + """ + self._add_bearer_token_header(token) + data_raw = self.connection.raw_post( + self.uma_well_known["resource_registration_endpoint"], data=json.dumps(payload) + ) + return raise_error_from_response(data_raw, KeycloakPostError, expected_codes=[201]) + + def resource_set_update(self, token, resource_id, payload): + """Update a resource set. + + Spec + https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#update-resource-set + + ResourceRepresentation + https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation + + :param token: client access token + :type token: str + :param resource_id: id of the resource + :type resource_id: str + :param payload: ResourceRepresentation + :type payload: dict + :return: Response dict (empty) + :rtype: dict + """ + self._add_bearer_token_header(token) + url = self.format_url( + self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id + ) + data_raw = self.connection.raw_put(url, data=json.dumps(payload)) + return raise_error_from_response(data_raw, KeycloakPutError, expected_codes=[204]) + + def resource_set_read(self, token, resource_id): + """Read a resource set. + + Spec + https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#read-resource-set + + ResourceRepresentation + https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation + + :param token: client access token + :type token: str + :param resource_id: id of the resource + :type resource_id: str + :return: ResourceRepresentation + :rtype: dict + """ + self._add_bearer_token_header(token) + url = self.format_url( + self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id + ) + data_raw = self.connection.raw_get(url) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + + def resource_set_delete(self, token, resource_id): + """Delete a resource set. + + Spec + https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#delete-resource-set + + :param token: client access token + :type token: str + :param resource_id: id of the resource + :type resource_id: str + :return: Response dict (empty) + :rtype: dict + """ + self._add_bearer_token_header(token) + url = self.format_url( + self.uma_well_known["resource_registration_endpoint"] + "/{id}", id=resource_id + ) + data_raw = self.connection.raw_delete(url) + return raise_error_from_response(data_raw, KeycloakDeleteError, expected_codes=[204]) + + def resource_set_list_ids(self, token): + """List all resource set ids. + + Spec + https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets + + :param token: client access token + :type token: str + :return: List of ids + :rtype: List[str] + """ + self._add_bearer_token_header(token) + data_raw = self.connection.raw_get(self.uma_well_known["resource_registration_endpoint"]) + return raise_error_from_response(data_raw, KeycloakGetError, expected_codes=[200]) + + def resource_set_list(self, token): + """List all resource sets. + + Spec + https://docs.kantarainitiative.org/uma/rec-oauth-resource-reg-v1_0_1.html#list-resource-sets + + ResourceRepresentation + https://www.keycloak.org/docs-api/20.0.0/rest-api/index.html#_resourcerepresentation + + :param token: client access token + :type token: str + :yields: Iterator over a list of ResourceRepresentations + :rtype: Iterator[dict] + """ + for resource_id in self.resource_set_list_ids(token): + resource = self.resource_set_read(token, resource_id) + yield resource diff --git a/src/keycloak/uma_permissions.py b/src/keycloak/uma_permissions.py index eadcd722..1560dd58 100644 --- a/src/keycloak/uma_permissions.py +++ b/src/keycloak/uma_permissions.py @@ -27,7 +27,7 @@ class UMAPermission: - """A class to conveniently assembly permissions. + """A class to conveniently assemble permissions. The class itself is callable, and will return the assembled permission. @@ -143,7 +143,7 @@ def __call__(self, permission=None, resource="", scope="") -> "UMAPermission": class Resource(UMAPermission): - """An UMAPermission Resource class to conveniently assembly permissions. + """A UMAPermission Resource class to conveniently assemble permissions. The class itself is callable, and will return the assembled permission. @@ -161,7 +161,7 @@ def __init__(self, resource): class Scope(UMAPermission): - """An UMAPermission Scope class to conveniently assembly permissions. + """A UMAPermission Scope class to conveniently assemble permissions. The class itself is callable, and will return the assembled permission. diff --git a/src/keycloak/urls_patterns.py b/src/keycloak/urls_patterns.py index d8c5f0f3..4fedee17 100644 --- a/src/keycloak/urls_patterns.py +++ b/src/keycloak/urls_patterns.py @@ -25,7 +25,8 @@ # OPENID URLS URL_REALM = "realms/{realm-name}" -URL_WELL_KNOWN = "realms/{realm-name}/.well-known/openid-configuration" +URL_WELL_KNOWN_BASE = "realms/{realm-name}/.well-known" +URL_WELL_KNOWN = URL_WELL_KNOWN_BASE + "/openid-configuration" URL_TOKEN = "realms/{realm-name}/protocol/openid-connect/token" URL_USERINFO = "realms/{realm-name}/protocol/openid-connect/userinfo" URL_LOGOUT = "realms/{realm-name}/protocol/openid-connect/logout" @@ -208,3 +209,7 @@ URL_ADMIN_ATTACK_DETECTION_USER = ( "admin/realms/{realm-name}/attack-detection/brute-force/users/{id}" ) + + +# UMA URLS +URL_UMA_WELL_KNOWN = URL_WELL_KNOWN_BASE + "/uma2-configuration" diff --git a/tests/conftest.py b/tests/conftest.py index e0d93ead..18cb6a3b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,7 +12,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID -from keycloak import KeycloakAdmin, KeycloakOpenID +from keycloak import KeycloakAdmin, KeycloakOpenID, KeycloakUMA class KeycloakTestEnv(object): @@ -475,3 +475,20 @@ def selfsigned_cert(): ) return cert_pem, key_pem + + +@pytest.fixture +def uma(env: KeycloakTestEnv, realm: str): + """Fixture for initialized KeycloakUMA class. + + :param env: Keycloak test environment + :type env: KeycloakTestEnv + :param realm: Keycloak realm + :type realm: str + :yields: Keycloak OpenID client + :rtype: KeycloakOpenID + """ + # Return UMA + yield KeycloakUMA( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", realm_name=realm + ) diff --git a/tests/test_keycloak_uma.py b/tests/test_keycloak_uma.py new file mode 100644 index 00000000..2a1dde79 --- /dev/null +++ b/tests/test_keycloak_uma.py @@ -0,0 +1,120 @@ +"""Test module for KeycloakUMA.""" +import re +from typing import Tuple + +import pytest + +from keycloak import KeycloakOpenID +from keycloak.connection import ConnectionManager +from keycloak.exceptions import ( + KeycloakDeleteError, + KeycloakGetError, + KeycloakPostError, + KeycloakPutError, +) +from keycloak.keycloak_uma import KeycloakUMA + + +def test_keycloak_uma_init(env): + """Test KeycloakUMA's init method. + + :param env: Environment fixture + :type env: KeycloakTestEnv + """ + uma = KeycloakUMA( + server_url=f"http://{env.KEYCLOAK_HOST}:{env.KEYCLOAK_PORT}", realm_name="master" + ) + + assert uma.realm_name == "master" + assert isinstance(uma.connection, ConnectionManager) + # should initially be empty + assert uma._well_known is None + assert uma.uma_well_known + # should be cached after first reference + assert uma._well_known is not None + + +def test_uma_well_known(uma: KeycloakUMA): + """Test the well_known method. + + :param uma: Keycloak UMA client + :type uma: KeycloakUMA + """ + res = uma.uma_well_known + assert res is not None + assert res != dict() + for key in ["resource_registration_endpoint"]: + assert key in res + + +def test_uma_resource_sets( + uma: KeycloakUMA, oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] +): + """Test resource sets. + + :param uma: Keycloak UMA client + :type uma: KeycloakUMA + :param oid_with_credentials_authz: Keycloak OpenID client with pre-configured user credentials + :type oid_with_credentials_authz: Tuple[KeycloakOpenID, str, str] + """ + oid, _, _ = oid_with_credentials_authz + + token = oid.token(grant_type="client_credentials") + access_token = token["access_token"] + + # Check that only the default resource is present + resource_sets = uma.resource_set_list(access_token) + resource_set_list = list(resource_sets) + assert len(resource_set_list) == 1, resource_set_list + assert resource_set_list[0]["name"] == "Default Resource", resource_set_list[0]["name"] + + # Test create resource set + resource_to_create = { + "name": "mytest", + "scopes": ["test:read", "test:write"], + "type": "urn:test", + } + created_resource = uma.resource_set_create(access_token, resource_to_create) + assert created_resource + assert created_resource["_id"], created_resource + assert set(resource_to_create).issubset(set(created_resource)), created_resource + + # Test create the same resource set + with pytest.raises(KeycloakPostError) as err: + uma.resource_set_create(access_token, resource_to_create) + assert err.match( + re.escape( + '409: b\'{"error":"invalid_request","error_description":' + '"Resource with name [mytest] already exists."}\'' + ) + ) + + # Test get resource set + latest_resource = uma.resource_set_read(access_token, created_resource["_id"]) + assert latest_resource["name"] == created_resource["name"] + + # Test update resource set + latest_resource["name"] = "New Resource Name" + res = uma.resource_set_update(access_token, created_resource["_id"], latest_resource) + assert res == dict(), res + updated_resource = uma.resource_set_read(access_token, created_resource["_id"]) + assert updated_resource["name"] == "New Resource Name" + + # Test update resource set fail + with pytest.raises(KeycloakPutError) as err: + uma.resource_set_update( + token=access_token, resource_id=created_resource["_id"], payload={"wrong": "payload"} + ) + assert err.match('400: b\'{"error":"Unrecognized field') + + # Test delete resource set + res = uma.resource_set_delete(token=access_token, resource_id=created_resource["_id"]) + assert res == dict(), res + with pytest.raises(KeycloakGetError) as err: + uma.resource_set_read(access_token, created_resource["_id"]) + err.match("404: b''") + + # Test delete fail + with pytest.raises(KeycloakDeleteError) as err: + uma.resource_set_delete(token=access_token, resource_id=created_resource["_id"]) + assert err.match("404: b''") From c86400d6860878c08b3a2d5544b35e05a984b166 Mon Sep 17 00:00:00 2001 From: ryshoooo Date: Fri, 10 Feb 2023 07:59:05 +0000 Subject: [PATCH 301/566] docs: changelog update --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1695ce5..98a98946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v2.12.0 (2023-02-10) + +### Feat + +- add Keycloak UMA client (#403) + ## v2.11.1 (2023-02-08) ### Fix From 33f768fd746da2b4ba903d92988ab7e917523ee3 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 10 Feb 2023 08:52:16 +0000 Subject: [PATCH 302/566] chore: upgrade dependencies --- poetry.lock | 1644 +++++++++++++++++++++++++----------------------- pyproject.toml | 6 +- tox.ini | 15 +- 3 files changed, 878 insertions(+), 787 deletions(-) diff --git a/poetry.lock b/poetry.lock index 72a50654..94c356dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,16 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + [[package]] name = "alabaster" -version = "0.7.12" +version = "0.7.13" description = "A configurable sidebar-enabled Sphinx theme" category = "main" optional = true -python-versions = "*" +python-versions = ">=3.6" +files = [ + {file = "alabaster-0.7.13-py3-none-any.whl", hash = "sha256:1ee19aca801bbabb5ba3f5f258e4422dfa86f82f3e9cefb0859b283cdd7f62a3"}, + {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, +] [[package]] name = "argcomplete" @@ -13,6 +19,10 @@ description = "Bash tab completion for argparse" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, + {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, +] [package.dependencies] importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""} @@ -27,6 +37,10 @@ description = "An abstract syntax tree for Python with inference support." category = "main" optional = true python-versions = ">=3.6.2" +files = [ + {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, + {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, +] [package.dependencies] lazy-object-proxy = ">=1.4.0" @@ -37,17 +51,22 @@ wrapt = ">=1.11,<2" [[package]] name = "attrs" -version = "22.1.0" +version = "22.2.0" description = "Classes Without Boilerplate" category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.6" +files = [ + {file = "attrs-22.2.0-py3-none-any.whl", hash = "sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836"}, + {file = "attrs-22.2.0.tar.gz", hash = "sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"}, +] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +cov = ["attrs[tests]", "coverage-enable-subprocess", "coverage[toml] (>=5.3)"] +dev = ["attrs[docs,tests]"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope.interface"] +tests = ["attrs[tests-no-zope]", "zope.interface"] +tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] [[package]] name = "babel" @@ -56,6 +75,10 @@ description = "Internationalization utilities" category = "main" optional = true python-versions = ">=3.6" +files = [ + {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, + {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, +] [package.dependencies] pytz = ">=2015.7" @@ -67,6 +90,20 @@ description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, + {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, + {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, + {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, + {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, + {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, + {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, + {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, + {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, + {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, + {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, + {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, +] [package.dependencies] click = ">=8.0.0" @@ -85,11 +122,15 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "bleach" -version = "5.0.1" +version = "6.0.0" description = "An easy safelist-based HTML-sanitizing tool." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "bleach-6.0.0-py3-none-any.whl", hash = "sha256:33c16e3353dbd13028ab4799a0f89a83f113405c766e9c122df8a06f5b85b3f4"}, + {file = "bleach-6.0.0.tar.gz", hash = "sha256:1a1a85c1595e07d8db14c5f09f09e6433502c51c595970edc090551f0db99414"}, +] [package.dependencies] six = ">=1.9.0" @@ -97,7 +138,6 @@ webencodings = "*" [package.extras] css = ["tinycss2 (>=1.1.0,<1.2)"] -dev = ["Sphinx (==4.3.2)", "black (==22.3.0)", "build (==0.8.0)", "flake8 (==4.0.1)", "hashin (==0.17.0)", "mypy (==0.961)", "pip-tools (==6.6.2)", "pytest (==7.1.2)", "tox (==3.25.0)", "twine (==4.0.1)", "wheel (==0.37.1)"] [[package]] name = "certifi" @@ -106,6 +146,10 @@ description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] [[package]] name = "cffi" @@ -114,6 +158,72 @@ description = "Foreign Function Interface for Python calling C code." category = "dev" optional = false python-versions = "*" +files = [ + {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, + {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, + {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, + {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, + {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, + {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, + {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, + {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, + {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, + {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, + {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, + {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, + {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, + {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, + {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, + {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, + {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, + {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, + {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, + {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, + {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, + {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, + {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, + {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, + {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, + {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, + {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, + {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, + {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, + {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, + {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, + {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, + {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, +] [package.dependencies] pycparser = "*" @@ -125,6 +235,10 @@ description = "Validate configuration and produce human readable error messages. category = "dev" optional = false python-versions = ">=3.6.1" +files = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] [[package]] name = "charset-normalizer" @@ -133,6 +247,10 @@ description = "The Real First Universal Charset Detector. Open, modern and activ category = "main" optional = false python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] [package.extras] unicode-backport = ["unicodedata2"] @@ -144,6 +262,10 @@ description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} @@ -156,6 +278,10 @@ description = "Codespell" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "codespell-2.2.2-py3-none-any.whl", hash = "sha256:87dfcd9bdc9b3cb8b067b37f0af22044d7a84e28174adfc8eaa203056b7f9ecc"}, + {file = "codespell-2.2.2.tar.gz", hash = "sha256:c4d00c02b5a2a55661f00d5b4b3b5a710fa803ced9a9d7e45438268b099c319c"}, +] [package.extras] dev = ["check-manifest", "flake8", "pytest", "pytest-cov", "pytest-dependency", "tomli"] @@ -169,14 +295,22 @@ description = "Cross-platform colored terminal text." category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] [[package]] name = "commitizen" -version = "2.38.0" +version = "2.41.0" description = "Python commitizen client tool" category = "dev" optional = false python-versions = ">=3.6.2,<4.0.0" +files = [ + {file = "commitizen-2.41.0-py3-none-any.whl", hash = "sha256:2044f6d1cf002f363280e0dcefc8b557095dc42530060bfe5ad917ea4d5c48a1"}, + {file = "commitizen-2.41.0.tar.gz", hash = "sha256:5d50093fe546cc3b5217780a8cdfc5a9de6a27ce8aa7db6443e2e47dc2b10b6a"}, +] [package.dependencies] argcomplete = ">=1.12.1,<2.1" @@ -196,19 +330,76 @@ name = "commonmark" version = "0.9.1" description = "Python parser for the CommonMark Markdown spec" category = "main" -optional = false +optional = true python-versions = "*" +files = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] [package.extras] test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "6.5.0" +version = "7.1.0" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, + {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, + {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, + {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, + {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, + {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, + {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, + {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, + {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, + {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, + {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, + {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, + {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, + {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, + {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, + {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, + {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, + {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, + {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, + {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, + {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, + {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, + {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, + {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, + {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, + {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, + {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, + {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, +] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} @@ -223,6 +414,30 @@ description = "cryptography is a package which provides cryptographic recipes an category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, + {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, + {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, + {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, + {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, + {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, + {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, + {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, + {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, + {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, +] [package.dependencies] cffi = ">=1.12" @@ -242,6 +457,10 @@ description = "A utility for ensuring Google-style docstrings stay up to date wi category = "dev" optional = false python-versions = ">=3.6,<4.0" +files = [ + {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, + {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, +] [[package]] name = "decli" @@ -250,6 +469,10 @@ description = "Minimal, easy-to-use, declarative cli tool" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, + {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, +] [[package]] name = "distlib" @@ -258,14 +481,22 @@ description = "Distribution utilities" category = "dev" optional = false python-versions = "*" +files = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] [[package]] name = "docutils" -version = "0.17.1" +version = "0.18.1" description = "Docutils -- Python Documentation Utilities" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"}, + {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"}, +] [[package]] name = "ecdsa" @@ -274,6 +505,10 @@ description = "ECDSA cryptographic signature library (pure python)" category = "main" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, + {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, +] [package.dependencies] six = ">=1.9.0" @@ -284,26 +519,34 @@ gmpy2 = ["gmpy2"] [[package]] name = "exceptiongroup" -version = "1.0.4" +version = "1.1.0" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.1.0-py3-none-any.whl", hash = "sha256:327cbda3da756e2de031a3107b81ab7b3770a602c4d16ca618298c526f4bec1e"}, + {file = "exceptiongroup-1.1.0.tar.gz", hash = "sha256:bcb67d800a4497e1b404c2dd44fca47d3b7a5e5433dbab67f96c1a685cdfdf23"}, +] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" -version = "3.8.2" +version = "3.9.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "filelock-3.9.0-py3-none-any.whl", hash = "sha256:f58d535af89bb9ad5cd4df046f741f8553a418c01a7856bf0d173bbc9f6bd16d"}, + {file = "filelock-3.9.0.tar.gz", hash = "sha256:7b319f24340b51f55a2bf7a12ac0755a9b03e718311dac567a0f4f7fabd2f5de"}, +] [package.extras] -docs = ["furo (>=2022.9.29)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=6.5)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2022.12.7)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.5)"] +testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.1)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -312,6 +555,10 @@ description = "the modular source code checker: pep8 pyflakes and co" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] [package.dependencies] importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} @@ -321,11 +568,15 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "flake8-docstrings" -version = "1.6.0" +version = "1.7.0" description = "Extension for flake8 which uses pydocstyle to check docstrings" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"}, + {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"}, +] [package.dependencies] flake8 = ">=3" @@ -333,11 +584,15 @@ pydocstyle = ">=2.1" [[package]] name = "identify" -version = "2.5.9" +version = "2.5.17" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "identify-2.5.17-py2.py3-none-any.whl", hash = "sha256:7d526dd1283555aafcc91539acc061d8f6f59adb0a7bba462735b0a318bff7ed"}, + {file = "identify-2.5.17.tar.gz", hash = "sha256:93cc61a861052de9d4c541a7acb7e3dcc9c11b398a2144f6e52ae5285f5f4f06"}, +] [package.extras] license = ["ukkonen"] @@ -349,6 +604,10 @@ description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] [[package]] name = "imagesize" @@ -357,6 +616,10 @@ description = "Getting image size from png/jpeg/jpeg2000/gif file" category = "main" optional = true python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] [[package]] name = "importlib-metadata" @@ -365,6 +628,10 @@ description = "Read metadata from Python packages" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, + {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, +] [package.dependencies] typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} @@ -375,25 +642,52 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker perf = ["ipython"] testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +[[package]] +name = "importlib-resources" +version = "5.10.2" +description = "Read resources from Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "importlib_resources-5.10.2-py3-none-any.whl", hash = "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6"}, + {file = "importlib_resources-5.10.2.tar.gz", hash = "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + [[package]] name = "iniconfig" -version = "1.1.1" -description = "iniconfig: brain-dead simple config-ini parsing" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] [[package]] name = "isort" -version = "5.11.2" +version = "5.11.5" description = "A Python utility / library to sort Python imports." category = "dev" optional = false python-versions = ">=3.7.0" +files = [ + {file = "isort-5.11.5-py3-none-any.whl", hash = "sha256:ba1d72fb2595a01c7895a5128f9585a5cc4b6d395f1c8d514989b9a7eb2a8746"}, + {file = "isort-5.11.5.tar.gz", hash = "sha256:6be1f76a507cb2ecf16c7cf14a37e41609ca082330be4e3436a18ef74add55db"}, +] [package.extras] colors = ["colorama (>=0.4.3,<0.5.0)"] -pipfile-deprecated-finder = ["pipreqs", "requirementslib"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] @@ -404,6 +698,10 @@ description = "Utility functions for Python class constructs" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, + {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, +] [package.dependencies] more-itertools = "*" @@ -419,6 +717,10 @@ description = "Low-level, pure Python DBus protocol wrapper." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, + {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, +] [package.extras] test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] @@ -431,6 +733,10 @@ description = "A very fast and expressive template engine." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, + {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, +] [package.dependencies] MarkupSafe = ">=2.0" @@ -440,30 +746,74 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "keyring" -version = "23.11.0" +version = "23.13.1" description = "Store and access your passwords safely." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "keyring-23.13.1-py3-none-any.whl", hash = "sha256:771ed2a91909389ed6148631de678f82ddc73737d85a927f382a8a1b157898cd"}, + {file = "keyring-23.13.1.tar.gz", hash = "sha256:ba2e15a9b35e21908d0aaf4e0a47acc52d6ae33444df0da2b49d41a46ef6d678"}, +] [package.dependencies] importlib-metadata = {version = ">=4.11.4", markers = "python_version < \"3.12\""} +importlib-resources = {version = "*", markers = "python_version < \"3.9\""} "jaraco.classes" = "*" jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} -pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""} +pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} [package.extras] +completion = ["shtab"] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] testing = ["flake8 (<5)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [[package]] name = "lazy-object-proxy" -version = "1.8.0" +version = "1.9.0" description = "A fast and thorough lazy object proxy." category = "main" optional = true python-versions = ">=3.7" +files = [ + {file = "lazy-object-proxy-1.9.0.tar.gz", hash = "sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win32.whl", hash = "sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455"}, + {file = "lazy_object_proxy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win32.whl", hash = "sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586"}, + {file = "lazy_object_proxy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win32.whl", hash = "sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734"}, + {file = "lazy_object_proxy-1.9.0-cp37-cp37m-win_amd64.whl", hash = "sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win32.whl", hash = "sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82"}, + {file = "lazy_object_proxy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win32.whl", hash = "sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821"}, + {file = "lazy_object_proxy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f"}, +] [[package]] name = "m2r2" @@ -472,18 +822,100 @@ description = "Markdown and reStructuredText in a single file." category = "main" optional = true python-versions = "*" +files = [ + {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, + {file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"}, +] [package.dependencies] docutils = "*" mistune = "0.8.4" +[[package]] +name = "markdown-it-py" +version = "2.1.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, + {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" +typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] +code-style = ["pre-commit (==2.6)"] +compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] +linkify = ["linkify-it-py (>=1.0,<2.0)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + [[package]] name = "markupsafe" -version = "2.1.1" +version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] [[package]] name = "mccabe" @@ -492,6 +924,22 @@ description = "McCabe checker, plugin for flake8" category = "dev" optional = false python-versions = "*" +files = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] [[package]] name = "mistune" @@ -500,6 +948,10 @@ description = "The fastest markdown parser in pure Python" category = "main" optional = true python-versions = "*" +files = [ + {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, + {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, +] [[package]] name = "mock" @@ -508,6 +960,10 @@ description = "Rolling backport of unittest.mock for all Pythons" category = "main" optional = true python-versions = ">=3.6" +files = [ + {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, + {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, +] [package.extras] build = ["blurb", "twine", "wheel"] @@ -521,14 +977,22 @@ description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, + {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, +] [[package]] name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." category = "dev" optional = false -python-versions = "*" +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] [[package]] name = "nodeenv" @@ -537,48 +1001,71 @@ description = "Node.js virtual environment builder" category = "dev" optional = false python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] [package.dependencies] setuptools = "*" [[package]] name = "packaging" -version = "22.0" +version = "23.0" description = "Core utilities for Python packages" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "packaging-23.0-py3-none-any.whl", hash = "sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2"}, + {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, +] [[package]] name = "pathspec" -version = "0.10.3" +version = "0.11.0" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.0-py3-none-any.whl", hash = "sha256:3a66eb970cbac598f9e5ccb5b2cf58930cd8e3ed86d393d541eaf2d8b1705229"}, + {file = "pathspec-0.11.0.tar.gz", hash = "sha256:64d338d4e0914e91c1792321e6907b5a593f1ab1851de7fc269557a21b30ebbc"}, +] [[package]] name = "pkginfo" -version = "1.9.2" -description = "Query metadatdata from sdists / bdists / installed packages." +version = "1.9.6" +description = "Query metadata from sdists / bdists / installed packages." category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pkginfo-1.9.6-py3-none-any.whl", hash = "sha256:4b7a555a6d5a22169fcc9cf7bfd78d296b0361adad412a346c1226849af5e546"}, + {file = "pkginfo-1.9.6.tar.gz", hash = "sha256:8fd5896e8718a4372f0ea9cc9d96f6417c9b986e23a4d116dda26b62cc29d046"}, +] [package.extras] testing = ["pytest", "pytest-cov"] [[package]] name = "platformdirs" -version = "2.6.0" +version = "3.0.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, + {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.4", markers = "python_version < \"3.8\""} [package.extras] -docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] -test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.2.2)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] [[package]] name = "pluggy" @@ -587,6 +1074,10 @@ description = "plugin and hook calling mechanisms for python" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] [package.dependencies] importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -597,11 +1088,15 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" -version = "2.20.0" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] [package.dependencies] cfgv = ">=2.0.0" @@ -609,8 +1104,7 @@ identify = ">=1.0.0" importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" +virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" @@ -619,6 +1113,10 @@ description = "Library for building powerful interactive command lines in Python category = "dev" optional = false python-versions = ">=3.6.2" +files = [ + {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, + {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, +] [package.dependencies] wcwidth = "*" @@ -630,6 +1128,10 @@ description = "library with cross-python path, ini-parsing, io, code, log facili category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] [[package]] name = "pyasn1" @@ -638,6 +1140,21 @@ description = "ASN.1 types and codecs" category = "main" optional = false python-versions = "*" +files = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, + {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, + {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, +] [[package]] name = "pycodestyle" @@ -646,6 +1163,10 @@ description = "Python style guide checker" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] [[package]] name = "pycparser" @@ -654,20 +1175,29 @@ description = "C parser in Python" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] [[package]] name = "pydocstyle" -version = "6.1.1" +version = "6.3.0" description = "Python docstring style checker" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, + {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, +] [package.dependencies] -snowballstemmer = "*" +importlib-metadata = {version = ">=2.0.0,<5.0.0", markers = "python_version < \"3.8\""} +snowballstemmer = ">=2.2.0" [package.extras] -toml = ["toml"] +toml = ["tomli (>=1.2.3)"] [[package]] name = "pyflakes" @@ -676,25 +1206,37 @@ description = "passive checker of Python programs" category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] [[package]] name = "pygments" -version = "2.13.0" +version = "2.14.0" description = "Pygments is a syntax highlighting package written in Python." category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "Pygments-2.14.0-py3-none-any.whl", hash = "sha256:fa7bd7bd2771287c0de303af8bfdfc731f51bd2c6a47ab69d117138893b82717"}, + {file = "Pygments-2.14.0.tar.gz", hash = "sha256:b3ed06a9e8ac9a9aae5a6f5dbe78a8a58655d17b43b93c078f094ddc476ae297"}, +] [package.extras] plugins = ["importlib-metadata"] [[package]] name = "pytest" -version = "7.2.0" +version = "7.2.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, + {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, +] [package.dependencies] attrs = ">=19.2.0" @@ -716,6 +1258,10 @@ description = "Pytest plugin for measuring coverage." category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, + {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, +] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} @@ -731,6 +1277,10 @@ description = "JOSE implementation in Python" category = "main" optional = false python-versions = "*" +files = [ + {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, + {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, +] [package.dependencies] ecdsa = "!=0.15" @@ -744,11 +1294,15 @@ pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] [[package]] name = "pytz" -version = "2022.6" +version = "2022.7.1" description = "World timezone definitions, modern and historical" category = "main" optional = true python-versions = "*" +files = [ + {file = "pytz-2022.7.1-py2.py3-none-any.whl", hash = "sha256:78f4f37d8198e0627c5f1143240bb0206b8691d8d7ac6d78fee88b78733f8c4a"}, + {file = "pytz-2022.7.1.tar.gz", hash = "sha256:01a0681c4b9684a28304615eba55d1ab31ae00bf68ec157ec3708a8182dbbcd0"}, +] [[package]] name = "pywin32-ctypes" @@ -757,6 +1311,10 @@ description = "" category = "dev" optional = false python-versions = "*" +files = [ + {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, + {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, +] [[package]] name = "pyyaml" @@ -765,6 +1323,41 @@ description = "YAML parser and emitter for Python" category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] [[package]] name = "questionary" @@ -773,6 +1366,10 @@ description = "Python library to build pretty command line user prompts ⭐️" category = "dev" optional = false python-versions = ">=3.6,<4.0" +files = [ + {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, + {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, +] [package.dependencies] prompt_toolkit = ">=2.0,<4.0" @@ -787,6 +1384,10 @@ description = "readme_renderer is a library for rendering \"readme\" description category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "readme_renderer-37.3-py3-none-any.whl", hash = "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343"}, + {file = "readme_renderer-37.3.tar.gz", hash = "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273"}, +] [package.dependencies] bleach = ">=2.1.0" @@ -803,6 +1404,10 @@ description = "Sphinx extension for Read the Docs overrides" category = "main" optional = true python-versions = "*" +files = [ + {file = "readthedocs-sphinx-ext-2.2.0.tar.gz", hash = "sha256:e5effcd825816111a377ab7a897b819215138f8e5e8acc86f99218328f957240"}, + {file = "readthedocs_sphinx_ext-2.2.0-py2.py3-none-any.whl", hash = "sha256:d801f0bfb125d2837f18f40451462528d4a97eefd8de8a12ad526b4f1ce14205"}, +] [package.dependencies] Jinja2 = ">=2.9" @@ -816,6 +1421,10 @@ description = "A docutils-compatibility bridge to CommonMark, enabling you to wr category = "main" optional = true python-versions = "*" +files = [ + {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, + {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, +] [package.dependencies] commonmark = ">=0.8.1" @@ -824,15 +1433,19 @@ sphinx = ">=1.3.1" [[package]] name = "requests" -version = "2.28.1" +version = "2.28.2" description = "Python HTTP for Humans." category = "main" optional = false python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.2-py3-none-any.whl", hash = "sha256:64299f4909223da747622c030b781c0d7811e359c37124b4bd368fb8c6518baa"}, + {file = "requests-2.28.2.tar.gz", hash = "sha256:98b1b2782e3c6c4904938b84c0eb932721069dfdb9134313beff7c83c2df24bf"}, +] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<1.27" @@ -842,11 +1455,15 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "requests-toolbelt" -version = "0.9.1" +version = "0.10.1" description = "A utility belt for advanced users of python-requests" category = "main" optional = false -python-versions = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "requests-toolbelt-0.10.1.tar.gz", hash = "sha256:62e09f7ff5ccbda92772a29f394a49c3ad6cb181d568b1337626b2abb628a63d"}, + {file = "requests_toolbelt-0.10.1-py2.py3-none-any.whl", hash = "sha256:18565aa58116d9951ac39baa288d3adb5b3ff975c4f25eee78555d89e8f247f7"}, +] [package.dependencies] requests = ">=2.0.1,<3.0.0" @@ -858,25 +1475,33 @@ description = "Validating URI References per RFC 3986" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, + {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, +] [package.extras] idna2008 = ["idna"] [[package]] name = "rich" -version = "12.6.0" +version = "13.3.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "dev" optional = false -python-versions = ">=3.6.3,<4.0.0" +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"}, + {file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"}, +] [package.dependencies] -commonmark = ">=0.9.0,<0.10.0" -pygments = ">=2.6.0,<3.0.0" +markdown-it-py = ">=2.1.0,<3.0.0" +pygments = ">=2.14.0,<3.0.0" typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] -jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] +jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rsa" @@ -885,6 +1510,10 @@ description = "Pure-Python RSA implementation" category = "main" optional = false python-versions = ">=3.6,<4" +files = [ + {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, + {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, +] [package.dependencies] pyasn1 = ">=0.1.3" @@ -896,6 +1525,10 @@ description = "Python bindings to FreeDesktop.org Secret Service API" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, + {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, +] [package.dependencies] cryptography = ">=2.0" @@ -903,14 +1536,18 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "65.6.3" +version = "67.2.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "setuptools-67.2.0-py3-none-any.whl", hash = "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c"}, + {file = "setuptools-67.2.0.tar.gz", hash = "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"}, +] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] @@ -921,6 +1558,10 @@ description = "Python 2 and 3 compatibility utilities" category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] [[package]] name = "snowballstemmer" @@ -929,6 +1570,10 @@ description = "This package provides 29 stemmers for 28 languages generated from category = "main" optional = false python-versions = "*" +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] [[package]] name = "sphinx" @@ -937,6 +1582,10 @@ description = "Python documentation generator" category = "main" optional = true python-versions = ">=3.6" +files = [ + {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, + {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, +] [package.dependencies] alabaster = ">=0.7,<0.8" @@ -964,11 +1613,15 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" -version = "2.0.0" +version = "2.0.1" description = "Sphinx API documentation generator" category = "main" optional = true python-versions = ">=3.7" +files = [ + {file = "sphinx-autoapi-2.0.1.tar.gz", hash = "sha256:cdf47968c20852f4feb0ccefd09e414bb820af8af8f82fab15a24b09a3d1baba"}, + {file = "sphinx_autoapi-2.0.1-py2.py3-none-any.whl", hash = "sha256:8ed197a0c9108770aa442a5445744c1405b356ea64df848e8553411b9b9e129b"}, +] [package.dependencies] astroid = ">=2.7" @@ -984,15 +1637,20 @@ go = ["sphinxcontrib-golangdomain"] [[package]] name = "sphinx-rtd-theme" -version = "1.1.1" +version = "1.2.0" description = "Read the Docs theme for Sphinx" category = "main" optional = true -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"}, + {file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"}, +] [package.dependencies] -docutils = "<0.18" -sphinx = ">=1.6,<6" +docutils = "<0.19" +sphinx = ">=1.6,<7" +sphinxcontrib-jquery = {version = ">=2.0.0,<3.0.0 || >3.0.0", markers = "python_version > \"3\""} [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] @@ -1004,6 +1662,10 @@ description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple category = "main" optional = true python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, + {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, +] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] @@ -1016,6 +1678,10 @@ description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp category = "main" optional = true python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, + {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, +] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] @@ -1028,11 +1694,30 @@ description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML h category = "main" optional = true python-versions = ">=3.6" +files = [ + {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, + {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, +] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] +[[package]] +name = "sphinxcontrib-jquery" +version = "2.0.0" +description = "Extension to include jQuery on newer Sphinx releases" +category = "main" +optional = true +python-versions = ">=2.7" +files = [ + {file = "sphinxcontrib-jquery-2.0.0.tar.gz", hash = "sha256:8fb65f6dba84bf7bcd1aea1f02ab3955ac34611d838bcc95d4983b805b234daa"}, + {file = "sphinxcontrib_jquery-2.0.0-py3-none-any.whl", hash = "sha256:ed47fa425c338ffebe3c37e1cdb56e30eb806116b85f01055b158c7057fdb995"}, +] + +[package.dependencies] +setuptools = "*" + [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -1040,6 +1725,10 @@ description = "A sphinx extension which renders display math in HTML via JavaScr category = "main" optional = true python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] [package.extras] test = ["flake8", "mypy", "pytest"] @@ -1051,6 +1740,10 @@ description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp d category = "main" optional = true python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, + {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, +] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] @@ -1063,6 +1756,10 @@ description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs category = "main" optional = true python-versions = ">=3.5" +files = [ + {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, + {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, +] [package.extras] lint = ["docutils-stubs", "flake8", "mypy"] @@ -1070,23 +1767,19 @@ test = ["pytest"] [[package]] name = "termcolor" -version = "2.1.1" +version = "2.2.0" description = "ANSI color formatting for output in terminal" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "termcolor-2.2.0-py3-none-any.whl", hash = "sha256:91ddd848e7251200eac969846cbae2dacd7d71c2871e92733289e7e3666f48e7"}, + {file = "termcolor-2.2.0.tar.gz", hash = "sha256:dfc8ac3f350788f23b2947b3e6cfa5a53b630b612e6cd8965a015a776020b99a"}, +] [package.extras] tests = ["pytest", "pytest-cov"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "tomli" version = "2.0.1" @@ -1094,6 +1787,10 @@ description = "A lil' TOML parser" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] [[package]] name = "tomlkit" @@ -1102,14 +1799,22 @@ description = "Style preserving TOML library" category = "dev" optional = false python-versions = ">=3.6" +files = [ + {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, + {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, +] [[package]] name = "tox" -version = "3.27.1" +version = "3.28.0" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "tox-3.28.0-py2.py3-none-any.whl", hash = "sha256:57b5ab7e8bb3074edc3c0c0b4b192a4f3799d3723b2c5b76f1fa9f2d40316eea"}, + {file = "tox-3.28.0.tar.gz", hash = "sha256:d0d28f3fe6d6d7195c27f8b054c3e99d5451952b54abdae673b71609a581f640"}, +] [package.dependencies] colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} @@ -1133,6 +1838,10 @@ description = "Collection of utilities for publishing packages on PyPI" category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "twine-4.0.2-py3-none-any.whl", hash = "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8"}, + {file = "twine-4.0.2.tar.gz", hash = "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8"}, +] [package.dependencies] importlib-metadata = ">=3.6" @@ -1152,6 +1861,32 @@ description = "a fork of Python 2 and 3 ast modules with type comment support" category = "main" optional = false python-versions = ">=3.6" +files = [ + {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, + {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, + {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, + {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, + {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, + {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, + {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, + {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, + {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, + {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, + {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, + {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, + {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, + {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, + {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, + {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, + {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, +] [[package]] name = "typing-extensions" @@ -1160,6 +1895,10 @@ description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" +files = [ + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, +] [[package]] name = "unidecode" @@ -1168,14 +1907,22 @@ description = "ASCII transliterations of Unicode text" category = "main" optional = true python-versions = ">=3.5" +files = [ + {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, + {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, +] [[package]] name = "urllib3" -version = "1.26.13" +version = "1.26.14" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] @@ -1184,29 +1931,37 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.17.1" +version = "20.19.0" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, + {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, +] [package.dependencies] distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} -platformdirs = ">=2.4,<3" +platformdirs = ">=2.4,<4" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] -testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=22.12)"] +test = ["covdefaults (>=2.2.2)", "coverage (>=7.1)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23)", "pytest (>=7.2.1)", "pytest-env (>=0.8.1)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)"] [[package]] name = "wcwidth" -version = "0.2.5" +version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" category = "dev" optional = false python-versions = "*" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] [[package]] name = "webencodings" @@ -1215,6 +1970,10 @@ description = "Character encoding aliases for legacy web content" category = "dev" optional = false python-versions = "*" +files = [ + {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, + {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, +] [[package]] name = "wheel" @@ -1223,6 +1982,10 @@ description = "A built-package format for Python" category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +files = [ + {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, + {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, +] [package.extras] test = ["pytest (>=3.0.0)", "pytest-cov"] @@ -1234,696 +1997,7 @@ description = "Module for decorators, wrappers and monkey patching." category = "main" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[[package]] -name = "zipp" -version = "3.11.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] - -[extras] -docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "48a656650bc98b40759fa591d4878a407f5af1fe65d1035ab7bb6430bf9fae29" - -[metadata.files] -alabaster = [ - {file = "alabaster-0.7.12-py2.py3-none-any.whl", hash = "sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359"}, - {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, -] -argcomplete = [ - {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, - {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, -] -astroid = [ - {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, - {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -babel = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, -] -black = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, -] -bleach = [ - {file = "bleach-5.0.1-py3-none-any.whl", hash = "sha256:085f7f33c15bd408dd9b17a4ad77c577db66d76203e5984b1bd59baeee948b2a"}, - {file = "bleach-5.0.1.tar.gz", hash = "sha256:0d03255c47eb9bd2f26aa9bb7f2107732e7e8fe195ca2f64709fcf3b0a4a085c"}, -] -certifi = [ - {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, - {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, -] -cffi = [ - {file = "cffi-1.15.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a66d3508133af6e8548451b25058d5812812ec3798c886bf38ed24a98216fab2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:470c103ae716238bbe698d67ad020e1db9d9dba34fa5a899b5e21577e6d52ed2"}, - {file = "cffi-1.15.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:9ad5db27f9cabae298d151c85cf2bad1d359a1b9c686a275df03385758e2f914"}, - {file = "cffi-1.15.1-cp27-cp27m-win32.whl", hash = "sha256:b3bbeb01c2b273cca1e1e0c5df57f12dce9a4dd331b4fa1635b8bec26350bde3"}, - {file = "cffi-1.15.1-cp27-cp27m-win_amd64.whl", hash = "sha256:e00b098126fd45523dd056d2efba6c5a63b71ffe9f2bbe1a4fe1716e1d0c331e"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:d61f4695e6c866a23a21acab0509af1cdfd2c013cf256bbf5b6b5e2695827162"}, - {file = "cffi-1.15.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:ed9cb427ba5504c1dc15ede7d516b84757c3e3d7868ccc85121d9310d27eed0b"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d39875251ca8f612b6f33e6b1195af86d1b3e60086068be9cc053aa4376e21"}, - {file = "cffi-1.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:285d29981935eb726a4399badae8f0ffdff4f5050eaa6d0cfc3f64b857b77185"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3eb6971dcff08619f8d91607cfc726518b6fa2a9eba42856be181c6d0d9515fd"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21157295583fe8943475029ed5abdcf71eb3911894724e360acff1d61c1d54bc"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5635bd9cb9731e6d4a1132a498dd34f764034a8ce60cef4f5319c0541159392f"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2012c72d854c2d03e45d06ae57f40d78e5770d252f195b93f581acf3ba44496e"}, - {file = "cffi-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd86c085fae2efd48ac91dd7ccffcfc0571387fe1193d33b6394db7ef31fe2a4"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:fa6693661a4c91757f4412306191b6dc88c1703f780c8234035eac011922bc01"}, - {file = "cffi-1.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59c0b02d0a6c384d453fece7566d1c7e6b7bae4fc5874ef2ef46d56776d61c9e"}, - {file = "cffi-1.15.1-cp310-cp310-win32.whl", hash = "sha256:cba9d6b9a7d64d4bd46167096fc9d2f835e25d7e4c121fb2ddfc6528fb0413b2"}, - {file = "cffi-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:ce4bcc037df4fc5e3d184794f27bdaab018943698f4ca31630bc7f84a7b69c6d"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d08afd128ddaa624a48cf2b859afef385b720bb4b43df214f85616922e6a5ac"}, - {file = "cffi-1.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3799aecf2e17cf585d977b780ce79ff0dc9b78d799fc694221ce814c2c19db83"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a591fe9e525846e4d154205572a029f653ada1a78b93697f3b5a8f1f2bc055b9"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3548db281cd7d2561c9ad9984681c95f7b0e38881201e157833a2342c30d5e8c"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91fc98adde3d7881af9b59ed0294046f3806221863722ba7d8d120c575314325"}, - {file = "cffi-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94411f22c3985acaec6f83c6df553f2dbe17b698cc7f8ae751ff2237d96b9e3c"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:03425bdae262c76aad70202debd780501fabeaca237cdfddc008987c0e0f59ef"}, - {file = "cffi-1.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cc4d65aeeaa04136a12677d3dd0b1c0c94dc43abac5860ab33cceb42b801c1e8"}, - {file = "cffi-1.15.1-cp311-cp311-win32.whl", hash = "sha256:a0f100c8912c114ff53e1202d0078b425bee3649ae34d7b070e9697f93c5d52d"}, - {file = "cffi-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:04ed324bda3cda42b9b695d51bb7d54b680b9719cfab04227cdd1e04e5de3104"}, - {file = "cffi-1.15.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50a74364d85fd319352182ef59c5c790484a336f6db772c1a9231f1c3ed0cbd7"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e263d77ee3dd201c3a142934a086a4450861778baaeeb45db4591ef65550b0a6"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cec7d9412a9102bdc577382c3929b337320c4c4c4849f2c5cdd14d7368c5562d"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4289fc34b2f5316fbb762d75362931e351941fa95fa18789191b33fc4cf9504a"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:173379135477dc8cac4bc58f45db08ab45d228b3363adb7af79436135d028405"}, - {file = "cffi-1.15.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6975a3fac6bc83c4a65c9f9fcab9e47019a11d3d2cf7f3c0d03431bf145a941e"}, - {file = "cffi-1.15.1-cp36-cp36m-win32.whl", hash = "sha256:2470043b93ff09bf8fb1d46d1cb756ce6132c54826661a32d4e4d132e1977adf"}, - {file = "cffi-1.15.1-cp36-cp36m-win_amd64.whl", hash = "sha256:30d78fbc8ebf9c92c9b7823ee18eb92f2e6ef79b45ac84db507f52fbe3ec4497"}, - {file = "cffi-1.15.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:198caafb44239b60e252492445da556afafc7d1e3ab7a1fb3f0584ef6d742375"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef34d190326c3b1f822a5b7a45f6c4535e2f47ed06fec77d3d799c450b2651e"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8102eaf27e1e448db915d08afa8b41d6c7ca7a04b7d73af6514df10a3e74bd82"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5df2768244d19ab7f60546d0c7c63ce1581f7af8b5de3eb3004b9b6fc8a9f84b"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8c4917bd7ad33e8eb21e9a5bbba979b49d9a97acb3a803092cbc1133e20343c"}, - {file = "cffi-1.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2642fe3142e4cc4af0799748233ad6da94c62a8bec3a6648bf8ee68b1c7426"}, - {file = "cffi-1.15.1-cp37-cp37m-win32.whl", hash = "sha256:e229a521186c75c8ad9490854fd8bbdd9a0c9aa3a524326b55be83b54d4e0ad9"}, - {file = "cffi-1.15.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a0b71b1b8fbf2b96e41c4d990244165e2c9be83d54962a9a1d118fd8657d2045"}, - {file = "cffi-1.15.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:320dab6e7cb2eacdf0e658569d2575c4dad258c0fcc794f46215e1e39f90f2c3"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e74c6b51a9ed6589199c787bf5f9875612ca4a8a0785fb2d4a84429badaf22a"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5c84c68147988265e60416b57fc83425a78058853509c1b0629c180094904a5"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b926aa83d1edb5aa5b427b4053dc420ec295a08e40911296b9eb1b6170f6cca"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87c450779d0914f2861b8526e035c5e6da0a3199d8f1add1a665e1cbc6fc6d02"}, - {file = "cffi-1.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f2c9f67e9821cad2e5f480bc8d83b8742896f1242dba247911072d4fa94c192"}, - {file = "cffi-1.15.1-cp38-cp38-win32.whl", hash = "sha256:8b7ee99e510d7b66cdb6c593f21c043c248537a32e0bedf02e01e9553a172314"}, - {file = "cffi-1.15.1-cp38-cp38-win_amd64.whl", hash = "sha256:00a9ed42e88df81ffae7a8ab6d9356b371399b91dbdf0c3cb1e84c03a13aceb5"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:54a2db7b78338edd780e7ef7f9f6c442500fb0d41a5a4ea24fff1c929d5af585"}, - {file = "cffi-1.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fcd131dd944808b5bdb38e6f5b53013c5aa4f334c5cad0c72742f6eba4b73db0"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7473e861101c9e72452f9bf8acb984947aa1661a7704553a9f6e4baa5ba64415"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c9a799e985904922a4d207a94eae35c78ebae90e128f0c4e521ce339396be9d"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bcde07039e586f91b45c88f8583ea7cf7a0770df3a1649627bf598332cb6984"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33ab79603146aace82c2427da5ca6e58f2b3f2fb5da893ceac0c42218a40be35"}, - {file = "cffi-1.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d598b938678ebf3c67377cdd45e09d431369c3b1a5b331058c338e201f12b27"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db0fbb9c62743ce59a9ff687eb5f4afbe77e5e8403d6697f7446e5f609976f76"}, - {file = "cffi-1.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:98d85c6a2bef81588d9227dde12db8a7f47f639f4a17c9ae08e773aa9c697bf3"}, - {file = "cffi-1.15.1-cp39-cp39-win32.whl", hash = "sha256:40f4774f5a9d4f5e344f31a32b5096977b5d48560c5592e2f3d2c4374bd543ee"}, - {file = "cffi-1.15.1-cp39-cp39-win_amd64.whl", hash = "sha256:70df4e3b545a17496c9b3f41f5115e69a4f2e77e94e1d2a8e1070bc0c38c8a3c"}, - {file = "cffi-1.15.1.tar.gz", hash = "sha256:d400bfb9a37b1351253cb402671cea7e89bdecc294e8016a707f6d1d8ac934f9"}, -] -cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -codespell = [ - {file = "codespell-2.2.2-py3-none-any.whl", hash = "sha256:87dfcd9bdc9b3cb8b067b37f0af22044d7a84e28174adfc8eaa203056b7f9ecc"}, - {file = "codespell-2.2.2.tar.gz", hash = "sha256:c4d00c02b5a2a55661f00d5b4b3b5a710fa803ced9a9d7e45438268b099c319c"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -commitizen = [ - {file = "commitizen-2.38.0-py3-none-any.whl", hash = "sha256:401e1d6907d752dbb00fd5a8b0d0201ff36bc110870168776d49de20bf5b8b61"}, - {file = "commitizen-2.38.0.tar.gz", hash = "sha256:7daa217f703f330c18548304400d133a834840fd01bc79ef2966426c74bdbf1f"}, -] -commonmark = [ - {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, - {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, -] -coverage = [ - {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, - {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, - {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, - {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, - {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, - {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, - {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, - {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, - {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, - {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, - {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, - {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, - {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, - {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, - {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, - {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, - {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, - {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, - {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, - {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, - {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, - {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, - {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, - {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, - {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, - {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, - {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, - {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, -] -cryptography = [ - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:549153378611c0cca1042f20fd9c5030d37a72f634c9326e225c9f666d472884"}, - {file = "cryptography-37.0.4-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:a958c52505c8adf0d3822703078580d2c0456dd1d27fabfb6f76fe63d2971cd6"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f721d1885ecae9078c3f6bbe8a88bc0786b6e749bf32ccec1ef2b18929a05046"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:3d41b965b3380f10e4611dbae366f6dc3cefc7c9ac4e8842a806b9672ae9add5"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80f49023dd13ba35f7c34072fa17f604d2f19bf0989f292cedf7ab5770b87a0b"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2dcb0b3b63afb6df7fd94ec6fbddac81b5492513f7b0436210d390c14d46ee8"}, - {file = "cryptography-37.0.4-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b7f8dd0d4c1f21759695c05a5ec8536c12f31611541f8904083f3dc582604280"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:30788e070800fec9bbcf9faa71ea6d8068f5136f60029759fd8c3efec3c9dcb3"}, - {file = "cryptography-37.0.4-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:190f82f3e87033821828f60787cfa42bff98404483577b591429ed99bed39d59"}, - {file = "cryptography-37.0.4-cp36-abi3-win32.whl", hash = "sha256:b62439d7cd1222f3da897e9a9fe53bbf5c104fff4d60893ad1355d4c14a24157"}, - {file = "cryptography-37.0.4-cp36-abi3-win_amd64.whl", hash = "sha256:f7a6de3e98771e183645181b3627e2563dcde3ce94a9e42a3f427d2255190327"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bc95ed67b6741b2607298f9ea4932ff157e570ef456ef7ff0ef4884a134cc4b"}, - {file = "cryptography-37.0.4-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f8c0a6e9e1dd3eb0414ba320f85da6b0dcbd543126e30fcc546e7372a7fbf3b9"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:e007f052ed10cc316df59bc90fbb7ff7950d7e2919c9757fd42a2b8ecf8a5f67"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bc997818309f56c0038a33b8da5c0bfbb3f1f067f315f9abd6fc07ad359398d"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:d204833f3c8a33bbe11eda63a54b1aad7aa7456ed769a982f21ec599ba5fa282"}, - {file = "cryptography-37.0.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:75976c217f10d48a8b5a8de3d70c454c249e4b91851f6838a4e48b8f41eb71aa"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:7099a8d55cd49b737ffc99c17de504f2257e3787e02abe6d1a6d136574873441"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2be53f9f5505673eeda5f2736bea736c40f051a739bfae2f92d18aed1eb54596"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:91ce48d35f4e3d3f1d83e29ef4a9267246e6a3be51864a5b7d2247d5086fa99a"}, - {file = "cryptography-37.0.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4c590ec31550a724ef893c50f9a97a0c14e9c851c85621c5650d699a7b88f7ab"}, - {file = "cryptography-37.0.4.tar.gz", hash = "sha256:63f9c17c0e2474ccbebc9302ce2f07b55b3b3fcb211ded18a42d5764f5c10a82"}, -] -darglint = [ - {file = "darglint-1.8.1-py3-none-any.whl", hash = "sha256:5ae11c259c17b0701618a20c3da343a3eb98b3bc4b5a83d31cdd94f5ebdced8d"}, - {file = "darglint-1.8.1.tar.gz", hash = "sha256:080d5106df149b199822e7ee7deb9c012b49891538f14a11be681044f0bb20da"}, -] -decli = [ - {file = "decli-0.5.2-py3-none-any.whl", hash = "sha256:d3207bc02d0169bf6ed74ccca09ce62edca0eb25b0ebf8bf4ae3fb8333e15ca0"}, - {file = "decli-0.5.2.tar.gz", hash = "sha256:f2cde55034a75c819c630c7655a844c612f2598c42c21299160465df6ad463ad"}, -] -distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] -docutils = [ - {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, - {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, -] -ecdsa = [ - {file = "ecdsa-0.18.0-py2.py3-none-any.whl", hash = "sha256:80600258e7ed2f16b9aa1d7c295bd70194109ad5a30fdee0eaeefef1d4c559dd"}, - {file = "ecdsa-0.18.0.tar.gz", hash = "sha256:190348041559e21b22a1d65cee485282ca11a6f81d503fddb84d5017e9ed1e49"}, -] -exceptiongroup = [ - {file = "exceptiongroup-1.0.4-py3-none-any.whl", hash = "sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828"}, - {file = "exceptiongroup-1.0.4.tar.gz", hash = "sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec"}, -] -filelock = [ - {file = "filelock-3.8.2-py3-none-any.whl", hash = "sha256:8df285554452285f79c035efb0c861eb33a4bcfa5b7a137016e32e6a90f9792c"}, - {file = "filelock-3.8.2.tar.gz", hash = "sha256:7565f628ea56bfcd8e54e42bdc55da899c85c1abfe1b5bcfd147e9188cebb3b2"}, -] -flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] -flake8-docstrings = [ - {file = "flake8-docstrings-1.6.0.tar.gz", hash = "sha256:9fe7c6a306064af8e62a055c2f61e9eb1da55f84bb39caef2b84ce53708ac34b"}, - {file = "flake8_docstrings-1.6.0-py2.py3-none-any.whl", hash = "sha256:99cac583d6c7e32dd28bbfbef120a7c0d1b6dde4adb5a9fd441c4227a6534bde"}, -] -identify = [ - {file = "identify-2.5.9-py2.py3-none-any.whl", hash = "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d"}, - {file = "identify-2.5.9.tar.gz", hash = "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -imagesize = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] -importlib-metadata = [ - {file = "importlib_metadata-4.13.0-py3-none-any.whl", hash = "sha256:8a8a81bcf996e74fee46f0d16bd3eaa382a7eb20fd82445c3ad11f4090334116"}, - {file = "importlib_metadata-4.13.0.tar.gz", hash = "sha256:dd0173e8f150d6815e098fd354f6414b0f079af4644ddfe90c71e2fc6174346d"}, -] -iniconfig = [ - {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, - {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, -] -isort = [ - {file = "isort-5.11.2-py3-none-any.whl", hash = "sha256:e486966fba83f25b8045f8dd7455b0a0d1e4de481e1d7ce4669902d9fb85e622"}, - {file = "isort-5.11.2.tar.gz", hash = "sha256:dd8bbc5c0990f2a095d754e50360915f73b4c26fc82733eb5bfc6b48396af4d2"}, -] -jaraco-classes = [ - {file = "jaraco.classes-3.2.3-py3-none-any.whl", hash = "sha256:2353de3288bc6b82120752201c6b1c1a14b058267fa424ed5ce5984e3b922158"}, - {file = "jaraco.classes-3.2.3.tar.gz", hash = "sha256:89559fa5c1d3c34eff6f631ad80bb21f378dbcbb35dd161fd2c6b93f5be2f98a"}, -] -jeepney = [ - {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, - {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -keyring = [ - {file = "keyring-23.11.0-py3-none-any.whl", hash = "sha256:3dd30011d555f1345dec2c262f0153f2f0ca6bca041fb1dc4588349bb4c0ac1e"}, - {file = "keyring-23.11.0.tar.gz", hash = "sha256:ad192263e2cdd5f12875dedc2da13534359a7e760e77f8d04b50968a821c2361"}, -] -lazy-object-proxy = [ - {file = "lazy-object-proxy-1.8.0.tar.gz", hash = "sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-win32.whl", hash = "sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25"}, - {file = "lazy_object_proxy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-win32.whl", hash = "sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e"}, - {file = "lazy_object_proxy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win32.whl", hash = "sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd"}, - {file = "lazy_object_proxy-1.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-win32.whl", hash = "sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f"}, - {file = "lazy_object_proxy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-win32.whl", hash = "sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f"}, - {file = "lazy_object_proxy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0"}, - {file = "lazy_object_proxy-1.8.0-pp37-pypy37_pp73-any.whl", hash = "sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891"}, - {file = "lazy_object_proxy-1.8.0-pp38-pypy38_pp73-any.whl", hash = "sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec"}, - {file = "lazy_object_proxy-1.8.0-pp39-pypy39_pp73-any.whl", hash = "sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8"}, -] -m2r2 = [ - {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, - {file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mistune = [ - {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, - {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, -] -mock = [ - {file = "mock-4.0.3-py3-none-any.whl", hash = "sha256:122fcb64ee37cfad5b3f48d7a7d51875d7031aaf3d8be7c42e2bee25044eee62"}, - {file = "mock-4.0.3.tar.gz", hash = "sha256:7d3fbbde18228f4ff2f1f119a45cdffa458b4c0dee32eb4d2bb2f82554bac7bc"}, -] -more-itertools = [ - {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, - {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -nodeenv = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] -packaging = [ - {file = "packaging-22.0-py3-none-any.whl", hash = "sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"}, - {file = "packaging-22.0.tar.gz", hash = "sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3"}, -] -pathspec = [ - {file = "pathspec-0.10.3-py3-none-any.whl", hash = "sha256:3c95343af8b756205e2aba76e843ba9520a24dd84f68c22b9f93251507509dd6"}, - {file = "pathspec-0.10.3.tar.gz", hash = "sha256:56200de4077d9d0791465aa9095a01d421861e405b5096955051deefd697d6f6"}, -] -pkginfo = [ - {file = "pkginfo-1.9.2-py3-none-any.whl", hash = "sha256:d580059503f2f4549ad6e4c106d7437356dbd430e2c7df99ee1efe03d75f691e"}, - {file = "pkginfo-1.9.2.tar.gz", hash = "sha256:ac03e37e4d601aaee40f8087f63fc4a2a6c9814dda2c8fa6aab1b1829653bdfa"}, -] -platformdirs = [ - {file = "platformdirs-2.6.0-py3-none-any.whl", hash = "sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca"}, - {file = "platformdirs-2.6.0.tar.gz", hash = "sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"}, -] -pluggy = [ - {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, - {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, -] -pre-commit = [ - {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, - {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, -] -prompt-toolkit = [ - {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, - {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, - {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, - {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, -] -pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] -pycparser = [ - {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, - {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, -] -pydocstyle = [ - {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, - {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, -] -pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] -pygments = [ - {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, - {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, -] -pytest = [ - {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, - {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, -] -pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, -] -python-jose = [ - {file = "python-jose-3.3.0.tar.gz", hash = "sha256:55779b5e6ad599c6336191246e95eb2293a9ddebd555f796a65f838f07e5d78a"}, - {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, -] -pytz = [ - {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, - {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, -] -pywin32-ctypes = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -questionary = [ - {file = "questionary-1.10.0-py3-none-any.whl", hash = "sha256:fecfcc8cca110fda9d561cb83f1e97ecbb93c613ff857f655818839dac74ce90"}, - {file = "questionary-1.10.0.tar.gz", hash = "sha256:600d3aefecce26d48d97eee936fdb66e4bc27f934c3ab6dd1e292c4f43946d90"}, -] -readme-renderer = [ - {file = "readme_renderer-37.3-py3-none-any.whl", hash = "sha256:f67a16caedfa71eef48a31b39708637a6f4664c4394801a7b0d6432d13907343"}, - {file = "readme_renderer-37.3.tar.gz", hash = "sha256:cd653186dfc73055656f090f227f5cb22a046d7f71a841dfa305f55c9a513273"}, -] -readthedocs-sphinx-ext = [ - {file = "readthedocs-sphinx-ext-2.2.0.tar.gz", hash = "sha256:e5effcd825816111a377ab7a897b819215138f8e5e8acc86f99218328f957240"}, - {file = "readthedocs_sphinx_ext-2.2.0-py2.py3-none-any.whl", hash = "sha256:d801f0bfb125d2837f18f40451462528d4a97eefd8de8a12ad526b4f1ce14205"}, -] -recommonmark = [ - {file = "recommonmark-0.7.1-py2.py3-none-any.whl", hash = "sha256:1b1db69af0231efce3fa21b94ff627ea33dee7079a01dd0a7f8482c3da148b3f"}, - {file = "recommonmark-0.7.1.tar.gz", hash = "sha256:bdb4db649f2222dcd8d2d844f0006b958d627f732415d399791ee436a3686d67"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -requests-toolbelt = [ - {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, - {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, -] -rfc3986 = [ - {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, - {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, -] -rich = [ - {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, - {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, -] -rsa = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, -] -secretstorage = [ - {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, - {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, -] -setuptools = [ - {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, - {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, -] -six = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] -snowballstemmer = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, -] -sphinx = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, -] -sphinx-autoapi = [ - {file = "sphinx-autoapi-2.0.0.tar.gz", hash = "sha256:97dcf1b5b54cd0d8efef867594e4a4f3e2d3a2c0ec1e5a891e0a61bc77046006"}, - {file = "sphinx_autoapi-2.0.0-py2.py3-none-any.whl", hash = "sha256:dab2753a38cad907bf4e61473c0da365a26bfbe69fbf5aa6e4f7d48e1cf8a148"}, -] -sphinx-rtd-theme = [ - {file = "sphinx_rtd_theme-1.1.1-py2.py3-none-any.whl", hash = "sha256:31faa07d3e97c8955637fc3f1423a5ab2c44b74b8cc558a51498c202ce5cbda7"}, - {file = "sphinx_rtd_theme-1.1.1.tar.gz", hash = "sha256:6146c845f1e1947b3c3dd4432c28998a1693ccc742b4f9ad7c63129f0757c103"}, -] -sphinxcontrib-applehelp = [ - {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, - {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, -] -sphinxcontrib-devhelp = [ - {file = "sphinxcontrib-devhelp-1.0.2.tar.gz", hash = "sha256:ff7f1afa7b9642e7060379360a67e9c41e8f3121f2ce9164266f61b9f4b338e4"}, - {file = "sphinxcontrib_devhelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:8165223f9a335cc1af7ffe1ed31d2871f325254c0423bc0c4c7cd1c1e4734a2e"}, -] -sphinxcontrib-htmlhelp = [ - {file = "sphinxcontrib-htmlhelp-2.0.0.tar.gz", hash = "sha256:f5f8bb2d0d629f398bf47d0d69c07bc13b65f75a81ad9e2f71a63d4b7a2f6db2"}, - {file = "sphinxcontrib_htmlhelp-2.0.0-py2.py3-none-any.whl", hash = "sha256:d412243dfb797ae3ec2b59eca0e52dac12e75a241bf0e4eb861e450d06c6ed07"}, -] -sphinxcontrib-jsmath = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] -sphinxcontrib-qthelp = [ - {file = "sphinxcontrib-qthelp-1.0.3.tar.gz", hash = "sha256:4c33767ee058b70dba89a6fc5c1892c0d57a54be67ddd3e7875a18d14cba5a72"}, - {file = "sphinxcontrib_qthelp-1.0.3-py2.py3-none-any.whl", hash = "sha256:bd9fc24bcb748a8d51fd4ecaade681350aa63009a347a8c14e637895444dfab6"}, -] -sphinxcontrib-serializinghtml = [ - {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, - {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, -] -termcolor = [ - {file = "termcolor-2.1.1-py3-none-any.whl", hash = "sha256:fa852e957f97252205e105dd55bbc23b419a70fec0085708fc0515e399f304fd"}, - {file = "termcolor-2.1.1.tar.gz", hash = "sha256:67cee2009adc6449c650f6bcf3bdeed00c8ba53a8cda5362733c53e0a39fb70b"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -tomlkit = [ - {file = "tomlkit-0.11.6-py3-none-any.whl", hash = "sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b"}, - {file = "tomlkit-0.11.6.tar.gz", hash = "sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"}, -] -tox = [ - {file = "tox-3.27.1-py2.py3-none-any.whl", hash = "sha256:f52ca66eae115fcfef0e77ef81fd107133d295c97c52df337adedb8dfac6ab84"}, - {file = "tox-3.27.1.tar.gz", hash = "sha256:b2a920e35a668cc06942ffd1cf3a4fb221a4d909ca72191fb6d84b0b18a7be04"}, -] -twine = [ - {file = "twine-4.0.2-py3-none-any.whl", hash = "sha256:929bc3c280033347a00f847236564d1c52a3e61b1ac2516c97c48f3ceab756d8"}, - {file = "twine-4.0.2.tar.gz", hash = "sha256:9e102ef5fdd5a20661eb88fad46338806c3bd32cf1db729603fe3697b1bc83c8"}, -] -typed-ast = [ - {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, - {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:267e3f78697a6c00c689c03db4876dd1efdfea2f251a5ad6555e82a26847b4ac"}, - {file = "typed_ast-1.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c542eeda69212fa10a7ada75e668876fdec5f856cd3d06829e6aa64ad17c8dfe"}, - {file = "typed_ast-1.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:a9916d2bb8865f973824fb47436fa45e1ebf2efd920f2b9f99342cb7fab93f72"}, - {file = "typed_ast-1.5.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:79b1e0869db7c830ba6a981d58711c88b6677506e648496b1f64ac7d15633aec"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a94d55d142c9265f4ea46fab70977a1944ecae359ae867397757d836ea5a3f47"}, - {file = "typed_ast-1.5.4-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:183afdf0ec5b1b211724dfef3d2cad2d767cbefac291f24d69b00546c1837fb6"}, - {file = "typed_ast-1.5.4-cp36-cp36m-win_amd64.whl", hash = "sha256:639c5f0b21776605dd6c9dbe592d5228f021404dafd377e2b7ac046b0349b1a1"}, - {file = "typed_ast-1.5.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf4afcfac006ece570e32d6fa90ab74a17245b83dfd6655a6f68568098345ff6"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed855bbe3eb3715fca349c80174cfcfd699c2f9de574d40527b8429acae23a66"}, - {file = "typed_ast-1.5.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6778e1b2f81dfc7bc58e4b259363b83d2e509a65198e85d5700dfae4c6c8ff1c"}, - {file = "typed_ast-1.5.4-cp37-cp37m-win_amd64.whl", hash = "sha256:0261195c2062caf107831e92a76764c81227dae162c4f75192c0d489faf751a2"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2efae9db7a8c05ad5547d522e7dbe62c83d838d3906a3716d1478b6c1d61388d"}, - {file = "typed_ast-1.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7d5d014b7daa8b0bf2eaef684295acae12b036d79f54178b92a2b6a56f92278f"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:370788a63915e82fd6f212865a596a0fefcbb7d408bbbb13dea723d971ed8bdc"}, - {file = "typed_ast-1.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4e964b4ff86550a7a7d56345c7864b18f403f5bd7380edf44a3c1fb4ee7ac6c6"}, - {file = "typed_ast-1.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:683407d92dc953c8a7347119596f0b0e6c55eb98ebebd9b23437501b28dcbb8e"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4879da6c9b73443f97e731b617184a596ac1235fe91f98d279a7af36c796da35"}, - {file = "typed_ast-1.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3e123d878ba170397916557d31c8f589951e353cc95fb7f24f6bb69adc1a8a97"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebd9d7f80ccf7a82ac5f88c521115cc55d84e35bf8b446fcd7836eb6b98929a3"}, - {file = "typed_ast-1.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98f80dee3c03455e92796b58b98ff6ca0b2a6f652120c263efdba4d6c5e58f72"}, - {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, - {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -unidecode = [ - {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, - {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, -] -urllib3 = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, -] -virtualenv = [ - {file = "virtualenv-20.17.1-py3-none-any.whl", hash = "sha256:ce3b1684d6e1a20a3e5ed36795a97dfc6af29bc3970ca8dab93e11ac6094b3c4"}, - {file = "virtualenv-20.17.1.tar.gz", hash = "sha256:f8b927684efc6f1cc206c9db297a570ab9ad0e51c16fa9e45487d36d1905c058"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -webencodings = [ - {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, - {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, -] -wheel = [ - {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, - {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, -] -wrapt = [ +files = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, @@ -1989,7 +2063,27 @@ wrapt = [ {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] -zipp = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, + +[[package]] +name = "zipp" +version = "3.13.0" +description = "Backport of pathlib-compatible object wrapper for zip files" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"}, + {file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"}, ] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] + +[extras] +docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.7" +content-hash = "5956d67888e27fa37f4283a03ab3564658926480b09ee5b9496e3d603a5ecb2a" diff --git a/pyproject.toml b/pyproject.toml index 14e02858..89e8450b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,9 +30,9 @@ Documentation = "https://python-keycloak.readthedocs.io/en/latest/" [tool.poetry.dependencies] python = "^3.7" -requests = "^2.20.0" +requests = "^2.28.2" python-jose = "^3.3.0" -urllib3 = "^1.26.0" +urllib3 = "^1.26.14" mock = {version = "^4.0.3", optional = true} alabaster = {version = "^0.7.12", optional = true} commonmark = {version = "^0.9.1", optional = true} @@ -42,7 +42,7 @@ sphinx-rtd-theme = {version = "^1.0.0", optional = true} readthedocs-sphinx-ext = {version = "^2.1.9", optional = true} m2r2 = {version = "^0.3.2", optional = true} sphinx-autoapi = {version = "^2.0.0", optional = true} -requests-toolbelt = "^0.9.1" +requests-toolbelt = "^0.10.1" [tool.poetry.extras] docs = [ diff --git a/tox.ini b/tox.ini index 174d0748..2f157055 100644 --- a/tox.ini +++ b/tox.ini @@ -1,13 +1,13 @@ [tox] -requires = - tox-poetry - poetry - tox<4.0.0 +isolated_build = true +skipsdist = true envlist = check, apply-check, docs, tests, build, changelog [testenv] whitelist_externals = bash +commands_pre = + poetry install --no-root --sync [testenv:check] commands = @@ -23,7 +23,8 @@ commands = isort src/keycloak tests docs [testenv:docs] -extras = docs +commands_pre = + poetry install --no-root --sync -E docs commands = sphinx-build -T -E -W -b html -d _build/doctrees -D language=en ./docs/source _build/html @@ -34,10 +35,6 @@ commands = ./test_keycloak_init.sh "pytest -vv --cov=keycloak --cov-report term-missing {posargs}" [testenv:build] -deps = - poetry -setenv = - POETRY_VIRTUALENVS_CREATE = false commands = poetry build --format sdist poetry build --format wheel From 9764ec6bd3895344e1f9772a1a3883efd9ea4151 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 10 Feb 2023 08:57:59 +0000 Subject: [PATCH 303/566] chore: upgrade wheel --- poetry.lock | 12 ++++++------ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 94c356dc..077e8cdc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1977,18 +1977,18 @@ files = [ [[package]] name = "wheel" -version = "0.37.1" +version = "0.38.4" description = "A built-package format for Python" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" files = [ - {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, - {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, + {file = "wheel-0.38.4-py3-none-any.whl", hash = "sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"}, + {file = "wheel-0.38.4.tar.gz", hash = "sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac"}, ] [package.extras] -test = ["pytest (>=3.0.0)", "pytest-cov"] +test = ["pytest (>=3.0.0)"] [[package]] name = "wrapt" @@ -2086,4 +2086,4 @@ docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "5956d67888e27fa37f4283a03ab3564658926480b09ee5b9496e3d603a5ecb2a" +content-hash = "8d76b155adddd2eacd0304397b33465d5c67f09165d8de641c71f5ce7b979be2" diff --git a/pyproject.toml b/pyproject.toml index 89e8450b..d855cbb5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ docs = [ tox = "^3.25.0" pytest = "^7.1.2" pytest-cov = "^3.0.0" -wheel = "^0.37.1" +wheel = "^0.38.4" pre-commit = "^2.19.0" isort = "^5.10.1" black = "^22.3.0" From d7bcca10db99bc902e6d9a92af2822e5259f9665 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Fri, 10 Feb 2023 08:58:55 +0000 Subject: [PATCH 304/566] ci: dont skip --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2f157055..82df0289 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,5 @@ [tox] isolated_build = true -skipsdist = true envlist = check, apply-check, docs, tests, build, changelog [testenv] From 4f1dea8d2c6ae9cac2a68858e25aa3d800e79a22 Mon Sep 17 00:00:00 2001 From: Richard Nemeth Date: Sun, 5 Mar 2023 13:30:04 +0100 Subject: [PATCH 305/566] fix: tests and upgraded deps (#419) * fix: tests and upgraded deps * test: use absolute path --- .pre-commit-config.yaml | 5 +- poetry.lock | 386 +++++++++++++------------- test_keycloak_init.sh | 3 +- tests/providers/asm-7.3.1.jar | Bin 0 -> 121836 bytes tests/providers/asm-commons-7.3.1.jar | Bin 0 -> 71548 bytes tests/providers/asm-tree-7.3.1.jar | Bin 0 -> 52826 bytes tests/providers/asm-util-7.3.1.jar | Bin 0 -> 84817 bytes tests/providers/nashorn-core-15.4.jar | Bin 0 -> 2167292 bytes 8 files changed, 202 insertions(+), 192 deletions(-) create mode 100644 tests/providers/asm-7.3.1.jar create mode 100644 tests/providers/asm-commons-7.3.1.jar create mode 100644 tests/providers/asm-tree-7.3.1.jar create mode 100644 tests/providers/asm-util-7.3.1.jar create mode 100644 tests/providers/nashorn-core-15.4.jar diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 806a12ce..7e03ac0c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,9 +8,10 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files + args: ["--maxkb=10000"] - repo: https://github.com/compilerla/conventional-pre-commit rev: v1.2.0 hooks: - id: conventional-pre-commit - stages: [ commit-msg ] - args: [ ] # optional: list of Conventional Commits types to allow + stages: [commit-msg] + args: [] # optional: list of Conventional Commits types to allow diff --git a/poetry.lock b/poetry.lock index 077e8cdc..83555c9f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.4.0 and should not be changed by hand. [[package]] name = "alabaster" @@ -14,21 +14,22 @@ files = [ [[package]] name = "argcomplete" -version = "2.0.0" +version = "2.0.5" description = "Bash tab completion for argparse" category = "dev" optional = false python-versions = ">=3.6" files = [ - {file = "argcomplete-2.0.0-py2.py3-none-any.whl", hash = "sha256:cffa11ea77999bb0dd27bb25ff6dc142a6796142f68d45b1a26b11f58724561e"}, - {file = "argcomplete-2.0.0.tar.gz", hash = "sha256:6372ad78c89d662035101418ae253668445b391755cfe94ea52f1b9d22425b20"}, + {file = "argcomplete-2.0.5-py3-none-any.whl", hash = "sha256:e2a2cdb8ee9634ff2fa368ba6b996b46205341e0cf106be8075fd9bdbc5cf2e7"}, + {file = "argcomplete-2.0.5.tar.gz", hash = "sha256:1cfd12928d62e41901783e4dc7d7ca03eccd589840face4c020693b13f754312"}, ] [package.dependencies] -importlib-metadata = {version = ">=0.23,<5", markers = "python_version == \"3.7\""} +importlib-metadata = {version = ">=0.23,<6", markers = "python_version == \"3.7\""} [package.extras] -test = ["coverage", "flake8", "pexpect", "wheel"] +lint = ["flake8", "mypy"] +test = ["coverage", "flake8", "mypy", "pexpect", "wheel"] [[package]] name = "astroid" @@ -70,18 +71,18 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy [[package]] name = "babel" -version = "2.11.0" +version = "2.12.1" description = "Internationalization utilities" category = "main" optional = true -python-versions = ">=3.6" +python-versions = ">=3.7" files = [ - {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, - {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, + {file = "Babel-2.12.1-py3-none-any.whl", hash = "sha256:b4246fb7677d3b98f501a39d43396d3cafdc8eadb045f4a31be01863f655c610"}, + {file = "Babel-2.12.1.tar.gz", hash = "sha256:cc2d99999cd01d44420ae725a21c9e3711b3aadc7976d6147f622d8581963455"}, ] [package.dependencies] -pytz = ">=2015.7" +pytz = {version = ">=2015.7", markers = "python_version < \"3.9\""} [[package]] name = "black" @@ -302,14 +303,14 @@ files = [ [[package]] name = "commitizen" -version = "2.41.0" +version = "2.42.1" description = "Python commitizen client tool" category = "dev" optional = false python-versions = ">=3.6.2,<4.0.0" files = [ - {file = "commitizen-2.41.0-py3-none-any.whl", hash = "sha256:2044f6d1cf002f363280e0dcefc8b557095dc42530060bfe5ad917ea4d5c48a1"}, - {file = "commitizen-2.41.0.tar.gz", hash = "sha256:5d50093fe546cc3b5217780a8cdfc5a9de6a27ce8aa7db6443e2e47dc2b10b6a"}, + {file = "commitizen-2.42.1-py3-none-any.whl", hash = "sha256:fad7d37cfae361a859b713d4ac591859d5ca03137dd52de4e1bd208f7f45d5dc"}, + {file = "commitizen-2.42.1.tar.gz", hash = "sha256:eac18c7c65587061aac6829534907aeb208405b8230bfd35ec08503c228a7f17"}, ] [package.dependencies] @@ -342,63 +343,63 @@ test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] [[package]] name = "coverage" -version = "7.1.0" +version = "7.2.1" description = "Code coverage measurement for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "coverage-7.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf"}, - {file = "coverage-7.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c"}, - {file = "coverage-7.1.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a"}, - {file = "coverage-7.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c"}, - {file = "coverage-7.1.0-cp310-cp310-win32.whl", hash = "sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352"}, - {file = "coverage-7.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038"}, - {file = "coverage-7.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040"}, - {file = "coverage-7.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222"}, - {file = "coverage-7.1.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"}, - {file = "coverage-7.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e"}, - {file = "coverage-7.1.0-cp311-cp311-win32.whl", hash = "sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7"}, - {file = "coverage-7.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c"}, - {file = "coverage-7.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8"}, - {file = "coverage-7.1.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d"}, - {file = "coverage-7.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3"}, - {file = "coverage-7.1.0-cp37-cp37m-win32.whl", hash = "sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73"}, - {file = "coverage-7.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5"}, - {file = "coverage-7.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06"}, - {file = "coverage-7.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d"}, - {file = "coverage-7.1.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8"}, - {file = "coverage-7.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0"}, - {file = "coverage-7.1.0-cp38-cp38-win32.whl", hash = "sha256:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab"}, - {file = "coverage-7.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c"}, - {file = "coverage-7.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6"}, - {file = "coverage-7.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311"}, - {file = "coverage-7.1.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8"}, - {file = "coverage-7.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c"}, - {file = "coverage-7.1.0-cp39-cp39-win32.whl", hash = "sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4"}, - {file = "coverage-7.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3"}, - {file = "coverage-7.1.0-pp37.pp38.pp39-none-any.whl", hash = "sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda"}, - {file = "coverage-7.1.0.tar.gz", hash = "sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265"}, + {file = "coverage-7.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:49567ec91fc5e0b15356da07a2feabb421d62f52a9fff4b1ec40e9e19772f5f8"}, + {file = "coverage-7.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d2ef6cae70168815ed91388948b5f4fcc69681480a0061114db737f957719f03"}, + {file = "coverage-7.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3004765bca3acd9e015794e5c2f0c9a05587f5e698127ff95e9cfba0d3f29339"}, + {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cca7c0b7f5881dfe0291ef09ba7bb1582cb92ab0aeffd8afb00c700bf692415a"}, + {file = "coverage-7.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2167d116309f564af56f9aa5e75ef710ef871c5f9b313a83050035097b56820"}, + {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cb5f152fb14857cbe7f3e8c9a5d98979c4c66319a33cad6e617f0067c9accdc4"}, + {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:87dc37f16fb5e3a28429e094145bf7c1753e32bb50f662722e378c5851f7fdc6"}, + {file = "coverage-7.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e191a63a05851f8bce77bc875e75457f9b01d42843f8bd7feed2fc26bbe60833"}, + {file = "coverage-7.2.1-cp310-cp310-win32.whl", hash = "sha256:e3ea04b23b114572b98a88c85379e9e9ae031272ba1fb9b532aa934c621626d4"}, + {file = "coverage-7.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:0cf557827be7eca1c38a2480484d706693e7bb1929e129785fe59ec155a59de6"}, + {file = "coverage-7.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:570c21a29493b350f591a4b04c158ce1601e8d18bdcd21db136fbb135d75efa6"}, + {file = "coverage-7.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9e872b082b32065ac2834149dc0adc2a2e6d8203080501e1e3c3c77851b466f9"}, + {file = "coverage-7.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fac6343bae03b176e9b58104a9810df3cdccd5cfed19f99adfa807ffbf43cf9b"}, + {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abacd0a738e71b20e224861bc87e819ef46fedba2fb01bc1af83dfd122e9c319"}, + {file = "coverage-7.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9256d4c60c4bbfec92721b51579c50f9e5062c21c12bec56b55292464873508"}, + {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80559eaf6c15ce3da10edb7977a1548b393db36cbc6cf417633eca05d84dd1ed"}, + {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0bd7e628f6c3ec4e7d2d24ec0e50aae4e5ae95ea644e849d92ae4805650b4c4e"}, + {file = "coverage-7.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:09643fb0df8e29f7417adc3f40aaf379d071ee8f0350ab290517c7004f05360b"}, + {file = "coverage-7.2.1-cp311-cp311-win32.whl", hash = "sha256:1b7fb13850ecb29b62a447ac3516c777b0e7a09ecb0f4bb6718a8654c87dfc80"}, + {file = "coverage-7.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:617a94ada56bbfe547aa8d1b1a2b8299e2ec1ba14aac1d4b26a9f7d6158e1273"}, + {file = "coverage-7.2.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8649371570551d2fd7dee22cfbf0b61f1747cdfb2b7587bb551e4beaaa44cb97"}, + {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d2b9b5e70a21474c105a133ba227c61bc95f2ac3b66861143ce39a5ea4b3f84"}, + {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae82c988954722fa07ec5045c57b6d55bc1a0890defb57cf4a712ced65b26ddd"}, + {file = "coverage-7.2.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:861cc85dfbf55a7a768443d90a07e0ac5207704a9f97a8eb753292a7fcbdfcfc"}, + {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:0339dc3237c0d31c3b574f19c57985fcbe494280153bbcad33f2cdf469f4ac3e"}, + {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:5928b85416a388dd557ddc006425b0c37e8468bd1c3dc118c1a3de42f59e2a54"}, + {file = "coverage-7.2.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d3843ca645f62c426c3d272902b9de90558e9886f15ddf5efe757b12dd376f5"}, + {file = "coverage-7.2.1-cp37-cp37m-win32.whl", hash = "sha256:6a034480e9ebd4e83d1aa0453fd78986414b5d237aea89a8fdc35d330aa13bae"}, + {file = "coverage-7.2.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6fce673f79a0e017a4dc35e18dc7bb90bf6d307c67a11ad5e61ca8d42b87cbff"}, + {file = "coverage-7.2.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7f099da6958ddfa2ed84bddea7515cb248583292e16bb9231d151cd528eab657"}, + {file = "coverage-7.2.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97a3189e019d27e914ecf5c5247ea9f13261d22c3bb0cfcfd2a9b179bb36f8b1"}, + {file = "coverage-7.2.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a81dbcf6c6c877986083d00b834ac1e84b375220207a059ad45d12f6e518a4e3"}, + {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:78d2c3dde4c0b9be4b02067185136b7ee4681978228ad5ec1278fa74f5ca3e99"}, + {file = "coverage-7.2.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a209d512d157379cc9ab697cbdbb4cfd18daa3e7eebaa84c3d20b6af0037384"}, + {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f3d07edb912a978915576a776756069dede66d012baa503022d3a0adba1b6afa"}, + {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8dca3c1706670297851bca1acff9618455122246bdae623be31eca744ade05ec"}, + {file = "coverage-7.2.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b1991a6d64231a3e5bbe3099fb0dd7c9aeaa4275ad0e0aeff4cb9ef885c62ba2"}, + {file = "coverage-7.2.1-cp38-cp38-win32.whl", hash = "sha256:22c308bc508372576ffa3d2dbc4824bb70d28eeb4fcd79d4d1aed663a06630d0"}, + {file = "coverage-7.2.1-cp38-cp38-win_amd64.whl", hash = "sha256:b0c0d46de5dd97f6c2d1b560bf0fcf0215658097b604f1840365296302a9d1fb"}, + {file = "coverage-7.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4dd34a935de268a133e4741827ae951283a28c0125ddcdbcbba41c4b98f2dfef"}, + {file = "coverage-7.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0f8318ed0f3c376cfad8d3520f496946977abde080439d6689d7799791457454"}, + {file = "coverage-7.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:834c2172edff5a08d78e2f53cf5e7164aacabeb66b369f76e7bb367ca4e2d993"}, + {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4d70c853f0546855f027890b77854508bdb4d6a81242a9d804482e667fff6e6"}, + {file = "coverage-7.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6450da4c7afc4534305b2b7d8650131e130610cea448ff240b6ab73d7eab63"}, + {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:99f4dd81b2bb8fc67c3da68b1f5ee1650aca06faa585cbc6818dbf67893c6d58"}, + {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bdd3f2f285ddcf2e75174248b2406189261a79e7fedee2ceeadc76219b6faa0e"}, + {file = "coverage-7.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f29351393eb05e6326f044a7b45ed8e38cb4dcc38570d12791f271399dc41431"}, + {file = "coverage-7.2.1-cp39-cp39-win32.whl", hash = "sha256:e2b50ebc2b6121edf352336d503357321b9d8738bb7a72d06fc56153fd3f4cd8"}, + {file = "coverage-7.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:bd5a12239c0006252244f94863f1c518ac256160cd316ea5c47fb1a11b25889a"}, + {file = "coverage-7.2.1-pp37.pp38.pp39-none-any.whl", hash = "sha256:436313d129db7cf5b4ac355dd2bd3f7c7e5294af077b090b85de75f8458b8616"}, + {file = "coverage-7.2.1.tar.gz", hash = "sha256:c77f2a9093ccf329dd523a9b2b3c854c20d2a3d968b6def3b820272ca6732242"}, ] [package.dependencies] @@ -443,10 +444,10 @@ files = [ cffi = ">=1.12" [package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx_rtd_theme"] +docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] docstest = ["pyenchant (>=1.6.11)", "sphinxcontrib-spelling (>=4.0.1)", "twine (>=1.12.0)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -sdist = ["setuptools_rust (>=0.11.4)"] +sdist = ["setuptools-rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] test = ["hypothesis (>=1.11.4,!=3.79.2)", "iso8601", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pytz"] @@ -584,14 +585,14 @@ pydocstyle = ">=2.1" [[package]] name = "identify" -version = "2.5.17" +version = "2.5.18" description = "File identification library for Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "identify-2.5.17-py2.py3-none-any.whl", hash = "sha256:7d526dd1283555aafcc91539acc061d8f6f59adb0a7bba462735b0a318bff7ed"}, - {file = "identify-2.5.17.tar.gz", hash = "sha256:93cc61a861052de9d4c541a7acb7e3dcc9c11b398a2144f6e52ae5285f5f4f06"}, + {file = "identify-2.5.18-py2.py3-none-any.whl", hash = "sha256:93aac7ecf2f6abf879b8f29a8002d3c6de7086b8c28d88e1ad15045a15ab63f9"}, + {file = "identify-2.5.18.tar.gz", hash = "sha256:89e144fa560cc4cffb6ef2ab5e9fb18ed9f9b3cb054384bab4b95c12f6c309fe"}, ] [package.extras] @@ -644,14 +645,14 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag [[package]] name = "importlib-resources" -version = "5.10.2" +version = "5.12.0" description = "Read resources from Python packages" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "importlib_resources-5.10.2-py3-none-any.whl", hash = "sha256:7d543798b0beca10b6a01ac7cafda9f822c54db9e8376a6bf57e0cbd74d486b6"}, - {file = "importlib_resources-5.10.2.tar.gz", hash = "sha256:e4a96c8cc0339647ff9a5e0550d9f276fc5a01ffa276012b58ec108cfd7b8484"}, + {file = "importlib_resources-5.12.0-py3-none-any.whl", hash = "sha256:7b1deeebbf351c7578e09bf2f63fa2ce8b5ffec296e0d349139d43cca061a81a"}, + {file = "importlib_resources-5.12.0.tar.gz", hash = "sha256:4be82589bf5c1d7999aedf2a45159d10cb3ca4f19b2271f8792bc8e6da7b22f6"}, ] [package.dependencies] @@ -833,14 +834,14 @@ mistune = "0.8.4" [[package]] name = "markdown-it-py" -version = "2.1.0" +version = "2.2.0" description = "Python port of markdown-it. Markdown parsing, done right!" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "markdown-it-py-2.1.0.tar.gz", hash = "sha256:cf7e59fed14b5ae17c0006eff14a2d9a00ed5f3a846148153899a0224e2c07da"}, - {file = "markdown_it_py-2.1.0-py3-none-any.whl", hash = "sha256:93de681e5c021a432c63147656fe21790bc01231e0cd2da73626f1aa3ac0fe27"}, + {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, + {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, ] [package.dependencies] @@ -848,10 +849,10 @@ mdurl = ">=0.1,<1.0" typing_extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark (>=3.2,<4.0)"] -code-style = ["pre-commit (==2.6)"] -compare = ["commonmark (>=0.9.1,<0.10.0)", "markdown (>=3.3.6,<3.4.0)", "mistletoe (>=0.8.1,<0.9.0)", "mistune (>=2.0.2,<2.1.0)", "panflute (>=2.1.3,<2.2.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] @@ -972,14 +973,14 @@ test = ["pytest (<5.4)", "pytest-cov"] [[package]] name = "more-itertools" -version = "9.0.0" +version = "9.1.0" description = "More routines for operating on iterables, beyond itertools" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, - {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, + {file = "more-itertools-9.1.0.tar.gz", hash = "sha256:cabaa341ad0389ea83c17a94566a53ae4c9d07349861ecb14dc6d0345cf9ac5d"}, + {file = "more_itertools-9.1.0-py3-none-any.whl", hash = "sha256:d2bc7f02446e86a68911e58ded76d6561eea00cddfb2a91e7019bbb586c799f3"}, ] [[package]] @@ -1050,14 +1051,14 @@ testing = ["pytest", "pytest-cov"] [[package]] name = "platformdirs" -version = "3.0.0" +version = "3.1.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.0.0-py3-none-any.whl", hash = "sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"}, - {file = "platformdirs-3.0.0.tar.gz", hash = "sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9"}, + {file = "platformdirs-3.1.0-py3-none-any.whl", hash = "sha256:13b08a53ed71021350c9e300d4ea8668438fb0046ab3937ac9a29913a1a1350a"}, + {file = "platformdirs-3.1.0.tar.gz", hash = "sha256:accc3665857288317f32c7bebb5a8e482ba717b474f3fc1d18ca7f9214be0cef"}, ] [package.dependencies] @@ -1108,14 +1109,14 @@ virtualenv = ">=20.10.0" [[package]] name = "prompt-toolkit" -version = "3.0.36" +version = "3.0.38" description = "Library for building powerful interactive command lines in Python" category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.36-py3-none-any.whl", hash = "sha256:aa64ad242a462c5ff0363a7b9cfe696c20d55d9fc60c11fd8e632d064804d305"}, - {file = "prompt_toolkit-3.0.36.tar.gz", hash = "sha256:3e163f254bef5a03b146397d7c1963bd3e2812f0964bb9a24e6ec761fd28db63"}, + {file = "prompt_toolkit-3.0.38-py3-none-any.whl", hash = "sha256:45ea77a2f7c60418850331366c81cf6b5b9cf4c7fd34616f733c5427e6abbb1f"}, + {file = "prompt_toolkit-3.0.38.tar.gz", hash = "sha256:23ac5d50538a9a38c8bde05fecb47d0b403ecd0662857a86f886f798563d5b9b"}, ] [package.dependencies] @@ -1141,18 +1142,7 @@ category = "main" optional = false python-versions = "*" files = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] @@ -1228,14 +1218,14 @@ plugins = ["importlib-metadata"] [[package]] name = "pytest" -version = "7.2.1" +version = "7.2.2" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-7.2.1-py3-none-any.whl", hash = "sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5"}, - {file = "pytest-7.2.1.tar.gz", hash = "sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"}, + {file = "pytest-7.2.2-py3-none-any.whl", hash = "sha256:130328f552dcfac0b1cec75c12e3f005619dc5f874f0a06e8ff7263f0ee6225e"}, + {file = "pytest-7.2.2.tar.gz", hash = "sha256:c99ab0c73aceb050f68929bc93af19ab6db0558791c6a0715723abe9d0ade9d4"}, ] [package.dependencies] @@ -1331,6 +1321,13 @@ files = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -1485,19 +1482,19 @@ idna2008 = ["idna"] [[package]] name = "rich" -version = "13.3.1" +version = "13.3.2" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" category = "dev" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.3.1-py3-none-any.whl", hash = "sha256:8aa57747f3fc3e977684f0176a88e789be314a99f99b43b75d1e9cb5dc6db9e9"}, - {file = "rich-13.3.1.tar.gz", hash = "sha256:125d96d20c92b946b983d0d392b84ff945461e5a06d3867e9f9e575f8697b67f"}, + {file = "rich-13.3.2-py3-none-any.whl", hash = "sha256:a104f37270bf677148d8acb07d33be1569eeee87e2d1beb286a4e9113caf6f2f"}, + {file = "rich-13.3.2.tar.gz", hash = "sha256:91954fe80cfb7985727a467ca98a7618e5dd15178cc2da10f553b36a93859001"}, ] [package.dependencies] -markdown-it-py = ">=2.1.0,<3.0.0" -pygments = ">=2.14.0,<3.0.0" +markdown-it-py = ">=2.2.0,<3.0.0" +pygments = ">=2.13.0,<3.0.0" typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9\""} [package.extras] @@ -1536,14 +1533,14 @@ jeepney = ">=0.6" [[package]] name = "setuptools" -version = "67.2.0" +version = "67.4.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "setuptools-67.2.0-py3-none-any.whl", hash = "sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c"}, - {file = "setuptools-67.2.0.tar.gz", hash = "sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"}, + {file = "setuptools-67.4.0-py3-none-any.whl", hash = "sha256:f106dee1b506dee5102cc3f3e9e68137bbad6d47b616be7991714b0c62204251"}, + {file = "setuptools-67.4.0.tar.gz", hash = "sha256:e5fd0a713141a4a105412233c63dc4e17ba0090c8e8334594ac790ec97792330"}, ] [package.extras] @@ -1890,14 +1887,14 @@ files = [ [[package]] name = "typing-extensions" -version = "4.4.0" +version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, + {file = "typing_extensions-4.5.0-py3-none-any.whl", hash = "sha256:fb33085c39dd998ac16d1431ebc293a8b3eedd00fd4a32de0ff79002c19511b4"}, + {file = "typing_extensions-4.5.0.tar.gz", hash = "sha256:5cb5f4a79139d699607b3ef622a1dedafa84e115ab0024e0d9c044a9479ca7cb"}, ] [[package]] @@ -1931,14 +1928,14 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.19.0" +version = "20.20.0" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "virtualenv-20.19.0-py3-none-any.whl", hash = "sha256:54eb59e7352b573aa04d53f80fc9736ed0ad5143af445a1e539aada6eb947dd1"}, - {file = "virtualenv-20.19.0.tar.gz", hash = "sha256:37a640ba82ed40b226599c522d411e4be5edb339a0c0de030c0dc7b646d61590"}, + {file = "virtualenv-20.20.0-py3-none-any.whl", hash = "sha256:3c22fa5a7c7aa106ced59934d2c20a2ecb7f49b4130b8bf444178a16b880fa45"}, + {file = "virtualenv-20.20.0.tar.gz", hash = "sha256:a8a4b8ca1e28f864b7514a253f98c1d62b64e31e77325ba279248c65fb4fcef4"}, ] [package.dependencies] @@ -1992,96 +1989,107 @@ test = ["pytest (>=3.0.0)"] [[package]] name = "wrapt" -version = "1.14.1" +version = "1.15.0" description = "Module for decorators, wrappers and monkey patching." category = "main" optional = true python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" files = [ - {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59"}, - {file = "wrapt-1.14.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462"}, - {file = "wrapt-1.14.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320"}, - {file = "wrapt-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069"}, - {file = "wrapt-1.14.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656"}, - {file = "wrapt-1.14.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c"}, - {file = "wrapt-1.14.1-cp310-cp310-win32.whl", hash = "sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8"}, - {file = "wrapt-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3"}, - {file = "wrapt-1.14.1-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d"}, - {file = "wrapt-1.14.1-cp35-cp35m-win32.whl", hash = "sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7"}, - {file = "wrapt-1.14.1-cp35-cp35m-win_amd64.whl", hash = "sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00"}, - {file = "wrapt-1.14.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1"}, - {file = "wrapt-1.14.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1"}, - {file = "wrapt-1.14.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569"}, - {file = "wrapt-1.14.1-cp36-cp36m-win32.whl", hash = "sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed"}, - {file = "wrapt-1.14.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471"}, - {file = "wrapt-1.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d"}, - {file = "wrapt-1.14.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015"}, - {file = "wrapt-1.14.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a"}, - {file = "wrapt-1.14.1-cp37-cp37m-win32.whl", hash = "sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853"}, - {file = "wrapt-1.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456"}, - {file = "wrapt-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1"}, - {file = "wrapt-1.14.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0"}, - {file = "wrapt-1.14.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57"}, - {file = "wrapt-1.14.1-cp38-cp38-win32.whl", hash = "sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5"}, - {file = "wrapt-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383"}, - {file = "wrapt-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735"}, - {file = "wrapt-1.14.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3"}, - {file = "wrapt-1.14.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe"}, - {file = "wrapt-1.14.1-cp39-cp39-win32.whl", hash = "sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5"}, - {file = "wrapt-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb"}, - {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, + {file = "wrapt-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ca1cccf838cd28d5a0883b342474c630ac48cac5df0ee6eacc9c7290f76b11c1"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:e826aadda3cae59295b95343db8f3d965fb31059da7de01ee8d1c40a60398b29"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:5fc8e02f5984a55d2c653f5fea93531e9836abbd84342c1d1e17abc4a15084c2"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:96e25c8603a155559231c19c0349245eeb4ac0096fe3c1d0be5c47e075bd4f46"}, + {file = "wrapt-1.15.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:40737a081d7497efea35ab9304b829b857f21558acfc7b3272f908d33b0d9d4c"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:f87ec75864c37c4c6cb908d282e1969e79763e0d9becdfe9fe5473b7bb1e5f09"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1286eb30261894e4c70d124d44b7fd07825340869945c79d05bda53a40caa079"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:493d389a2b63c88ad56cdc35d0fa5752daac56ca755805b1b0c530f785767d5e"}, + {file = "wrapt-1.15.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:58d7a75d731e8c63614222bcb21dd992b4ab01a399f1f09dd82af17bbfc2368a"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21f6d9a0d5b3a207cdf7acf8e58d7d13d463e639f0c7e01d82cdb671e6cb7923"}, + {file = "wrapt-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce42618f67741d4697684e501ef02f29e758a123aa2d669e2d964ff734ee00ee"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41d07d029dd4157ae27beab04d22b8e261eddfc6ecd64ff7000b10dc8b3a5727"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54accd4b8bc202966bafafd16e69da9d5640ff92389d33d28555c5fd4f25ccb7"}, + {file = "wrapt-1.15.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fbfbca668dd15b744418265a9607baa970c347eefd0db6a518aaf0cfbd153c0"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:76e9c727a874b4856d11a32fb0b389afc61ce8aaf281ada613713ddeadd1cfec"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e20076a211cd6f9b44a6be58f7eeafa7ab5720eb796975d0c03f05b47d89eb90"}, + {file = "wrapt-1.15.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a74d56552ddbde46c246b5b89199cb3fd182f9c346c784e1a93e4dc3f5ec9975"}, + {file = "wrapt-1.15.0-cp310-cp310-win32.whl", hash = "sha256:26458da5653aa5b3d8dc8b24192f574a58984c749401f98fff994d41d3f08da1"}, + {file = "wrapt-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:75760a47c06b5974aa5e01949bf7e66d2af4d08cb8c1d6516af5e39595397f5e"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ba1711cda2d30634a7e452fc79eabcadaffedf241ff206db2ee93dd2c89a60e7"}, + {file = "wrapt-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:56374914b132c702aa9aa9959c550004b8847148f95e1b824772d453ac204a72"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a89ce3fd220ff144bd9d54da333ec0de0399b52c9ac3d2ce34b569cf1a5748fb"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bbe623731d03b186b3d6b0d6f51865bf598587c38d6f7b0be2e27414f7f214e"}, + {file = "wrapt-1.15.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3abbe948c3cbde2689370a262a8d04e32ec2dd4f27103669a45c6929bcdbfe7c"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b67b819628e3b748fd3c2192c15fb951f549d0f47c0449af0764d7647302fda3"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7eebcdbe3677e58dd4c0e03b4f2cfa346ed4049687d839adad68cc38bb559c92"}, + {file = "wrapt-1.15.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:74934ebd71950e3db69960a7da29204f89624dde411afbfb3b4858c1409b1e98"}, + {file = "wrapt-1.15.0-cp311-cp311-win32.whl", hash = "sha256:bd84395aab8e4d36263cd1b9308cd504f6cf713b7d6d3ce25ea55670baec5416"}, + {file = "wrapt-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:a487f72a25904e2b4bbc0817ce7a8de94363bd7e79890510174da9d901c38705"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:4ff0d20f2e670800d3ed2b220d40984162089a6e2c9646fdb09b85e6f9a8fc29"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:9ed6aa0726b9b60911f4aed8ec5b8dd7bf3491476015819f56473ffaef8959bd"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:896689fddba4f23ef7c718279e42f8834041a21342d95e56922e1c10c0cc7afb"}, + {file = "wrapt-1.15.0-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:75669d77bb2c071333417617a235324a1618dba66f82a750362eccbe5b61d248"}, + {file = "wrapt-1.15.0-cp35-cp35m-win32.whl", hash = "sha256:fbec11614dba0424ca72f4e8ba3c420dba07b4a7c206c8c8e4e73f2e98f4c559"}, + {file = "wrapt-1.15.0-cp35-cp35m-win_amd64.whl", hash = "sha256:fd69666217b62fa5d7c6aa88e507493a34dec4fa20c5bd925e4bc12fce586639"}, + {file = "wrapt-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b0724f05c396b0a4c36a3226c31648385deb6a65d8992644c12a4963c70326ba"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bbeccb1aa40ab88cd29e6c7d8585582c99548f55f9b2581dfc5ba68c59a85752"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:38adf7198f8f154502883242f9fe7333ab05a5b02de7d83aa2d88ea621f13364"}, + {file = "wrapt-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:578383d740457fa790fdf85e6d346fda1416a40549fe8db08e5e9bd281c6a475"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbb9ff5795cd66f0066bdf5947f170f5d63a9274f99bdbca02fd973adcf2a8"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:af5bd9ccb188f6a5fdda9f1f09d9f4c86cc8a539bd48a0bfdc97723970348418"}, + {file = "wrapt-1.15.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:b56d5519e470d3f2fe4aa7585f0632b060d532d0696c5bdfb5e8319e1d0f69a2"}, + {file = "wrapt-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:77d4c1b881076c3ba173484dfa53d3582c1c8ff1f914c6461ab70c8428b796c1"}, + {file = "wrapt-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:077ff0d1f9d9e4ce6476c1a924a3332452c1406e59d90a2cf24aeb29eeac9420"}, + {file = "wrapt-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5c5aa28df055697d7c37d2099a7bc09f559d5053c3349b1ad0c39000e611d317"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a8564f283394634a7a7054b7983e47dbf39c07712d7b177b37e03f2467a024e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780c82a41dc493b62fc5884fb1d3a3b81106642c5c5c78d6a0d4cbe96d62ba7e"}, + {file = "wrapt-1.15.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e169e957c33576f47e21864cf3fc9ff47c223a4ebca8960079b8bd36cb014fd0"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:b02f21c1e2074943312d03d243ac4388319f2456576b2c6023041c4d57cd7019"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f2e69b3ed24544b0d3dbe2c5c0ba5153ce50dcebb576fdc4696d52aa22db6034"}, + {file = "wrapt-1.15.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d787272ed958a05b2c86311d3a4135d3c2aeea4fc655705f074130aa57d71653"}, + {file = "wrapt-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:02fce1852f755f44f95af51f69d22e45080102e9d00258053b79367d07af39c0"}, + {file = "wrapt-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:abd52a09d03adf9c763d706df707c343293d5d106aea53483e0ec8d9e310ad5e"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdb4f085756c96a3af04e6eca7f08b1345e94b53af8921b25c72f096e704e145"}, + {file = "wrapt-1.15.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:230ae493696a371f1dbffaad3dafbb742a4d27a0afd2b1aecebe52b740167e7f"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63424c681923b9f3bfbc5e3205aafe790904053d42ddcc08542181a30a7a51bd"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6bcbfc99f55655c3d93feb7ef3800bd5bbe963a755687cbf1f490a71fb7794b"}, + {file = "wrapt-1.15.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c99f4309f5145b93eca6e35ac1a988f0dc0a7ccf9ccdcd78d3c0adf57224e62f"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b130fe77361d6771ecf5a219d8e0817d61b236b7d8b37cc045172e574ed219e6"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:96177eb5645b1c6985f5c11d03fc2dbda9ad24ec0f3a46dcce91445747e15094"}, + {file = "wrapt-1.15.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5fe3e099cf07d0fb5a1e23d399e5d4d1ca3e6dfcbe5c8570ccff3e9208274f7"}, + {file = "wrapt-1.15.0-cp38-cp38-win32.whl", hash = "sha256:abd8f36c99512755b8456047b7be10372fca271bf1467a1caa88db991e7c421b"}, + {file = "wrapt-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:b06fa97478a5f478fb05e1980980a7cdf2712015493b44d0c87606c1513ed5b1"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2e51de54d4fb8fb50d6ee8327f9828306a959ae394d3e01a1ba8b2f937747d86"}, + {file = "wrapt-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0970ddb69bba00670e58955f8019bec4a42d1785db3faa043c33d81de2bf843c"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76407ab327158c510f44ded207e2f76b657303e17cb7a572ffe2f5a8a48aa04d"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd525e0e52a5ff16653a3fc9e3dd827981917d34996600bbc34c05d048ca35cc"}, + {file = "wrapt-1.15.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d37ac69edc5614b90516807de32d08cb8e7b12260a285ee330955604ed9dd29"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:078e2a1a86544e644a68422f881c48b84fef6d18f8c7a957ffd3f2e0a74a0d4a"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2cf56d0e237280baed46f0b5316661da892565ff58309d4d2ed7dba763d984b8"}, + {file = "wrapt-1.15.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7dc0713bf81287a00516ef43137273b23ee414fe41a3c14be10dd95ed98a2df9"}, + {file = "wrapt-1.15.0-cp39-cp39-win32.whl", hash = "sha256:46ed616d5fb42f98630ed70c3529541408166c22cdfd4540b88d5f21006b0eff"}, + {file = "wrapt-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:eef4d64c650f33347c1f9266fa5ae001440b232ad9b98f1f43dfe7a79435c0a6"}, + {file = "wrapt-1.15.0-py3-none-any.whl", hash = "sha256:64b1df0f83706b4ef4cfb4fb0e4c2669100fd7ecacfb59e091fad300d4e04640"}, + {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] [[package]] name = "zipp" -version = "3.13.0" +version = "3.15.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "zipp-3.13.0-py3-none-any.whl", hash = "sha256:e8b2a36ea17df80ffe9e2c4fda3f693c3dad6df1697d3cd3af232db680950b0b"}, - {file = "zipp-3.13.0.tar.gz", hash = "sha256:23f70e964bc11a34cef175bc90ba2914e1e4545ea1e3e2f67c079671883f9cb6"}, + {file = "zipp-3.15.0-py3-none-any.whl", hash = "sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556"}, + {file = "zipp-3.15.0.tar.gz", hash = "sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b"}, ] [package.extras] docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] -docs = ["mock", "alabaster", "commonmark", "recommonmark", "Sphinx", "sphinx-rtd-theme", "readthedocs-sphinx-ext", "m2r2", "sphinx-autoapi"] +docs = ["Sphinx", "alabaster", "commonmark", "m2r2", "mock", "readthedocs-sphinx-ext", "recommonmark", "sphinx-autoapi", "sphinx-rtd-theme"] [metadata] lock-version = "2.0" diff --git a/test_keycloak_init.sh b/test_keycloak_init.sh index 07df97d3..7f4abc57 100755 --- a/test_keycloak_init.sh +++ b/test_keycloak_init.sh @@ -13,7 +13,8 @@ function keycloak_stop() { function keycloak_start() { echo "Starting keycloak docker container" - docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -e KC_FEATURES="token-exchange,admin-fine-grained-authz" -p "${KEYCLOAK_PORT}:8080" "${KEYCLOAK_DOCKER_IMAGE}" start-dev + PWD=$(pwd) + docker run -d --name unittest_keycloak -e KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN}" -e KEYCLOAK_ADMIN_PASSWORD="${KEYCLOAK_ADMIN_PASSWORD}" -e KC_FEATURES="token-exchange,admin-fine-grained-authz" -p "${KEYCLOAK_PORT}:8080" -v $PWD/tests/providers:/opt/keycloak/providers "${KEYCLOAK_DOCKER_IMAGE}" start-dev SECONDS=0 until curl --silent --output /dev/null localhost:$KEYCLOAK_PORT; do sleep 5; diff --git a/tests/providers/asm-7.3.1.jar b/tests/providers/asm-7.3.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..8a502662ad9b513c68ca792e9f9eb58d38b47c0f GIT binary patch literal 121836 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

a9^RCI<oKuM?M>wTU^3VmMj>Vo|PLwN{ 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ͨ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 literal 0 HcmV?d00001 diff --git a/tests/providers/asm-tree-7.3.1.jar b/tests/providers/asm-tree-7.3.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..28858f4e2ec280215966c42db1f2c75d400f1f84 GIT binary patch literal 52826 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%0A-El^zIWkGW#c;VcGm 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 z

Wa>O}1x^SEtwT!bUXfM-lh?7YIJMA2-t?X%Dlktsp(I$2uaaj5t5xO`Q1Vg6ua zkYaC|7*F<@7RpW!`$N&54T3qFpfd!@xSP8W-S24$%hYouT2a$Dz|ts&&=*YFL>kue zEp2o{q5rFFoU?^_f~KlzV21J)^>)30I>YNUiwiVA<#H15y3 zy1Y?6!uSVja6WK;)MRAk*W^a0wFGUncIL{;vgo`tTBMk2Pbg$kHe7;Pv1{Qy+TEHh za7#1KN;wiWZCp8oZ~Bboh3fY(RW& zbVg(Z{1&O;)CY>5md+C-26GKThu%y)iG-lcO(9p1HUiv|F%T0zOZJ?IUOC@sAY^EC zVE-vhteEyF*Fh`ITfiOt#plZU2rGdCVR~7Zk`RYD%SGgEtpxyrj6eOu(~>N8^z6*P z3)F|I6yQFBF(G{0V4~6}BNP32lA3Z-`nDFMsi{}&t?!=1mknOw=ML-X-H^3jqx=VX zhG!e4S5LYfw4n``LwX~tB>1s0#}B6r0eLOwg*XUAH)6Yv56sYGxW3D(Hcl=9*Pufr zNN?tf&MGUyl6I=2JCVSNvx_lxd*CJnr;3Q%x_jl6KT}s`k4vO!v z{7+5xD1955hIhLxWQU`1+SwRMS%n_KEna-)YLVSee0A;JuX+a|=4qob$dIUAxfP)V z<#7Tf7A9I8b;bK$K$y_v+AMv2zL?ekZJE@_seqRJaN;ASJj-rja%Ms%W}@R+1XhLI zhHhUb5X(qv01kc1eC3S^sqo)*OxfJR21fkjDx->zDNY+dv40y&IIf1eg0V56$H|9B z@}V{+kIDLA*1zko7~h2T(d%Rde$EFnT)9_b(ax}^W^Zf${ep(0#Q2Hf?pLej&Bt_f z+9AL{!{3V_?lB*rLD77bWL-q7z;Lm9aN@|mL%&z!?{f&?9Rq!O7JXgaW$e6zHx}00 zI<^4&1fDG?BDIOTs`NeSS`NMxqKXONnhRk7+<=-D3&tK&dplQULM^H1djZMBD2!vY z=}G$gM=ot>huOd5yt2blIXEZm7Xzi;ezYF4ify0@{!bOF@O}dQe-g>+re@*_G(1^O zEQ8u=l56LLHTBnxiI*Z=@gu^7Z*6FEf;p_vFid|WClwYJIBCI9xWqRIG&~J$-kN=P z1|fSIgArVcHzcrDlv^k=dh3$tR4$ol;fPDe;}d#I`1cOA9=IACd(nrT=8W}ha(xqR zz3;n6TI6g%J`koz%9|}LXma$@d!M2Xm?2EXa*@Ck8dFmz*NiS917V-H!W6_L%dM@g zZR~6+Y+JcnN`uk>@(|{5xTcI~7D+yi-XcxIk9!f}VJpwsWIJ`+Io1iDDwuPA0^t^e z`1c*LIqRr_tDbYg$NgyCd9!T+m}BJDMha0;TQBRGa5-sJIEts6wB&uIJf{#BeO6zbOsRr$IM1W)Yina-A(7T1dfZLma zZgck)Lf-!9JX^1#WBvWZK?cHjBbwwOZf$r3XL84yzl~6O?V{h9f>EPAE8tQ1R{-6ZpsdQePNy z*btSA8&~9F-jI?5Jmpd+XGmNf$c3OiG;Tgpkr6G$H0V^024LuMGFY0UnHpyhf(X|w zNDw7&J(jYrsrb5!o-r0?nekKY{H^!p17 z%Go%}*gSIM48p;aoY~%bfuF{E-#tUu-vz!E;;E`ihSx*?ol8BBvA!8&`a}f+z`wH+ zlFC08{$z&^i-mell8xxF7bZ4}bqovmbrG<&aXIXO5Um>_DofH}&`&W4UE;5JZ$~{8 z`mmiAnWYi+c@3kt;+~_#b%C9P@%bbc z`a4cBtaJxn7bjR)Hl3>j0^sg{NzWy!t7mKb?dOhwB(PtvmrW^@yFg^vTcgtN8zBdIaQEnzgawZ9@Nn%6J|cDh_j`E0NG)s? z0`Di5b2BHJz&G`nJ`}}+3$1h$=G*(Jdg2#2Kn+Dnt15T(Vq}5AP=qD` z!%tj%8r&X`O%neUHR`liEk9!;Vu&4hHiH;_HY(H6HU-G&(d@$H9hya#McN%CP(H1k)umX&6Li+6A$7gNZ1Ses4t-{c*RwJ+iFYs* z_XlXxGrB>_6PbE5{RYJOsADfv-=tm`?M0S+%?%D)4NwSB8MVcOyOC9{qeC(KrdeuQf3*%C% zEN)+j)vAf!2Dn-zy%?p5Idrr9&a2@1YjbLST>Wt=4P*K+H@hDy=yVM{yPSfvr26!f zfkA3%HHU&>a~t6v+qiI@0*zs4KQD=)(FN`UQNmIAe#@H8h@G0xDormPB7Bq!wYOxb zNVVDAxyZhVK55mlbiSx1w7g$4Xl1)1+By;$=@CuEXjcx7Of=uS{6!6qa)Qdg-}S{m zr{nk|X};j&B>kh$vR|yPUC(DHPM-R zI;VDh(rqjM@cVHrC<{89?gB2nzR=4xd5dW2wU1B{`$IS2SuXVgs`dE55O49wPBG2W zK86UmY`d)Vkz27Me&YUcZrHtxg~IwAIBCcjY5Dak@I{J_+Edd`(GHoD@EX{o(@0~U zq`VC+x+k|V&=nilK1W$B+vWP8Qr~KmrpWGV^t8h0ynqYvJs)+(w~z72J$ zpa07Hy-Oqa6p2`~K$iEmMkDtUsZg^ZByiuMmU)2sBW_5ScY$g!AdwbFc#67P4npxR zj~?(1sRs0trwTmw1A5$nO;c{Gj`;njmTmCD@4o6y&N&#++2&QIV&4Otinym+{}J1A zy1OHD#)nnYbG=vf*sf1LU@;!%mbJv$-ons=V00idgXC%=TYIUe{G$j3hJric^_#D+ zR^ZoQJu_=WIUzh5qGu%bT6sYY<^?m_<iz& zi(kkV!fO`P*;XrDF)FDr#cOI6NN~<-G{RXx{L%eT8muQmJs2Wg<+kVa&0A|i;X1?8 zT3b@=1LG!WnbG}Y!^8kEB661QC~R@^qa=g(MO3t4)dJ0^+m>i9QPr&6h9vcHlOEve zPZ1-zhCTY~%I2^Oc3OBUr&U=1PQ^G%?r&_?VIV6a7sTNqLhk9V91M$*nCpDEB;QB@ zKkVm;WIJxFw)qh~S2n73G=E#joOo&!Uwl{Rt+n=Qgb!Gp1BJmj1fENT4_?S9(L5(n zNIfKfrB8Bv*I~9gJVO7J<%)^b_KDTbiPf%&AIB62*Axfm6bGji2bUCH-Q%3vhkvFw z&^W!xzeh4o`@VjCA`!TIOCxwjBX~t4ct9g~LL<1JoQI76)cpzYM`oq#4b_+5k#g7( zx^>x*t6B7s>J2aOc%pna{c(Soly-*nEfyd3C(&7i82-z7n?I{ZpMn3BQk7{MvOFE{ zm3V4IBm1!B0<8|P{S@bj8t*~zF1UxmA;(4a*8A1_cH5d{b$Cbik_p77~rBSMx-cpC^26<&}|J@OrNG5tUnHaPG+wqHk#RM3N=T*wFFB zLR+k{#^RMyQ@pB0+B2}Sn01Bb6`dpR&;)y~=Z@YpYhR403F$lLz9?};{E^NtmsgK@ zuJXd+J0eg#yRPvB{x|e{8vY~kwWRJ1>Lb%4pD(cYxar(GS3>XsboB4_wjBc14!G0{8#fIJ*d9&nAN+L}4@UqoA81=&*wn`!E^;c$p*cUkhQy(& z;v5&-FkFxfcexPcRh&eWW5t5hV3hB|b%qDbBM~$MYN%NB&#YKvO1N#|@Fx~@=eyPl zy3*lSAL!K+Pb%#~5*d@aP^V=Wpv@9@nxBO5>DbF{(U-JRC#`!Boa=;#RIT820nOUB zzHAfdiY-wSR^!+ik@Zy#`)t}W+HYjM*7@JQ#Pd> zbz0j>EEWb-T0qhmL5X~d&P-I?V97@k1!OuZENRD<(mSi)lt|K2H#=CJYI1A`hj_0` z_~)og;7bKWJy^wQ0Z+>D`VgCp;n$2|*NlPJj3s=cO(?C|)@v|nLfp(DAhB`K_3vKr zU_>aB`PJoaENuUo zxYLNaBl)yipJo|~aAi<@r4>|FffC)XL!=$b2JOsg^?5#ZiBMa1ya$|0!{?xF(*|?$W-W}t*tu5f1}3#e{80`Ibo<3Xk0SSZiXja+bgctg;%8X>v(EAZ+P^Lb zunN1KNTS3VGSGKT9E zIhEjW;nWJV%>jgqAm8Mb0f(9+u+rMTT{fPv>DId0R&naIROQwy<+-rCgQl1U) zdcF_No-rlr`nbHlhUYICub>od7cLok)Oo@|LM-i%aim(E9QexCV-??2uP+Bnov=aw3;hsWAViZs7d=k zw@@XJo=EUc^Y&1vN;%_))XsyfVGLNoM847&R{ml+^97fBu5#q7Q^NM%PB}~AZBA># zz(1O}jzN)DC-KFzF|*6e8?J|c$Bs^EGxEZfTH&pgGR~1QZV52W;+LZ1 zJCp^=N;;XH-cFJ<)g8kBQg@^;(jmjO}*w{)X9A`pFqN!cnU9F*RM@6kOD9Nox zZMCuoXK5K?i-T914;YZ<2f6T6sj!gb9I_Zc)Ur>E^QH>)wgP$$eA7>!F|ovmE746x z-0!IXycVQ%*O?SU&QXHnea{iT9{eb^AR4DZ(IPNku44>Iq|Z zZ{$iuh|mGZClOd^f!95fG)zb`U{uzMPVQfyVR(1yk*ST4f9>#YNH{B?JgzV_fm@RV z#XUD91+_I@kj)iydDo=bJ3Mm~*g5v6!E^tQsREr=Hj6rBNDwdAf6f8{l1OK-vqOjx z=Re0m4IJC2U$78nF>j)47YabOFr%MJ>Y=0dLH!AUqm!5_Ulf`PMz zVJcHF2I(3eeJW!4G2;)&ln16QD0Kr`ADmZ(xt0^gE3_%#wNadjbx+!ZQZ~>5(XB`a z5*{Y&7ZYKOkUg~dgM*fIf?LOIBi!8E$1@Exm>Z+5ZmE8sKsG1ADLwk@>v)iE^){VI zt!EwZm7pT}kewQX5UX>3-MNXLpzwz=u0RfjY&&p}E@Ver!qJ|z;J8^W-0FewNOU4*7X=j(*6JcGsrQk5FUxXj}FcNuhefZw@iWzSN}; z+Kkq>ZxX#bNna_~RlGW7TDBfZF5a;>krq7~F}F5}Y!I87&uU1KSVaj8@4AGh_?d#d zt+;mOus;QGW^cr)Z;rv!wHzp)ZNP`R2Fh%aV!6EhC)n8JICJkt*)PxM}LXZDPrI$ln0=uSX6&y6b1 z_ihGFXX{p$+!Ora{rUp6d|{hD5X_(Xr!FAVm*X@W1&PzXusg2&ErEYRQ|}Ba5`V;} zKl_>^e{L2I%_ShR*2A?SvT}Ufn-aqU1flgCe3w|`$cb2uTn`%r-1$~Wu+#3j`RL#B z`x7q95`2T%b<{|2F~>gwN~?pWp`pFW@~p6%SckyX25 zxY6ni_P)Jdhq%PS0ev1yQXre-n$M$%{<2EN`o&EWnHpiLO%Ji?#T*igEOL&QAdDGh zds>?KJ>kR#30Dy z6SAt{#jez(!w?ShOr}97tvEnhP0T!@tFYn|nxB00gmK^iud7?tOXjE2so0)p|7M;QCkL#+OiXJ$WwR5e6_4VB2!qEKNVM%2hy zM!bk+?&q;!)SQw}GsP1<{;IkdnX9a;@az2wi!N}@UJ<-cb8uWseTo~KUUivnLm&b& zZiOzMSj&N>Mz!|wUCYG2O^R20zgnH&O>isn1y1NaTW+n%?D|HER9eiv%H`C_jgn4y(QfN+K=AGihw?JJ9_f&%OsjY;S zqaNa=W)CVw` z)U`u@+M}e0mYaojMQDfJt;Kz7+H+%3Ip@|72xIpUB7N*{@bq&Ii$zcG&V9n%>}M<< zu*}Sx-fVk{YZq+#=8$#8A!nfz^zF%X4q?5`>8yFl>_KKZRUDjP0f*|pD>p5}+H+t; z!zZE9cy!Tt!W73I-v%b2@%}Wi50{;jF>Dra@n;`^)ZF%vKFA|=z>4mG4{ynl4)e3A zZ^^g}*=qnzx<^c@7mov}Oa81B+szkGM_7hNd%135-tZEFsUZ&N5K z;!c|WX}b8k9-ZF;mfs~4I760WxhO(ZWnxS6NjCjNPUb;6!#jD3x2E%APHhx4H6%t< zMBgb!6@)_=EIYx{bi8av2hPxE8%OODFfmjPN&*PNK!q$ep@k@(AZ%)Dw?=Lv$#DyWG?`#l&E#fpC5CVz+aKRTc289+57Gv8olZuXTA!oWykSA-XCF!c_W@LyaJD?oy< z)(BIkF32$Z%VpxEaSh=x;q9>#=i8KOBxvaP^&|ztT5CrR;%tl7ko6n1pr7FK&WZ0qeGBTGSqp5SBrBoPs z(QszbXofXEBt!8J9hDJBG83fqSSg4J13icH8IuV?3s#3~5cBS{k6kor8om5M^RQHQ zYx$_0GJ{TXlxm0u4U3vpk{8|Z8}p~8uXWQ1i~-$I79sM?_L~zAAN|al$QNCoN03`D zbAM$HRgVeeZf3uN8;Z-f#w7a(im%Y>+2s+A?*#jY?dsWYRX7*1&O4{OyMaKSJbr$z zIRFcBP{XYAzVlYqN;@k(fS_#$wTGWe{%M?G=y_~3vNu1HknyL#(1~ivK-M<~Wn-4A z#Iy(yhJEZB(12B1AHot-!nUm`9}O6^>~P`&2f%C?;t7ow_%tesS>P_aHYka3;sSS~ z9&d}&^V?S!rU7_9F!BU(ZPJ-8v>1y5;hAyPHH#wUm^0Tw4kgvCo2ghgB>i}FySxd% zUV`4;E>0~-BvAJghYebx&Hd+AJ9O`xCjXrezaM%viq;`F)tM*N893FM7}Xg$)tMR98M-v5eo4cK zjb>@xqRQK*`bEhZ?@Oln+2--RH7H`ia;0!*9i(j!b#<&>Or8J&a zm6%+oNdA}TM4a50 z6RPPu@&b@oG5r=Q&*pdia2-JW^O<?#! z!WVSii&&$WZ00x*sEG-8Ry$VZcTC3k2=it&BY@)0M)h}YhAl)^?XujC;o@^AH3Nbj zDx_Y%{#C&jVIx1cz;`;VZBg$$i}kjD68}uNlgQwR*07FkJX*k!oA5Q{g%UCAmiaW( z7<(|QL3oUeUFBV>oOZ75zmCcO}1^DQ=UAk7p{01Hi~Lwa6i>>zZ!CE(_w%Kr;o4 zFcpm)Ge@{|F-h#@vy8)KndNqb!zs@LXF=W6Q~@DlEqGu~@;>#QY%uk7DKS@Y0^B@=Cq_H1%CjI_q|O@5k(uEN$Pg<_KRnZQ z;mffgki^VNI&*em`5Md?Zp&0Tmv_P8H=fB)n_7RW&5*mz;(p4=kTJ-7JQq9>7B?q& z%!F(bWzCCWiq=f^q{6jGt>-q!&ay~tB5{G%F=sHNojSIS=?G%a2ycin2j`vV3(B!o zZ|KWnzY=EcuVuDbpFW{+ujZMdCsf9dLoePvmF z)N~^X~kJrf-xxp^@c( ze0^f&lj8{?xWbo|XP7&zz&X3WIpy*mdgA#h{dnusrriXVRWu&SDV^5>|oO13}Vzjn^q zVaKIL2mkQz6MyPrW3OD(_8Zfefc3VymiULg_4c^_nmu2VAECcXR!R!_U7H=L%7G&`*<&>Q|kSvnEjlB_urKSNE&-`R4!t#zgM#0VOJQAl&PBeU~ zP6M2&l(C>EwGkXbUm{d)DKt7|*#Z4Y#LM_pjRUxb_J(g;`(?CpWh$Mdc19)AJV=PGmNZLO|fqdq1o zuny>lt(OF|w^{NHWvBq=k}P~8ZA-HbVg&`(2Kw8o+HBiIuex3y0BC{IOw;UU91x6g z(|YZ;5KAWi0`{Se2LjoJ`CNQM@Yu&i#ok>*P{K}QOrR=e>`Kr_EMCS~u>M?>1l=aZ zUnN!_t^oj=;UqO2iiC;uV*GKed|xF;Jt9imGbJU18?L;xV>I1)vCSc$Sv}@}Tqup# z@RO}}q*SK#n)|z>C#JfrLOl_eGhdZ^IaZLj!sJ89zWHOw2#z|9U>#~p7tw7HpkDjeVK7nc(jE(RoGPX_c zEL}rBp?Y?`6E`ddBA=c){N}v_x$L{9b(szg9#d`WqbIx({I|}FK5>W~yQienh;ML@ zBDp4hA#V@Nbu5BHKJRGv$E>~MpV@hFvocfuk2cec$O^p&9+?J@;ZqK8NhfWes6KvH zesNe2!FwlhBqN_vWSN4|11J7NbUrhz{^+464#tu122LUGU79A}#M=C1GsCCg1Prwg zP) z_an{< z3=*Cjg$(tqhyqXJnlTBT;6-<^jg?5_W`4ssvHT43mB~#o(mK9NOrLw za9oNl#JXnrT075G#5gOut!$CztgH0XR55X`R`NxL+#$bmMyfL0nq+nudaO@@8PSQQP)`AhYcv z*$mYNzmkcLoNU7$E-hsI2757&sX<(#sdiN0zLMu86{yX;C(8R(U~=A_My zk2N!#Teq)be|xz?FkE7T)BW>NJRQ*E8vuP;SE6+w@x1AVeD6zEkj7DGr{j4ef7kpn+$VV_D(5g%X31{ql) zpN0o@|H!zq;F9_muO&IZWXls>jj+0!L>?*XcgD&bq3Z1!QPvHzKh^_DKjuOZ%oA}Q zoD<6r;(xS-UM;nd2%JDbBeDNGZ6V$NSzxE)=IH3)?D{`-b~%b&2!EL1_#XY};Cw)X zLL)N5!9Z1zMaW*7Luk}2tKtLR*dYW0;)FwBprO<6Tw8B#^Bi6$(fgt7gc(_xC;y0w zzmSPgAb6itFWAm#SeF&=Gt9D@ToMUWB1;T3OPaJYpt&VOZzEpZjgu^WQnm1+mVWKk z9IlkJ>2sB|Edcs(>ZF|pq>l~1sR%GL5OK>sr{M}NuI`v29f`M*_Nvi{dIBVuW0Vk2a4@8D|W zYUN-rWNPH-`hN=%W7Kz5P&LrL?NRh1gqR*gs)p5Qg@KBeKZ>Z(KvdBha56FKFDN=F zqO-PLn<9;?7BhHWI^wQW2tT$eSQlI`Wc~TvO8r&KPIu_Ceo-Z*@lS5M^xbyta-Syp ze}7$j0G-|i87(!sIMMb@u+E_7#@dt!cVC@}^Cg^Sj3$*@ac36koS3T9@@%R1W65(k zR8W>aCEjQzW0`U_vW{h_)f6(jCd>E;(calgvrc3O(z9OGMO?F*a};1h%(7hdRAzOu z&M>BV~B zNLA8eEqIvERP^Q!qt|nCiW+OX(SN0$ZG+I_tdddw zUtA3z9;% zKqnK;R%aS?EZoO4{VmT*ajo0YFK2OL6^1GHjP5h#XgO1D8J4Sd*;A@6+Oo++nHdDv z7n=nq9m^}lB83s#yY)vH3)6`=Xz;fL+YG+7tBN|~hA!VccZ(K)qYZj_2Y1FfaDU>a z=DCm7S->N!UT14uN9(6ks}N)&HS44VP=k5w`aIvyMm*`HKhVRJdJmpsxX3K;#o6P9 zG+^BOytEa#CoO%~wHIyuK53$cU6H`Zxuj)Lkb%Cz4>PLR5Re>#*UB1kbriu-P6{W-3 zBuj<-SePoSt6q5~&H8c>T|p&(Axvdf&{DTLiI4q+Zm!*65))0Kg$OjQ4E-v1U6xOQ zuB3~%Qc)zDoTZYzbd%eCTN{?Vwi)lBg4UhihP~91J>g1`_Be;*8Lr)9al>{ZxZl`0wFR(oUL^>%$p(y+YOV{?O z+_ic5r)A>T$+$GjOEXbNu|Jh9@B+5sh?{{T$=LZGnX@>+mrqa}#%fQa5eGwK2ES*A zWne%Va#gTgl?sqtjFhmP63QG$LdEy1qs?G$yCjv{_l$G@h+(p#F?3;--4#U3pw)JA z$rx=7BZbE4O0gXuK!sXiZljs&xpogfmz>4WoctjDy5!<}pk(+CQs{j7ntNmB;8Pyn z?nI_6N9$*&nd9o<47_!O+)eTpG)5LA4&`Kva&zJEap9n{P|9oyRG?N!7ZJJa_=&m` z9f$OB>70oH^B?}2@8f4iy@@sG?L6|CNUD zAqP!vpz<~eV53horl8nZOdcih4Ew&$+##tavl~|{__?x)l+MNDe=IgjY38$cEZjZ4 zeSQDS??)lNeLEPhx^&vKa#l$NRxy6*+7e3_g&S~o6()ZBSn)BDkW*b<`>F~alAyfLu+TU@`R74re=VI@o_fPd$QOOfpiJg~27FWz0SBphLeHt- zf2g_KDCnLC90-UK_J7y3nEnscEb^b<0Q@gjCS(4ejVmU8QyP>RDeT9}M(5I+pAflQ zv0O~Irxr>X9p(}^1!kvY^M8=`PQjT)-@12f+kC?g-WVO*PCB;Lv2EK%$F`G>ZQHh; ze7*O-&c%0e&Z%9e*1TDFvudvSjIri?eq*c@SL2TsN5mc`?*t?72!|(DVFyG|8Z*D9 ze`f-IUo+N#G}nogTF&1(S5OJRlW28I*nW1a#_L^=-4qjr16HjffBu*}a{(18{b=d- zqceEQ>|8~E4v6bX71QBIlcVi*HMHbRETEVDcMipmW^YCU&7E8W)js!CrdZd-8nN5- zSRBd&H?gf)SKkrTB(>iHC7VoLfqSUkPn{gs4?}9!K}%}=q5pnqW*Jo3stjR?(s=`) zReSf#v(Nk_?!7OfUxFWa81Y+i*+!BYnl@$lb3KI>y8yF}8TUj%MAjhbr*DKVc}$UH zUBb04Us%5c73~#)Ttrdk>`lE(Ywzawp* zQL&W$zbG5^uMWrmzP!0ZB{;`{uPrj1T{vu4#&dkaHV z@;29V_w$h~Gwkg2_x+SrV7{c##FBc|wR(-$Gd^0`&ZD2LO z-J;iRys=rN)m>9nxztBm)e%TkV-3?xDZ~U#(wDYXc$ntA>&A&%#Dv!urmHMHgZS)BZ# z0?%MZ4NG3NwvB5+V0=VRx)9N3GJm0aakg2#YcZAlJ*5TJVKjI(m;OC9crSc#wzbJp zE{HoNMDt9j4zu@-9^h{>0N#hf3Pg8Wv(`46c!aRfPnDp<;xI-WJf z>6G*(P0eXlJDzUwL~+KM1la;kM6&A4$Hbi-Agfd$O(D&+eA9TWxKcRHZ9);hxYDuT z9OoPi)CYoh7K3ufpLKpN6T`q^c^ymShB)#M6cR{~Wbh(5#IL_QaFTtJ>2#@y1SllJ zmIxFFbaVQ<7?~T!X6g9FLUr&Fc@PGbvZa3-hxG5yM1ZK8tyO#r86*`Bf9+v3^9#89 zA?FP!$8XX&7lT$AW(<&4wUq`V18*sn8o*;*dtrg|h;4DHq7x|P`8&HyJY+c5i1Wos zPeHYnwe9WC-2@WTLt8osbXuqx@^WGcH!Y2W=wIz|J-T?+{CN3Q_|XfYS+#x>UBFM; zLIc$CePf8!jT9w2+fq{|SoH$`7i(@CiPy^doXXsVc)`k>jiTBpXnJv+ZE4Wtx}oM2 z1%SbPuM|Y??ZTA=9QlE9F{eQjyc!ddYsFg3;O@v#Df&{;N~qA4pYkZ5P<+VCn_WQ} z@)3Kh0l2l3AiY;8j(Pl5zxut?(UQ7*{Tw?x*Z*(9ql{pk}!SV3)8Bd){in!vo1R#Lr-xc%rG$zjgg#}|WY7Kc zbkU?z&hwRrGZ$9dFHTQ=^ZRe4+YVCwn1~9m2#)TGCtUO2voo;gI3zJI zA$#JP<|7!lrf&8jjJ2||l}6pRfjYUjErATc`^O&L6rgI4mQRBCeTv8`&Xrm*D!Zme zvWzBqY#^0oSA}04kPAfbfaN%bIH1B4a<+-4gu(oaid6y{Z@Modxu3O&R_yB1Zx`%P z6RyO24Sgt^LIp&4F*DYg^TW0;ZkUsCQt!5H4R$T_%e4aO_!xFTbx<7_{pdraqP_f@ zL-gD!p-cOTLRk{cRc4Lbd&_{m0R>%2T{%b2vC&_#1@)|y@@qSiK}fL&l+OgHF}#yB z5f;T^$eKNK7!ohl{)~Q+WdAwx{L2;xQiJDQFbG}A43Zqtr$U@U_e-BnB$Hpzu!K!$ z{Va2RFDY$K2)QFaQ6~&3R;hi_2v5U@+1;^kAI3!Lv)<^{lqg+F4)i2)1>G34>WRSa z&hG;PcSrfgsyXhPf{lsHH_~t%cADhiZC`e_`3MS2m6MFOQ7|aydkHZV)3-+$hswSh zW>B*)T%^>%Sxd*z^M?sz<7_{HaNy*DmzF1wOW(ciOPA(8a&c6`C}X&=P`8xOCxQCbWC%;lv5OsmwR^X`m*sw3RsG;FLMyI1x>o z&F#PJjlw0eYsF^)CN2WO@&PsqC&Ea~oIBLmM|rWV)XC(%q}H#^mJ*51#h`$)m4(}j za}i$_S3GVUqm(DrSr`p9z^u5K^xoq>C)V|g5kZMWLdpzfI}`G~hI!(9`IZupRuQw^ z4oz;Cn{H(TONy6(2}J`p!bt{(ep@Z>l(xy~R(Gy0++kXgiWJh!-MfJY%8qPVo8BRf zbR)fNi{%u0gHuc6g}@QM6qVhGR~PpjHi;^9!lBYEC(6W@ZU`-fA-TE%vow$e3|9CLMYeI`K^>MSahdx-3ehKm~4`yv!-z zSeSc|k}CbC?C1np!Xd!qu%xvPFSV8^#X*vbgjR}nTt_|d#|vF_`#I;L8kL?EwN`CI z17FpO`$B|%0ZnuY^u+nf!+15E4bq@c5`fdLDk2{3+5@{FHFiC zA9YAY+`K0Q|0C<{#pnp^i!=J3xMecDB>kA_sgM;qNAR5zRfk~B6w83?6po|*kz}dO z4t>cnU@`+}*Ulu;YTlq=#a3;=rqJmMSWsrTMqpBuT**ZVmkqHbqEe#_vn7O7k0`NF zwdDI%;z@Zg*s!igEp42@d8~W(7eZ?jrf9k+O>weY)^s z+VEFO*Vp7^?~of!8=$-++j&<+wUOTNKyA^s6UMc6uR9W0(o|K=rOm&%jTcE7rYa@Q zb^1FxxCKhm_;lV|_u23cIkO*CO4;43mLX?npfVRWHGxR0(7vr=Lbw(+;tekG2~ISH z<>yoBI0yc*F~lqMi?9^*t@uF9XgNBgc$$7`vI^eAGmoX&H&1Z5UT?Z?&y<_2GM$G@ zTrFG>xiIW!wA%`5io1Vkm*sFUxPup1Z@;+jxhArzn zz~q}LK+ZDunJr%API+S-{W<-xMP|*tncn*Hmr<$6vc!w=p5-4A*81A=*ks?BOP+Hz z2MGM~qnc{nMq|OrAvU?r=*QZ?N4uZ93DWfWkRCIg%CrA27C0jFoqN^OimdQmTJT(u zg><{d?h&9DjoJb&H%M8eibjT`v(N0Y%cP3;exyw*o)Ii*4-lPpEWYT)ivZ-2*Ou9w zab5TOEjP%IxQagO@QIb>krP$p-ur3ioFrF=EDkY?lG8nBB!_}YdxgfdS}R53Ks8;ct4rb4S1w$r9UC%yke&3%innA`;T>M z3H9=lib*ateT{!6CpH#}gR;Jj@QX|OjGFw|zg38u^dQv}=DJbE?j9Nr?&d3ihfd*7 zEU|zcYKbEmpZmD&_6ffk1*33B^Mh_Lzo=( zicsrUaIVQN|E(kQy^s|Q=4+MB5souluwMHO`k%7~YQn7y);MF2Z$6(vtS3D~4jd~$ zrc2{p*il$!M5}1yAAvc&@JhREAr^Tziz3n8BL(MyfL#<^hWODIu)_D&at%zlJIUHS zFguk5%yqOeD!5c><8Z-;Q0(!IG1q*IvzY69BH1df=6pt?K@_GWHw>csM7x9xodXw? z&Z>m*Ia@9$9X{(YcJ>gD^BF(C+7I;=Pu=uIk=W5WYSYnSSKx=aD)rVe8La!8)oT~puFp%wXRPPOPY{Rge+9jM! zQ3$KLoDD5ySgn2`Xdf2OB`a-8-88L3ov$KlFks!?Pk6#1eAO$`Ef|j@buM~=jUXU` zgDR5WoFGlMPO*&rEnF#`niwDjnQXVUHBGoD0w%?^Qa{ri#51_IgOzOfMPi>&y0;Fs zn(bj@kThajr$D!7Pj%mADjcY#(w{D40o&Kghfz^ZSG0;3lwg!Td1fbV;IR* zu|GB$?N{rr_|D-HZ4l%so#0vF^cNAFVz+FiefXiF%^G>w!_x)IIckiwIG=N8Czf?| zaFn;`A4{D*l{)_gJdUfzMO~TpZQYKf=+~cntRZgPH1wq;Bw6tcm^{T`W5w1ylxLz@ zDRBiH;UtAsg;K4mkkKS2C`qKDO4`=)$Fn3t3`}AQYnk*V=|akK5Tpww8`i*B*g5T{ z=@s$}k+kfUbPNX$WvJ}~tVxwsg%Q$io|Sf>vRNhzw9Ir4$-SP=nqOp7cNx9}h&X)`z2pD0&j;^y(P_R|H_ zq35R3!xwjvVV>8!Mrd)GK*BvZK1JAEbce`_9q-guB?a#a z#harzoooi7@z-QAo9T0jrnW)0J#T$6wi?Y*es{A zk2Td@j+-9OLB6LR4pJGs8H~`E^a3^XDrENI%{?rvnaRE5GfdjmAB>T2NeN9k%t9@# zsviNrTInx;gn8rE2ya}Wx&Ea{@<3V5c?Rw=xU144#jSl5`IO|XtxiJw66mN&4-%=g zBGYW#`D;1RQtNz-4M0Eqp=h0So14V#RT3Pu!*b4;;4k$co&#ZAJr2pfOOxjrD^e#+ zHVi1=Lcq=4*bAc-&B?<{yWv^fe?&Q_{F6(o`oY+XCVzkK+z7w9$C1@@iPCd8a6;8J zMO3A7hIdO4`@(w&9=`Hau3yk?;p5oo1K;cG|4W_rYsOWreuz&v{Wy~0Hesc|DH*}3 z21OX-3u^e%uI%J85gjR+2^VDN%-~R!^fW|o_eWn7?0W{?^3J^wiJsrIOuuBqyEk6(8~-GZ@GE!2 z5$3&Co(?DZ?bM4(J@?aKW0Ob13qOv&`j9}5NKObH>!g& zlr6m8@N;!g^r4c0b^6Q7$kivf3G8Y{H|0&z-u^LY#{s)bWO$)9%#)Fjdggk>m=UZ|hm_l{I|1`K22K_`)f#lvz^7<`{ zT}kZ|KwMq^SbZAQgpBFA?E2SYS=goS8DZ7#5YsbIXXeC)_R8;QbLY{7z~7LIw1D!4 zvyfl>sl~xJhp`{O(4FrShSS>vyYUbi-R{d4=hWuTWJYNj-+20HwtIpcsFS%rV*kQS zyn&o}@0HFKgS%Oq9poJIyn)mT9G(T;zVwr}7|lkq=6^FJxkk~(8X0GNI_*Z#Go|_< zp_zce#rS&<2*8#|^92}mORTwvJ3V8;UfNLmyGp2B_l@2VRr>k<)pR4C@{7%bcgia^7vXPy zOJKEg-)2#}{4s9mPM$o!em>I764b=5D-q12gwQ4VLgOS587vLi(GG#Gx2_iNGa*s~ zE_rZ}n?^HbjNB`H3C#4BOZKGy3qJYzfWIa$`f$r9 z?dyi%J-d`ZfXXsWkxoO}gUp$jmM%ImL1}t~!a^0tAe9d@UK&On3YP^<$^$z!AeE_V zQl&| z!rq9kT}AX&v8J~CY7^YWRJN|kp!F&mdGN21PIU zm|kXS#=(<(5$h}a*e%`zW6{P=MNSpybszdpTEZF1b6%ozb7(uVmM*A!mz@>0xx=38 z3HZf@+xMQ!z?o5qyw07%PScQ9=t@SJo&_ElJ~|&ZIxo3l=7-H8$hQ=~Um6O#0y1nA zwy*`mwgS=A3K`(WUgn!=+!~-SpC?NzVS&qw(7TbQrNE-T9niiD-7+#Mg`c?X#vEkl z=S5qi*ef(LE(YCia@D|80l%3*SOw2(8rOSaJE2HK^_vzlM2=uJyiIbs;OWSHVEOe= zC%is{uEdcSYWh7=TEI^O23>@EyOPN&74?%J0KR{pls#t1UTaY&=Us(3)EoIiKdrW5 z9&xq^KRd5SOun3+7`EKWkR&^w7jCxtOt^nembP5h1)1Cv3Kc(v+!GqrdsyP9LI8i^ zq5GfHwF%GJAj)=EPT znX?cN8oUGr85T(h-`R{w+ho5op3uKVVdx zdVvRurJbx~P?OWzVz;&^xIYC8N$bB~fDFajB!Ogs>*|oNgT(zq0S`gL#~QW) z^yTBf#Q$b&s-nZJAWz1EMa?&;Ls(ZKr0$YY02>F(eepxQVjUMIu| z6VHd7o7}V2*ZsE*h#T>XkB!|zE`ve5-Jkj(o=Q{1|GcU(@s=(1g=Uta&1LmBaen=FDRBef{-NC%yyF zdqS);IHdCEWnSqd_+<3)Luq_6he(IW zZCa+(HShSwn^d}mAkcs$wnDV#)X7s_9GA;LwOq1$P95hEMZq9*LKIbz;d>c!%3zRN z4^ZL`WPLDpAQ(>LS&S`h@YDs?ch=FYQ6=l8RW!3iPVJ|qX8QVIUoHH;V!BCK@NesP zl3%mmmL^OU;m@fc+J!E`5ig|@@K1x89|Wk=!sV;U__He-H@sIlpx^mw-7;HHK-LoU zFgA!$eRm)Qb}UO!5+JdgV~XY^%)S*Peb`?T=v)Nvwuu%xIwjqzY^%)rJRqLs9h?bY zS%wyv(A2u3THnQCo9-uzA&DVixb7_ufJ!$*6f2V&INll}eRAhK*nu5{E2WW)<%?vA z5KIQ*76fpXH(Ki|T=fe6A~(9(XPeVE6#&(ZT$a@3z;WhonGom%b(LOD9Q;NZ-$|}_ zw5YCVx&yhK;LW-7j>F8G^ibyd?$Y?$+%H>&wtejQJP~(Sk??KB9$HUBvikHS9BlgNT&5v8w0!?)=$YkCN?-u-clRaQ>tW3U(|%?JJ< z+a>4;_xg@~xcVX;-U)t9(j|ET4fF1qLvz+Y+zJ*F_9I{YV?eB9 zATL`oy|&%jD1a$8ElO`**E&E{lyguR8ncM5yZL;!*ykeeQ783#Ca57>o1a+ktPv-Xl8Q+$da zyhH-mI^}a14+{5Oi}i`i9Vp(I^nN(^ap2kvy8H!H;{iz=5C+Qf$VhoDgw$>HsoNUR zH35klfs|3TEYf%zjE7M)N@XBy%o5<%5Pb0ZEPr@H^9fsZ)(p`ym6q;zuzmeT_e32C z7}t~7#`~DZm6rbH)bk-pXA&B0k|wPYww^MmHU_lCDu@ck7iFEv=yuMQPAK0UJENS%Je&~a#cJ(hDl^Eaoh__9$ z&CXc-&MDR$8ui{U>ZUJl$ShtwUAUIEC~Nw57Q%lE5refS!O>Z-{U&di;D?HaxUBq{ z`*|@7dHub2^dky+MU8z6c?aJ~97_Z&!X$BwIKP)D(V`@G7AX5ssz5q5Nuy?q|8iE1 zs5w1B4@z=fp^jOyfjPH8mQTe^QYTE}bu4+FO0A{1faOzD7$n65$83afOcaU!4FlL+ zxy)o(E}u}+p!L#CvD{Z?3D7K)_r}%kb&rSV=hNR48gy4gcdA+Rzt;8l ztwRS_ks=%5>8IN1r)*j0@+sW!@4)L{rlyBVz+MSoSl?5pAZ3pNatE zpy?zZ42#gm&#VKJLA9t}n&`MY=?bMb{)3v4h^J`q|1}KZPp4`6X#O1IN*rU=K{+7e zH}=vb*!eJ`GkgfMcS}@#0D$l*l?;uSaYfnSflZD!3P)1P``<@W3e@j1)c?-Ypprx% zGIbI;z9;VymhnuOJ#!x?{(*IUvHUC-rIXK=V^KNZtX3kFKe&hZm<3rFmLyU zTlojJQhdFN%YkKL=DY7(8ux3x6%VC|9%DI0P+`f!j*+aIzmRK-t?C_6^0QC74wsbL zMNRwMmui)ZR|E5n9d?N>9PAd)h7~R-uI8aP4pl4`u&vT+9*qoNINJ)O(cgzv20G`_ zdECr9=}Z2}Zj1fSiruf2(+-_Qc)D7Es4e6er!A2S_Bjxm07*|D?Dm#i= zF37svsz1)q`aKAYEo+2}pUn2|7#mHt$$sCl%Peg98(!*g%Dw+gvbYv5dh&NBVvnbl zXMkm+hLT0V=29ZN$8@n(Oy^8^p()%`9c{KGZ+kq%})Vk`0MS8ia z$2RW4p*cZOagy<4+A|ch7r<0`bBUptw$ zC?7aue}y3eO&FvHH!j@?!^j-8U=ANf@i`Hv?z2!)4&vjRo%y08Np)<{k4Htjz#Mp; zHyk$^VtDxzA2+dF5XSSwT~%+g&T9F9mIpAmUyMPqk02k)i~)=fWlEAiB~KoK>LM0K z#-QmKy3$QinG;VC_E@Sc3~o^g)0~Sefg=i2C|ZN74)ERN5vO+y(rh;|LUZ>))VvwoU4ZS_K8B@?s0v`)*x$X0y-Q-dI!f^1btqcnGJ`_Nmb}(n^ne=RSE!I3bX4V zmx?B+EsPr2dKXSaT`XTLFLrLi&AS;6aQPCvFWK8)?%fv*?fep{$10p7 z+H*a|jG63{qz6Be%&E>{z1iIJ*>;we`%t`jjn7N^)Vr{T&hhCPvk(ghI)tYzZR`>`JXkp?07zxw>8yj%@6LGXXR(c%sEHFtX|BM;rRo$pCIrGcH2EpvU}7l|3UJ{Vk}f!c`&Wz@PxSY_B;nL(i)0m~faV$9IhD%42VhpqwfI%?*O;3g)t*J6Uf5`Nv#=>=y(JjO7qX`3# zOb8cI262QXF0_KOPkNhL256wXYYO3@_A9yBf{gtI8w}%fa6?Eb?gI8(MdH$!He#KV zQZQA=X^LF-c>UHhGfoyJ0sw7?9JZU7y4OxJqs-u~y(s)uooozGo6W&n6ajGd;mdHU z6$KFlg)UF}5}MtyX(7)jW@)_il9_HlW*d>fmy5U?4sLaq&Z?Krb(Q)NY+1|)ks{)4 z0}u>ooVSjQi7DQWo-wy*K3wE8$4X0zSL8oqtF_M&J9?kpl7B)H4e8YD(e10U*6b2p zq3hjwE>*!DE=0vZzlc=NtY)6oKMOUfFgl7maelCzEvzSDpQq>~c`#Z^NKflca)uV$ zU?K{7MOh}sDHe7PT^54U&Bt^6z`UAy&Ar^6a9d8w4V>CPb_2}EjWfESxIi#U!s!9C z(67jS4s49V5m0f$5t#Ghk3*DOE?+=eW56O3$VS=Co0YY>*uXJoJHMSD$ay4M zLaX+JJH>_a_L+0pix#87Hi01)rTT@HSIGSvsH9h=>be91qPlG=Le!U7 z;iEIm>~al#>d`r6P%91){>UW2i~Q68eG^Ex2xV);n#am3-AGjJb4 zPM(qS#L&|KFL57AuNKTnl+=k-l%{yCb5M&5M<#pC1%DA3D;o12V@0)<_LlKsq>&nDc&5@NvT!}cFF~c)BQi$(QWu<+27zDbXAP+%>?#SG)aqm zoCvf$i3IkbJ0&fFbu?=d`s4*6!NK(dcAb@r5bpJ3CpR`s6H*Q3h64uAe4dnJwt^Kc%R->(Ds}E&=d@bK zI30}>lhdLk&81kcD*Q~|2dtIMbjY?EPqCy3EwcNS#x{<`ERv~nB+h8xko*kOvarzz zT;x(^aF@I5(CbAN!Hp5c2X@%IYf`C67w4H3aoR(M&JNFHxJC>ff}kPzTnQSs;@Xtw z65%iBuSC=PYXjXC5g_9Sm|*?j)^!i0amk0-euSZ)q+CiAA;hs;-EUly$a#((<036B z$jpoQTlJgv(!gz2?|zAfL>{+Ik~?P9;f#)Mk-ebyvx&>t4=c(Vh?}#7uSuLu_@+1RR##oa22l1#WBlreUocpm#v{T)h(kr z8udgO1efCE_eUN7@y~Bj+;|h@k4@5c_os|F^W>Ztc~;(|${5n_pj9{}AA8fB2c5D$ zJ&gugCSY=s!zyy;!0>mR$cC%Q0cPh5xk_D z5iOJBa?-x6rVLl_R<%1??-LbcxlzAl+f~shFF900=_;xoBc?;nepqOlmD^~*LAl?& z`LCy4YY4v6)B$ThnqVhsY!%(spUBVKbUb?%dPakuBEK5P#&|7$2UZP8gIM_SJxEg6 z$i-MQ6fj4)JL-StrsN==S9HXqRv#Nks9*(qX|lG-T1M`->lo3~QswLQ5Ho}V0I1Af zOyh>Z%4V%HPJo)MFVM*h?-B9UhOx@A1M8A$?6jJ&XpAX{xtxJAYp)Vmaxe<0k#ldC zbyQC7`7w4CJ$9(HIFq9a&)q_pc%ZMj9DK^2l4r$}G-pBso3Tu*5?#5Q#$_+K2tnpF z@^ZpRdVUh;%OJ5PwhfX4*3T^Ix?7_6x7uyx8&&8w$2+{1aRbj*OYm5Dt|bn7Mr=zz zch@Fi*c|e;FtXdOX!9h+CE+P zzH0Agf;MKNlB7A{OGfW_)(v;KhEKqd=ZjQsbJ*6S%Z~V`8WZQdd$BJhqc`ex;qnPM z^?r^BQT}x;FT=J;=Up5y7-n6Uz;4^jKI|#MJ)4r~Z?{O8cQEgT_=SmHVW@W?&U*?& z*EN$wd&32)4iWzj&pYhDOUaRXJ#C`Bpw`fvSW?;BX+=Jx))P~H)m_RU3-~vpjXe0Yy`U)kS~sQ)^4p%b!S=8l@5PV(;t4m*EN#>G z@ia!^^ROfXz!*@Hq2FQDPMR+9JVkJ|2Bvmy55=LDlEGSms2R%R%lil3Y4@$?tf%E`#%)j;!(f8~ zCHg_y4rq_MFWv2EizbIkMUDw|OFhPJo#pfQFYXMM!%u(AmA@;9g}{ zWsu@-6q^CnA?&hGZOsHDbkumh%_J4>=Yofar?X?5^WNIjTgo_0|EsYrRTZ;LPiMsH z-+WW{2fTi3fCnTfF5!uFAoL~}H|~dMgW)NHPW(wa@Ag4N(t4;5Y*(vb^J{(PZye@$ zmUPQ5OZ6hR2ioL`989Tn%_^)OiMB_S_%gwbgq>j@_W9;_Wn_SGJT@}nKDG`MWYH03V`!N$^VHcZKMPbr zTtR3aG}HuChrj5k5eS2z@ML|=eMoLnKZ;I!i}D(!c^C#Xem;CghV!j+3Mjf5b>IiuBD5*m z8?D4h^>~X(^oFuK?Q-KJ1cgwqr7nm}Qz3i^T#E}KOq(Ozxc*4u56iUuLU^s!DC=i= z!*%ufYjo_Xd8f=eCWm#peRyplY%X)hpO$JXfJ2DSDR^)~JP#>ibFO~G^-OA*BsM%E zPdm`f=M`Ks!*s;k8)y{eh;nZa9PRy=LE|Esf)h;G->C2C=LEKn^pZhKM)I(3S?A;!S(u84UD!~e(JX#D`87R?TxYqUX)o^e!(k=kubO=O`>3)aZ4r5L>{pVX(hwl7H2L z;{>95hmH%f`8Nvyf&y8ggx}?eeCvK^$Mz1)Z`mYuItqXVLadtZ@P3{q62y(@>klRr zMC>J~MoO1st6}_NN~^EG$SeS6`WH5K7S~obrU`1|5*iCNFs^Xs)1o0k;NR9Yu(nPu z6PVv{8lWI42L8fKRl!tEVJNFEH3qH_%6*j#Ry&tM1=}_TSmkc0Vfm9hw_cDX{rVJgl!FWBbt_6P{XpP5r+Hg zYbjL4lL6@mW9IaQLG1fYD2{_G>7JU%){h6CvIxcN$IR1L+0$td#Xh3HZZ6t5`3Ltb z0&$kD5@~%QpbrP19(rJ_(v~i)BI!sRs24u*asEK9Ye}&;zTH#{^7MPp)zwfh%;AZZ zi+g6va+c!a2DYv?)5ABSQf}$$l!#I6UI+kD(#Q2xY_sf{*o~fA@dlXNUxf6u(C6ks zTcc8bRu4h47|5YzA-1k1)#z1W=N^2xOa9G|^Cz-^Yn;`PTZbb-1J@K8=57NGjMs|< z2jAupeZY^HZpA|+06{1|O7Z!bkc1{VC9Ec%T7zQ63G%qOdJ>C~&lz8P5Yv!P!fn=( zM?jmP&qODftKW~TS~k0^jd?5jL4kkvS%tb_53NEeh-oj6T0aCZ-sBki#8(dQJ(*t- zQ8Ly*JE`4(RMuA^n==iDhtub2VmKksEsa2GrG{1O)y9?yl4OPZ)bemrG2pydm+;v0 zdjpu~+0Pq5MQa5bURogn)nn2g?WycNx|uw2wcmZ~A6oK!Y-~@zXGz12tRzD+6{`Zx zdvO4wT|WYCzBzvS!s7+WVJGtO{^)^hPhd_32%-h)05!)P2AtedS_VeQTdvQFSl?y1 zpvfT~rUTDM0St(`b20G)8Gb$a+Hu^^4-io)9T*Jc>^jw;8yNvC@+Ka^&UIi@KbT5U z*0IZ9{t8IVCH(4VleELMw(sRum)G>HI(;@#M?uXWtmK5&W@11BElezc@(NpN6@!F5 z%nyq~*S6#3B1hubi7}L@lRXL)6N1nI5RkfycqU!ZIG1x(4E(^N|C)Ok>%O-U4E6uA>?6RJ9AsBU|teQ7K^E_Od`o7JMsc^!^%hYxI z0MW78Rr?03Q`Ykf>iI#mWrXVxiE~M+JZ@Rhk`A%CsinEV`~{y@^V7iIC6m&}ug<|h zM+7?7U3pZ~7_-n#_ZXOl&z^AZ8jKa*dhHxY*o(N7|XYR?-t^2=zvLAx}Nh4{e zDErwr91*&njgwUJIU?|uXqwez4vthG=!eWX1?!VTJ(8jP;tdAXeq8Ffhlh??H+l&D zyjH-r7>!5Yxcnnka<<*vgHk{MTlbNR zFXIoiB5b3m!dSzMCLjDu9A9t$s6^ds2n6x~rkQ%rSV|X=fi)BtTU{_X_Bcv)j!6NYI5gdpjd^4UIGt9^sGwpMO%-jGuBxW4>XiN+M-5Bc z3e1rt1T=oQ7xueqV**{qV$G@j=EE|bloxMeG#Q3?#|qE^nj1gzixIFS7MeejN6j?D z6T}l1=}sZ{N-a@L7b3A3h$zRtl~i3WU^KH0^d(PEAicey$Y+X|C1a&5tX!v5jn{8m zQF*n)qipPIRt!O)fTWnFI#4tx6q4Nz{6W%AcgiJv9p9v z_F+UFD(;jvG|a-6AIicly!0_)l+skHV5CveD2$NE4))o&Ohf{X1{qvpw7o!w=+C{( zN1)YbQ?%XVv^6l^4(5(Wi?F#gGDaQYDEb8TSb|p>)EErS)KC+Pr8Br!UPp%BDD_%_ zWPzn**cFD0l9dVcgN-en#_1MT@h+EtyyN3a_%4PwwCC!gPnstNrA{HZE7)|V1N?g0 z1Pgf1S?@y=RDB>K7TB6LbXLm(!U#F?7NOw68t6 zdCB0+6R<3zp`iRC*?{~uK~LnW1(fG~a4k(}9E}+)@wGluZfkE$DL=I71~kLxQg_1M zN}rP1_35N6lvW1U0M}%7h&dPGFcq9 zJnhO`1EK?CeB6Uk*wPY;WO%qpsXf`U661&=y@)bwu9{T`rNxAVg1MUY!FfESJYk`D zF67v2uJO@ofiH;)MI7r=xTer^#q~sRo1!BE7CsS3q^d{k-vfVm6c|_4390OOt2SKn zsU@dk&UY2}-!pggwcPKyqC`-a;F$mp`U?1Y$O)J)rhIi#-(xTa1++_)=} zx73Ib$x9lu(TkDO~ zMrc5ly9vS1zDD|GBad6LU4IpT!}l8 zv4IJs;lyxNHov2bf8>STjvjzG6v>hu~~ zPUx=5z8=y*^)w+wEeD~XDS#hrOWK`tGDNWpg1Sw+gl z>>h|P9X_9+o=_yYJ&Uvp*-Ms1e#U=>cOK^ACOvf&M5q|5i7H`e?K{}Kgl~TB8TOz* z@;{-D$xNs>EQzv)9UU?}l~D)Q_T=}b+!1Z59|)UH4L^V;1}uI$-fQuM;bS5y{9ct= z9on#`HT2)YnY~#+C9+A8Ni!{-ie1e|i5FNw3-Rq(;a@Rx%KFwn!FqR@>g|>={U&Fz z3R|wHzAxA~ROrY=Gs@W(BS#7l%SvZO!&Qs+aG%I~eeilcb$hNM)u);Lp>tG&k*GAZ z6WJL0$zZE6cut$+S4KEQM^@)-PlX9be4OSt#Z)BIok)pUuPad4sra)RBXV~?i)VF^ ztr{H@h%0N&@+@izb)dEAZ}{hutpa|zantmR&Z;@*Nql;d^3-Jjk0S9N(Vlxyxh&}|mQmIvq-@z)?&Y4@4GYuE zPN71?JD=+flMwdW*eAYmwtH~o@tyss7u|H>vt+@~W+JOM)0}B|a{|*I;dg_OK>2}H z>3BD*8w{x{k+jQ$#Ig*$m7qOt%%_5wI}7jff!V)7(|M?7OgAFfkpp<}^8_~T0ARC z_Nu6xI9BNq^BP{(akP~viLOWY!LZAQmaKxJ+W9KE?gSVqsv+{$PUo9&sU9*f;V``L zW8@*<7{)$6BokgL*`k1J7@-gp zCGSlaQ03bqN=4Vv-aSjH<9P*$_mE zqp5YjU=#g3I(g$F(T(b@o?Z<%n~r2wL?IuRpNb9}6=uKc=QE-c!Kd+#%(%!8iNrG% za2i8*xTThkqvDzr>=y;^Wh#ME!m$)9yxvpWd@Rwe7T?qd;v(sZaJ#iidS&y>O)1|o z^q7c@sv0M=VxZ1m*^-w7TS51sNc5D$C_MRt(dZoQl=X#S@391~k|S|g%J~KhL#Cxf z8Z$jR_MXc=IBq9ss`X$j`0k=)o3Ij|L|CwDS>@-Mk=Wgkt{8LZ&4U#J`nB#mmDY6`hFMMpNxC};nU^SGixihi)5_DlCF|;?uf$>s6 ztoe57Yl(c)>edi!f!dF8G+enm{DEVr9TUYr9fx#1D=$k11c}ZsTAU^MjicFp*Qh3I ze~*5a1<*twC5Z8YdNAKht_b~&1f}%H6qoM1;gUPncQI9aFN2tG3S-pE)Js+If>U5E z2%iXD$ZDmD(1RTc?+sHqDQ?H|jWN+3RD^s^RHfmpztVV0 zW*r_3jsLV){Gj$;&ro{KYYz$M^q_VU(2fDagxzkTEc{)oFO$&T7r9wB*iuT+Sd>va z3ASK<-6{JM7p(B3W(n^rQfRD2{em6lWFss#MejOhdQ{b?(Hgm4He|-@hf4 z@UDnWyG3G~0p(la8K5M07NQIeUCmi-4jq#Hu6I7;JGNf7o1SA=4;dU}ua-P9ejY&PLBR8T6Uqgwk)aBU(ZaFTp}_oWujWaD1><_c1rE|? zM6fcvcCmmniAY0HjC2a<8EAkhe>4aj7XVCvbRGgDGpXVW*KA|5*S zzF6;rM}_hs+{*l7|EvtZSPY>$LH^Rflvlwrsdui8606lcj|UeU;#(qOww(;MOkM~$ zmI3EPQO^V`hD>I+!9B%IPYOy~KD-*JXQSZvN>5YDh6*TaDgR;V2>g;?{poE{otgyn41*o>yp_O_Ga1SMwpq0bZzhRm)g zp$)BsS>414Iuz)22-EfE>BRCU1*pp5ClJXl^F+z8zYE!#;uIno8D*k+trc#CS_6KM zt4T5S#U%MfsBS*1r5wCwOm=e%SnHu{eY=j9>P^TT_!;l)(v2bB2Owx2pCIXLo-A7f z?LY%af=m<1L3H=9+{ULZ5hHgMmOav@8L#b$VoR0!xvLQS+IL|?b>W=8?Mg499{;IP zXKdpduL-efTXwCu5Ny+`sYsf)&Rd#aF+s2a4>c57_!h+cFXuy;ZkKdfZ) zcd8{e1GZOwbgG5pP0C&7>I+~|1h2&k%O~?_g--m(3486bwLU$e3_QKPI z9)@71xLZJ)2<&(hBY6?lZlunP!vY^)@R)t$wNij(Uo|&&!i7n&Aac&AprY_TF{S&Z`^~KS(`VB=T4gYc?o<%gTvu zuxdN3-|Lop-+CvE)3Q{vo>^mBXfCH>5S81aad)-kSFQd1sGZ}B!6%IKz2P+o89zl% zWJM-KmJ2larksTlT{pBx{Hks{$SFgmYLNBhfVK-v z>CqgPuB7La&dEKGb0~wfO>%5NFUb;Ew0&YpN62|*v~`5K-bcahRwxI`2fogUzRn5n z$iWmx?;rB1O>qdFA4nf4sS76cW~e@s)+g!9r#(d7KWdd1Kp!O9gF|;hZ9v@%VY^qk z2dN9L(}lNl5_5}Z7bxcr@IAT8G?S%)G=PFUn#0L3YLFOlGkHS1F8jAHgqQVOxygfs_BES;RCCxSGfE_ z%2lLn&ABe@{9Pb&U~vJpIkHexVo0s?u4oNwYP+x3Qct*68nq&S$*fO(XbEpQ&Wqs# zUGi9^VDT8=9iN_^h*QaXeo0fzCy5-}pwYZ-%_gDEo`rnl+&K3N*b6ALt4-5Y%BxU~?m8(8F)uI#`~D0~u-DLj6{pI$yB5YeAwn`1brDLngoBpmB7_ zI0keQqjmm3QL>C;x-(@gO{;;TsNtU-vTWLG1E~hZ{WCm_v|5+?Oq8Z1Vnd2<9Lj8b zSW*faGwOqVx&rWTj)QNIgKv@pUjUmYf!q7iJ#Cmn^ancikxm;kb=z!%c9+oOGBDs_ zOw|VHB}};dHsrr{+=CTonBMMSh}+?N@tT-iZ}U8e$!?AB-}OFV^*(6zyrFA+VQYNw zg|E;IIlU0fuP=G=y(D$nRBo+x*>(opxz(z&EiaTY0)9) zZB^aquuPA5Ede8LR{_qg^@~lTS;2Pw-^2xeY3qI&>v8|Q%z1`Mu@~2m)rzE>wFgay z-H~VAAFGCruwUgEaoj;=-Yd;neFKm_u%ZuZg#i74OYZdYQMw>cA6oZ-zS&;ixDifn z3Sz&|YEEzZvp;=Uj`8~zKjUtFzRBG~e%4bRyDaCwRa}b;zADkK^qS3oDpDM~CTa!< zM*Ggb75b||+9G>i6+gv?$;bSpiBQU?j|34>BEpk*auVkNb?5Czz}C+%J(VFnk(vcC z-)Vq#D#F3G;M{;lyv~Ghlgv0DnEq~Gme9gE7vZ0H6wB`*#f2|2@`8V}qfGP`%6|h# zykSBe^D9vNfWCe1VjO>gL%!&#N8KWQ^REr{2Gf30Qxm)t?zX2~5zGQ<(I(n7CuUcx z!nCDqkJzG+?t*+mu{xql2vT;rp?~ooaPEfU5hHJnB*V8JE)`C zv6vy?VFBwH=`XinfN<(EX#2MJ=7Mv`Dd4mj(XR-(QI_g1Y0#-s!U(#l3q!T#R{koS zGt6EUzCh4E;~mJ^yKSIs2Uphj$xj37!E=3u;4UC+*|BwPVlSW*b_O8gLQrfL2sefz z$^dI@c1|yaZSm+6gq{}+!1L%c!%#bWOWB-`C{|o$0G)Ht91IMNJh4<@u9CBz4Iy0dF}jsxTeom3$XsGWNZ`~dpS85 zXJW>BR>>_b+e0*{Bpq=#&o?$rti__yxboFL=DF-xWfT=K@hqA{;pp6`_q;r9t;DVn z%KTR`JS5Z?Ax`;$(s2GhK^auM;%=-2lAUy;??1Y_4^NhHnp(6syAN795sN@@Q#S7bR07_1@Mc{6B!Y2wsm!k13 z9;ej5Y<7q@{U4LAY$Fd}n7Kagzbp!mFpggg=PIYZ@k zq+4_7zDn9)3r$L8!m4Goe#>f@70W1tDAibD9T+zYDg8Lt9+SI~ATFgR5`{2ow=_6ZNHeYMOU|@uQefk@a-uVYpWz z>)^~Q8IAQ}z9lvOHLEQ>qfv#B2p^m{k}BNFDom02hql;Q8?RLbMCp%sm(i_BLGO1Hs?o3G4EYJ|XDd1;nx|5SHC4j-AKBx&V~Yyx68 zZSG-RxoX9C4i_w-J*O=ErM(Qk9a!QjpPEBMEGfh+xPeN6`_hb%wec%p{@IKIICT)- z!{fP?<+-Ip3GkXX*$Lb6WdB>}vS;w}r?>?jzH^iesCi}Ik`}S7SvYS6DL_*SBvJ-3 z4<0-PLVDOJF{!h|?UKR?USU8e>mSA0KHQ^X2TB}_5c>VTY&<2w#Gg>Q;5`rbt|)RD zw{W__9g${?Fw77q4aK_&blB@)TfVwh0G`mtSGt8m2N=SfKbyBC(Q|Np85>IRwYY+f zo)}lc=*s5XXO*4=dAI@L&oJ**W}q>2@udys&dsQ7x;7y(Y7YXDqH550b0Segg9vg; z!bLiOO70aD;!6Qxr4fqYg>vAipnf4QkAgQ3?ty_}vKeAzgBgk#a=L#Vj!B6H5_rR! z%rTZ*7v}jODb9U8`ob($h%#P^5p&`Qmtvl%54eY!o9 zw#ToF)Lm^Pgu&hAay+?{6ZTx49}E;2>B5|iH{s)L_=Q`(AckK@$7*u>ysyma{&<0= znx}7=*A9wa$t*4E4Nh-p)k%3_ASXUU96^DR z^i!t`Apj@NXI7);B&L714MM^Gupv^PuDPSu>1X+gj(VY#Xd(& z$H2BS;Ln6nPlZ7%2(ydkvQQy*0=bo{=rt!R^G}H>S~~{Enabub?6%v9rS!&XfuRVgizmvk){@}V(ax{EDt$WG3-cLrQf-%aMhS6D(~i zCW^GNVNy-`zH{9xo_-S^6hiefj1huv3!WIEY6pH0Vd*AJ8N}*Y2q~~nC=dU(r;o~Q zK!3j~$1LUCn^qJfn_=(+D3|2pUso9zll@IJtfEI$xT{BHLr1rwnGcclf?1#O7fpKk zZ1BUQhJ6no_OI;AKEyYcHg4NmV#+jSlx+TIvz@RF zEjAO0zMOl$mo`;{r1YTP-kCOAKrcC)Z2)%(wk(jo^yV!$n=LmXGs@Kr7ZQ^*I^K-C zV%W1i50@F_U;5pNm(d9l# z$guaMzZ^NlcC8n4JOum1y=)m?PSp;+k8s%Xr|dQ(2%jFjwa|C@$hk?&ftebWkht~j zT_qb(skG^l*e}9WMaVVazD1v$?AqzY* z(sU)QxVfrxg7x->HM;+a}Zt&h?mm)^jbg>Vp3%-HX4o%)K`&-Xq5V z$zRUjm-zHGRn0#%GsmqF@Xe4_#XoRz9@oJ1#i&~Cm$rNn*U0q+xf0D#g|C10F3_UD zZ!FRu!~pEB&{nUt$K)Hn9b3(<&&yAPvr8U7*h%qbfL?~~!IcH`;Ld&{U{NXZ;vvi> z-ZJjl-YSv7P5!k_Qw$rmq(&Bzp&N7ha%<-0GgOQZT>AZ&_E|!Cw@_gcW*Rvp(?Pkh z^i2WvgD22Xwo%rd5ou=MOP?U~`m_<>|5MifgaF3`zCua9N1kV|zUk6jU8XV79Bl-U zb1_Dm^4-foqDAAHQsR`}a~o3U^F!woyoS!;aRQoUFU1{tLEFj_8|M)X>869u<>yKkgelI@T9j?)=8U_>udy{+sU;_oHXwSWGx&tOtt%l}64VTtCE;u$Xq{ zZykqIU~h4x8~N)dl_3{lTd@&}i+b`ijQs*WioCa0h#bX=FYX&zcrWz`MU>O&i(&F` z22-@W#S9;X^y!VK6V1rch!#=yPYe37W4OH&w;Z2WpJo}68M-JUCz(`9p^bvuSb}fr zmk$3V-#oS2@=xf_;msK@Uy#K+zrw#C(Bt>seAvyje|q|B{sNWXs9ROJ{vVG?b@=BG z#Ft*v_Ugltn00?b3e*IfQImUY>knL_P;0X!1^qmPEOxi4cq!k&hMqI+#C0-sEvyyX z6rB8asiPPb3D?DeYZ)<0Fm2PVU_uOu2TR6D(GvetP~#-;Et-UHH5F{jmQLN2$PDFY z2YyxAHNv~T@oIRvyne9_rFmIDsJxUmym-G&><=#m zBNlrjnj*~hc$4TLW?1@YcCtPaVtg`e(7LU3a}^a1&-XZxD{#Y_pL&X6*-*w$7a79%{*~hUJQjlMuF0`S)&s`1=g2>#B!t+c0^Zypm)-`3 z0ICN!J7;o_)aEVK-@T$AUO$E>)@L6uT#l}R#9De^Ym^j1ad%=n>KFI=^zTNLT@eGs ze&&1v8m|Aa6hD>!c_^8h6<*4)Hz+(OZi~7CYhEeUlPyMVb~1K<(L){GaPLQJKz>Ef zh!Y4nW0&$FFu0;=^-0ol>I5K?V?_0tB=^e~;#@PN_VJFq+2<;NZjXf6FR6phVpq-^ zzV6W$;_)$H?GYQnI$;2&8fs5DMmJy55Rai@q}3@B520aJSB*n7q7fSw56l_?eTLlL zM9rNh36s~Mw8@V_V_sgBXd6h4qi)(}`dIsr@ijUv=mjf~`F1sM%?ek?B305Cf|0qw zlPop{D&w~$6)U>8HurPbYQ+eMO!o4itK8iHigNpZ+gHK8Agh3 z?mvIA= zNzqRS!9+4%Xrt*cUL*~p8$4$Uq{HioE}F!2=1ih1JY&kFGdg2RrE7P_6iegja6eC! zO9SeNPAij**DnA`B1xy|M}aJnWRM9wM;gXN`b*j`4QVRDkhQ{}Mdx(Jltys^P zIdGMtEo9!MvZGVA-v-K7?UoC)R_zuH)K2-94Af5b77f%+#Xc;smD0U4(2c^qG|-LG zy*2QKqV0iTQ^Gzi@P*1fH}HkJO{*Uc^!KS+zYi2&-d+&Ywc0HqC^zb7Y#=L`lsuF9~(l;(^AC@AQphPS9MnFM5jCf{UM=K>CQ~9{L*kN+mqP}@E>Z6Xrt>SpuSEz)hVTCpK~x<1Q5ewz0PvXnPyFrQlpg>)M>BdmLrW7QXEzf= zdIKjLdKYI4Yx@6+s*$lXcCj}3zXK6u4Qx!5O#WA%Pl}S39I^n)cL}X^IjXruRCtZa<>TP@d3t|9x~h_u-+Tpc$WinTWohs#Q%kHzZD9;?W<4pDBx zLhe`?gGY3J(=NZQ({CDz6$F!XI5pZb;m`^GMRN?3IWTP)M!Q;SjD;+rfzVS;*Q}kZx*&WR(dVoqFp@Et9d-LC6#`p(bq$GRD|(5JPcN#?T=Q zqI5&?5Dpe&_mLs|D1pEiVd~aUFw17Te6y z;1}=)ZI6s05p6|7I%bs6J6KAQtV!0^E4Sb)UQes=8Z*w8P->QeMOgJ0T8oN$ZJE4q z6qhKs&H=K8w_X?OBdjHQatB;ePzBTQBPj=Ei;7~N8$Aj;I9rra*wD7q$0$suV+Ilf z+COUQ?90kzNEG@J9aTBNgX)8pn_CY$4Mr}bMYBUCCQ-D#i%8c_JBj^XjxcmtTg^)z zd(yURE%yHsUpy}~NU>sXuny`;LGM|{Fus@(i zs_}?)vNy#_JcGBM2}@oWANVNRvHdCL=$r77FV&21VPD3Q{-Gc8mDO^ai%W{RUQsdLyKa*o0BE!v#E9h=SP=+<;& zqbgXp*!&(k3QyX~4Zdj2Iy-l#G$kgX3si;XZp_P?p}p=?3kA5#3?i&>={IUpRJozC z*dBdOL9$p_)REu*{?j<`HoZvaqykz#O)qx*LyuPFyx=%`m3+o_w7u1qU4yfrgxM&M z(XEj=QM{4LaCSF`={JM!%9ofBL-)ys1UPqP(6g(eeQT1q&_Y) zqsYv2u>gll3Jpl*=!cbck}IOIXIn6!JsKm($_>g9~fPS$mWySfnydG0VfNCC@dh z8AFcalG{{N8wR#I**%91y7GzQePAtQ zo68kq8)&W2K%oVOiOCNeG)egrTGW&;p$iMb;E$N12zC+R*uK)rWy{wE0_enBJ$m2v z2nIFDxWJF-^C^!Dr3((ylUB$mHO#QQxgf_B%5F)qv3laYglFzlLt*;(-z_y3=4ZrB zNoWC=XowW)B9^K|t04-7A?6yB5;@yOch4Gsqw)=IMcA$he-}pw9+9~GWY+44;P|Z( z1;oqr*y8YTq;W+@0k7?F4T9^fRhmEL2ty8B2$-1}1AiCP@aoI2@?wFy7_?DXt1`?0 z9o(AIn{jFfKxO0aw}iE#T*+u3IWR{k_o1ngW;mL~u}g{!aK_UML-2@$@e$)QPE+#P zU1^-rPhsEcJtA@j7KuKYu`;!gcYAKh4fo|(hhh?iMJJqL$WJkeQxl$>*$=wA2Z28h zzs86D7}|FxI30^S67a>J;qG)Y=WO8(G&U*FmpYoO?{M)mEAhpl3s0aArX!Fs1qT(x z`^mOc)PdOi2nQWD(u-z$(iU~!LeVR)hoRBgZ1=K;4rW5_yC`sZvtu=+#G1IbBkU~Q zNp+F+bxkj=ue&*|0|jWvBgK2#-O$eB+d~a^c`|UGW)>h&Q5~=k;kgwdzOR-QvBJv7 zlzNOS=J3ka-GtoHaZQ2|oPsVraP26_`-zQ{CIf>~q0^L+vj<0U@hPL-g(c=v;Onem z^aymL`6X!~{Q1hXcMlQ)RA29MV+xjo=zlS150>KM8yF?JGpS(V)=dwQ@S3le)_76D zRSWI~VJP13*G1(QIc1a^xc0n^2KQz3u(^sbXfOie4U87z z-NyKh;g8mhU)t*~*joq#?zbvr;^#{JN@`(3uU=i6sWKXYswgqF;k1%@r0l_r_%i5{ zOreQ6l-sO!s@D$@F%(E=_`br(r4aHv;WrDbGRRg?Gt;i*R+Q=pI&FXo!6`ns@5tn3 z1V?9K z0<4D2}lx>1aRDHM6qo2HrtuPBZyM)@wycw~4op@(_*Mt!Vpw-`s7F<;Ku zhrR;v)9_A4zdGq(^3PCm5bDEf330mvYt~3avxtpW;|*pl&V2E)4HG^|MrMgNwr-D< z%p6u($U;jTB28`w$KuExIy{ifh-T{(k;sNnYGAwLPbi29>e=-p9N8lI>|D;zXm^W@I(x5#7L{8Hzz1*@pf zRD*p*1ab8USLTt;p|;Ly%CBo|s%)w_<4Q{Y7}==F)fpHmG2Ekxa41#keD3z5hBcbN zDR@ZDFibWc-QsZa$idO?!F7dy+#%texPj}MI&=|vY=vRP(kGq;0jWhO^!nixuuhiY zpWa%HpJgKz`n^Smf)X>WyP`#v-d)QdwJN#$l>{2jy5+z2<3yrQpmY{7c8xUEF2e2P zVRl6SdW}zYsxP>wradi~^3zoB&b0!&=Vu~0J=&mhZ1xM0 z|A=uCJd4O5D=o;}{NT2A2CwL}!m`2wK6UUCGm=r0qiwE=`HbM6U6P;t5jJ9}o;^{W z6a#r<6z~;{&sCg|YKos+i=I`dOtaurgSfUVzhGAykiscP-IGK~yuj^NLgLaaR24o;Mwh_u3t)Xp7v zoYG#*$dA@rxcgB>WK@jg7~EwT4YUqhdsmEI`TQ@iUE-X&Ck6ok(2DRsVHA%41K3tH zF|u7-W7trDNmvZ#nPOv zI49#M(PHg7XsT54GbHDG-#KHxSkHVvkJoMb$<8*!F z_kB<85BfVsHRdiZ%HBNzeSw`Nf6^q?aD!#epnDjoWYw+9Hr%LDwu#cZHdLtX*n$jV z9O{_GZP;P347Eyfi5K7=*#2zkDP;b;hWI?G%2=I6M$c3y6Kcc`J85snJTUi! zgJ}E(Fz8M5peI{?HtGNhSOzl)q`a_Ht$3fngF6Bl`ijJ991rfeot0jt7p=XwXgm>q-InYSiu)d# zblta0%8wFa&U11*$DO*{WciOme)*2=>Z{bXeP!QZn`A+ZN!R6n86^Z~caT_d*DSe- z4$EvI>6Jy5c4ivRQ!b0(;riS`s$4M~6?Yh*fxbvVEfgrtm{j3zvqWY}xwDi){z@{m zuMTYaQ!U)jmZ@4J9o?NB5N=%KeIf>yGae{crvE+pKm^J9ijEDG=vy z{|pONQK|P6XJXfEv+$w*v-kk9A~IHW{|Bgg|Ir>OV*QBIZPPs=v6Og$4pVUn*=tS3 zCX(UkUUFchjfmwd3+mKe3shE%6*^wXBBD||4wd^mdzIpNaADlx3MqfOj47ct8Me>Y z5bJKQ9;SNAtW3aY=2@-kuo(I1S3!|Sv5y!CNTnTICniSNp4KWx&WJREfS(J_U#4cX z60F0hDRr3#(hMkxFR{AMQ%ztNUDQL$)?7?eU9kd*)mlhJ|4={${<{EKd_|OxrO8Os zqgi{@-dHn97hAPv+?mfE`g&tWrO|nf^2$4S#35%dh|sF~C$A7yr&0M4@V|X{-huwg z3MXfPfgyWwU_yHT#@2_QU}5-u5c|ON6`KijNhSnkITLOVa9rofau?; z@sV8f@=J#ffQZ}<4Os)5ZOWp0yaF0%CCsqQRvptsJEaqm8odCfv&_4)JKDkLihMlZrSEvD+wGnI61uEm z>8F|*WT$3CgkZZ~v8O;7fi6lAatuJUgzud;yaFm;Bghwd?2=fdqw^OC!|u=u92(Y3Afz9I@y~0DQX`3oNCo)|1))FcmUI^v35!=4#5)IC zFS74_z=vXX?lF>gi6V04-&Ftn8u8R(+jD_jO6 z&=Tx?z}AqA{uI$-3@vs1k{3HQnv|Jn*tZ53GAl+*?pM&KxwqE_M);6$#}Ut4Hs9%$ zSI*+oXziCB0GWb8+q|`nN#>JH<9-k0KTIPx@?)HTr^a(m+Oi#E~L<9qIy!Nw>Q)7&uTyRJPjDk;# zBFpxU8fIzyQnk=E$zs)+w6YmT6QiqGHMd_vf_UYdW{-4eW=D^LmeJWl==sK_s&>m+!p^=+NaQz|xbJUoKY`-;S**Tq@Pt$hGZr*+!Tis^vc% zI`MGBV8R0o?j_%Y89ojr zl1M6AA;C0sP0?T)q38Y3a9hl6zNzrDg!#ik8N_ZTPoJ?!d1DGb`Efo>AHE|I5@QnY zHsS(3(O!7B@-)E*&a0|cW6l#zuTtBVm6(p{mkn!-=aTsb9yqc?KPJ=os92*G_rE(? zL>AMol?FBHE$u7<&4rJ!@u{I*w17p9dD{WcARE|aNalKZ5q~~-RT6hTgi(pZ4?$!D z^Tk;NFLV^ah43Aoxf5@~F|Ow^4qu$_a|hr0_+@@gNfn7$o-xsTiI3id!aiRtl6}xc z9U^v13k&PVdMyp!0SDHW_vJ9We&UXSYCNk8hj=N&F-qs{j>v6>I-h$9ipXn1`1jwz zrR~$B?b8n!aE}=#4d_M;at3JwgklG65(bIFNQYl&ov0{!6pot;UT0KDzf(B2`h!cv zxY8FM&A}Tp@&4)SE{4E4X41LVm7PP$&?e3Ol@v^>+yIB^{y~1k00rgA3-S^F*%w1u zHiCMGymyaeGe*wy>`2QIDg)LK6XEH-uL>&0BDd9($l0}RLC}4vFfKhM7@f0u4|@D4POZwL2g&455abl`0VOnaeAQ#KU3A+pJ4p&08)Yz-1x|0qc_sUkDruX{`L4ihF zL<$mCrk`YJt%6{)swt=YQweN>wb;b8f~xn+`d-1tH5p;D*00MF%F)7MaH}ma+Vz&3 zn^;BW<#m@?tF5%p{b%KCiDHbE@nJ$9bT_FB|7A+7sSf+HQu>+vM4exV%qWmf>qpTV zOe}G{rgSrSi&AWD{ThgSX(3ryL$e91LcJ!lKyF`FM4Ku)G)D1x5}mN|BHrA3F$m>R zGU3X8V1 zR8=F8GJ&Im6A|lH?1O-GQT)k3F_YrClJ#*{5bvfyPzXKoQV0gcGVRxrOoFC#cDHf* z=Lt(bpeLCa#fn3$1gMhFwM?$xF-8SeQPxWz00r8+)kfvg(v+rdopopQMw+-_tw zk5|x`9FkWN6iQmC(->q@mMYew=$7Ci(pU&V^5oJTpu$0Vg(3FG|h8>R2wqyY{q7Orl|OVv$xw#(Xxc+%X@c`q*SC zT3UGc@My+Z>R0|(Qs#nRd|82H(I!sYr9KF1xj)J>HB#@mL5}gAzFBf+c3c$Oz^9_| zQxrOxQZ5G>T~D@hZc(^QB_k3buYoNWPS}ni?PAiIAql*2=QO3!u1KmEmJnNfzzb87 z`($A|vTOUg$sY_$f>{f%aPZrQ_2C32_ zTx(rXxMIGf^uW_Aup+vip>^E{-gebKYXijXxm1E7WucWi`$qA?xA`ZfnK8Lbmm2WP zant?QMYit1Ovd~uJ=AM?X(t^CxF~!vb(6hqc7aDiG{|_ZqCy| zd{unmV-B8>X6}vZa2U+`KaF_bV0eyXfnjsb?nmoKobB zB2795LWU)Z>Mt02Nh|teyLJ79(@rJ>$F zy{p}`+PhWBM-!7t_wrZRq|`7qiPi+!e>%~Akkx9MCwj=~%vA{!{ieQh=$3|%A8_*V zN7vBs$x?Wh%VGc0+P5%RKOy~6!MF54WHAgK+EDE#Q2kIj-s*5d8*f{hZe#m85WO6V zApFuGZW`Ev6UlCzOiN_sj4}Nc&7v48@{PlI@Uag{=F_iiPv7#OB5MgT7m>d}NAQ*$ zz1H{U_hZN!%hMI`LBNKDyZQSUJ)R4pA4wHuZ6Y$Wma`BgU2FD=OES9^4$!s5p2(-~^Csu_Pg5e&?U ziC<4huC)iMDiw(Cn7m4c{R#Da+<9^TQD5dxK5{ke$d>j<)C61zPt?@|Ux`m!dz-xx zCc3~Sv(SOBBB$Tvt^7!oNz5U&)8oJ!(iw1nO|TY2;*5OL#g6P)D&m|N=oGG6xC|8; z&rY4SB;qWp$WJPE0fVmSRi9uDSD^1xe{@eGIb`c@(ui*MZ1CU%S8stayt^rW>59~u zG~`+s8D>PAo0wd(XRRoxyJk&AnI)+bEBPV+o72q&<{&@c8HRIB8un2aPJXV)=k%%q z*R#K;XMVUU`H73vGpQ0$JxG;cuW$h1wgH^puXM}7Ba8-b#F@)U)(n?q8#dm~mQ)Ov zbd&()P!oj^moRWxbc;)KwBHdti}R@n&Uc)HEYeBhJE9Wt5UOI>8HFUrj5o`GH_Jp0 z#|Sh#{~}bw=@1}v%%zwhp`Bakm`|M|6u@NUl?HIlE?2!|#OHhbDen!gV znSrC*FPSsiex+pB$Q43OXC!5tJGt$ode?a%F7#xPCf_Y7+Dlrvn%-E;%u(BZLUU8k z_6^usV%_jOMHij0S7q*Y)K?m3wHX45)9Vawia(U{&qwWLe?!bCH+LtB9^F2I)wK|( zFt!zjj_YqDwVE^9?GfOP{<3cpqfgZEi_O$9_t86D)TGxZn-4zhUrf?}F|YRiky(F< znjrQ1t5YLeIaBVKHlr%`yY)4?gi?mJLX(HLJxml(2FN2BWTFSG6$1Tl^AwZsv-}fj)$IrxNBH{LJ|rN@HT5pi`&O*oVsnZ`vTlOz3^W7Z+V5!4p&_{<8lWRT zlMK*d=6za=dH7bY{ZVsBDNJ<0Xf+rtQha4^%O(%Y>0FMzbmt0X~Tw z0rUj1egt|mur%Tu#3j7pH3hw5s^Q-XI4t@AcL?vBeKUPND1CUn@e1JYer-o7u5&H< z>p@-NQ>$!0P+M%x&A95aUHMr)R^44CfBAiT_xZ^STPOleB~1M?%M~n)-LZC!hW$5d z-MdO3^htA7a8Y5tNlBu`cT^SdXXUfk+v}^Y=WAcwn>(o|TV!{p#E#3ys?^SGv2_OV zJiWwf=-8+ac!-{C0=W zI5b*&jCn27dl=7(T%tGic2anyS#if>B=G|!`0Sr#PD|z4q$Q0hjOE;zDzcweT>yDl z7d+Y?a9@W~nz)^+^q~e(Gu1cze+HaJPoc`+zo*%C%>M~pll*_9Yk5ZtTW1r;|HjkB znjZQ~!%u$9>(jo-KqN3AM8bd~f^CTMfb%j9gw$Ev>DvH+EOg^oXo=fxo%^>Vo1V*B zO{zTQt*Wq8PZdvR`?by0s#?v}D><50%fGdII{ni5u}#v6aE3pA+MV&T)8+ASGVM+N zHm@efBO>f{tQH6tP0nRDx@r^NRLUl{G7U%GY4%c5pwpOzG_OvTXrF+f+(Fny$?lx9p=y~et*yQ1)i)j+!WSk1 zBhAnL)Zc!%)0omKn)y9Tr_heS-dveBnkeftnG*7<2-yg`2vUT(m>7wvd_-QLy_|CV zEfiMHQUh!Ed`+ylp!_favnqNOr(yyv<%;S~E7sqW@!8nEKMeV&Oc$f9&!MG#ZYAfJ^>{*M2j`P&301ms zYHvqz*seHd7yCCDqL@Z1knGX|Cpj!uynz?-N@5rp$C0Bewo$lhWMwQ;6iT;BD7hZE z!VzQY@gKvlMX8!mkrys{=z%&KvxM94JK3H`K_Xl0jO~817B~>@B(7I(lalvP1-T&N zjS%<3DELjeH}lu^v5T6|?E;B*zPq`|7r;-k8ni4Hlu1 z6y-MBSGEIUre3L85+l~Z4P?8%$qSl1N7$;16eVfqi>>c`cDvLrw!R61Z)KiE9=s)% zHXE2vZ|)P>X45e&QEKxsYC~ppdSY;qB-x(F;cRE0=&!;u+!umEan%}Lhz3H#FP52W z(nh;Su}ZTD^kJ1!mIK+`nRAv({E`+$$||_R@?(WV*0}1Z#Zp*h-Tl;Ibs(Y&E^qu)LRd;Hp`}9G-mIKA|`ySvGmyfCf z$k|xti%#;-_x+{H1|plnay9JSJlRarDqjkS*uC`B*s|_v=*)d_Z4LARBGqawe#>zzyx!>hpx!mM>Ea4?K&iMv-e)HSvHi5u{-iOBYhzaTA(;4H)L3RF`S+hv^EGm3IMv;` zDKuk<74+qR_SdxZyOG3t=Zvcn`eis7dPB}SSgI*gryQO_O1)ew(`v2(c~})kkS~d4 z2H9qGkcI8_N2b>F%E08&jiGi|Z-ge{rNxSa^9a`{w^EpRl5XsWv;kx8K2Vi&m`mcM zWd|P5hHDfB=^UBaJ%nJOKDKsjho8=HyVZD$gL z6=O-=V5V>{-R*arE>Jp!*@1p|E5P1~eW_>TtG%sP=mx3PG%hwhYZ)ib?(F>7jAwS7!LkM(>!6pbg9Ld?AZq?|HTfKowvCY$3>8w5*8%mBO z@t9&sW}0@RcU4?rH*p-^;9XdK!|p@3%EtjtKxUIfVs_3c{O^`8jzHX4Eeh2;Z^JjW z1z_4-)ZIL@XMpb*$w`NY^O~zGz0A?*9Y(5GhLFgv=>TGK^L%-*FFvt|Ajv(25Z zSA_E&6nEI&({?hw=Z~Id@a;#BzZb9~Ty7q@3y&EJ+>TL(?~uF0Vju3egZWGZt(H>f z@{VTz-Qt{uHM0bLP@0?0RNdriKl|%E^Jz)p-l2>%L%%0EGzjN0Kj7anJ+Ye25C(n3 zK#zRF(l*xI;Qbwsy3+HBxH|aNR~LCK^MYTs5Syp@CdN`yjAaAkR|*GE-GB1MQJjE8zk8Ad_t0-aunx-ZzC~GI;;JVAwD+-BRG={)q2m!!YFbx0o!(QZ)}z@1K|- z0&cxM7Pt385f;#Q6I9O|<|tV@zKz}edxH`3uzX%g%T|GSo;_g?rQgT+cs5qY8B8hy zCZiQV-ZI?O6wImh#od#h3yfDVnTYkUx*b-KANu9fn>)97hHwXa90d4>_z;1i!!s;H zESzCI_*q$I;k;RVQ>gcxT$`v!oUMJRqk*?3)-Tq`obOvFY(8>dt4y;7-AV0T@W!z? zVaC*>%G&GvT>GS{*Q#{i)?5-*(2o!x)?=MuW56*xV@CQU!smynAH8Ha%f4L;D&;cby4hX>88#fs=GKh8YI zgCy&tc^HidnbY`%%*=ctII=pLhi_j90vn=pPl7?d6CAFa=3&DZVrPnbq@D4Fa10zZ z?>I1oEf&_n=-7d{{A(MJ>;hc1?pi(C={;eNG^2V_(q;6FINcKFWU4 z>V&RkIGH>Zp=A+QxgoohkiM)!By}OV-!Xa?d_7y^XdB7JOL4Wv2&$efrx`6pf^IKOX?CYMDcl`KU!p*rbDr{3N zrlK>fe}7JI2QC+mduY$eI`+h-l3Z()7Y#3>X`tGHU-rO=<=TM#?{i=gYx70_t?F!`wVjSIAKZZ)Zq+ML;UL}mWNd<q9fjj4Jd513Naz`8k+q3J7T$)Y5yvL$$-mm}b^x8=k71+rK4zAQO4%y?Up}%Z@yjn6 z_9KD6#_Os3PBy6wMXr3Q-$cMg3+-_T35G25&2iYh^kKhwX^iRY-a@H+iZ&yKih&rf zzd=lDWzI8bwUV0-Wuwd4v;z6VXT7xPW8|AUC(wGrE~(2Fm4XGI14@r)kKg z$|XM}m`1IT+_0~7TA?L+v1Pi{__~ozuFv8S%mPFmQ((4jZCzXO(bam!Y#QUPYQm)R zMtkXlkM6E5&@9c7e`2`wl(+P&bY@3okSZNTb+@Uvy*KBAC8Cw|6tiqSI$9X1BStt1 z(XTg5AYiF_j{@w7_{Ew@0z}YmOp!hy1;Dj z^~(8_EJ6Zj->YkmX4)FHb#DwtbXshE&f{7;YZmn(DSf_U?JD|qw*MA{J*MfE{E~oV z0|~@6@e}J{VR%}h@1N?}WyW2a3spoiAMIC6#h%gp^h;JuIOd)S-)?SmDjTR@jI>v0 z5(l~7I=cBPn5UIsG6BO=G&>vhR%5f7VJ{UKM+1Oa1;B}Z1S$RQ`$nUBZ5 zaFdTme1#b0ws5wrH0TEEDq}Ok5=k7YSaWTrNvYS74ZH5*;2BRi_h|k~YU9;IlVajUGb*kkxxlhK zW_!-3!l5dI9IYWqEMWL9ICb#M-}tMvh@u?41wPdLbcw$ub_ws1hwVlvwMl){oaeb| zG-j_=V$~~Ja1x{FIRp0^fD)|)mP}TLBIlevVhV4G2+}xLnVc)4E<4P4hNp|9d+c9+ z<8J}}YSFZ~S9}cQr<~Q*NOEJc9)u?pGJcKmIIEj5_=H*{Y1wQ+c~HM>^bw}I5Uu`u zJH%UkZdc^8XuE3ZUlS;>7HOe{QR7Wu9#w(tK9&cIW}I%}((~Z108Co5j4%<_TDrd` zkI4bn{8CVgAuul*VWbP|R0@I*J@V5d{#?iVY@ZqH_k!&;Dt8e5*L$=7Ugy9w8e4RN zR4+2lbyMwzl9OE}vh4 zp9aEt+!*$Gz@KUzV+u@EEWgSB4SQZX`+S@8uT_5$;YNT@&p& zo{>umljs<1WAa$lw4OS)Eu8fsX%3tjsm@60d;n@mm*=@a>B2EV`se zZ3uj_Y?t4VJH&Tu#*`nPTM7;njP5_16l}}*?!w(J2E(;J%Fx{A-M&sh>&xL~KL`Ho zhV$KrPnRz0N_2%?G)r`$B)2m@EQRBCNql(7|!%T&Mh~%=fv@^vFc;?Z~Rr7I3@?b z#5Rkl45qe(#Iyu$T6?^(W&N_R-v4s%I=rAAuBxIwRo>5fr{=E8lU14tiq7i)&S9_i zG$ncOiqrl3d~e0?96ge#c$N9E4Y?Z?;ydK51&J!(A-GTJd zfT`G*`P)lH8<#~W>KDaPv@&5$2Igf?KG_I4bhJ{_&v-76SR{6kfOE@Zh70 zaMHx9dl|$Hb?#$43!{4Q#25HTs>b^vtU^Qrk{uN~wjUjJPTs}PJa|gRC;BW*iJdK9 zzqVW3ddl`B{47u*)SKfR^*c5kO>-{UrCW<#{CAw}OmZf0<~)xvGcd(qPp?B491p99 zw$1=w-MMLd`WGwN{zzpURbI*bAT*p5Eo!c{=!IsK;Yr_T!h;~Df-${J%pniAJ_i7D zWW!SDE`5w>2y;bPqBWiRM=^oJ(-&)ovaDwSiuW?jKt!k*9mu?TKriz(b zdyavzkBkrQ$eF~oP`qyFXeGYQ=y3JJs5O0F)8GtU4Qqy_R*=Prie^1{BLkR31oF&{ zapEUB%KjO~O=l75^kdqESVWSiOflwG(b68MROE!u!DFVFSffa163yhr+29pK^OAY)1hFd!1 z;Ezrb=)VnU0+XPwM`u=!0RF5%w*oT(5Tf9gh!OmxL4%G477{T_M8M{2N67bSP!#$* z0IZ=@sX<8GKO?yHyU?j1DX9~nRB1s7+=>y?{z1uq;M2>8D*UAZ*~sbDLmB=-DSx23 z#Uh-*RFb(-?90({z)EQy(DDHhQQ!;7ZJ3HRL;L>60B!K}f+02k41hLdddZNB|0=*5 zW=d&CqEDN`7UH{!9W-I-5QG0S;2*R`{t%IW2;dBguy#ntpDeiz#=aQ+5geQ58ce?) z9R)m<`kHx*nu7Qny_zA;mN^A63}Mlbt-n?T6*vcgAE{hz=hx31^jUCz06$8(`i^)X zD}^ttx#G?!SRFbOWUSgwng2eZ3*w6A8sTRVdL6_Y;0+#CaR=#V5_&HwG01yHlKnx{C%L5m$e53!fE>Q(6D>)nf zPxT&qA2vl9WD6}1hJ9pWBX}7t4~~6iVi>r*x+C}&A_WRecEw&o|1vAD%=mEh-(>#C zYio))Q+`QluHAOPGHY&GaoDvtMH-84esS8h2t^v3Zm}`hE>|)lvu^dCLv$K7o^hAz zIQGb7vi7)pdty!Wrm7>-E*5}z*1aUrCR&{a&$LT(+;zk%Iht9w%9v``1t5V}r|w9$ z>q^myXQ#+I<6e|V#pWwBPQEJ#cwqI_8lR5lqH9la2umzu-6-5Uj&7y(pxm_q*cx@| zj6308D&Fvq@TGM5JCr5ju=q-jN8w#6-zblCrgUXr6I0abcd3t8;a;lT;E%kfbj4nS zQ?#-PmhZ(xqtJL@?7F3NWn3Fj*co_>jsL|ZQM*AN5u|Wqy->bk92rgS3cGfoxHR$> z8Hd4jqkBf(eN6UFzm}#TG4TeB>qINiJ#+7}0WM6vf#Xzoy>)vF(X%wq^t&j@-m%vn z6nSRe!s9r2da5^J{bBoYGBQ)}jCu#~Grc)4ZUrCb$~R@&{rhwsfs=YiUx_6w?GJ#u z{c~vT)(zJe*s6cuS~C1z{h_Pnqpsznp!J^A@`AH)vMF0dk`5GNuknTABe~_Fz2o5( z$Vz&`>C0>gv4 z@p!x6I7I zW*MtMHH)gspsIzFv=WXw*iwc%U94)Es_Oo~X!Tff0sH+C7Bhqv2%+lV(pu>C(m6eT z$;eHwHK}MqaNNkH5GMd}%8>$j7Bqo67^?Xdf;TKDKt=6bJu4@Bq}J zr{izWAs$h;Hn zZ7oA%L!H6o1T_*=KyvIbsjLlGkLK?j(0Uy3xLOz*_|-I_|FZfz5p76ck`6sL zeGfeVNk_kw@aX_FGYK?Bd=9Kk7$QsBn{bZ;V`6;aj|`aWEI>z0(ISS<`C_^NwR!Zw zc#l@ED;GwwX%%=!7g6YKr9 zH|P2X(KP+{BR7x!(ut9i(1RC=cHSath0Gz6BUYXhM~y&Y4utNTz<9YLNB`(h2{dtM zZS^@4irSi;zJP^I@h%Xo=$%f@aVK=YVJ7Q_Nx;PSAzvcx&Q2f5#eB8hqAc(Epb@w1 zX_?l16~$1C>IQPIfVFFRe9ldU)_I%yML_Ey^!yf_aHLo}QgDgd3g;q}RS>Ji6)0yO zsWvLa{xVSzOXuAgg@`ofRJZN&z*B&xNcR|q4l#30*hX39JUlvqWk#m6IH|hwX2G_D zA@-u^HG#O##4`qDz?&f{MOLG$i(_tUp*qWe)IVbQ4Fv((`^5^9?v&s#IP}G;>QsB0 zyIFqnL<^seg;{vb&BsQxSvEM8V|hg$sz_nL8ce+%8RDtI6V6++)}>ic)jnv}FEuym zjUVO2xDpa4Un01ay0XqPVGd8}>_=t-(M$-qpTHSZ@mYwmC6+6@ZW5^(5%Gq;nBYqm zIh?PGKR8Ku$kKPLhgfY`HMQaXCHZP2cE*!60qf|y1+P04i-OBR6dL)cAh~I_)2vLs z^z^U4ahQi`q+E1#FmfdQz>{n=0ZeG9eElN04~&kc*uq&1b>#p;m3_PPV-=c)C6~mN zKeQ`9E=vwn7ed%@B}OGO+*cDj1Lz9Bva}Hzeugg zXR~hqW@0_-YNbJ-MwsCoH%o67SE z)kJ=Y$4zNQxV5%gH-Yop!0`%c zh66LVIy3=oePPL`mus)r#~4 zW)swqU0Yfhxhy$8TT<()eM|eMX_*Pa4+U!8MvlBJH5gIKQs7of<*Z=ixedxLY0!sR1PhTt|;-~yiH?4FC1>hULg}D$m=Uar{7q-%eNto@qHba z$PCLhZi;O0tbtR`s`jxSBfp*AycTgY=}IR+zIjC7$I@6lN!DI7GkNR4KwBoMC~P(6 zzN)f{7bHr>8g4NcMBX9ZkgBV*-UY$NlwgUxjo@Nw?MM%}E|NWGes(o~pMe{GMb5(V zZHa!6X5oR$8re2m!tX&}`gtd{Z^A(ukV;pVgU-M^t_~n_sn*@Gg5B?q?8rV#ysVUU=G+Fh&AKi!)8rDhPh(`3cjZUi`^=E{x zzGH4xGw6C5LvEz6IYVwr=C>l3yVd@e70FpRXNFh62vb~>+B}_!57bFUgS3#R)OU&z z?Fa{iP7%X5BdE=9uq2ZdJ#+@8;S>_}>SQct>h)G4Em5|4tL7XcMLr$1V&W0mgsHEZe$Xu*su!m!TnRlxGcsJ$ze|&Ld6E&wApYPcG0+ za>YD^ynYsXGY9JJTXvrr&^Mfvo{fz3X$pC%u8okLvCzMNpRsRF<|b^K=ssk>&D=NO?70P`b(8m+d92dicy*+8)ArtWow*ntT2B0q z$t>k$T2OQ~$Ssi<)u)+L^edrTU@l~-Q60ODM|%~(tN#q3_f9?If1k?{O3ryKxOHCT zx%`R~ay}zwJnYSm6Qc|SAMn=N<6S!p(fpafsgsC`x#vFc&j}-ZJ__pnsOtSLt+9gv z=V&l|{d46<_1l|gLdpq35Gv-qYNr0a4{ViAz{!uOonkgKY}WO+j#Nzvw$7GKP|1r$ zdfe?CD_f@Vsn0(NiKTwG!TVQ>wl$<(K?yw24N(0u9MM&*X(mBjY0uw4sXi<+WUNP0 zY&!q6@`u+wJXOa9pxO@iU%ix!a}2CH%ld#K7Gh`CUIki*HF9N4wLBoumbQxZecrnZ zr-`?{c7t&7Y6Evoc+Z2E$(WtITV@TBB2mSgP}(Jt?$zZs71W$SKPMV z6c?Ae=@#>kcyOMV8YaTM*8pu)>KEFjiRA_=(37dPdT_*Wz@l5WLYCS}RZS1*DJ+I; z;aF=FIIx2?|7{f%z1HS7>{FRO8x`t$bv_AR9^zgGBvasqKB0HOsR*b zjU*`r&sz=fr*!-1O;&eqhL{j8qYb8uct?MVdyQn#K~U$OGNy;Ad;4k69xOHSg5_c* z3n_q>55<;(*R}Xs%&Z#u*S9Y1xbpbF#!3%8`^YqTQ9~hl;U4jYMXVL@q&XPoi&De1 z8JI3ki3&*P#I7*-UoQikQzv7&?<*sTbGE@PR55PzcEfync&MqsTUKtbD=hVORn)a6 zr049w4z+tKnA_z695wY5xXI3zcZaK!Ot|AEC@L4jiFNqM!(_s|Jgz$K&hqaoqgDIY z;AfY{R0Aj1(Jzvd=SYqgv&~)dYfjw{)!;@Eau_~unB{^s$PRs@S%%|@EGtpzTA^Zn zv+Co7&8c-G%zXorsJ=bGZ-%jmy>@08UOebN2`SK*P2j;yLtM(QFHa~huJ_v+L&=$gn6rB;M{gZee=`9Kx03=m;qV_x3iY>FYDLan60_x6Pk z(mE67Q!FJ76MFKKdg!rw_*+MV5u7kwn(|sUva`TI!{-weOO)ycr5yXUAYTY*NU*$kU+34iys)9*P4|aM>NUNW2 zORRX-0swkN(cYOuSn-vCmzN|h{2vh)*?WPa80$5^i}#))SCN;vf{K%ZtV&c?F2Ib{ z&2V7UtHk6KR*K3Ce|LTFH!T_j?12~^)`jCRUa%?=+%f;t$4f(~2vR;T2 zG)kYJki)Ig9rMrie=$Wkb|Vp6BQC2!zk8?AmOlA7E_{2xK!FqfM(zG`;!nG0>AO2~ zS(g?Mud?1R?7E$OHV1IO*%JNYnb%ekycUZp(ubvKSa(aj%5~myubZSg2RStEJ{zh&Me%jo+ zXqcRoL2#szQB{l`Gos4BlOGrlsWi=v7mV(Ut6W8 z0|PCTq~%?{y3a`W2~lWiUagB|j$GJG(#6HT%oX&akF{1FD==Ak3c=z20o3)<&{A(N ztnaCPQN4fhsz`mo*C^nJ-~Svj@6JQ;i+zZ@*)yv^v`uMvZscc!2Bl0-;?KQ_+wxb} zSm~mvzIF#Dm!4EYSXu^nE+P*EZG#f)<#J{e>fpv+X>S%aAm>x1rPe@J-UD=QNvCGe zE9pLBXhjV?XjNNS_E0hH0j2qRPy0OXRY#?4(1&6K823K`8yO#cV!!DZ-C~;xUnJ^| z@aWL@VoIih!g#_Zc?SVO;2!IucZoRO55tk{JYE<0;dWc|`KZFs70E!lI3!6;P$Lc}%->FjUZlXwErW0V@ zra?pg6Fmzzw3>cx5w>UtCs@=?P^Kx(`vtT9h+|Bw;=t@N)Iq#@kvv>DArMs6U6j&Y zcCaXwhz=n>$u|R>GF-aYgz^z#$^06Auq^zzri3~ijJs(<=^=TU3^Zv<3 zv4U)XaM3qOwOCAb!mzYPIuWMEYxJ7ANp1HZIp|ednG20hw?02M$CD;a4-c`8gJ$ur zT%e65u(@*%Fq2c&$K*!`@#Ep0aW#wUo>ImjJ$k7FcL1W9c#>Ck<4VO`pT8A4OO}4cd$pZan**H6klAy5VWaU$pff~ z(iN^{XiHxBQ)fBs6KuQZsrkfqW%R5j^6X~24%=m&S@?bGDADb8s+s=q`L~9|$oYTX zIJhR_b#E9>w`OTL$(WLJ&H+k!e4FeGZ}JLGXgeapEK{A_T)oB}3iQCz_PphB zJOq@_54cWc`r3U#w6-brmz+XKK(5iDJd`yg2ai8|J4b>JRMq84^dP#6c|K0l6&lwJ zD}+iIc)lS=s_W~hbACLJ);|UBQQiTH#2r=GJ;CQV=Z0lxZ3sjr54F4_?0n%zP|ct8 z#PM$BzACvVhoT-G**ga9Ri5GNQ&;}r-uZ-AxJ5?3O9Ca#p{g!;(q{(jZMQN1>>qnH zH5Fbx+yt(E3B@fb%(CIXS~13jBxs(Tq`OCe?mkzq1A-EJwq*zuSd0O`>vC?<6{_scA3Z1;*tQ^~|W?L%o6z;;KN<=Lf1D zmf*G0QMeMR8ijX~y7!NTK1z#sd(mUi!xXYE`6qhbNTeUU@vZc>Tl+l+#}U&lXWdU* zl1*iGExiyEG8u^&KYLF}ZKe229KGR4rf)Z&w(WgOgyjf1(<8|tb^e;33Z0T|zH}=k zP9kCzmj3n6wNZ5SFVIKMs>tl&+`q(ed+3ydR6xXaEZJF}|RiOz{@ zUHR7pbk3ogFm%p!&8gbgxVvB$E7Do_o7O5d}54vqe2N(Udkd+{iliK1^ z`DdHSUYAqK@7zjA8N2~=#_(ATh>{`$gm6?(V7a?Al^i5w2d?zc( zDK#PAJIn8i4nF2HHrSbO!`O<>A{et6#yf)nS7VTn&!<5=p2M>L76J(nw-(Mr9-26oHJ?lIm?lpYu|e@ zX3UGGr8hWvseHuTgX2}4J6nV@ty&f_j4u;M zPUGexlImn6WZExTa76M_JE6#^c(?q8E2OCGW%2x{5eK8x+6w$ZD) z!=TCkWm~s`^6H9pPwt6cnuBemAI=K0jc0>a=8f+!m~L`#ags10w_?~a*ZHMcia;vD z)s?4rgx3~Z4i^Ww%br_E99Om>&U`j$HJoehQN&y9T-C{1ziGp52jQRD7Cm5=pK>%> z{rWY#5UkE@s|&n-%U^_?+m<;r1yL#yR9ENRm{Uy`>!^x6g7`b~ut7nX#bM?Y0*K-Q z5?03!Yf=hx;!R;Kb4kt^0wH+0(4l4)TmlNSa|m~|-BNyKX~$%qArA|>FA=&XoZTay zbEotf4-El#lmrrX^$~Y1tm!6-uUz>D!WwST?Edk?yT_y^*EiC&mbJOW(CQ)r0)bRA z0s@g#S^@&ljkk(FWME1h{3#pz^SO|f_4A+OOlvk^wC82hf~{$tQ0~)apZ0`X;1ss& zWW9HcO7VfSy3!L1aiB#I2LT?9)ltQvei_GyDNu>0D*P<*fTH@$%dK9G_h4BC%w z_EVb+h%W)~$D8a~AEMwdO7*D)(k>cWwW{NbNlHw)#L6coccf6o8T?6$OnT;tf5C%Q zuj_Snw``*{X-H$7OJUAkP{#7?1~?U|nB@o4^CPAlJ#i6!$RW z3+K6fr=G@DR9ID(DEf|dr8@Nq|l!Z>U(M3=sz;7j9fM5)>HI&AR3Via?hmF1jxDBsF+by9>79^ z5uF=Z=EjxINE3!Xun|JS?&=XwMp6p95E6N*6x9XnIn!C*po%8^mzx3$4$Z0`6!jvLC^&7UlCNT?EF?Gg)H@cg15dQ*O7ibRErlJ zTS;P9gQvO>;wDQp#F{as9>anqOLFp$^x_!yyk$%JawCN^xE3I-5zrB$*1p$1op8uW z!BUzl!f2J%*wdOZHfy|XzSoXrS8TTV_(|q5;e4fG(?`?s4%=*7;#;|*eHt@|YfwI7$)@H72(o5Qzv-zZwOU!8cx-WJ9G2Y~vzA=eapiF){u^$h1MEUP)>U05t~Geg{_>z^x3R*9K8&qsX*@ zr`nUzZ<}fiFxXqvI8%0}FdDWlU_Y331gs0UHt-$my5gK0JkCv8!=OKKdZH}&Ot{WJ zTI2kC@u^1ni`0E#D7q?Yjp>!X|64M-d5WCrCLtGALpNn+)_MLpgS4|hsh)rzud`o; zIwx;j;(J7zE89HHKdP(&9e}sj4}v97!D+Aoii?p#hvnZ3Q`^ujEch4~N;KTPukZ0m!&lCU?CNc!CVJs*~?kZ!bz+&XUU1YAuCq1sm)AU!Qsdx3M_g49X zT>XRG@I!wHGbS8L3vT_sPBBn2a(Itk-%TdD4U`_&T-l*(uAoA zAVt*r*8VZRG3t#H;WL2=!^;KS2XHx)SN(Dl5ZswjrGB0w$16_GgrG}F%PFD63aP|t zTjvZ7>2e0TA=#wl-tyFJ2+w$e173u+sp-j;EAS7MYBq!8`E8E zAW+Y5R~|vNSDR%;n7jy}So#Hu91x^|ky1QNBn@94Q&jQZm|y9@s5YM6 z_G$j52vShrU~~7=418b}j0fJbkEVw$ zK$Q^E+;Je+S;Ze&>{knSw2F8-0QcSRWskG=srb9sX7?mJvbhm>94z&P1?G4~&1Stt zB~1CW5=!PPFYtb$s95pwwWs8z-@{fdp#uNT$Xo0$fLp9ZrhrMQ5NNH>EH~dMfz@*L z-P>3j@$prqltH-1g6A%bFq)h7+U@ItF|qW?to_+Px3;2t#qX;^ysG4Mwc+b5Na;q} zl7n_vA%GO{vo%owcMk*K0Da9~G2_!*?FMTh(Dx$vyFNo~X|iH?EN^ZUXJ?XD@wG>s z6Y~!|SeMY>{S6WtHq1^jMhZK49Yc>0$O$;{K*U|M7H-aKYFIt`tiJf9|8QRZtXBMF zzw^_5^dt5wd=P>*WPZ00dg(2H?F|=FdW{PS7I`O;_)K&8OuMGC{6s_5E&m)o>Q#T- zjQUSH7*_N>E&tQ1wV2OE!f4Qz5nG- zD9lMB?d!{azg=VwMj|)(gv*;5S5onYJ8`&M9!(;1`|PgSPo*f3Cw2NvzBWa*P~r`5 zv#(N-LL%vQW8o|`TRQ5F+&$%Ve90g8aP9pPtx#t7jQA2VYmCjL?<*KYakcx4mdRMu z9gUzrYE?sbfO3~7EUypVgNE+V#2SssOsy+$X{$Y9)}-tjW7SkJPG{(_aq-KNXTU~O-W1Fg zzF_dm4LyI{!M!)D-Im82rtTDh2anwj)wTzXUWl&MlV=aG<}8dR!8adspPBw875aB? z1C`Y80#aN8h(DWu*!-tm^!NR{$8#ai2w__Ae)l%wfLXce{{{C#IL`P9jTYzw`{(Z& z7P`F^^oSSu1F{#^Z#-mrl;3U%hH%F^BDRPMr9FoPs!BoZ@*i*WZS{mn%fo=vco9De;j8;vebiI#F%3>diec>!!}+Z&1I`^!DR0QXe_pRc=0A9U2n2Q9#xNEO5Ecvk z+=GEXt~uj`si0IQ6)24H!4wj}pP&Be2`rNm0!m$)p@e)@n7T3f3Se$T&BBlK*c+5E~ zi#@89XzLvbVH^?08C-m(pIfT;46;qNV!|2*dlg?ku?#CB+c>SD>Mcm}SqvC^OaF)K z{IE33{(6!@TWO{-^N`EGL*ScEU)>6TwGBn+3E6R{qqxKtfY?L<)|5+krBNhfP^Da$i{*gDlYlbwG%Q?J%H(NA1X^K?B zIynd-<{oLKwA9LkHw?Rera-*9(GHt{w`t4waL%w2)L1qs9HOgjCmdzh3AOEDdJ~LH zqnJVn^tzOsY~DwzsFl9b(@E8j(cu_Q1>vf?=d)$Fo`qiyHv@&8TT>y zrm*;CO8%U2Fne&#DDp}22p?-R7x9p}hRcBthekTdy@|H9lNCeqpd)MD&dTa=inA%ecAR5`$_sD zng*Fa8Q>f&Rdj8LiWT0^SU`9g@5e7R5%1SUmR!KU9{-XWQO=S~-n4m-s z!@t^tQX7~nk_1DZb?<}iM*O_nt9Fq%9tp4T?Q`!yXj#_7qVX2+VWvehsi?bGngmD@ zyRzd}M>8%pe6y;JaQVGePcvq@R>vjtn68u^*V{LbbMF_?Fz!g$Nees*QF*rfn~X&n z%%A&>=B3nul~ESC)CIIGS(Za^O@L$(k^QD$s&mhV$U2k<^H_$&>pu<_Y<9@k7_pT* zHDu!=DejtJ=bQ=?b+Lp!CFzTph8{d{?P0U>cFYxZ9}TjYI(o9iN_&onf=mMC*W!#T;*4b#p!d@kqGD+_KnpVa7`&_6ZXZ1Ty4! zo5UZAY7WOKlZe9!)qQ5)9E$peyl+M@!MDM|9e%U;yOlc|k;{K(fkrf2IB7@%N3NC+ z8d)ysSvs^d#5tqj*ilsFV%ywN@5hm5x&T6P4$^(DsOWGzJ0KfI-L)rbNIUGm?b%5} zeCJ2cpZKTjcCg<^EAhCr!RQ`+^vYFLi&Y5bJCFGvhKhc0<-M2#?~vua(eT6jO^VhI z!=vKJI8lU**cD!4+5#Ce?(Q!lB*V8QUGw%2?bm_Ly&;Wr=hVX*iA22KJN<*&ibwW3 z0BsnFI+!9Ze-HthgKeSpHB}~~Z7GHr9Rq6X0Mhz_BUv-@;Qv~v!KxizVZ0o{vt!Qy zp&d^8?)kowhQc1*;9G_zOmrxD{FoK8p%OBK>fE+X7wJir*hV>u7jf5o_U)DF`LtW; zE0q?;x$$ei12M}?)}GwWKdCRJM(GU@hR?;9?3g8qj@tcrPgsQum7PV&D%j-o9iqR+ z`B3^^I%f~1;Du*oSmIUmfRhZ}urc}QSd1%v@P+p~@PzdB_WnDcb7MbF$7=kTf;ttN z?y_h8D(ZJ8MbF9~7m#A%wz~$Eel9Nz7oqc)!c*;O^TV$Aq^@&N)TB5i?h${a_YH9v z&>xk8{=_^v2^r1?IVZS>+hU=RZSadcMoAw3y2d@nid)FqfqPLrE2tY%xeIa+b-1QK zWz0468IC%Gy+$RV)HTHz(oLn)HNo29Xp!wA=6x^asu0TSNABC+0Guwm7ARx}J0V~E z^BPK$w)o@N6OmUj)1P)b9WL)Eo-;hlNq54!JL8DlTiCGn^xQj;)eRT%PBgj4zzgV# z(Z51Y9q(kcOWmc9A~gH8cZKj2GLiKjn`cF>Z8u+MRA?%g(HmQi@^djvisXUR>J1j} zYLp%5y%SkH$}L7zB$beG_v6D$bb>83d8FLx3SC;Ul-0TW5VhepTGr&?ojrbuc9Qe2 zX3gFT-NT%l6Yop@Y~SeN*iNs%e{XSup->El9_fUysMC$gz7GSxxCR)!Y*sYv8A{-8F)_z)#5Sf3d%DOSG~Z=!I42CrwaQ zi15oQ+a?zm_GuyP_O#kohfLroVY5k6)@n}P=6P%cNA^RN6VE8V3nl24$V!q3c1}wY zaR?D@L)5`NkJbLl=y!F4O_5kLx#Ol>RQ&HJi-Hx2EI%^GZ| zo6qczgiCfVm}lE{-=B;{@pK9eEs0Fqr@#h`yBRqNGXgy9N=+-u;{Ph`9l$GFwzc6T zopd@KTOHfBZQHgxM#r{o+qP}nwmQa_z3)9|K>uG97C!qbvBj zibL)_YE=>1=gBpn0KIOL>}3AH0Zkjs0Q0MbqQ>9hJe$jqOR8#pTFNh-SphkbPpB$m zfO%I{J}9CeR252^H=$oz2EcM;O}#^vx3tI=HG}WC3)Ew}?=S5^9%{%oYwUifK8o}~QX;9!QVvlf% zMuLjFK1geI2oW*4&MrO>a1^~Uqf-NqE4QZ^y@JE1O_(-5fu`;oQGer{*dmSS&~F@; zGbDI@9g$rA8JShZN*d;h=g0$Xz&f6|F#5KlgNlm1;QLdEx*9Q5T|}>}Bx}|zGDBUA ztn@^ERGw{A{$>G^ZtevVzV*#zF-_t`N`^}P$By};mL9L|0LKOUg5dXkop_nz+P#q3 z{i_sEtZ>zP?}@Wm^E2*B6CY@kf}r{FL)UdlPlvnKz4z>%$fp=EY;yqiZI(eDac%2r-S4O{W+ULYL-nUBzD25rz}Z;^&-SW`eJ8H0H5d&OSr z&(wxS=_4PRoL2`2Nz3Oq2qR4iqaE^U#||i$aX?Nzo>mChoSYDkl6-C$mP`eWmfSa3 z%d9UDp6>-^J60eMQfT^q9x|QpB|MzShn}DN0(iVhBA?1?yuPfNB$vcsI2UDy%VJMo z4C%dcbCkVHW1R@0nZH6n-)jM{exipZjIpvMtpypYvryG{EnL2n@xkCbIb~*@xtkGi za9q~C&qO~>w@XRmUOCgW>qz65TK;he(n9q*b}sZ@DR)TM(ji8X!DjlX?sV%%@YsPt z7hk_vNFX4079(j>5NHO!mR5EIMf7wRwObdF&Fo72vpy<*rAIv`k2xkUaftlUgN!c5 zf)>izJotk3I9^vvs2$ui&d}to#9ZjrpL=J>2P*@dF z)61B0KycM~SGTgg2tTc>-;LV##2D(z@*1uVrlF}Na!_B3z;tV=!<3Oz3N`N$)qxP5 z1vG(Jpqm}z3#<9p;{~BaSfR5le2biDP_V#N;?u~w73bZCo1zMiwD3wJ&#EN)amq}S zyDNEZWLZBAexjv5d83JZhC8rwzJ`xgidovOS(#|lE*F9fnSZd|Q1MHEB;<)>N+I>V zb_WhlQcLd_DETr06)D+x7EwtAAs@Fr$zBw}K%YwtfnwKrN}JNmCYjni#)`AY$|R6j zZK_;)S6Ij@WV!k_-{8|c`hjvg6U*|RLG>E0Siynhj8x_8j0T35I{3PMWfQ}cL}q~! zlWK_ZX@@ik2O5UCZwqTy*+!w&P)#g?^eMZ9%w0mT8p78N?$_i4sf~oeMJ~^Ww3Tu8M_eMJs+;^;PxT(riyx8cupDp7-#p zDVO~a0!?mf8!vdEgp&OOn|%{Zu9KvZ2*Wm=bbLS$=QiYtY3{}5FMA!=5hIr~$Ih0;&eVpsNYIF?J@3BMy zbR)amH=SL|C{r{V<&hl*#{xs6t-XhKr^Z=|mI3*o9x;*g(CRzE_TWZH_5IbM_1!3A z{Q;uOh@O+~vZ%8FE<^5;C2jYo*mlF#4K}B1UD{`bNEotN9_ALwPfCIS_I`w>^p%>Q z)aWl`ttX@fiIeRkXxH=<4mbplFwJJ8jR;NCiq)3qLT2nnO4QJAFTP%^lGT@N-7%>RT*TJ=c!d8I@DnD}hyI$M(r-#pCB^ zHTU#ajE>1SFDgF(V&jqW(X3ttDZ$^gIPZ5d(c&JX?r|HH7YS|L2m>rugx6`2GWe8@B@L5tz zT5nK>mHajli-ZA18$G9R9x_uMV=t%T?J$lx!Ac!GtW2jE12RmrS+9d9B3aB*qy)~N zCjln7S%+f;bJ%^1qYu6eUwsqlOV25skC7~ya%>}-A&CjkmbWDxF-g0@!#}v=d}tlm zwL#is_7<38^+>DT=RLEq6>d= z3eW3G2P-Gn52SmCsZz_6yEa^Z^~3>>djFo0&C(0*z=`0uz++_LxY8M^NIC;*{ItgtmirVcQQ46OnwClltsLQe?4x#RqxM+9THo z4+=Tadr8#7*jnK<41q-=)qH8e_i5srY2_Kj`TEf6guTW2qNIPvWZhQ0(-Z{Pvi+HO zt6Skz`lRubVC(nG-O47V7J3$Rd`(;Oft>pJmuq6&CDHnoUi5(OEoG<^vbSZHaHbYt zpY~IJe=B=0$m^hnG>M|eVY?v0epjr_d1qXiRyD>6QG|=Qu^RY3uHk)WxV~qDo7SLh zpK9lJ{;CRdO?GaqgfoPU3xthTvU(Av?(QI@I3B7!daqr~^@wP+>i6H7GKh|PW1|=W z0Ls08mE`upTlb%6>HeAICTDMLZT0_qQ*etubVSea!exAfR>~O!WR~}BLx{5Ok+(8jH`>2;d;_8Gf!9$_R*zkZXSy3qihNG9 zRy3*Fk5SkNmN+ghre_3#O>ely_KO#wRMdpa;zL=pEr1MzQQ>;WX_bujFfa`xy=E%7 zMNWCBzl^%54%XuQk`YmTAoBdcZ&n{;0vHZFQCC2 z_@oALOv&9L$QR+F^jij?!vL#N_ea;60Dq+%x?d>gyN;d#kENy6$7%K^R+fq;wkCE~ z|9dcCTCBy#vGxc9?~b4PMRjtrZMQ#t9h|q2hrzOD4tP&H&{?I4bKoFa-j>$bQ|mtW@oJxt>qq{6I~S(J#_? z^DKl7o<~*?Xn|n0lUm9?aar#O(T9g6WE1Rg&qaGChlXFkp`z5qPv>qMVnSm}qa{El zeF9~|D3oWJ&Xiz<9U(0ktY5?mW!;Wn)Nyeu?p=|M!J1Z9+8GwrGS8WyNlvK4-pvTV z0efY>qxM3zAZfx51%2=aFyA>Hx{0Ch#;q6ogdKycPxC_k9;KUvQ(klv##pSMZuC1& zwA*kQ<>(ACh0?vTs_sd?f%z<%K+pPAhye}PT(lb!vf?h3V`n%UT$OJ zZjeQca^m`HF&rL)z}Q>oWYDhXVrovM^Pn)k;K(uWt9YC(M#v1v_`wr{1{> zlwp1J#v;RNiPf*Pn$kZ;0=Epn@y_YC8^hSVVGG(?ym=h^^bm@)TJ?fQ1{tU&Bh}&W zePR_z-5`COD2R*E_v8>VvbDWSBP#_5zs`_C)(p^1QsBl6Fc>hop+U|?1slr@#3y=h6ej!G-axw$7FskJNmY2pt*yuC5>KiGja4)YL zA4(b;O44utN=4^7e){_Uu6m;NeERzQp6-m=Dd5D1BSSnEh9nCbR%^c~9Cp07YTQR# zlk+UxRyaF*?Fc}R7b8G*?1(hdk+jNRZvGQg?tXiA4w2>o(#Cu zxu!JIoX|w27T+>+Nqr~KU_ALVfoTq}Yq#Ms6rQ&&f~C5~IjM;XPAYN04JHW18o*)3 z5kc&kDMh9=kFIWA5~GwA3gE4YvX3+&|&Jt;8s6|P@VZ?7)}YstZZ z;Jqrnj-?ryl%?idpxC+IWIu%8r8^`tHfFyezOsq8JO-_(K}{sli9}?@t=%%QT-PUO z4kKdz_#0xo*s-QoO9j!-5o7{6^aL?IytHEe*q)YHL9@VckL$d7vH2h=@MXQjNo~Lh z)rTX>w+6YE^P|&G+Wo=``$#g5h_s0A%`>2LL}O}hq^5iyp60V9J_?(B|FQ>aP;QJC zgFH!9Tx{Qn%Wg83!w6FKods+$#ESeL#v=wD^BxD$4Rghe9NDr7u)*7Gjs-E+g3#)^ z8fn6my9a%*C_-zD0XvS97EK+Cb&V<>7zd>dqE%ug>ror_Za~BuddKxRLzx~GirWh{A8dgoYngO4mr3&ro|v`)gsfw_2Sw+! z^6`Bm%o4lY!(ZUHPZf6?_PwS2Y{2c-0gP3G(S@Sg!Bf(c(qk2EO=H&Dv*xLCl}j49ZBpEIcktu zh_0O*4qjO+&51H~HNKbP_U%QuybV1+R@! zpm$2_t|=%4Z0;i}XN?0)^y$7fq+6t;rJo{+v?syGyC|$1FiNGhhZ8kQN|k@FFrG!} zW^!KJQBNyC?0+q;!Ox#Jy*5XY+ER0yo~=QgU>OezfaIf;wseQc=D<^kQ}o4l9*K3CE-%a0blS1 z99F~6jW>w*VvK_xf)Q14Ie$IKSYU$#@Aa-?P%sy1<}wwYe`F)bb`9F&yxnr&S~tOK zTzy69uC%__^A)t9ZHCFIuE)a4l;IXco+2jB3Ay5tEqHj^BTmU$V($ovgM?JN%G!l7 zc>Z1yzk{&m)B7sT*WaQ}q0#8g92?qi+>2Te65b<2D7k+DAHE(%T8;@$uD#-g|5~WS zj#Q{K0G@u$$s6P$KS)`H+@k*jE;o9rPIB`{ECiH#j_etJ5#UosIbFEtiLqn~N-&5# zO;T?6#kKzfGs`m=?OY6`pauP0Y$a)XJ98t+c2ybvP)_Ha^c`_-o91Z&NwJaeN{xhh z?L-hUZq*XwQzC!vL;L3C9x;3?tszl$a7qD^HF!alzZ9HBaXSA>*9O7z1?-vaBL9Z{ zaiSOoJQ_g`{ZAA0vPJH;Wt>?%cZb4@1WAz3eO*0x9A2cB7~o&qUP^J0!JmYgc~F@} zZYN?)B!hiR%(0cavFoZWc6m$>v-!CW`6uw=5wT+}qY!hG>;f%im%b|9RWdyn6jm1$ zRnHN|ad@xfRtu%D?mP6i^@O5La7jt31s4*`vr!WAtU(w4J+;`YN?Jkk`% zd*n18jJq97+up!e9L9kjU8wU5p~Y`nB`J8MxgMF`Z&CbhBA*}OQFq4(Kcm_}Rxe_P zeXeq3WgOHC=Q#w}7j_Mm9eOrgjY9%gyb7yg0CyUO9T-mEf_zxfym(6jZuQfA@vFaX zKx5o$B%?SN4QZL)F0XBD0UNl&Fzv@KJI)e3jg&}~#3T!Tfem4w!oGG!dW5eS?0`F5 z!N0b`zdn1H+m$+c)ed3b3(V#7#iONYX~wG#&X`*0JUQIbVlUP0dUGLA=`ph(NF(ce zVH0@*jXTV!9eWA+`f3IDj0Ic5(rsS#wPIv=^ZkSG81%~w^8lRs2e;td$I&4Fng95| zty=_549xZavTiApw~&LAL2##}%03AOGmTb`RICiv&x=c@N_v2c%HfasY2c->z@r`C zm!C1&QIr&&lzaqwo0B}=R_f_VMfcNd&GsVwnf-%DaO>^uW(V1mq!*PJM2|~V7|Rs( zCMQ0;I4{R|Y1VcsH_Na#v3#$Zq%AiEqz9zmx`yG|JUmUOa_?|A%cYdI5|p)^GQSUHu;ikE%lNzm=oF!FP*(70ZkkgSRI8;&?&iv5OIuX<*h==7uUd$eUNuiW? zX$uIYpBFvBr1<-th;=7k&Zlxp-3z10d$j81mAT$E*U_Fmw|4E~e9b|+QUC4LOJm8g z3n54G=z6XObW0z3F8KhnnJvK@^~SDADq_EI!S$n$BWP{3{IT%h{B2@zzDFHUm}-0( zMDdPDaZ1r?Tf?r6cd9c%iGk_G_)Jm~N9(VNuy$EHFVuEmm}`qRc2CKdp^8@vfZEz! zzFUwze>lHV&(~d3FMp$bj-|>hZzV-{bmxI%alC<_-Rf~~AdcOga^IHMSr?G&I2#lIuFLAu+jE5va zXC|T?&6*zNQ+QjOGj%tPUW!4N=XmlUaYxlZtA&uVw-a|i`+lC zA?pJI%i+I8FnfjRK2fOx3t0!7x(zcygg#4IlU>1gISw~=cuxs~ICjhqvg6vs)%F$M zG5`gE@#cM%#=G6Z$E0SY0w47_6 zet{I8s;!B$gPhDEB^$F2qg9+0caGMP;!=&TDgtk`MH$F|ZMzCzN{U#@kaHfLe1F&v z>P4z_Lm}^G%ngp!)StIHiTcrmb1+jQF|lMWGqKnLi4O zKaQL7Mv~NUn=~|#hsB@vMr&5bUNETDmmI4FC*_b8Gi}NNK3iaqx!&gFAxo_#>}r@W zCloe7_#zE92D>{3X{N$MVOf&j4_feQvKAlk!tV`b96oj!7p{tkO{D-@6#KH*sfBHP zE?kwI*^f#AR*Yg`lNK#gsoa-sZHpYU9xoluXTib3%ruLs*L+-X&@IKlvmAg6sLEe= zs!H)iq`NrtxCkkVoB&g)CR3~zcq~TK4nL+SHF`!?gl7a}^-${+X0kUsD3fY2Q?+-ru$RNZG{QBbh25XDPQ zf?^|~&n%Z-z|6?6-o*$b^;L?KZXyyFceSeGqb>u*dS&*amW{6*xwx{iU)heYTtVjI zOAPFo4~_$|F$Beu9`-T`KvM=vQh?+3AWpnZ3wjvQbMQcSz1*Ne8JP_yKF<0a_TFx$ z>i{nD;q0ZvfX~fQxt{*Y^TA28*K7irLtwF1#k-cf3(vv>$~eIYDG{E`c&s>6b$Vb! z-nTfxpk4F@-Om6rcwC+n4BBn1%01l8<1GzUhvJ7XY`BpL2HvgrVeQyHXN-`nOF=Fdg$hM~O?(-5(4bB4}n+(lSHVa+RU$1jjNQ#F z+9=Rh<`OPAb}6V@H`z3l2UN2ygUv;UWXtoD;S^zW`kB=FBwmB3WMZl-t<|_{vSq$r zc_lStwD_=O{$kD_k0$n=^Z>E0GJIRvw#FF`y(cHP4m(XqVh@0AgO784H-*qnFUXfQ z7oU}u?-7VoTyWE<1PcAcXR@g|?3!S~T0r3s+!6N&L5&?lPTyv`jVb$4pFUIX1nweM*c z+YWFaV>j*N^Pm^2ps*W^VBQbRsFbx+l{n^xJ z&n~m|@xt>y^4>k$6i*Hn2UuxQ4O%4EEnhRAQK(!XR&@ueovr*FUM zEODd80M4{+Yp2inIHwb!j%JK$@7DWLaLetA(9^VBQKxv(@a29tbMeyD@6gmu(BxGy zs>{la9x>Vsk`jX|UKynu!)FSCMm>=5Ah)l`H)(H8&>IvbU25+8C7^Kr>Kd6t+67Igprlf;U2d0NM$ag_1G+ ztRYXRRJt2nO1vC@g-gtS?McweBhwwN6bmHG-jum%1=eLk>7(ihijj(-L zkf_-Gt>iApa^%l$%XQ%i3nR`F){;RnaW47{qHm(0B|!p5s_U^jnblr6vH?FFE8i z++g~vZIGUvpE7A(!2MKB2Z|_{Wz{lW*!lxtH%v<-5Ow5LQQ~T;*1<>^WMg_eHRd}} zx zYW@PcpDaNVKb-5l5HgMz(PA5>zcZwLWmBPSF?mHvXYsgdToiK`GQp=^dFxO)s$l8- z63bLZ%6qh-VzNmQ*b2U4S>jiEip(;3F%vMCM9W^FvYZ&mN!FxHI1-vxP_q*KT1~eB ziBaoaVpI+pX`=4{-}=wI!gc3R#qf3(wpUjTZF+!h5|fw-NF zSGB`Sjm#%DFUN4Ptv&qq&62bkl&#}M!VtZdwQFY-yn`G$Dr;0c7QAZFr$!n~VqP|=)RY;pd@(->AD~RMbEDwP zCn{-Ta1%w?a%71oSsZdp0xOtsnwtx&dyr%odB4sWg$+ZE=$uBuDX}RrQd)g}vdlqy zMtwzU&V0I2k~14@C=9l234U2O8#-x<-e2cIwk&LKHds~5hylDOFH?rIV}7mrS{XBiu~5AqWm8(L3wfwKg>HE z*G2%=s+DZEx1Yu#@HSX3^vyF8eZ_3oxi5CpLGwRA`x-L{%G;B3J&K`AggiL zlK%?Qyb*p&ekFm=KhdK93RqI$t~n6gT(#3GZ&zGYA6Y^W9660Qs2LQC5+TLVvIcF@5o=VWwKS#qIj6#NN=XBS zVM01?u$U7JE8}kWHf4yYV1p$+RnzB@gPElbYRC0%dI*eZ(q#Gh-q7{EOR`f<_;v+> zmxGn)3DxJE`c#_fw5(gZ87G)mXcW+1=dC2BgO^fxRXP zjXftxTj&?js?W%rJvBhbX|QT#Kk&9gV*OusZY%Z-2rc$UfZC8qQusBM->?qb>80o= zfu@9?LkLxI(eiqTj{PYD)F^;{du{Dg9$AyEt`cBa`lUpO2B;Zm=gI22yfX6(;4ZH`MwOMA-;$ERZC5M`OIS;z_lOc*s2kFs9`tKZ6$o_bEuD(O(2~=Hay># zw!Fvj6Q@GNc4MB0v%7?7E%F?eZR-NQvc#U{U02ZG_6BCf1YI5A008S)|G)MIR{Hkl z27lSO$Hi*M%}B%eZ_H%-!HJgPx@QIk$0HPNf|7p0%|sdr@NSc)5*H)yVk{C-!?<`6 z87EG=>c-lP_`b>k1~TIxj%*_s4x-_%l;r<3h9fS;|7q20%_NdV&{hE6)lPo8> zP%KE&W?%;LV{W-T?lyG`MPVeI`wK*a|Fd^DkWbkVe2c0EJPe|j*b=~=zeS?LS~@H< zr&*$)z2v57wcMq$q;`?UEZ0-gI10#Gpdc)71-Fv@fHklzk4o8vkbn>=C0XbfZ0h7u zyVBvh^uTOzFpvS*Bb;{+QAFunH$F1A$>3&hSf*Fn0~EeP1JN+!kkU8 ziss8yL650z-KQG=qKdg;=e70e>(xX4c~axNR6}&Pv$ar5Rfd*KE*eqR*EHWRT{#}! zLP?4;s*X7U?Mg?cm^#4#xW&Q@nQl1-U(9Gjt)yh8DN1LKYtj8VdjXn`3nC&b3qe=# zl@r~cXZcf&iRP}bP?)*V`*NT$`#za_6Z#e6FWvfaQ9Hz-asN*J#^NQ|ig zBtBj230~%&pP-uRO_&0gyGCSM-vLjC4ClQ2eLJ58db6YWv7IOR<#zrT-173jJ%5)q z(6jok&u3*}ZDna-Y4;b~EmHuKg6Bc`!Ke21L>8o0l_zgoCO&5!R6!aLk-`|jj2>;R z91RSek;QTq-6_&DZ;aF&--on7;LYPCo9kZMpvF&sXOHb zX)k7)Fc(go7MAfd3OaGqX5}iIWUZTxeJlm)Hs+QgsdXk`17Y4YxvCR&d;utetdkOL zj2p=iSEb)(2xc^;CT@a9!1FizAbPa>>dd>={O%FYRSj5;;{$0{`XBh9B|uv$RO$%~ zQ3T~YK1J-I#CuD3#nX8l%uI!FpfzXG`S$tO4OwTc6+sPUQD?iq;LLxb7SBU+yqLM6 z$;1ie!H~JVjn4TLYt95Sp?#X3qH|c5x?XD`l-T|KGz~FX(xT+BI1_uGPHF(?0Ua3f zI42GMTOW+G!nMTta#K^I8WY=b;m((EQrgf+wnj73_nSCQ?2RQ~a2`&<9!Go&zOEmf#B&4j-;#WS2xw#C zjdW=zb()DoT>Ruwc?^XHZhnN#ih(LFwy|KR9#=zsn190Xu8kUQC$G0yQHylCEt!xN zOHYfYp9@x^hiN*?W_v0utRm?}VED0AXlf&SXVxyGO8%NgP(BD(L<59pVNZ}R!cQEWucTo(XwRWu z?%Nwy;%)-1IB;^%HT?C!7$c&_y=>CR$2~mXaAjFm76=ImsSuvxqOD+LScFluV6d>( zy9ciwzj^3}LN8eBR{PIn9ADdbi$<&|=Qv@>On=A!DZE6@9^D)UEQ=z?2zP6qibC%~ z%j3^mD@GMa^&aIN6(23@ZmfIF?F4?1k2mbCI&X~w3s!j*I(?VAs~PE+5bK9L+#f2W z*bvn#pf1Z8-55$%Oe|tRk!v`unmFk*tm+$i3-vI64y(GYM@pyGXeljXm{M{TJYFVO z&Qu@A@o_fr)@MsNteCK~oAE-1OtE!}RYudsl~L8~+Snor^S)wNrwj)ExP(JtGXsy` zg9E4_jGJRWEI$hDS2yclFx>xc`Tu0N|BKDXDVfQu&!Mh?3ZwXoeDg<%jzkG1?B@2) z???7UAk?OYq2>$Xq0lt6)V51Z8pTU4PORr#emd+a7AO;Z<}Ojl@#5E;D`xmnUeERP zFm)H9VWHHr{AR3_VK#SeJD%2)rn5~O(l^?4aCP>2c42ew_U=*9`gs$`O)8Th3DZg< zT*FGJnrT_DOwq8KAt9_!Enr%)7iyp%UXd}OK0q%?nK7x{m)IFMW@DlGlE105IJ%!v z!wRDkc>D|G?Y(2Te+wv^qazgLDbknQp}6_RI20>LqnWB+rxb1#F&|J&ujKO*|7J%E z=oV;shD9cWGk*FWiaDSWmuJxD=I0hiNW{}?hF(HB8`o(`{k{G?p!M=`>-)pW^{#Vj zXJh$2w~jd6T>?M4Gs+L@rVvVlGrJYuatwA=jFDTy#?}aT@Icsc{xjj!fa)rJUhO?^ zJ&h)1c2QUTxcVYl-9znQp|(2-ST+J^gTkG5eV6IrkvN{@qyB48jUjbh z)BS|JYqdQVx-+Jgu~|4Wj`Wyo-x|f;&(r4w?QxQ7plLzHp%h^>4nQF*dO^zde3{u* z4JM7>;yj^^^wN+KnUw%A%`dY6;}nSba-i#!359sPn&q4{gOI$g`}1{73}cYdr;dA) zlqn-LxD!wLL*dMi+0xwNqAGD=*=XdtY5=n{-1VEX`kh(1fC@dQyD+t46G!;AfZ7UMycvD!r z>&bx`NNOZl&?c*prNvo3S~tnJLa+gw#vQGDs}Fzjw8?M`v2^w#84n35wDMIdM`gs4%%D>{(L*Klj^<(_ zwloiPQSVRjYa2GQ%C0o1Po(A`N1Ni0Fa!DwmH;jO9m?sVxxn*d6Lb;9a@5ET`i$*cdI*$c~p zYvL{9^phIV%g~9&H)kt`^tbEYhiB6%(0JK@9SI?{_6j{QL5guqIe~imMzA^`88z2k zKt;W+VwP&lN?^c2Ver-5QR~YHniz90-cl~+^Cba0KDc%=NncNkhNG|Bxq}A0fs)oq zSAugR2b^QF-IyEFc_l48DLl06m9J<}l|HSd8W?mtZ+_7?#Kv5HWlrsScSx-S2P0)! zJF zoKQ-qYihNQPE7-`VonkhI!#wz-EYxArPAGm(8LUJW&v*N@gf6!x^%_31+_P3UM%@djo55%=V%lF7P5g zH3rsPlVNQ_U%k?nGaoDC_5~TpwAK$_8TUHhIrB6plmZ)3;@I0(=u0-$>uy0 zyl@;~ZcBH(Ds3B1{TN^gYVv7Q#(dvng`VB37?u31c^aP)-TOT-nc);+72i!IAmX4J z$pr#IE9Aj;B>49>FZGBs(nz4SLmO$}6t7&$ ztas+(>JaXRp7a9MV0`W0_pEaE(X+*4(PE*nFU(#-Esd6>7b9|;4x;-;R89^Q+xLl` zR|6~WaUiigVu!uH!I1q3Xx-S;5q&ckFn;`7-z5F3?&>E6+HtxsgP=XAOp26b@M@56_~jWyT!P`W`%Ed` z6OQpN1)C}2Nhg?c0A(1js1mQKd9UDTFL@=SCy@GWBsXfI-5TND?r{RH5xT|OljQkG zu}>9&S7zZJa@D4mbiEtfoJ|{0ZmDh;iT!>09t+vCJ(tJk(-_Vt$0kV8BTOD89y8z0 z=TY5@5eFXIrnh^^&yy3hKUENIYPi3J+q`j%cVSthz6F*&!24esg>F&>ZdVY(nUVB! zgvuqEmOFcy<4v~pXAK-ht}~Ya6$t#m@VEr>!28)z!}`{B z{6JK)qg5K<5i{;l3I1*`VA~mqL9Kt$Ll|J*AG9CedRT&>+h^|_d={N8H{5JDd&|h8 zkvejb6zRmrFsgzo9)Y-pO;RyKJ|=~UsonalF_;_%fq2}I)`Jf=lZMRamNu5dum$T`a9b(}fSpzsM?urpT-(ONO z?}x9zUEKV>brcS8e0P(NXc)Ol#e^QYvSHv1U(n>`f08MGSrpI57J?>oM%^$$A8hiY zO1rMJWotRWURFjrb2C^ACSREvTQq#Rshn#aZ$V^Mp}z4SuDl>ELwfx^P9AteNQ~x# z6rKXFM_;=yphG%{SAO8#afdcy%gr|>B zEPi)aQJ7d7TKx|l{okwqR^uOS|7*)PKR&klZbbE~8c=@`i15*g-)dOtni}ZYIU4Bx zvM$_T>dO90T^(DCU)KK!zxh-7ZL{!K*}v7dw=*&S7Z8`aa!@EH0D%5*004qNK>z@( z{AUmzIf*|}`TSicyU+NGogYxjA1WvR3nV++e{9!3Apep$@pt4x9q%hv5C8z856|Uq zi3WcZMfU%Q%x7$%XZA z|6=3Q{QKcGbrHOBi3%k5cM~1eJK3?L&Qh&&7aDP&Hn}d ze}w&e^%U=V5bJ#;?ywsG0Q?s5@<-vZ|3Aq1_u!b5CZF^_Iw1X_{|{68QvuBRFW|q= zNdM4}q>hDwoWcJl4gGy+=G(<@3Vlqm4KRQ2!EoOHLCC-FfmNmU1jvW_M?OZ+Zyv-y zie>q)!T;t@{N*?T8~kDi^)ah(eEz$XrJ7$$`KRyVAF7i3$RRTL_kGd(=`a8G(T^om z0D#|oB7YQ#hX0$Ce~|2XUXIqmE>2DLv*&GLUl{Aa4! zzi;KAv$cLpSo@n^PIalScvv3{Ue}?`=^4H&o`k#YJ|2mh!A^lpw zzb3%^9sAEgaeu`g#{4z*zXqxP9sAD@)&4rk7~=jK``-lF{T=+z?*G4ndsF@w@PB&}{|vOh2LGG=|HZfXcPW2fHvZKzsThAP<)8NdH++zfnFIg;>*L4u!{R%+ JKW^Os{|_cxuZaKv literal 0 HcmV?d00001 diff --git a/tests/providers/nashorn-core-15.4.jar b/tests/providers/nashorn-core-15.4.jar new file mode 100644 index 0000000000000000000000000000000000000000..b472660654ef96a02ce1c6464eba68620186bd00 GIT binary patch literal 2167292 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⪼w7>j`3KzB>g!?fQvgf#NxFj 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

1A{h3WV?Q2ihdI_%!Dw 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

AsrsW$xhSe)P6K+3~JR+3pcX z!j+hIhy@0Q-)?M<1a>7OK<-~IcHOk0;oO$_IFWy&zL$52eHLC>+gr^jx1PfMHInQX zONDDwTc?P4*dQrW9JoS}KHC5V zZ}F3VYd+h%HnHfvM%K5@O1EBWU^USNd+X9{d-cf3V=ls~K^`B6@nOMq5TMwqWh{+2 z%f3XcW)Rut&J>F6z4m&Qwqn_u8gk)<#l7BHMP#ATEn@QfAc==+t*yE+D;_iC_8$Z9 zr=XMGznW7rxw(VviE7%6^5siV)!MjhYN{x2oZc8q7MAFj8nS71Dhzvi#J+f2r)im( z_?WZ5>by?Gf$E2v4jO9}wqF+;eVm2})T&$bXq8kp@vkQenu|H7QT%H>pc=}}>ZSXR zZ6ciDj_xclCewup;{%oOpi^_el|68#_JA^`4-N6q2W)u|FFJ5;4nSy5p)>}Q_ZaI! z!17?U+o9YZ(h0`(!`$+KM%%&sW|jAW^T56j>iabRQX$EY(&Pnt@*q#XY2^$w-AU#B z9E{-P%&s1~zhAaz^ae_M(EUxYA2R)t{T<&M%72=DC)tCU-44foPurf|8$0boUFJd9 z)8Dpl*n=zoNc6=`KQjCF>5+EZr}sCI;*_)~>Qa<;m$L3C^ngo`D(#NR5q_8U=G1#g zOOGzgBXn)#HB$GBxiMTlN==V+!=uF`cQvB;E5RchAMN&$_`q0?`fvZ`P_HL1XnUa8 z{US?PexZ?F$@rECvj_3?$uvuPetEVh^!B*+x!sN!wFftEA2*D!%hKSN+}eRlw!xtF z9`WF;5C=ZbY0Zg71Bw^St%Y@Xir z5_ah^Ss{uL;r4}&!-MM+oz~tHhMB4)JC{DkhO#Ntw4>K_5>s$pqJa?QmgqWCi#I#l zK7T2MNd=zGK(AC1;BxQwTO74RN555f@y}gh`tx48OF7PQ+B_}{Pg=sU|ckx=~iSi^0uJ=BVhgHkuZD{_-lKma8 zFX`{I06Oc=vvU>tT3)g+Ims6YLSv>yy4a|rOJn2&gUWcXrY=q1UGT4pOsfk@I?7{( z%$XisCFWKA4{5ND*7jSWuur^(PjZanI_NEa;f_x~u0%J0hbi^C#~ZSR`T?YKmbpk5 ztg{fw)LKd9pw8az_Z-eg z`%N$7UB6h!a}nK(3PvP9Su1aC3DF9pk@G4RZX%)rx2?N0;=AvvWM$T&;HZqF=%3E( z#cQb|1Gj3Z0M@XAR({iZEET6GIrPVCY!}g0(1xn(q5z)-wPL!o&GGTS3n|f*v2Em+ zw2RZ56E@FNOY$uC-jNqJ`8%7Y<*gKIGQM|P#VN=e&4Do+@_#|-g^m{DzVu}~K*0&2 z;|xIH^aF8(4LbnUhqXDth)PUe4+0;HUFO4J9dikWRG#G z!Gc(&f#soiTmpX`bg?RDcR*ZL2FFc$cf!FfEPjCE5+gSWTmxS$$da<)i7lR=$YY#H zI2nfeWT1v6V(V3JsUIPpp`bMq)9$z zBBVYdStFo6Hd+3-ta8^6wwAWjkr|~no7mhBcY1P)TWLy<*{&=e#UPQb84irVh#qUoW1dCp@ z_1{g@pX&-lz(dIFuS7sME%fZyL>Nuq>IJ6-h9EoQ8y##SL|Dv6yMbo5V$7t9%a6l5BiuT8)3jqqmnOL=j zbN*JO7hM;^u>-DI9AT1t2|?ff2wbakU2MnTNQc;-$tf_=Rcyz|q&qPlt7DM2qUQmw zZSk7H-@ne(qAP7*hnO5g)AUG%*q+_#*&k3r8W;o$00064K-eZ-r8b~Z@*M#Hz?cRA zKnwr?z~0H6-rm8~&dS7^-pj$ZiZQ{2?f)XC7r z-bu#P!_wH$M%e!6R~{~O#x{n|&eIyuE;!4cf9u^!HEYEdE-brkCNW}uC(!_puq7d; znT)5^qD!RR1s5#eNML!lP`?DyLKRA4FOZ~=m9QjABV_@|LEFOiUveM>BP(fbl*w+e zluBlcBWJaeST39ZwsY+Io4dKW(P~m3ALXoxyWebey!!L^bkX-dXF7{C%Z6;bPy4Kyeq;&hyDQh-Kh*PlVZ-n4?Pv4&NEh07NwTtial`K_o^{#r=pXO%qkQ1; ze<$~Tr3-yumq~fW!u#ZV{s}PWrz!X@#HV~RW?!EFez^HQJkGyM%%^%{X0I()_jsN4 ze0Rm$+1pp;dnJQM_wZ0H{B}=HKlYa{jJC5vJ3;~~7lrHPfU1z>*d|gxq7-smMB6_| z^}*ZPqk_^A3iT*T=taGEVx;%>px7@=31^BRo!BlCQVGh|04)?v&E_fU6j+Mb0P>Pt zx^K`zwBB0!2^&$E9pu{a67A8TuRGeO!ehI0V1 zfPOb~`EKCnsdf!LEX*L7ryj#ih3+w6psHxUjkuN5m*y!0*Z%NrvJH9&f38fRM~k-z zx{>_uET+fFwCdlVOYECZOQ7e%jSJCwQjm!b@##nN=O~c1EKJ+C)E9w2e0p6({DGnjTW)j|_`C8d~F6J^%C=E5<@TRhr;JmprO;Lw{J zM^X#&RI!T=3DNl@)jUf`zuvD$pv7M7CzPmS!{a`R67gL@z|4$1Ve)b{jiN(=luo(p zO7RZ{KCqra%eKIb5_wrvO}7?OS}tr@O0cYQ=(V6!%~fOKl99CuZ)fjUNa)EFn$%S9$$ z)}6>2xMwV=ru#uTWl&1TM8iony@jZUvx-0;Xm?hi?5kE*5EXJ7) zFBi?F#BiKL2Hg=rXOpe&D4~Fsnp-W?&Uq-aGR@coB%j+Yrn$8)#K{|Y#y*8bie-n( z23i0EmfYxEy2GzHgUf8NDF`gBRX1&g33WT+dW7dLe;Fuhjpj_NB7d7RyGPW^BE438 ztbv~*eG!#zpF*mR7$C!pGBfP^$<5w$65#Ud;^d$edf%d zGc)qO&Aw_3wlj44^f5k1^A&P8coR&r%w}PgXsK3ET3vM+V8XcjN08$>GKg-IjUiwp zF&2RR7{OlNF&Jea%z*2AXR)9Rn6ENN&4nT}v~efCl2)Jg+)8RzIRU0d7i-8Lbidq# zi*$!hEI8N4x*3Btf0YDx%iYOb+n5C>@)%)h0?|ttDPVLVY$X@ACniGps3ifeu&I2R zY2Gb>q6F;v6IZA|NEpho7C#m;*&Av=vqdsMOkm!`I~e)_Yk{10wM=fTCe`}%@zdb} zu*5G(UZhw|=hYHX?%DPZiUTYZ&attcGf43_W2lH?do~x-^Sovaw|neGK%tu;BAZU# z@lTn_g{ZX~dL$)lk5lP>rWwGWu&-^-QI#vp8I)m;>5qS;%DBP|1v4~Be&tE-Vm&nkSqti1TgmnNrj&= z(5Y3z<67J}KMWKvT(i_IfWQOdk$Gm6ISQ(qx*98;L*S}XN-#_3N`w_t)s=#)IOeVe z9sFK`?CD|HQ90+Bu5<}7_*+&A_dF#0>wY5mz{{byZPDDLT^&{XJcp>2@%y@BauPhS zsy%R~(gh7flzMgmVQo33I#V}KUKw` zY^5@a&O_=7X}jtSu?umyR&7ve7;nBhs@o`^>O-VgDt=K?6{aDWZ?ctBK3|zq1RRyI zB8t$liOSB989o|O^$NW>3PX9g_8@=?&Itt9h}dqd4z}`u0l!1`w=8N~a4H5mpQud5 z06kFE+!>^*OcbT6%%jYxfPNW$P(&A%^83#gkq68B&7 zqOx`}2!ROd@UHVRW2f&)b$=`gt!y=S1J{jpo@oZdXUMpP0`_D0^JXTDv@>t1V5a1e zQH%bn8rIgq58_dLf8=aMvNK`D9oWsMRB+~Gei;xNP6YMp6<|{!wKPGdR0yJ8%NTJc zRBAvZ8Fw?`WkG^JNxpLnTtIIvHZb(*qrB$NVj2C-GP22N=!4Jycv5i54^wcIY3yL9 zF}Vr#(Tut_6P8KSTdJWsn9{4jSWOyUIerunYlPL;kc`k8VK%t$4@osN*Hvp;AmFsc2 zXf*axVJ56?vUdQurnNWl_cwo~og`sJ$e$l~I=amQ)X3JhL70 z%J<4jVHhK@De?DQP77>QWolK0*ymIiA+ep6v0iun?AtP*6mhy$gs9t+qeq?uN-;F| zwr@#;UMzX!vplMv$SO`jQg;-*@}`d^9P&wv_atr{B&$4<=Vp>E3mle8$LqRJ*l&L9 zCmkuuZOU`JJg6G42Dk)hDjAoOJnuGLZH$7)k4N`-9${2Dhvk(AB(?z*sgKRA_n=iW zb5)*HGQQ7z!lH2?LpD6Z%jE+7l%-n2P$P;?^BAfGLKg_Q*af#{LnCe?qEZ`f(i?6P z&)EqVxM{u~$GK@=aZy;qbiIg(y`lF%hwktOq4Fz@I*?VEk46Vv(k(%H1)U(Za&uWJ zqfrH{gm}6UgIYs|6U?q@D!0EZJ{_Y->sT%7x9?e&^9v3(!YlVQwZa?P58lgZ+AvDW z1-!5V3pP>eSS7nEVs3&ZVV?I!pjCpgX^njbDT`3#%be)3-F& zJu=`(O&M~Q%{i%^{VN-HlQiknFt~I%^R?L3>aeV(JYA6=auQrU$2y3Pv{9MrFxOi_ z*{s6Hyc;grD$v7&UI~gc&%J~}C-Yth+;0Q3Jm3E6O*xhH!Y*!Ee{826%#+PB)HN!k~FGF*h(w7K77L$#7xUvpR&h8_w=)Oo~^*BI)QHLBL&nzXaylqR-r zx5Jx7W?QJHBm5KobO%&eK!XH@Km2GM9Wo$(w+s~;FG1};Mf8`mDK19)=OPMJRHcM- z@0R*bN`WkSMS^=uYfj`8JG;-;= z=Xs*%JLH&`z?5Zi)bUr;6OT3{E&IHzwo-i5%9fnt0D~^R#otxUJ+L0=S z`PqjW`eMCUp6#&%g=~-QWFTW(PL&&L(YZZ+H=$rHq!n||!z(TvR<&Dcj4p6w1OVLG z<5(d2yzw^>n1AHQIelqNIPBlF{_v8_&>Y{SzZxS82!9}HjuM*48J#EA^B3|Z#o+`u zgOer^a)qYV`!2rYgKKVpvGe$Ur zie*~IHrJN6XsB{fJ0Lq57nk%?t(vj9X<4Ia{^wOPCdX-<=v-=n+F@B#D#9fTsa*2u z5Fx4R-EH&jHq>!QqF@wcCp9OB%*SD4`OezhU%$EVYa^*4YlgrnMrlom6P-{hPN*1) zgX<`{939g$vs%u2dE%H^V`g6;LiQfdh#sdEdNdUs!U@;kU=T9DOq7vz~XSFzwozS9SCFmi> zO4nh?lXPP29b7TZB$k5Nl&S8+3R%VE&YTsXhP!FjK5P5q=U6^|!P*zX{2yY2as16K zE{mvExSt4b>35%kg>HzyTwvFyik7hS-Ea$ZJn%~`o0x4}fcu@ZO7R6NTNhhR&Dob{ z&pexh*k0O?RgXx`?s+RJr+`&A*xJ*#zK7bWR;L$Gq=J7?r9Q`_>1*Xw+3AnSJ|Ho^ zQAWNob%puEf%VICpTM3MvfD9r3J|@ZIlhXP%;0~up=Vq?&I$J4JTrN&0fEyT;xhoJ zo6{TNzm_q60GYwvor(Is6Z{H!7ANTiJ)S#2^|JiLUK>qJ$R|F05&h~=2>CnFJTXbe z!LRoiVuGB;DY7IOlhOgnG)BfiphFb-a&YVRbv=r&|L$|r6VM^g{))@^+UO;oUlyn= zzew;m?9QF}uN`NqjZ@nE(X+_Wg5iFVqoM$yqR3)VNKjFZnP@33QM~X)FIll_JLRn` z*VUf?c!0kq*w^mdZWfhWrtfb6yV=JOp@9(p6bJ%GYF@*5wzlj&P^ z<84!Wqka4Lq1iP*+4dDvMk;2;A#U2gn1zS^3;bC$uNgH{H=DpV;z%s~#kLV*+4PRl z>YYBIxB`avsF^^%$EnD)OOzv}q{NX~(uSb&i$2O1Di5*jrK)dC817Wx-}C=fToL*a zSAy3t<%t0S02Y7V{QvLbimijCjiHOBy`89sv8ls9zWleUlB>`qH^_j(3s)FgHm3lB zWCH<)r^HL7vQ!#GNg$%=&fbWWJ`|?|ey=Sf0pkOJAKDHH0hZBVvg>?vxJBIC-TNDe zW7rH_8e((JqIhcD{g$n(rDqTUvV6OXFkuQvuq=z$nw;dM_-~ z)nAOx+Ggd@FFGT5JYRcX3|%eJ-=Y>G^Tlgie4b$5L`Q|P_T}R@%DaGS5VrnqhN)B* z3ITMMwTNV!fI>u<(;m0L|6APp$Ng92aAFb$0st@q0{~F@5AMH$g`u-4iJ*gnji<7M zsj;P@jiuLrJ(&Nx@ZS%mR%O!;nGu0^rfj>Jgawi|#XC?SIn4zT38)f6g-Vs6R8gc5 zDgI_eR)_6n;!-7OcTaq;5`u&;0Gtn!QBFt@P$lgb_4?s`rt`?lT7IwJ8^|7=TG{?# zRDAW~*j^U4ic|Z|VOYFq*!ZWCA!$Crg)udkgKAxNe!SsX8MK$!vc|<+VDyw)TBq zK?*4{^XD{HC07iy>Xs3_-BZCKRdNh3N((RPS6K?SlIq_b_a zh4%<00i8K`GwAnVPACU7%^l+&xNmLZ0YtN?lA0v?pxZg5I{2j4spVs=$Mcb1#I*3} zU=`9E8P0sC6!gg^ug4^nXo{nhGd{rn9kfSWN55tu0RUjp0RWW$W6%nDx|se$uKlM> z`?H4pD`wRpeNdJ$zx~I}cFo+Jh!Td2`o$oPG(?Wp_5^_voA)W8*Ag}u)^kacZOqa# z*9E-rSJb@MR@7iyThulwik@}g;FqXcH>|D=Z@XSf!n<26KDyVZL7~Tu9zRV_yV-Z| zdoI6yAD61Z@xU5VCQxWXOAWA-pjd3w6`-~4Z~@r^Qh2V^8-TfiF%;%P+9*A^cL?Qo zDDUVb-|aw4J>pXy^*X%r`#z)R11FDX!BPHp2nFKftK1RlR2pE_RhdlVD?70A(Mj?V z&hk+@#P*e!w8~2puBrt&bO$XmQm9@*9f0|eMR4#@`}2`I6#qUuMLlbmtm{GOn|5-R zW=uPbPMS>`)lZrdc5@)9jPw2+VzjyygtbcEy&SDGq6J--tiHn*FZbo@$vD00?1>Q2$7*lC~DW~+@Kx9?WA>~?F5a9Gsr zaz@O+1>c})f5>B*#W4pN8q(RASdhZpO5e?ENZsPcZx~Ev99P@HE2A(2n!~c`9Fq`H z&knT*iCj&@pChuo#G@xUvnGRM>o}_HElVV{BW$|-;_N>mrsOA0%4OD1=ld2@Y)Lm( zO}h=WP4eue;1e>wqaKAIiQV*u_bS&|wRrSDp7lDkOWkVHZYi{O^*z48PtXM*Z_pRW z8uT@J3jOM?Z8e`@Gv^phGH<3mt;{a#%9MiI21KBUxN#$L&U`$ix0a=HYW znSQx9?bbA-s*OPC6&wNJDm)S4Dm*l8;P(*vCNi@had^BV>n!261`^C&RpP&Xby}kk zRAC}lsq#eO1&*rtfW|+XR$W%vs#;$qUEgggBkr{FIAkxjrv_xU&Dljco38d6$w7K+l>Ty1)epiGvsN$ind~< zgr4K|cMCZpomZ@YaK)rF(0+~&LS)0yQGkSyAM@bvbREi7iID7V8B~Os>8?zfiZex? z^M!33FXEQur!nL}bIM5@oZS#Kc`{meY^1mwJGY}Xl_@0cnmRhmp{Pnd+I|=tV~^CG z#ES_B=|UQ+Mc(w>8Wf@Vmz3jQ5lvR{pockcJkt(rUJM*`ufG8kPWz)6y}*h?>h7`U z>q(V9KI8^sHfj$^%_vQhFOLF$e6ZG>SQVGYI-HZyK-h+5Jg(KUt9WY$je$A0JsX}( zO=zdw6)mjN^QgtddlFg5b(tbd^SWM27k-0`9E*cVUeac{-+OAAw(?#qBRa8KMExVx z*MIclEoNg>`(H@J$ER<;uGqxv@?OQbY7Il#C4!`FiMf*`XD0>@+T|=pcM5ax;ePLpoGQJ1Plx;;zj;&`aPbO(IZ(@%&v8hWC zt>L^`3y)I=%n37RvCPrEpVu_{@*xa|ifS-FIcYa>mUEuvYb+lTbMc(~6=^UWSBDW< z8Q!j_E2KCc0xSn;j1plkLx2xx5y`Tpy>iZ-xnb@VAK9ecs?x-s5wLhFk4aDNM~!kp z7F@dPz*K?hLc7kSzR}8-KxHX;Bm-u%(U3~sfB)q~YOgHij`j&hk* zi*lWGf_HcL*(|dJvYwGHlZs1zuR7lbWomr5%nT|Mu!GHcveI=}HD?*NIA7JuXW(T* zGT`bh_x5O0Nn7{^C_W6 zB1CuDNVB_=WdGxyaE8g$Y?n0)Q~pd&HjD|p9Vgy#Q9x-{Ft3^sms%*uDP^)!Pl0lO zigG*FVI%Uq0b#Nc1t3*@+%R384`s|A7vU#mB_n@mwy5bf=;lNeknTXgm#4(qk+FUm z22jli^=Q|s#*oU4&gMip_1x%jks{*lf=JZW66J@G`6hZm{9PH4LbQFpw=}?yDy|hx zm1G2@>(%9IZo^2-s$7*cf_R{l#|5ep&|$Z{eYB|L1IL%aIuzOQ0w zp*S+D#^W8Cz$f9 z=22+EkA_ZQ?9o61vN3-KAFEYZ)V*<%-3Z$#!YapVwZC*MgzY#lkVwC2o{>n^~&>rAziX_kh^NnLSN3#X%~mWC_dfKPF1 zZ$`JU3W^;pv8G)FJ~}3$uSWfi%R4vAo`GJB?vf!XjC~RnKVJhhC@~@;( zRaYKa5QTS<*0z!sh32icf`V29jd27#CQK!g3{+Bz;e*X4+^rzj(hU7?g#BM2Wc;C_ z2>eL?K5+k}`x!S<4Sa*)Tob;V9PgQ}Ozx&#f4_IIJ-i!}#QtX*GDqUrY)y`gUujBU z$1G)w8HN(u=~7_u7&%IwxrCa_u>A}=$x<~}E^Hy1huF5iVpf_*H!rs&xZ`5R3rKy# zd{Vt>8_&S2X>XD_UsWg#t88yRgUaw%n=Ly#sh8A@H&wUnEsJ$3fecaXyYML4LY=BR z7gKHVBx;57#`!hSF~EQaQ+q3A$z!!)i2uNmJQt&gOJ{_7+>Q5=#1kZp=x!+H4c8;A z3UT7)5FUyyz}QXAo!NBFx6mX_DS!ut;@2Tl_FX1++obqNJ=xT@po)m3ARuR!RMLxOBf{hWvSwn z5jv^BsduJWr+M0eV`rSV;6c?aN3*i}MHzPCKV}rx^*uCjcNaqLaTUuk*2Z|wuN*HD zUOhtCdROQ%&hR7jtgo6L?SXyEA&04pBRrojC7$V?PYPhK7Oi^!X@7-tmRRJrRIvPU*2JOM-15*R+zuy1qB=%$ zd4^ws4xm`H3gD$Xl0uasfgZ+`y?hA0`%s{eBe^ml7S$bSt(is4gr!5gT4kKcFUFTe z^y~k7VI*`Wv&sE~Y}p@VEBxokwzqS3F|>0Lv$rv^v@`!#fdB7Gp5-5v{HydoD|sOz z9gsw-C?S{2_h#CK0sm9TGaG-d?fUurh1vsL zdAM`j9*yPs3C9Cl#<6>Ezc$V^d`VPU;4{3ldZrU@E&U8K?4eCPixJ-(_5oo< z2m8+=ey~715?lzBmUknWuuZ8tqx^&Xfow2`RY#P2BgSV5b7Ijk<=&fCR?0Bzd~bPI z2~U_nwt-KLH~bbsy(tfFCgGB_5;G`8x;S}9y7hQHGJQN@T@l9G@{+zA$*tlud+RQd z00lL<$!ijeOFc=kc4z1KlLw*$3j~=S^tkP5?P;jfB}zMo&TLu~K~`MkB{a?kkaLQ0 z>$94aOA>d2+9{D5wB`V=()WF?J7p}cZesGgTtDpdTjZp;|L!SdRJ<$s#i+f!(8pyT zPCj-252MH;5(ip0J+WmABu*!H@8o!PmfOTIWg$v8L(7Z&Ie^M$z@TpwRj zVn3yox$&tmN(HS907VfBa!R8B1^O45=0~8>AvBY{I{com(90mo_+f^LDG(14r4gd} z;XBdHK94BJB@msGZ+gx(Vu6SHo~aqv>Z3l?OOg&=-AIMCMwB=H zLcv6#f#ui2JB_7|A(A)eK84L*`lz^=HcMT&aWKV!VN(SZ^QIP?KUGcLB zo_EXThHlrM*gbZ8AO2nvwCa!whe8hL9eBIrApi;vC6q^#l8g;HDRd3h^wX1Duiqjs zZ`bbM_vKXpy8Ue$8xAv${6T^fCl5S&7y~?XFohveW}RPmMsTAJ>oD}k4FS|>daZX+ z0o}CSLDYOk0Td)GrSJjS7FwP|1RySOYQz#`0)TVPVtpw)pq5P!{AaoM_ z?qrzkLh?mUNK^#k|M8&AH&{u8Kf<@g)gpJRd2k{dP+o<85h>n%P`h^ zn55E!R-M$>C^M3zR|X}Bk& zVoOtOafD_KYzp5jjBSgVv|XazYP@IjfU*|%%>$uxH*TsW#sxPi)y~diO4YNp2B{L4 z4xcu8xV9PIa88RLN~T!nNbz%FhtNjb**Dyc9|e1>E2j>k-Ol#h7Z{@h0MDvkmFv9A zXQED^%bK)0P=M_p+*NTb2bl78{PiIJLCqC4ZfsE>X0CU;ay*0=9{yaEEJ~;P?3XiV z=xqLF;`mkAP5!6Mxw^4V2YFyYahp_Aszmv?v~{-`FWiPVYvxIX)z0#{PtrIU9bPEA zME6g=I8gcf1l|!fPwH&8H^APMbYK?Cw=qW`3SPO&i4DtOY2_zE>G`KbYLCc=^I?=@ z+#duS|2|}#SY%m8Bzd4n69Uk@q8wSmZ$e|nxIRjr?f%}eR7a`O+w0)3d&IFNIuoeb z1aItL@(zZ3dBd4-TcbJ5Ij7}r37!TtY329@`g_PZg$9s5gV_#d;Op{?Djz_UQ60E2 zJ};}Uncm|kqsaxnIQ~S_`HFq?4tLbO)_&3uxnLV9DcjQW*z1{Sal0$rfh!7^le~xP?4Y<6@ugvAZ&+Cg1;uoQG#c zEV=x%Yc+uX|7g(vx5eO}OM&XV+&`$9uy$(=D0EO!MF5&5w5g!z5|J2zNV1ZI6hqb< z!w)wz;NHBllf0Yn-J`z)MUwpj!2bfwe)tY< z+n?Z3nBss&bJICsYLTBR<>d^M=wg;j*lCas>C#A$O2KA@J@7-~&}yGRR4%s?|9V?9 zUZ|2f)}Yyx8?Ct(woYt@9oQ|qr4Xi);|$Xdl1xM?j0EXcOGkSzs0q?k`h#YgpZ$bQ zZpm&|TDDKl`EI4F^p*^cL6PIb<$Xv$fB>CiNW+?byb(DbGw<)gI5v?^8-)A}em3rI zp`D${Ars#Gp=%J>{^sL{CofF(ZB@h_G{38K^^W1{As>qN4)Sy#a4M-_y3b_{qi-Ds z52}n<)FVac*I`Fdq!-N3xbe0^ZUtFH_q}FqSEUP(b+K+p9q<<+dHR~eGC}YT5LJ)g z(M+u-(7kI2^ZRJ>7+_++G78ZwD$X$&Vwq58)pHF$dLc0L@)~h4oC+~2R9SQym0nF` zJM&mp-7fhcThdpbgiE`M&_rXa9}o${?ior)3RFYb?bF>cna-2r*MFO4Z>nhB3bDp4 zK2;`^FWEN`mq(?|Ftf`lzGCd;Ah32^NvM0Z8oy;X?d0SaeS(yuC7%^|5Bn2YJjx?i zd@g1SrX|Lj!uRhimcUA4Z3Yk5pOrDrgA{GbNT0vQ{`Wn#jB6rT?PotlMgstl_>cEg$$xM^8=HS2eWmKq9?I%JLo2db z*k$dqN)m)Hf?>F_fJiq(29yknK}%{}VjeJz;q8()pBA2W9GnL!ocTt|vgv(fLS@V4 znz`_exuP*A09+Ront$9}y_c7HhT;@ImF{XH6d?m@r1PJZBvH#PM6eg_Em zCW!d@FdY7NX9%zK76IPWm=6!$PSLiq0LwQ5k9Su9j_-bmOvFcdAPBaqq^dl93=?N@ zXa;7Uou`8Fg4$4gkwJ{dN#`tzUHtIaS=+-R*Q&YwQ?IIA_J zWf?aXoY<=8&L`luC5E43imoMIhFLf_tFg6YmSu+Lj$CL2VU(lLtQS9-t+d!wb(_wX z@+F%?3pZFyXg7TZsYa8TD?o#JEO>^vS`wB;D=;X)ggce3krL;vh&}g~oWfGxkLGH9D z(?3E@w`NvShZk!oSUTcR@{A(Bq-ayRkSpyvH_$06(Bi8l87C6n zyM(l*##@z0O4@`y1_-0ePOG6Ls5P%-Mvq%VN_-iXbuN=N?Yd5mL!?yqc7bUVlW;tkk!G_yu z+1#jO;nn$+lc=Cpt%F_IXxAQKjF2fuYF#(qR2DMn&=C@T=NU#>T%?S3qnu}@6y^qa7v`>MT z?f^52Sy#hiT)&a4z~m$3xU3ftjdI9agb`*pM7)(wtR@z1!E!n8to4B8caMzJ6KNm$ z)*3>7R}yl&*M*cf@Jix?&u?JCP2RTiopbvj%{ z@(bRTdc*Dgy1nP=Pz)BOQi9-@u7!oEMc3oOM9W1-46CMNSY={S6$v!-BbCfF9DgS> zj`~UhD9lpxd?+E}lq$(3FjYR)WG>ARrIRj3M)?B?1(t|4`0HJVGH&`&uV|So=^}Wz z##QXt(QKJFY8kSXS%&z|wWGOG%Z^hFqHS5;h4Ms0sCikbNE>>jHjbja4&-+Q%K+lI zF<0t#*>Jv8zH$><>AGvlO}oU;>BNP}7Rx$Kxid+dMnxSA?7Y)^Pos?S(b!SMc+!~% z-l6*h6cW{76#IhTq?6n85oAP=1JPit*dPU#WBYF-5~a|}dIp$Gcdz-8U!j{V(bTa- zE>@9(8!k)@s0OsH#G9%yTCL8m+}MHW0)HD6j+>8_wTq84RYaPmMBA&ZIf`SyTnnhw zs*`f|a2dj`-a{5EBE_pf?B|!R%4(9D57Ta2Vqb3KP~t;4O?hf-^dmL7A3Q-~9<^tD zCn~(6IT@{qRjSujvFL+IBI2_Zu4hY7wV@JMd`|kI-+(#&?#0H14DAeH6K!q$Ld=6q zX$G_huf8CD8XF*Pzl+yLvLpO^x5^V%Nll!itzKGV-JynYKyQQj8la9bl|tsC~R4-4oHz`x{l@mzG#{J@J(i)cC$GI|V(g z21KZjec^WQq!(-b`j&x3Nw|9G6yykIAIg$$13)u? z6y2AqA*B#<^@ONH+kc(Q4nlat12noa=rB^9GfjtvUX9*m*-Ld(IFI@o*6kT_#0?GG&wlw0I>R)IaD$JJZwF{>bZ2+!GCl52#^nc7|7+$ z;qXM~*~m>3`e$DNuLW)>m~a))W1zqN%)%|S`ivlBlmxsL1`Z!4T7JMCB)M%R46gXb z5y^G}p$tizQYTeB1CLku$MvOuB(i4Y)4YCb0T%*9_#K>qTn7nS7Xd!lf0*G1@WdN- zjuyre5`7EP(7%6b!8zCyP1@p`ZpPPi!<2mV3Qy>SuM<2-pJ1Wt<0*;P1j3ce8{*Z| zLWhKUwF*B5S)yIHm~cDB!e{uGz7I`lUFC2WFGZrTYxCqU+jNcV#D zb!EErOC7pi|7cbSR;$Ea6RA*wD@4l`mm$(7MaWvL z5bh?OjA0Z*s-;lIb=kA+z;K#0?jXq%ah$0J96#6?d;P#NgaLk;5vUeoNX)XuAl1?y z$%VyVF%V7Ui$<{P5J5V17xApxVb-*+8&U9;MC%7l@;Xv^_r}deywyaruc;w0mqhH< zU%6`$vPYeSWroa-$dc^7YeAR$WU!QLQP_KgyxT#xA^W7ZwIi|3Sx zC>nyv-vjov?l6n7LFS?j`yy{5al#Evfrd%qSrE__m|`e_k(ICW@riZj`9jN<9ueD? zA|g{hu;|l*S#~GR37kMC5dfJ40%qsoY7(rnp(=+Jwp=P%79pDEBv|78zvRx)RKU1v z?O~$Nxuah^$*f0M9`HeFE6A*$j7qDq{D&qzAPu*~9qfu+`TzJo`Tsk2OUI(Cy#)aP zxP<`#(EX3O+kXimYKBghhDJ7~DxMCe!iF}+uK#V6`WHgiqHe2|xBY=Et3~3{r^rW)^bS)=M`+5>bFnzbh z!>T}QNf%=w{yK6;;ABgtZ>~s1A7g@i5&E)cGcoI3CWh=GIT`Z1+Q3aPD42WcIF}{U zFbO-4I6|&{R8V32okXKDJzb=~xIITnB)P9PH#3gA^Pgo5RTFami?w%btSsEtL{qUU zb}F`QW5u>@+o)(o72CG$RGf-!+cvAS&*|=a_P%{T^gj1LjCamwJQ_U?*Bev#O9SBU z7wuz7dm}5%h!P8(R7KYmI{4s$G8nhn`t_aZv15Y~WS$C-K9*ccF#1kQ^;8k@v5Ud# zRa|Edz*R3C^J6vslwPo@8l2Yt=@#VR^}h*U$>!@}9%TY6#}&w)o2rra#->kr!ze`e z;>pg8@_ zWsd!bJ@0zzLgMkT>#`poFQ(mnbi)m1zRVdW02h}f_FLT80F{a z=513IfoNrvFbC*pX~|e_2S8j4`V_JXBC=AG-io*8RG0g-<1LLu<<&p~Gtu1QPQkS0 zAov9@?IOFhCj;AUzOA0l@T1#@^tle3$@dNc>dF zJeh~qLr>~lFjZ?b^5dv}NrM$p2vb!0a#n|nOte9zE)!k4bxSn;VVb5Y^Zeq19~=0B z%fGoO94Ot1vy2UG>#YYzPESpiW_p=`FEOIb@JE|)X*;7(_>922D|dV>H#;W;EHQ4_ zuxMGy(ly})-!iy<2lgdk#Uf!MzMeGhOn99D!r$t~sVwh{Lw90URux^d zSQ66K9H9-)ubTkXjY3o7i-->Nk3pC_pjl4<+oIJ@&%BZB6{hZ0+%=}BApMGNd&S6Y zJHJx`J;h?=p&dfPJmCp^9kJtWvyoGmQuy->Y}{?*%pR(@#uJ**kBKIf6Y}-+0g+f$ z>Kd?dlpvF`3}a0(NmWmPjK}Pu)uy7U7DR+J0BQi**{N*lBZDii4a-gdEK1wM4s58Z z$kebxA=|ISh3yi`B@{dz<75`+b!s4vMi0Q+#j%^U&;EVH7)(Gp;Wk^nYzfOx|Ek1t zK6jmpKC&7bh?BloMfH(m=z*ZMaAg~>Km)Frt)0t+&2gQ&^QYO!n6tJCU(T4rH+;b# z`BhB%i4cpwC<|aOcC~ziY2ZJd59fV?y(FAG#IKd{9pYS^9h@_aXqMR6Exr&^GW%Dj z*iv6OYDHM5;60g&fyM#c0blkGqE04&Bu9m4Tztp#&hkk>O8Idbi+#dX9s302# zxdz|Xp-1CnQ^y>A}rPw?T`g2V?atl<&)bh%*|EkId3 zdgcH$x)Bl}Gv8~dY*COP-`-(WuqA*6QVhx)$}xF>4_R0iSuY}1Dfkn6l8uLH_FR;7 z=wgwrQAa_z)+-=P+{~NQDxu%=7iu&L>ORM1^|yo!|21 z&<-|VI{ogCYP>6B!bULZiM2i>F)Tc_NAwUpJf|rNwtVG<2B-UNVLkVBFHst_$s>Pb zkQ;Sd2pgif{?v|E$B?)*-ye7HN!b+PA@#eC9cwmkhkAL*>^Q$qA=NckVQb9M2xcw} zis1v<2_b-0&$_Z5<2OJjN{W|Lq@3sb#P;Ai&Vm~&U-FUpCb@`_KGXc(Va^uXj^I;s z+m6UT{Eg_RSzq}7oCT$@T5#;9xt2rd@|dZs&R7V7JShLvcT&igg#m& z2^N*VR*{PGnlV-wxUEyQ?VIz|tgHIgFRvCCg-6F`tbaw8n0a3*FfiGG=H|3G{r#;= zz5L%o=B(Y6q=8S)VPpy;-fER#Z*V|mExc0QyFSgDd=lHC%)!W^WR1S@?V~4TT5pbOXGjQ z03l~*OLIG0Q@j5vRyX}`vD&5#?n|tOh=`aCG(|S{=`oN%&6g}A1)%uiCgVDGS!WY?%c(&!reid8KQmYB6txK~RTNgrMJUV?cihR6EWVIJ$=K-vNw zHj|9NmBmVOT=M4PS+qtZ@rsIV-o06*IJk2*6zJDNRENo8Yw`-e1E4%@<5AI z;#v712_7urO|ijVVjbYZZ>f)^)yVR%M^}g(4745T^`&KznaJuhgSR_ulsLlrI^}nc zjsJ;C)1TmM$0b6B4yH@AA#q=2>~qm+q#@@`KcUO!=CtXLhTfmS681`SEpwmui`2Hk zdU}nI=Ynbw#ireL!kEao91$MVVqa;hlps|A(py{%5}^w!Dq&`YaP8z9V_;pX!4K46 z=H74h^FK#Ip8j+K@@`=k$3l!v1Pr!IozNn&n~_g@>uXm)BF3XNkG$h3Dih}E2c;5P zf6){gR93W#)hRQf(io-xGK_rx60eV~1Bk2l#NuVhB!_Sacu%DVU#QS&kee7$3+#5) z){nv_3sb_EtP+nIX6-A&@~-}Q^52u#d)dDxALVQEW&iETS8%d7cQUk9u>Y!L_%E2Z zN@eWJgO0{GS(rsCV~L}scMvB`h$sO?M1&zkh*qXR6-C?>F)%B;K45Ajo-OGQPE@x; zV4w<)LeLW^FT9=xubM>Pr(kBNub<;R`LJ&N_xa(0=VysWYEaa7Tz4l4LlT|!uVaaO z*->xskyC~7z0QyoN*Bz98SM;M$=XWck0y-_-of0Qjw@Sob(|2W|B%=Id~!qeat zKmlX8waRl@_W5nO)}&zO?pv?m0&Q9vv0J@p=MDc>RkRAsi?xAciaAbQ#GMLKwv>q* zpWFyqq2DA%9R zk#QZ%`+D}w=(H@Iukzo=ulI9OZFmhl94B9AY`NNa{iz48?Ivy0=#F&mV@kKBozo$tc_+e@tozAKPxSUL1bZz5 zuf8M%2#5$W2#D&xJ)laamUfmd{}Gh<+Rwf`L?-{FSud#Ds^bFC{K!c)p|w$J8@J&M zWUZm6iAJU0UW@_;vc%8A#b_^5z&19Ot+0pc#JT2a_O0b6)!!LFG*z^ zT8XAsYEMDiKXD^l=q8=D>r&zrJ76_7UhK1WP*ygbx0^X~*z+WtX=FRl+F7+{p>r8p zmW@K9AjN@3cwWM)_lm=B9I#ztH@i+yp#G^eg`qHYTKe0${6oSgMl|rdw`YG;49#=u z&T&4Ygh)`vkZs(qm!63)KQP2B)(Yfi7&@`_=wKABpCBA#%+5hKs3iS-)JNOmY4g1$q0gPuUiXQ_R3$m|aPqDur>B#u?xvPgTQY;wPxd422lh~w@d6iXO* zG74j_h$dSd8CXl%sGe~f+A3s_&_7{ss9?iFQv#0)wX#Xy48O!u`J*U;>bW?TUbCIG zztjHwz;~YUN0nc{A2m|P)X;RZl`LbXjU4wu@(3J_CjOS~wc%yFcC@ zl#_o`D)mDBnjTH=j#1GefZ1(iX0VaSxu6C4y>LEL=`5lsqgi@JA=&{wXInX*gF(lB z$c+yorwprSj5@pD5WR~tf0y!A<$jM0efYW!wAu)QyWgXsT|rB?-gfHYhaPP~#|&dV zQ&?%>0+y4KgI++M(f2c08FUB@SQ~VJO{s$*#FnyRUoq_WJ!-pM)}(ce5`q46V5~^Cbi@E>Ge~OtOTQ9KUPe|RM=*x^hz=jYSEcY?~Gig~vI!j?N8sBaI zSyYymD@SXJYAd};v;gXHx`fy6ZzC>~!7E<Jt;sHf9d+XG=o;cSBFzG;J z4a=c-m|1B&=7FHQmk_ho>QX_8(5wOUx!v69s-Ut<$L>c>fbBNq{8V{mgy&5|6j6~n zke^qeCD%szDRwIL9*2YKU${9yP-agwNys;R4GS7JXA?fe7$q_iXIr~}%nmwIXWK~j z7PJOU{wA^pO-4ZY&UXXukhy8d_|tY9d?E%Z>Ty?<6vUsL2@7bsiQCu1$DPO<%lnHL znokgg1l<7_cGcpC^ykl>Kk^Eo&kz^R1tZABjgrKCLSDj%GvD6Bpy{@1vPp?&(n9Ig zZsl22Fx2=au*6RCa>(!B-bp7wzd|M|nMfqeO%kdk;J$#@cocgRs4N}@ORunET|}fR zAvZ~AHz-3l%uvfp?7SM-wNT~MQmlX;xwsQBX1FlY-o=r?*9YNmD5rO8wLu$iGlp!7u%#+{$rNrqUIX8lO+d|7Pf{hdL zzY`hy|M+(Qr^R4FT~`@b70XYKeI&ki8YCorHNBh?MpC1+YX0Xu4xrpeQ!Dn>Q$ntP zwicHo!|h(letwd3@)^@>EzJwe!u+aq}au+#xDG^S1z0D>}gae>1ASVe01E;xOK2OTWBI_cp9<*kQE^XB;w?dIvw zk7~CaTlA~aRC7*OS*+q6tWFyr`rFQ0s)(_SPHL>iiejpBpEUJ~{86Or(rZzvU(eh zHp(0_;>+X=XAn-BD8<<=%`{?ZJV4X-Hu%3H(aP}1!-r-M$)@%d8V1MK1X4o&vajLP z=T!dJJ77eJjt0xGHWRcG@5C?ZQZGR)F6gIu*e*Hq7C1By^*UV4vcMj?1jBLrPpc?$ zX8@-XpY}V#l6AD=;z;dQOAk$zuSsrqEY;*h_Bj;x@PynMk51|gYtSFKBTM( z^0G(T+|d}WZ>2F}Z2fR8u$EQa7ed=XoRtXs^P0k(A#ahnMfI^YWtb??|Hn5Jpenr= z1n1U_!)!DMZ@p2X%;mgI?{bTHwW&DHn>_gA@6-eOVJ%0Sep;qXnc`0Fq&Cf2J{qEY z8np@fFijEq=ibL!V<+%7O*B>W$#QNSNC&0Cd>|R^oY!l|wR0j8y8d7XPPHLG1jE=x zkZfyFZtLD{oLKSk~@DDiaPOqy0XF)WO13t@On zp4(xEmoO&-tS1_%_@01^DL%H>$D|n-JCpZJA^0w`oyOqy*mH@R-*2mepMz1wzSeeY ztufx;=KwV|j{U0)MEy^p({=)Ph=47oa-i`_yJ(GYojsD7RCewpGERk@&oB{1BbtKbF7N2cacGQMi(V z38s~c<6_Srr?cS9=WecJrqL=*O$sP}Hkqk??Bku&h%PT-8EkFCcPtH%&ln`+hGV&} zkgkt|*-4vKx6+NU9XFFMAvt0bi|xtUbjB4##aZG|t-u5Sxc+K4GWo@pn&n>dN(Sp0 zFuH9?q`ee4yY*Hp3$w*WB~)f!wQY*Y7A}3sB4%^IOIDS(bv6~T|3|LcRP_s={)>?^Y3s|_jx_vx4I?_J01Zv$JwSju z8G}<084amOf|UwgF+9d@Y!+Yd>GXr}i;uA+#_?t3f7{M6$+V>jFq*|!D{Sd-X30s* zO51pQd0TS_an@Lv3DG`{I|fGA;giPAGFFqKZlng68A|oD1~m^~c!Z~5zcP3D#AF26 zuN-nSFfh8j0>&Z}HsSlHi=lbr+@ws$Flq`9ym0}SFpzs?iG8wb>vzG!u@jtjy)L7| zmv+C|$etpiX=J5e`;yk#?`2yH3Pw)C2Szmm(Q}IVh8IKhU79&s$D{)}_sO)7 zK8q>Wcp0RGU*UP^iVAiO4J%zyf%L;ouozwDsa#QAhH%{!YJ&)fHW55)gmzjhY{qm~z{PgXu+9oErMck_+hbpfRSKZoJYMA;{yP+mL3KM*oagj7Z()3m*SsIm)ueY7S{GG$K7K^ zEqkqG-)Z1K9)SBd>ejv&^o_;%0z38Je&U$Z#d;QPndsvyo2T-r!eaNqxRO2KcN9rT zJ%O-!=X-gt=wOVSx9P^IhrR`e5t$s75#a(ek>4?sJeRms?4cVt)Zhq(1EQ zB1@I{Zj)0GS}wnz3b$~51W8nrvkj>52Ac4*K4B(>QhQvv%*qnLJJux;#?6G9)LX}I z{cNAGQKOXU@4*VEG{i*jg2SQQ?{_56$3*-Qk*zBI?u=TL`YDb@K)v$32edjK0g;Ll zJG{KpP!&9q#4M3hXho7%HKwj;TvoB^NKQL7(t<|uX|q03+@haAs;Y%8Jl;Oqdx3v` zT@Ii=S|(qF#NRL6p!DC^`TkF@>px*bm-^X1)VRO8D9fa@=%9ghh%FFkqR{zDaACJd zicA{B3O z_JqyH!E~`0c_kz`5BsT8g*R_|l4I|8IOQZbM-*0v6ELnAhfuUKpU%>4FYr|9CJC?R zUG3ZkRL6EPL>xmbQk%_=)k@6D`W^924c@SCwOEy`O&wHK5gA)~eo>Y&oXxo4OGSTo zcFsXEyMDq#X5-IIPd_e!+ITD#TNL2~V#DFUR1<{be?Ki}vr6?+n6tGzwc^DhN?@vn zQnoAagBc!+7|gOayIw6W*CqjSksD^_S>%yVcotD-NVctHW_b*LSEe@4j+=@Lb)BY;4i~qGP7i(!_*Mx{_Kv9GK<9iG7wA zjp}7d-am}WLMw09J{#}rRlj6G7Rt?B&>7+xk`!25B^DjOCC7?_pK=`>OhLv=wqpr1 z#fWyf^H_?!QIUR%MPUtIJIvnKEQ!UGsG?f(9##RaX{BOPK!zf(j-bCRk&e7s1XG*L zKq{Q2K>mW%a&FgOZ(0e6`bDiW=7CXyr8cfYOlc`hBN^vQ7SAeM zy>{a`IDH}nsu&^_GAoWc{}@eaEXSIhdc;+Z=ZxisY7!juYwoL21kNKauaaa^6*-QX z*UtG{9*QDKfzFV$hvpDX-<*2Ly|st#5H2U#o^0o!LXIu9NoSP1NoOQW@RLqYSHHMN z@Z$L>LISGEK2#hpiC#!~Wqpwb*?=9?K8Q!~}Yg@uu4 zXjGGexoIk%JRZ$bnnk!@0?9Wp{6zybj)i82YCqc8;4|zjrC@SKo^_`8qA%&-we1@! zndZ$*0}OT9A7U1*#p*_mm5~jWeJaSJAFsKTc_j6k261Q>Y3rQ4#1BJGGa-*1YCFr# ziPSR&sq#pPkGTo-hMV%!l$dH5y^Yti(|#9Oh3noNLag%T!yuS*CRK~~l?8@rj9r|# z%4C2~@aBN11U0UdO0(!>35lE+Bpx6MNY1pn9wll1oh3vPWJ10<9R$&m;f>M=%Sk;# z=~w^zMr9TbPwUr8S;bw;iL4V|n7dC*`7W-bW5xQ)z)SLpo*@^tmu7jdPG^vIz3-qV zLGKH#>+p`Gj5d)iWv)ispGH=!>(QyH^xA4>IWUkLi!agCARtYjAu`tnssLHAJ}ZNt zwPARoVn9JRKetevSjo{eSlP+HKO zi9$Hz6}IJNLrSG&=095+BUbGflnIkqnGtLa3W2$Ue|scHbae}m5K+EnZDZ(JOVnn8 z(O1?ZoXQb^N#lmN|80#kcC0}GQD}+AygE7rs0iB;f6m~IUa9}dvz;d9h~OvGUCS2| zKgM=OZ{V505liv$bCxLOH!ZKI(oUYv--v4ky&l|bQNd$~&N+>fzFgxn#_bQ+Nl`*| zpH3dAprh&>@|sIzJqRZXM+Vo+0=Wx7C5{%ZgS=4N&$lU_X&4)~nGhr!u+e35%1Y!U z8x|`KR2&T!oD~fCdbJ z%!J5V6fP1PxiHuUi*IvBzNp#^iOAts=fg|{|F&o{fYVX9X3?rk>Z}+rwGqKJErOi0 z5QjRbOCXh&XU=%AU#i>CYfo%SVBtN{ta~V z`UJQ@$7uyb@=}xP!;VA>-&SIJ&n1@2DvuucZ@+^I@(zu z4uX?Dakc7h5g&<}T{>{4k=PgD1o0m^W4&d%b1hDLlSRQ@))X&N7y5m7Iobrd`j>wi zbN{a_;V;B!@%3N(_ro{J#$P4KqF?1Z7XN^at}g%Y))!lQB_=e#QPU2_UZ(Z=iI%dt zZwb1_tbVC8SnxB!ae%~u7(AD07`hX>H=nnT0#Bg6YF}b|Y0xs@MPB5<6{!#nlMsBMPgqMy=#t)+~GU!0xAb_X)3V>D7Bf=zj7AG<$ z;UkI&Eg!Mja)0YxuzPdO^;ljIgHX~mM7x5Zi@$)yM1QEfqQPiL{%n?FQYHIaw&pG+vL*aJj3#NFPtm7|+D*N0fU zQZDa~ZwjqI7YL(3Pq81Ai+sOIUq~GyR!{!CKww+3kbu zt6RTaZ<4&Y1@3E$D&c%Xl&II}S+U@cbmo*tp41vZIdJb?!T;vOnnYqA{S(vCGSkU3 zvpyv!eg4&t)X={2V8|kT*;_|oae&g4ih_8-7Ntydxt(u2kT^gnlNTdoX35ci8kr`K7 zbh~L^I@g7XPkl~cRFrw&M!XIZ7~yoM6P^K=TPy5fab}rnVBu(Z`Z!v$2QiD|h!f<7 z0pkEWQA25A$|kk!6C4suCfZRiN}0s|@)zls^UOXKl~<~^QD9Y~7X{OE{$qcsRdI10 zbUPEawe4;~e=4t5YgXx=j^J@y*+4_jI}g;ZkPdyDRV9*Z$77c-yi=0&4=^6u3xTM&znY;?Sl&(&p7DfTz(x+&Vk~QPe z2S0$t*HIFZP&aY6b|~HFE~eT5$>en)eIyw+n$fcdg^cZut(@+YN)p~u0noxNAVhK> z%S$cfh@m6f-e6T2krvLk)p`g25;wK(8l(XO$Rymbkj8J4%0pFUdm_2D;3m0v8Vwz< zW3Ppk?Xrt>f9gAt^fa`<2IdSOGz@0J>?exjlXo|>KIgBH4I=2OX;td9?LT^H;cTAz zQZmcr5(VY1K2DWm^=BZu@EcvL$=Xdg8*biSSwfJux|8^2{Ov2+-mDzv1wHe1*v^fA zH(PrjG0`@)?;8B;eVqz57Ds&h@m8qc0Hfe6=mpbiLfW}`?KtKDe>{dr#3DFWRR>m~ zYvD-d$W9)A1Y^p$Z6mkW|^OlE@oZh=6nPcb&82?z9d0pm8TDqLY& zEATAu0Mg5npdcZGltNM}_a&oGWSQ|_JJgbZM7}Ivx|9n72#D1GPcP$trl!=jmC?k} zKGQc$lU;v8K&)V)Pn1VO??Etpvu~9csI&+)nkMBQXCLdadE#b?WLg?6il@Fy?kw~f zQKqgTF9VgXh=1bUCm>6jYZwD)geV^6mAs9g)J(lhp3L$2|9yIv2N}KrfL>rpY?I1a za%KsFnXfW>w3#&6wpuo3C*=1bMSF8OPnICG_QYJ263r}AsT{Fw=M zy|HBUX15JM#xBbf*nSLt_mC4T3?YnG#`=1VdOT%pWQ{bx)#GnK$St-I4zi7WAPg{h zfyJWApV<&&?^2NS;Xa*g>07O?aVsrtk${v2nC{Lz_O6<%B_v6qHCmTaQgUyKTSA_l z6yrUx$v={It|kBeo%)$=lmghF2e0G|U7>m1L9>I6Gwd^$-er;;_-oEVq+b@ePj)Fk z_KrTs7MY0@{!CZ07SL8u_inBlMOks*oUzZf>-o# zCw`qP!B{fiy_78Sq_?%oNtSn^iwiHTm}9!SmNwERJphid{B)rFUGp^6y(b@S^xJ{6 z_%-+an0o|z7bhr_;aUL-_k+9us{X3j#0}_+&z;&!n!9w^_mkPJ6wkb0bI|6Ar=FnB ztz)&mQkh0j+$EwlO_hsjhPqiDa<0t4Du8pcc~Py*-}Vc6<*7T_ow@4H98K-de%yP| zx^mak#wyms+kN;JYN4pL?Af~a?Lz=&9qN?s6dl|ay;FZ&2D$%%n@f^3ys2xGEitw;4oq_IJ4eoO8tz zbC_;UyHINK{2Ih87KKe@hp`)c`Ez%ifqc02QHH~J4-T<*?-wq^<|7EMu*wV(+^#dZ zXCHDn*!O1l(HHt4Z&BQ?$A!)ONbSpjDm}#>yUuS9g>^BFZqzdIiwXOF?)|){uLXSa zNNI$*$0P%<#_|dRGg%Qu`Eb)pb;rEBA9ZMwxa94avCt}h!^Kd`2@w+_@&NzdW(teU z<1}$))myBI9?*Dy2ro-A+mmeZv#Dc+bgT7y=nMj5X#gF%x5T0-D59HSL9@)outk5$ zai2P8K+Os9{rB$vmH_h-nnearHv;TgVgtGw5WE%rmPZ-2|}9 zWp9`*Y(EwqOc-7zX0eD;NxzX=TE%Zi?`kAnWK*xjEjEW%w0`j0D zLs(ea<_s@FP3Qk6#(4qDScJS#K&s~cp#9*An>xMAI7ZyO@QB*%kgT311{6S3f3=t0 ztx5k4d%C7#O6~*1v(5Z!fnmjLy!u4^XV4O-=3g)P1zHrpptj<_|7re@Z2SMw#Z{%U zW&MR*KG{ItHdGdHEtZD3>u{UVmx zl{8sD6uey9tf2&li#+2vM-fWZje~k>iO&*+bX{4;`jrF8g^!U;C>S;)ZHqpFd(&@h z@cJ5{a0m|={*sb(NH7RtuR)9OwtIpAr4;em2RI2eAYwTc%kVT!_ihH zz89N$L(&40!O+rWv0$XxCc9MzIG=4)N-^=fmaZ@0^1`7%G*BT?hJ5~FT4KRmgIYcP z?fs`Wzq&6&bNrl+c8sjLjpUm1*xbl`71T_lC=&myP^t)|CXmNp)o5MSF%)94A-$j5 zj)}H;_-2rW%z+zL}Ve#tdn9xF@I)KBftGA zKtiT92v=Bq!JfSbds+1^%&sBm^NhSdql%YDmSls(kK3>N#WLfC|5YQxTj_?6N%=cc z%^g}Hw+Iv)4!kpXEFsZ4Au(%r+@*5rH|hCTpM5Jzo?0!AB<3AyWZ?=Pmn?E5r7`l# zzgVzpnHRp%eO`Nyc7{2Lh_yN?edDAgc!WG zo&bojSuGvO2Q))cQn#MwhZmjxhZCJoZelTfZ`=JK+-j)LQ}QbW zi21~Z6Bw``b5tu; zvZP7Dyb|10Z-dzUbkt-(Qr$Q)6eka6$vc!8mwcpVrdZZQ7&22z#o}$LqQq8iq6*-L zpKQzzS&4QV;Sh2PvBm*39}&uOvIBfB%U?`;mIX*Hle1YtEqyK)x1fiyDD~2m0d4;BXNTEs`2XTRC$|@^4*;sK8 zjLL@!e0O0QK}AEU!sc~0qR}+ep%q%m_2MSJYvBv%8!H)V1>=?i6YI^6mZv{P3z~@# zCds_So+d25VTDE);nbX_L^07}Bp#2C76Kfp^L&sTbl8RG!t+~|%fWH(WlSsjrpegY z%1=;v{iG=TAJd3SDdcQR%_}%2m|p%)CT(+}6~V>ZD&Pwg_@*_gYmwK{LJNLsO@tV1g+*CxJ)|>qCdok!e%IU{rcxKWXxYL9l zmrnkehxUc>*_-_~k`_DP%3@N>;OVXW!-Ob50lq#WKW&gWxA&Z@DBO=;D!hJxA`rB=sR*sWa>}qU3rP{X{;yn z_mE+I!25T9%o{}BiM<@+&k+AeiCtmjzr8FfaWXs?>963PF1mAoAAN;k9NV(Q^v3J{ zH?*OTMf*woFL1%41I`}l7$Pln!;T2t+_KzNoQ%{?WIAPEO&w#$Na(85*FPAX4&a^T z74I?*D>_6oW0o8oFhe;D>QRvm6+U-!G0F)5Iz0;B5h7Wd#SyVsYWj{E&Z7p%BoxRU z*s9H8ltJ7ze^e*c|3ro{{V|A(g=0q`-cOLY6Yh!#Qv~8rBRmA-#fNbQZEBxm=obp1 zG7W7NIM5FW4DhguYL1hj2#qGTx=dzwI+cdIhbfkouTgucfU}iHsZ8925QM#{4rrPjY zblb>Xq%oFWQCdw}mcgBSobLJ8;n=HNI+Y~_!@#LQx6Kj7VdIYZ1Ar=iSdPVvjT$Ze)PSiHx+VLulviB}0=EE}>|z*&TerJaU$#n?88-6tJI zrnBZlxYSeTlbtfPnVty%8;DE>MM$qLs#GVWWA}t6yU(dzt9LL_oP7n6x)RrXBDqiadbGRzZdT!M7hC%6kH6Fm?~%z`fnH`QzPS=x6_PY3J7G z{)gNBpKaZS{G$vCub#7`tnsPrADKL9?LD%hO5v6} zD2WZ47Hx>ThVzZe9%?^^ZhugR#uy)vRM}S9UR2p;)R&GLsbVb9YLOMA+9dQKi*y;! zyA&?;jnw3m5fow6zq1$1{)kkE*|f`dc%XPx?oss!y)gfI$Pyf(HflUkP@3g75HZwx zBHC3HTrmPKbBWWLu&JLBgNEA{&B;RAg;TUZS;+5c&7|?gpNfW=0}y?`0ik6ZwfxZQWDY#!J5kUjG2=p z^kJZjv_I3H{Bj(geScbbeF$F5*@C2+FG!Zl1?gyfyNJt%{L&P=lQ)!ZAS6q9~8*4ss6~&&^rI*eaUr5Fe^K?b( zAdz(d0cht^a5u~q3y1pm&f^eV4r5HaUIdps)D|N+_JlZgFMr(hI(+LLiVKobvJ0{M z1d=XMFwqUsAW?T@Ux*nzQK&FC6QZGEZb%!_jZw!m;mt^R`%GQsZGZAIr)`lb&0A)v$XQjw~3PPI^L6LncL>sH+Ar)a~F?36= zs9J?xU93<$HXU5GD!@ph*_jPo75OUZ_9gHSiivsc-Wr!PggGrB5vwK`YYG@QBjW{V`%sWMh|~4 zCkKAd(MM`14;o<)MlXB+pbR>Dhy@vbcdnC{W{(A#bvx|W)a@M?b`G2|PPX|fX5TY! zH&S0D7R(;MPh1K^-A{%FHwFktR++$VC0QJTqXtxFx!Xx9>;mQ?lgwnK35(?FVh$@c zF^;J3;i@QwY$(*LG9<YJGO%k-{xg z0(I-2tfQ&A5mDnd(=K_oNTaN+*}oXt`0PbziSYf*($!n*Ojhf)HVaI%Xw`~_Mzh+S z-J}tX>(eeGlsq$b)!^7ILz1vDi!hyx+m<#V)*}7*-rC_N-4mA#eDydCuK-ue-OZxI z?4d6GhX#uzSDupDw7olv0pjug&|0-pdaaf*%bx+*#q3#|1l&c76tl1UKd$9~aM8?Q z?Tkj&g+(8qE*wQQwuw7Hcr$cWm&27TvAPU}n@^Hm?ewo9J;yBF)q(7?lcn=Vn1I}d zU=1p{+n?!<>*_Ps6(d5U1WWQxVhvdyqrtn=^ZTrm(%Y}$9dfJ8!TlO*tN}3(6;p`Q z-8(w5IONkT`38#0Wjd#1nHI-yZNlyxx+yZ*IyYhW(#QmWyY{e>p@x16!M^tqDpJFU z?3;IC)dw{$pi_m=({2KfvvZXUi;Xn_-&$5lJR|%BisND_@bngU^@k9km`}^*ZD3TskND1rNP5t0dR}Z!Ynlhg`t~i@;g@@|Hp?m(-KQ21(@7 z{n3dGWA|8;CFYT3l^NloVKZC374IOHfuGGHPR?=k`iWEqNL{}gPy(Az&f~d6@;l5t zw3Jy@q|aDZ0!Q2>FWpgY7GEAtR=N{F2`4MV*FHZ#l!M%2T@tuE$;t!b{v0~g zq&3B_T4mwt%@4EWV53!v#spjLVJAo^E*w90BcMpH)a0T|qpbr&U^fF^VC{zSSmB0F z-e_eD^70M1)F?H8?jxKf+q{=b*RFaPJ95}t3fIi^3dLxR+Sr)rKB!wt*S238EiAWb zXZbc;XZ1FZM$L9dtLR;>1RwP?K0%XiD7W&pFVHGY#+UN>n_ua+c;$5r`a8Hl(hUoQ zfH~p^=x>#4n$EoKrDGKbfye78Q8pRlw7{-hF*IGvbhn}Ac|-GhbLz313_`z(ZTIJ{ zz^5HV6uT@N1jem5m=4C_(%=X(O}h?4<74i+N+y%yd%WFtRzKmE;No57qx$~iuM*I z1$10Z7;OJTjB5@U=OKAl;})*cHqmJEiUrbxS$^!lxfX8ehX%>@Z&U(>WBZ)BB5X>Q zbS%SpHd&M}PQQFq>4m!wMh|v{8_P~y7#6^EjXtRiFI7*-4Tq9WYJYbBF{Tt^(Lx$< zSr2@PMJk!PKv|C%_Eea86X7i$NH9zGn8hM)cZY2s6ctyVcv~-y9PA$RWJ3F=~ zTRpZx@7f4ex)%Q!U$&H}yQ5t3!6_<$9!M8g#OSa@JE%)m{4)%p!BaJ-D0c=6E+Iqq zZ2@JTH85SMq~e5Bi~>B=y+Gt$zE>u%nRHX*2IVH1a6!5bxJe@wPADa=xcV!8!+PfA zbdfSFVA~U1vTB;?GX^S8v<>9k!Sg=%hwY(cVP~)2JeTkmC0vfN@@P$YqjilRU`9(PhO*&h~^f((3VTp zl}+P9LEq)Zt{o$JJJJ04X)CM;p<+W}m5fQ9cR^_@dO|6w=xAbFMA5JWt7)z%8}hbm zO8k;4uTw=)y@;xQ7U5i=-K9gF2ydZR8`B!Et$C|CFTok91(RiBpA}rfl%jV^rXwnX zdBT>oRXj81;^(=BWry{$g?paxQ=wLC{)}k6&(P@%b^m~(D|I~ItPZUGTC1YMjh=jR zMNa($TIw@M3Ond;F(imoGtrWC%s(n zxGjIkZ5p*}N#8cV{1m2-B>GJ!UN8NO{07=8S)V1*o>2{j0Hd3%j9^aBxX6?qc{;<( zwKeZM4(3*r;leGbBD?C$068({>#oTRc|ERo1Y-(1a}Gn_k3;V^>wS#+peeIqy-?by z?I8_Te&A625#g|y0=BasWOLL`u`z_0{QPzaBrFT{6`g`)?IrDjN}KBxf? zLsgT+`Dz1M(oY=+S-s0<(#vMkiF*!U3nu%eG{40lT&XZ#xiPO{sV+br={ku1S{RcP z&AL0Rm}#6wH0$b4;}2lM5Kxe4Cl?_Qj2LnB4L;0?mZ$=u;5TpZ@9nE+n(s?BupH9Q zI3IX#{{;{VeT6sJ#>7eZzi7!5Y!DE@|0N)FBK=QS^Z#Q{asFT0rsqEh%D+`~+Vu_5 zHGhWOKw&3#mgO=F0}3T2Vog^t;>hw;cc}xynmU`&$FxYQz$EQqi|9gTXW!^;p>5|- z68pKNj&*aN09TP;yro{_NB3jRY+4JAW^)p_QRUR#=H&R>#Lc0u{QK277YOdnl~gCT zeOWN7eHEb-=F_HHc!wk6)23dywF3j(rr|ITjbeaz+WPRmuFU2H>{S^_DI#qT)D00i zf!Rk4;|t|iui=d&)Md*aD!Ro0McNOeTabi)8GMslD)Jn&TkK|^;84HCfGMUUB69IG z-tqHFu;J%DPMU0kNlB>UROg5h%fn2^ZQ4kIDS{YIhiC!(gBS+RYk=MF{hpMzj|edR z14Di1?K1>kv*A5AUZT8B)N{m__s!)=1yZcLRzBZNJD4VXUmxN)m8kzg+Btt${&d^A zV<#QEW81cE+qR8Pc5J(2+w9o3v14|u4sN~&_nvd^PiMSi?9V^2$9k$})mk-YNG@zl z1vVXt&|~pe_IQ#i3vl6bWq~B;H6(SNgLOGWnWfO1dnrQ_TbsL>oM-qm;dkcW>@qJ& z%JUu(EyPu!h(bcOlrC$7;@dc=V=##)Tk1c--rtO1mBIO9B}~4wC|PP;gj$ZAnBpK; zgfh$y^Iq*TDqQlV$MD54Cr^dTw@CmV}oeHzZ4zPO6OS3~YmehMC-Bb&vP`K!pb zLp3US#(xTHDNNW}8-GPIXZDH;Wxh+sOP>q7C^w|@_*4VrtZTribsT_t8ck%hPe!qm zx{PIL*u&=Pj|nxUw!jv^2{RFO7_e+@T8O|x^Uy0=qy40hw>!T=qm#PtO~=jL#zufSF;yj_xx>w) zSj`pO1uE zP5Me5@-2lR|I;b+jU4t)f2$cKR^nV-AUr;*yRXEs#|+T4%e{kOV4GVTF@R(UYRAXW_ zh#q@|>84Po|Iv$HsR!4l9C5nyHpG~#$|@qsanbqYNz5OkX931Q zGq=(d+2EX@3c-}Q0h}1F7#$SWtfXPq`dN3+4=0tFv%yPx>(CS5e08HP97D zZ2}K7R_@Ehh@aG<67i535A`2saDZLyOXjp(8{OozXaN%O4Y_GKkfgF8W)xbk*KpOlG^yB0fMC8|-VXIgP1#&i~zu>AV z`7x~J*xuuVI?J!YyW>mwL>`IqdnNsL{y18;6)8??P2k@9zNSe6=PnRK-{#U$ZoPf&e>=4D0O25=`>n|ip2UbD&K~j|>o_j1;zEP@ zg-zoW!-G53S&yJju(XQsx?t;+{VOi2a!S6ECADn+S6)SQ!NhiZ{-0ItRAX z1x}p)nvgdnCUpeohu7|uiu&eilH%NAFtu;uwN2nZws)R@P%eh=ubt)@b?{$2K=>jf zH*-?1uT9 zodeeI1=af`*|y;22EpI`7V72RsW*qiP;cDl(+G<1!KgRZdE|vMg~tYj12FH(An1-^ z%fZ3!FPYhHq=RPo#FIs936$oy<=!Jvj90!Fies&tV0R$1zk)QIo^VQQB-N|bq1S5w zaLRR8ZCIcVE|-ojmw=GKl<*%q^ysOT(xHdAwC7~L&=~dpux6EGKY`0Mquz&N)qlh$ z8~jnFEHrmyJ~_1NE5(v@^#^Uy$-MT&l6b?PCrOdCjS%Mu*;&O6c)L36U>{o^Q5~Dy zvIVJhXwa9>I4R2^*~?RDmyPmO4Gf#P9sc=gCo93Qo=tekTV9!Fxv|UI&x_679%DI; zm+J*!pwCgMzHYqoOxw|PuOBthwG+tU%Wu3+nO(}6vEozI8}7Pixjw&GtSboH3rH8* z_8*#y$K&kA6TaR`2q*pnMCD+p>koO9)r1$bWa~*e#VHfHra>h`4 zMrffO;3BoZ{SN z2`4#gzT`v?9=+*jVfEw8tnG1Dk5`ACF|BNKVRNKG)O_g7(WZZbfS>orVI$*YhAYq$ zWKFmB3Wwm7q!Al0z9;0OzTw;H-x4$^Xuy7AwJzq$t){!CZ6*Io(-XOG_RK*)dr&W< zv(s9{F9>fe6A*3FqV@jfzG1W~9d}SV9I~zWZOd4QBD85y%bU){AaBOB^w1L9!nq|b z3pQ|^mJn)NkS7p}c1z6Qp5l@d&Z4O%fA|I(r?6N{w?x!ViKHgyJ09dqKPtaUfx7B})lGMz!(OwtlVge9zN!knBDt36(6mPgL3 z!lyG4Pn5VhlS?aJvAbJZJGU7rn`?qh!baci@C`8^sBO}9o;{4HmM=Y%)0T>{zH^QD zAzymxGvqoy74ot38SwwcJpK`=yItDR{v_YNq3M77#`3=vsQ)fUFMVNrRhQbIpK^GT zcv#w{$b-J2g2H1FniA21;$XlK2{MwVgH!$*9y3jZCuMOm?+2={tZP><>R=PN2-rsZb1$74<=-& zdu)F)y-sR8-!kBS;2Nmy+4y_P4XxNQg6!;uabRETYjJ-p%Z9`fxL=OEFBmc$)fRGw4iyMof#onf{`r^iZL+Bi? z$|Q;Y)2>eU0L7mvM3oS5r0BsPp%uNWMDoHsPM&S-${mYr^6VT!4QsoLVuF3f*%RH0 zaw2RmSGVV_R1|3(MQKD7&^uF%!88Y{5XtPHKA1KZ?F)hQx!RG5nCdUid8>TQ7rNJ%Z$!Sp~gP zYc)=L@Fw~i9NU?meJ3 z!FyQ6u$(#Nnodz|^m16arcj}u!3H-L7k{bVT$9n~RZgKAYJSk4Jwl4;bAZ+e*ReXl zdu@JTnCqm;d5mr-wG1wa6;>l`v**z);{R$j@%f#zq!C|vJeALtsZ%l3IC+c!;R9xO zU57{Q^EAoLN+7G9HKL)8{gZn^u<+crhvwi+1BZ#SUTQKgjev)n7bJv4CCP$Js3LZf z_`7u2Z-ji_<}ucm7*aL%orTRUbrB?#5b3HAUR+^lRFz_(*wXT&*j0%?m|YggE6Z`!nsf9iAx8B$JX zy0+qsb6>x99NtqKP}Y4-a%<|v7;;2+mqAa) zlSx%yQDvj6thTqUsMb_oVs%&xhERKuLs+V$G&X7$CW}NRYV4`VukiO;^5rTEYSYhQ zO>Z|bs?S_ZxFk`D*iZW#xqmMh-Ps8S5~*ZXv?yQOStYzp3`;b~9f}^a=bT?Ld ztg%R6RZ*+6JgeGXR*)-2wHuCWnBDiXO~@i+=`2OG6f%E)79O<(?E{N6vI2?5hD5?y zlgao?axjre4JT;f{BF_RgWU%-W|BKAcjxj<^9t7UW$mQN^CtqJb2ZIV`0^vQnBEx? zsgHukCBDx3UCG@C=As}Oq67*65^=`({6n~D85@y6f%3!Ons75^N_-sY6l9_ci{sDql{@FmvliFsC#cAzGpD2el%#K6cq-#C6N? zjEKGrljI)ZX&YMgQ{P35wD4*qknEZ<;6kaC? zCm(VZJ$g$Eb2BRxefpGR-YG)Pp(I~>E6#%Xea_xr{7|%#Fug#}dFjNJ({Pue7gXT1 zipVncn)@zKVNYu;uZ)Gio5P2sU_r?$+2DmXg`6?V!%_FNI*tPUav$KlgE+kWs{NhG z%qbGLWZwkEhKy5OscO`R$Z@`#F_%S7d7IlY#zK!P%QGa11iNPPF?6H|d?+FpthoCD zItTV)2hEic3nbaXf6+;>CZgCs@M;;KQh%>JPJl#dv2(LvZtv_?#n_;t zfuz!t4quwzq)rmVH#{JNo!`27&2{EnU+#L1t$jjkug;h|y?rIk;fBI!o!GkKT5d$% z8~l?sNZcP=r-VOHMF8jO;Urys(M@DeU1hF5b$&n4BfBiiyRra_NsyXhffZeQ?YXfI zBj3Wa8)3|0$RJx#JTSN{_r%$5ZEvsP<}E_RzNk7g03@&PjXqR&aG0W!byUa53kBxh z_zUeJ?Y5zq)6ji$6Uyi~q_gWLlGSNkIfk`e^qPuUKDenw)4#k=D^uZ5sTWg*)R8UW zXsa98!P5yfit$=VT|*Qz?d*Z|H-()~?Nq`AJ0Y7pflP!fpc~Pno#TWo zMfKVDBR&~M%6Pq(GLrt5R-Bf^L9+G%TttS~S5V}e3{>(18!v_pD_*b&n^SdLtd5s2 zmx>4~b>=E@&C2&Zoe&r*f{L3rIULwbq|cxy)*Bk#vc8=?0-P=g2NKyIjve#62%aja z9J}o}U9h!EsS(I3`w!I*O|-;}Xl87l*!pvIpvP9%h+UO^jn@hYxnY>BPKD3SkTZP? zIKH3^R&_`{bGv@efy7$y>S*Jo;1K*_464uCs2mYlY|pSgCCN-VTSO>`dCdekU3r`k4t z;@>?wBAKgXAA(X(nAh_Jx{ zoM}>V%SU?zOd>DeL!|G$&vl&4fq!AK`hLjq3b#;y(EeB&Gyysxj5I*U4Lc(&X!Z^m z(`tyyyBzXU*qp$~@s1JuNnen?ybsam{@Pe@ztNC;ljAMiV*>ucq3_d1h#T-$dlNyO zi43p?a@};1>l^j}qsy3f_a2CU`Y8>d)=Qx>?h!O*Que_Uc>tU%}Iqnvbm-K z@lld@&0Gkd?lm_YJUZ_nrQ{!>LkCat-935L5dkbeL2fF@^+^Ik!AwPgZt4%XnVu?L z#SyJTJei)Dm25WzH@Vc?6#R}gszs)y4$VS!cTx0%13|G83insI)HJCGhS0O82C7#W0Fj*?7YWekE2Pl#LUHBnFvU zLo#X(G+x`vjgjpu0VqY-%+i5`aAZ45x16>PeKu@nZHWp6wU$;9s6;EY)5(P%x~IkY z;=|%?64z!$CDFk9Tey~OI!Vi%aZ5SuLcDw`h;dDXS|ZSE56>)s2VrPO8roOi8LSHA zJC3^Q;)?nTJ*|iG?L&P+%@bffrwi;XItqW9(5-V#`K`JdH7DkB`&ZJ3Vz7HhHeIf| zhKIx`_I=3eB!%_3XY6Qu{X1`|=&36NFDbT+n%z)WX2lC?xnjD()~{BpSWh)IST-}^ z;Fa?r4V6erS?VEQsy!Jh1M=HqS>DY&4{{o$`x&O#m)xk&572Id;VQ!bjPyE<4Fg!Y zj@~aDhhBT30F2Cw=t=>4lortP{`2M*8FaHZD&>kkLMbX~n^dgV)uWp^u$cY1(X8uw zwR;`dZQB#N5`PX{#9JeBk&5eP)xVZYWdux=p{S$y^H^X3UfgXn&WWkouOG&5R61gj z$CSgvO7ku(jh#Q4rGXK8$yN#il&^S81V=Ey`vD%qeC0wa_qqjBU!wBFY*(0KmEB@C zwI~Y_y>lVNMaipWLk|~4eqwm4sv3D}$|X&PyZG_(A`wV7w`V<74tBRWhK{I-5vHR4 zq|O*%j7CP$pCNX$L}qLu!fHx@d&hEOho8NMB?QEgWHZ6RR(DY+v{apq+&M3Z!}&0| zp9R&B;@EkpAht|YVRV)N_v7ied2ZgG7DXPm&C41=G07dda#ATz63|1$799=DJ))Ld zxFl$D_i1aG7hHHI0_~j$b##h2p}R^9RoY4`+j)Vf)E$Y(zvYiYd&o+~R5^?z>7xv? z#su{%RhQ4yBL1n^TnW1~pV1;cR^UgD^3gBgdNS3xD()|5(*6sy{e`|SlV)A}*fHorIF0Ja z9j4gD9%rr1Eh>oLC3Q%2Vc9p`h$)SKZ4DB2M&-oaqlmiYNE`abMnt!=&k`khBHofh zO_pTLOf@V``o-}HM(cZ%+2v8s8rDs0&ea!hIsWuf=iLO=yd2mEj21#kuw#?6NaZFu zTR1 zzfJSsl+BdFn|r;j+O!i+tY4JWgnNp}-HwJ2-|BOEeQXY_`~4vc@DUbAY!Dh!!Q7Aj zoR)0x*Pe&JUw{O=Y1pRVupTu)hHhhb9?yw)O-Z1Sk*P&P_GjKr>KSCf`zx>TC}erh z1S59~E(g<4f~FxeAyfeoxiJCt>UAt+8TBW@06_yu6IN=b3uB_N*Bq%Bw}wtzo0^-_ ziwia>3)H2kL$62dBNk&zuWUfNk=Vk2vzd1fZb;Y*l3p(LX!L^!koA)}aM*DB&_ zNEr*xZ2`_j#B58M+EFehs$y(t;j9uTZArykp|Yy1yu1MO>^AZ>@-_5+TDsLtrotK* zSJf<&+&j5Y_0Up|O2gdR6X{X0>+G&MhF->RtG@S;d}5=Fx)H`ZX6JWHR@dv9=aIKx zLF4G5&FM8b+~^eLv88$&1M0EIPm&_&Yai z(cp{{iSJ1fMKOg7+C&8iWpt1~(q}4YS-OMD2#R-&>6y760Pzh3?)1gMX=X+ckw9OG zCoC@M;GN5R=IMA3!Ycq-Kk+GT4?XjVf$PuC*K5o!3_t&z+?_y*<-73+jE zXTh1K&`mQc5?@{1mf9^H&s-u?#7(p1XGVk~DKh&J90NFwr0%3IEGCNmvf}ss487R$7tu(;XDY50*|z%(}X`C*%E{U zDTAaLhNH@y}YwSVYyCO)oiUjE$Ke~UzkaruA%oA#|cV_^v3$@~gzHG#< z4)hNI_4>!PAyU;4W45fdb{x;H@Fq+eu)7Z3LkQ^u z9?g-;UuH)9z&j!l2v}hfRlIT_C3;RcHLa3UQ>2NirG`;gq{J+AFBmvcai1_EjtawZ z8Cbdq(85V>IT4$3EMEYM=GTrU9uy`11zaN4#QbkK@>)_7tRDcX4I^u;*ea^&3=F_& zTuq&fEqU0)YO-HDB zBYShMJJUf`joq?A}7lr3k_D_S`(RFNN?iP~xm>P3p{1`H?lz9aW@Io3DjdIj&J)~Zj;%KlG>4LuLz59pE zNHczP*#%85Hw@kv1ad!$5zn9?{e@Y!KOo^x*SE$Swz1`e5I@cX6PQ$#pIVH_6eujV z=XZ6oN9rswIi!tNkTB-V@#UKOAylSMz+gezSwXPK5 z8dD2%7#CdY0I?5t!f%9%fjUn3k#)F}a)7Dgfc8XraSeKr_&LU?`K9u3d6Gz*%l!{c zaSjWtRQn*4s2K8?c!t8b5jv`Jy6%7usSNvRw&P5Da0#m#FKI9BdJ;RRMVYk>DR&2q z^FXc~cE)v}euM6}HPH{9_*e%K_8t-QTl(nUr(fT8!i?XbOLj*{17~(Q-?C+aa3{SC zd$wjtx%R=qcLdI^V^j ziTXhYLfH_F{Lzf~r}Hh#T`ImEvV+~>^+&&T1CYtaM{hZqGhrR&| zrV_?_meMvZxYA2LBj0=C-Y*56WjdAI4yT(93)dpzLJNKBt2Pp`;Yf1IOAYTGW=g%$8a7a<;H0X8+C{$BRNB>12xu#jTYKky(tdUCtyM z7EcL=>zdRR`-cN)0=C+)lrr|z3Hx=fDJ4nT&B;&^xso!`Y$9bU!H^XRMFD zb}mVQv_X+FvSk_I`+|=6RiCp_Jy-(G?{7LAqG}*{T-MnanUL)8tdll=kD{GZ(;r;eloYD2E z%G+?@I?#E6{e+r>XYxrwL8YYf>w0HBhH=tpTCFr$$1>DBU)gi0owN_k;bzh9JG6*n zN3^gJv|Zw(JV-qc{COFB|1Ik|e9CM!Lo0U`j}h3$Van!ucoQLSaW+JJ8aa&HYWQrn zO2n`-@dayGb=ja0+1Tk}Ni|~)Zw}T>rI;50{ybzu}8h4?%%8!w#qf*ZgCzYR0BB!e$mx&fSK4+ciiS+c7atC&E|9 zOI;f?Y%ej}0EbLESL(QLeBCMiDw&67Mp@1mU__Lr`~#{U@QKZyPR;#G_t_|y6T3q6 zyF3b!Wof5ERhY`4c-vRKXhbaR**ih<5={qN3@Pk+Ik9(9P6u%ewqNISBK06253~Y7 zUBls=0Bz6{Lqlzny1@uQW?izHUUDb08f1+Ma?o9Z-gTOWmI~R%*^1*-`kbQf}iomPt#{n2s}|0|P843w%-9{ujgq zd0}be+DbvxU04bwt>l~#8b*RO@$@6@SRo!`N45QHSpD|57JhCf3|v^wot*vkBOIDH%glVge#UL#NIfEruZDis@O90ugX#T#9` zR<;Y9+u>lg473u|RONjMDadu-e@x6IPiF+R#jZE2} z^-ISb`ljkx;TfGGvy?16a+0-0Cz)23X=|f4e~PzA@glDIXk@X?L=t;S4HXciu7&j+rCUUNE;RYXbN*v8GW>oO+P^$Iy z0mwk~$kk845>Sm(uM1Kmd}Yu222z(~J2-1_isT0$rE;rXDuxe&zRfOI2A28bFlG$w0A2G1u$X609fVma^WpW# zBl*H?ND;@S2QQbrCxrC|-88D!sA?6{B7eVd9w%-^=|iyLd``J%?r_;HF_F%*TWD;? z653%5{ZH^lTdsKk{+LbAM(Y6nVTqpO;ka>FgzwptshKML<|!kE@*V!^=Qlhda5QS- z4h><#!bPNll^FY{B%eRtLG!ciR<|3M&2$)Kep;XrXcN_=imiI03sl3XO_fF(Cr7qMfWbdLV z77Q}fDrpRO^$RD{;gwyEJ1WJ^EApqVA0%%_C&RlA;ecwTc3p{Htwjk57X`cWy?2RZ z@c}Rh1Qe|q@>x?7*`bwXu$jhFJB9F|aYC$ec)*P`eKNI(VInhp3AL9J94-vkv(7wv z*43QSS=_KrQT!d*L*$i*$rCnHR@rYc@3rc=>aKD?npV_>*<*weC$&-rgi}H5S*Oj- zU$fqw58OI>R^#9L2}jrN6C~Zr4KAKo?z9OGmt=7#JWKaau_8G>(^ zeWt~n)0HLSTse`g6DjAC`fPn1b8QUHPXmtVsiNUsdMI8{AKQct!-UL9afs|;d)vm5 zkDVw8HMEEp=4ck?u70UhWJJI;(+<|F!Olc1SEB;eQgXHvJI_`jvlOG*wS<&+`RAX6 zU>yv|v(cO+)3GR-)ePq(%)f@ZwuUV(Agz9riKHB>do_vEjlYhf?Z;@m}}M$U_>!K%iIejzzPrYzf%AKGi0m!hv-rHr$$ zq5_6QKiNG+*(W!4XBrY(5PqZ}xZ(1WfhCdN~c3!Fe8c3?s zkXBNJ+oOsUSxC_uhj;wtPfvR-lX;th1sUlW&dc=n`Cq~U{u$>nc;*jY{Bmt6f%^81 z|9?BqBjWH+g1Wt{%YT~7HUFo%+i1sf_F^DEhgFsYK!N}MAK0)OQm=Y%c4SkNo zaktuSz4M)UQKFll0=)!g+1)n3f{;$gSlhSOa=grQGqT!0KJM-?1L?Y?h$DnysnAkX zm1VT(Ca9_m*+BS0Ei`2e`GSt&_rdHBb^6(`O04nucRPm6bOdhkR9a}@Z}e0$w`~CO zMxu7(L4L&dB7687)qeCksb#uiVVzX$SOsdGWLV@jC1?-+tyI}aT~;tdbL?$PyRqfd z0~2K)wjJ>6LCO8)+M8|=_#W0R3(J?e@j1&8^zA;jJc;Q({Rx}%2$8cG%C+lTxmL*2 zsWl_T$44uacGWzII0J8N?JT4S=&jS8k#YB}$<&o5kGva3l2DI-+C{hv1i|-%D1JA6 zin2M@udwkct410LqOpxe8%;REtSE z|6l>D%g=ay4(+Nc`c)_{qy`%F&f>o1^}-=!trkGmz^!bw>M0D+S>lk_Rqw?nUR97m z(FLX9xC_=hYn*nSmy2$JyR&tHKD@ZE))H?()6I#=x}2q}|$*NU8+dls4O0>79t zN*{xm2kzW~!G9lrvma;VUHjst=wIW7`QInun$@=CQI)=b$gj1y>J^Das;de{Jwj=U zfftowB0wf8%Q42>*mv?#>9km6ZQ1!DpgP>su`03ZKY!y-cAB;ai_!g+HETMP;bPvC z;b3mj^4tIO8O9K7Mp-idm#~Z-{ORmR@m^vq<@Y|(6DrC}0VM>z3s<^4v=n2s?_vlnCYn)z8Kg0Gp3VE6+Y)=v zt$d|{TUu#$&!6>Ifku0}Di!lV3m$SGn~83@TzBm1re6=(-9xN+emqEoD*zJ!ZnlbG zEA7XBxN~dwP*@RAR~H@Cq*o3Sf2IzaAn5$kY;Sbczc($=N7=y14Se-wv0h}wZsw=Bc+=YNEDbV6quG5z?CW;!4&Pf zfBXHD_YxMv3G*p9OH4Q4FR}=&KTq1jM*8f(ScCnu$prC-|LOaJhWKBDhUMR($x;0e z2UEP{e>NG)o19Lqe)0@CB@giI%Nonw71iEA4=wPYKkY;;4lN?@GgK zosc*-;D6{lIe2PkCZ{)N3=DceR)vWoz^}*|BYnIl&gjwxQEr`RDe8llIIB3XSfY7b ziZ2C>JTvInywY?K(-0<7_1tKy!1f5rUw@>zNua$BhV|G?42N>I!~RrKb?al)Zz!hp z&{o^)*;NbUL?u((mk;f1;eSd81ep53AN3^n({XR!f#Z8fkK1Wh(3v*++e%&GQ)!^c z8?+_zsK*eqhBQT1EVbY_sLbEB-**o^fWBBiko7Zq$d|xJWX9?oRew7WR0dauY%taH zWLvTSJ8mW!Dxd3;W*QQu7#DJdow*jZhopDWVw(pZs*6SviBmFBhdkw957n}c#`alV zroCzNkWAfTq0-RTJglt;cyUoHmkz7gUmjG^!3^-;jZ}B@E9Y}Jn^O9*#L*OIGBwD} zjwL?PfA^GpoFxfNjG!`=l|8?NWp?6at&gAsWC)SYtaD2+#M+_8)eY3)H%DQ?BFjZ> zI^DyA=S^A4W07~ZoO`qtZY8TpmG8@E?dg5aLN?z1$%j|dM{=ZJwG2>*wfDUI{2OgV zix;bJ3@^_PWFXg0o5@*dTTIZC3!U^kl{yQ$@Mn##bw=MT$a4gJ#V!!biN=yG*fvbp z2htvVKwq7QD>O7$2KDT|mt^|&` ztK^&!B!|y7(x@h`>8(}vJrowV&>QVSk-G5?(~$qY*jTpsm?4IXg_1c{q!8KC(OsLI z0T`U-U`}A;cT-NGMaq5(p)M$KAjf~-Q7*#+um`^2Aph6kVEs=xzIGHLw2uKu(PQq{E5^{*~ZRAifcbsCY-Rae`q_^VXnYoW8B8 z$y~1AJ6tZ){_hVjaDi^giIThKU%sQot=ZeRQnUr%N0d<}G$RHg_t^(l!F1L?L;x1; z(pgV!hmLCvwreU-{p;u-J=XRvYp$YJf?da*Ew(w4NwrK^W=m-BsOUP8XGW#;o26Y> z-z^#na5l@GR+f+X7p|Y|;`%ldl-tb0d~$#7n~Sy6$+(5uav;%;b^9Gk_n*RPIFDE> zhNQ-;Yq@o_h`398^QZN+D$*v@P74ylH&T50>sX$&scpjuB9e&_ni0ydhpsqY*3{|( zR8J0IJfy_ipdU0(a`SU|@IPjU)JGnr6vri)3d)Q+;D;<9;r!|zS}dPy#kPR@rM0S! zd|8PIHEheLOp`ff(rW7}VfG@t4Q@B3VKtUwD{1Q{GVFMVN`4}LIo0Jh#S)CVsCff) zym}%?r_UV#Yju-qu4?V!sjY6 zm;QK|Wc?e!Y-~kE!D63L+TPUeD&)fLpZYI8;GN{xtOFF{t~?(;d17jK@!|>>v9fgs zcVwI7^Z4?;V2l-1h+HJR6JH5iQ4#jegNf5`zS9To*TJQX& z$F}(?OBAE<6NFOujq@Ko1Q>_>5k<%F#KsIE8%LDPDgO-*mo|9=ushA(m_QZ+Q%h$x1Z`@#eF%U1GVIH0iohu~7(uwM}T&X?8JzEmt9=t!XpdoaJONDmPl zPbe)`{9TRu?C-J`8E&?lsl)4_L{NwzFbHW^A>K0xNpO-IIZa)%52eMOm#2}%!u^Oe zuP5Lc+7PuBZgfxjhn6mz@ld!8gzg9eNrl#8Lm7+d4=J(-69ClGbUcCM%5lY?t24z4 zf0rLJUv+K`@s(%2nIP>-QRs)W|UzzHN2s5V|~xgkP5f z^4}k~3~3kZp61nfalrp?{;%)}++j=boaQSUW|^c*abZfQ9H7f$-t~oYm{*y^sA5&) zBw`fqP!o#i=bi3}(Co$@(F-&nQt~O(983MU(hO(khm>5bIi1vZ1Vc1jtSizeg*Xb; zTl0oQEBDYXLlkdgnasK5xF(M+)+F%_^;`nc#Yrmwt-4*6Q4Y}xdV)WO=(_k12wbsb zTRaXRxU^OqyFD;91_2H0iUOZ$U?C)5NBOy%BA0>~=MRve(`QT>#RbWb-Ea=>;4EgT z`N_~hQlXDL0|acKj-NQk&Pjb0D&D_V zsO&zhVayj}?qd3p%4l47QGin>fO9`ebQ`~p&u11Ay z&%^+5BGdc$lM^MXd!tO`H%fCDaoYAmCX_y7Xu3VI#OKkKZzR2g&QS*fWm?f=DTsfy zDGrY(QE#u>F(3FLEIbUu9B1G~sF3ql&X@?GprVVzPt()@j6O%BMkQF2McsSR2g*7$ z=^LLPH7T80ZLEUJK=ucFuPOC>-I4 zr?Y^gX0f-_O~s$zYT}2~Hzb6a6^19q+-d6yj)mCl^Z!^~4H~-Uw2Q*lFM;2}~n*x?33qn+r!(ya@uE%3@i9E>+{rWYN2bo z%;_h;6xWGCDux^qvnJ6cG~|1bZg+kA1NH^jDfTL9TAw7HVaomM1^jfM6r;2EeN!H5 zra2O-m#OQCA-0W6KfBQB{*d@P33F3JQ?!DncR*x&GGm%WSO7C- zeXPvE3av~uJI*_E&Ey=?XK>Hkx&G_XXjR`>6~nmUt~H83!pf=DFX6+k3{7gvSryD) z@T21LUq%N3-~M}%em|#|{^$6b_pgZ!j{g)Jb_xrMX#Q7}G+AowrYDL*rYFOJ)Wi-V zj9QHG@T`5b;_vfr9EF9Qdh5)e$poxSmj6TvNMO&{VQLQ6t+78nZr;6SyUd){?g;pU zY7Rt^qIjnFLjM?}V`vpdxudMmlr%<`p>{=&!v^Z4*1g#QT3@6WPOv)c!U6UXu`c}& zU{?e@4}2nvcKTfF7H!Om3&_ zkU$GBq@4luRjS><9Vb2qP3*>O4m;-5YXm*~pH(M_z58|=cpVNZ;h%8b7ftJ_a_+I$ zz9P^1_SK?2+I$g=vQJ~lL`LInVd6C3yr`%^sX(lC@=kUU~OC0tQ=rm&`7Cp zJ;?4L53m{oD(H3;kZBoM$LSfZ$4WdPT&mI9ct)xL5=5?Ed}5DnB!QU#CsS70(Mt?w z>Sfj{lb=LB*khF~lXEKd<@q6WfQSE38tk?@(}LGpF-IZRk|7Qm^HH!kE1pA6^70$- zd9RbzX>e>l7A)x%l_MOgUI7s-hB6bV6ZNH2LqIvz8Yo=dy5_T zQeT3655_w6?MsmVfmK82fp-DZqaIjX@@`WI_y9j(5sey#VzLzV>t*a7%wV9WHBWIc zH2BiyZtMGoqBTIsIhuS7@dU&!QYJu_vOL-pk(Oj6p#D6M90k@MHAh{NO-z~!rBpJW z*c>BC?^|aPdCbDlOYN8TC@9W0dcHH@WCz7V&A;V}Wxd*^A2Ta55)VVeZ)jV2@E07O zCsdK$_i+f!XDP$XATL(7#oe#Z$)(J=qr{dqdL2~BbgF)p)f86mT)i6A<1{GLpM zAtAQmplpGE`$p#6+!nnZ$G<_7{@h2f8^@fxL0`u?V7{@r;WBgXGR;@|@3%du3?|_~ zf3!kR!q(``1}0jfQe!RvdR4`&+Sgsr8dK;gp9SX_+IiQMTU1o0ePoND`3V%j!&>91 ztxQ|IN5=PptE3EJa4Z!oIarSki#)P~5m6_mVXy1ZEQ&Kpth2uw#Y5KXiV^+KqAPZj z9}#@1q0LenX>ez03kh_o1!u61cPQ~9d_Lxd_(Id@G)B!x=y6Hw z?Lp;S3iN<(o-B3&I`&gMhi^Qe6td*rgt6K{HK+MPBR;@$_khhq@*jOJxtp7xiwz&J zYusw&O*(2ZfTgrZs0Vm({6D0urgNb`) zzV4kDksle6zk99RYim89!LB-NmAd6b)lYAUij^6)lagHMS{BZkq~28gR_1Khlo@g} z9^aRJ(V8o-q-z?fE>&xme1f_BhudfrOvs$jz|*tU$^!gAY6t}}i`2vwXgnn=i`5>5 zP&wL|yyEd``Ra1)2<(8$Dy_k2C^L=SL_E{3FKa)CKDuQ_%*uYUQ$fJ7bD`OWX%*?f z32)#-j6hHL$1eKxiT@{Of7LNa9dxE3iy)1oCKfqZy_02v0AUN4Auq%vD`F2nAPz%f z3QmLd(^g0r4&R(-6E=gHQIHJS0^}=58zXyo{w%KF-7jD>b6j~SV{)wA3$=yz_ z-dT>t2`NJbihEeaxP)r*u}E@>NrP*~aq55J2KVI$Hvs0;@0lQc0~nNpt{ zM!Z7H#=id#++;n5N^SiA#|_8-#0?de$bbH922zar_!(z`8_=I5SGJ;Ju3egqzbN?3 z4DkQeT;Oie)58UwidVd4x0nz=aS|327AKY|qLjI`QQUi)`s+#U36sDT8P<6(kTkc|g20IeU2=nESSI7OPG38g?5rf%ZLAw?!tn87qR&D#ukn6@avt?N)eYcSqD)5pS zZT;1D2?R{7Q{$4QW~+L~pcAbA7Y9v}`=xVAhT$nqvBiRybkj#4%HuHbnIx!}TGZfjQagHFRrrYLNO znH@)NT_MB*OwzbHdXg5b(bCn~+Ml}5r805NI-{8EP;&^P3p#6o3xW_uzWG|MH3v|i zDNj05hf_Kw`qC(5)R&)W;998=|& zuOwqsu)DKmuigpKS9Ctipj8fT$?=sqrO`E%tX#+8OM$+ZYH7LKbS?dC731WGCYtm6l4OjT4h>%Q0!bjoq>gA>9yw%^ zQu@put~bggQE2c#rLJ@^y1*-O*o(M?974`MME!sIcmJbWT85dQPxS+vAU|cq&owCd{l&C! zs}@V%fRlI{DtKXW@H+r6wZ#MN zuvna#69-EhW zzL!aDSK@`+;D&s3|8bW5#3wa;@zxzwT7@e@mKW-gp1spi6ycGL4#IYFZv-{HuO#EncZQD+u~|G9t8TdmGx053ZeEY} zy2GE{R)rhWSwIE~P7X_;K_PS3s+AuW!2&_HR#U6hzmV$>@aOt|V)aBp3hUVmVK;{-Vj7#hyV?nm903fz z+NwH2zg5m5`b^}Ek<1*z+^f@TL?>@lBWF@m*T0=RzHN~9zCxBz8=#C|)rdq?)rgj? zvMx@}t7vplEJ14W-=tjZX6EM|cVMWv# zmV-&shcCryh9slmP}Xs*sO|8Q`fUT-AtX6pzQm4m%G55ZUL+NJ31#6OkT|NB@J(zmku7mF7uO56SvV7xClb%%>=6d{i)5<$i1QFYOK4{pL_j}=XUF|}+#KYE{$wiukl?aVpo~Js!8yXH~_hTuI%1=0EHLiPqwvMQ3}&W6X%>iZLZB~g3WRIW?&*+%c&-OVyCZ)Equ)Kd z_s{5M66usl5r>5M@bmoTzD%&MCSu*{elu0qH|EJ+&@Tdd&$IrCdM~q(P)SQ&Kr@dW z{iK#b??fZS8vab-&O@h=$FY@Z7AhZblYMnxMpgZklUmH!w`rRTcSKMAA`rGs9Dpu3 zHrg6{lO;-%&t2ez3eO{SNXcVJzKRx(?Ly+oDVbyURxOU#FuOJu359-`QjuRt`3{Jz zIlCMLlfciG*h&gkr1MgJz$o7!oNC@@uF#tjL4l$@w%-!b&QQ0eH3-jpz%vw9bQ66R zg%3+CF8SDK^KA~iG)_FDTI!*+dSB7&%iFR{G=p1Qw{o`r1iu8brp_d~n1#HcDqm)6 zZL~cJPYp4JEthva?u}9mKfN}z}u{#xVi<_V(cJxW`e$nV~O|YH^BaO7lPb=L|YN|uO zKG}Yuk}5S%xur$w%e_PNpdmncL;)pVlJh9(K0FRcLyOLd=L$m zZ4i`+5J2#bKc~F1>w3Q5?@@Y5SpTfX5K(Ej?=1kz7C*=VwdRSX80e2d zmTyG?CM22hu1FMbCdw`j*j(9SKl^*9R;urINR$gCQXuE@O!cA4ueo5b}qN*~_te&ne5Jf#qsxOJ1dILfNc(o?Vq>$=-`tCgX?% zxa@i)ff$}^{+9|J^>=t5Q=%u~QipvxPNQ~W-F1Ep7OjOSHI_Y63%btdcQA&|}k zc+>o%o;PXj^#@?99?nIA%3M7P(WD>z!(wx;KCyN2if}yQv2497#>U55PaFS7{?dNG z&W4_T_?+DmadYKjP#W-$X+Gy{FD12o3 zb*yefdPMwg{Rn!6ji=A_<@1w91r%? zI<@(Q0_*Jc6K9X#vjV_Db;f6?LbwUwruJ5}wMq6K0De>MLin%L z?rQ+ZLS~*0UZ}O}^eO^=4^Q<#xoPae$b5iFek0#T^Fio>e*E!61KysV;sU-zbt7ZDLA=fH?Z_f;u&eP)0p1>&`U}!YeG5RwJ321} zsDtoI&xVI^Tiz?y_Ll0m0?a!-MF!kSdn;kXOR@_yTmsTL_8R5af}VqN%X;7lxQ+12 z&87=~Tiv_0=_TLm38=e&JOQ|k_6o_Si*oDVyVCmB?gt0*d%_m-RXmsZz1i;%NcX^$ zD~K22Essq%-L9fPp522Gna|2S6k+u9$|{cwI_1l@q$%gf5Ardou0)f>yiXY6iKT`K>UuJ&y1_l$K7i%#(B z^kPw8OJ~E19Je=3O)r+JEEwsgMMb5HstTJ5+pEWw#@BRBF02YHajxR_y0W^4>Z-cp zC9&vZRNE|D6hVoCm7Vh{Y3J9jsU@1FDrp*UL+`bx>Eq<$Bbp@`@B>SY&ZVktnW9=T zq^)HXUIUfhS@jE^M(Z~Rx~}w#8gmO$R(G21JwM+b<9p= zb+R*Z1x-=%=W2`U5^_tfzez5{1zuG;60^%u&v<*5RqAtV8eN;q7~9G$+s6PDvv9T# zOV}onZ&lkNCYqF$DJB<+1)7}`T@_YyP4(?v?F*19w^GimN^C@}#)=@CoxBN%fA$B=(`xESH#_|3wNE7&0UggS2njhh#WIW|*H9TykimS&5rg84B%>V(s6T=xbRIGK<6NkiL zeD_e3?nn_NgCtb3V!%{fL)%Bc;*@qWQW;3B$13j;*zFeO$V_>$Ay}Smv~VJt>XG*8 zuGp#sN^@A)9ghZH%$CIh{jZ<-sV8F+*~ElRHqF3EmPIF)$a(}svljhW&|*fBX-HD?Ry@X{e9tnOxq zVJYc+TsM2EEc@`M z()kd;ZOp(QOVvguDQB9^B0)?4rX05RX;{k7Z!UN^xd!j|skM?JM9(!N9@ePdnd73tJUcATejE>1T9em=#wi%X`ouZ#h?|4-FjHMr1~z!m z8I_#*A1a*3@MV({j6Psrz{EE*Xy8TLoL&i-O(#$8S)&+xsS$#H@f>;^sRXK9Fl_`y zO-`iexxEqgtMfG8W2PlIe@oLuPb$lIk|9HbwW~`htKMw46!KVzjt}>Fg7vq6lDUIt zoT2f-?aXk(32jvMB(5<^NCgD~kyV%}vL~NTW%k$2pQrQFGt1Rf%-s_P+3kOO6fe-~ zAcj(;d2r3!VF7xnma_;dVMb70xTbdT2Am^O>Bj!!9|!#n}4Uk*YqjaCg#@*5*=; z6>9gmpQYf$WY<{9)X0trW{1pUPbxY8lFVR3JrG*BoagrfLg7D4H>+fJhNp?HrZ{>) zS(5K$E^Upsk2a&}dy|W5FgRd zsy+bp_@m*J$7h!M#Qf(hYS|ul8m~LNE6GIvSif8C3>&U0edp@lh&-<;L;AZBRNkWxu@!+=5%4!>+ z8w4A0wE(K$9(h>u^6$9ojUL|QscQKHf!t9(9R_=P`Thp>77Na9;+!uNAf*EX)aFk~ zoZS61g#drXhqZ8U4?;>W!$nqx0kO^V<+*X24jA5x3nCaBytx1z$JB+_52b_SVtmAndFmt!0L(!^iJT6Eo zM1&(1!Y#DXB5_C9ym54z-~TM_p;U+f^jI{N8t61=7~MHnm$JRyk@9Wtk8ISt8dt&I zs8x~TY$y54+vE$RGvaJ3p52_mCLFz}&(oAK!AF8LpWgbToUP0CzgI2q2~iCcnu9-{ z-o}v(C%rO6!d@@!A(?kcslO){_51UsgG-9u2FBQ(j{b25mkc!*{_F`-386O>FClnK z!vAqJ1rei>4gv~w=%$@vce_1V2hjC14>3m)2kwt^8y+D&3gcrv0&Z`Ap52I%V<=1< z{h8LRx@VdsPrv&*NN`r6+=s+?>>C-QTe7ckS=|v<@ZoTANVvwY>R&I|qy5T>gZ+~A zf68$*!kOo>5O<{+-zldXSB8h@4oA&`D@qDn8Yg+5WiDwuM6@<(XFQ+44Jp|mLM38%1d~!01{8} zwY8rNnYZkgB3PEnOLVURGDq>XvmXulTIDsrKLxqF@)jkyhSE!V?*ej1?zOnT1-ZNG z7AN?I(o1&lFC?GbYhM2!WS`1gq~ILJfDAe>KXJHROH!#`Y^T`F2}W!{QeCt%>-*?A z+!Mky+CRUBfx8P3Q6EHN-;4bt=|Y+36Mvi&=_Goua<$<&Qh+?E@=dcZD{}7)?Word z@1Xb-G+NGXFa0(z`s~{tQ&@3F<`i2*=e(0!^%l-1o|5 z|1K)=PAmZ>oE&^uLyB>J^v^!KaOvkNtWijd^4d`CK9X#9qs5uOFdO+*W3%x9eCD2h zznOO3Hkd~-!(pC%L)v-b$m6KYVb`8zy4+gtXuqyCPMm@f@pHh4@=vYW)Pq*`AtKn! z{i+tr6o>hO7hZHX-o5z%x^%{no11w2<0uDJvjpk}aU@NA{ZRP0bb)53sH@dAetX&D zm82sVl4%*IZvJ_v9vCul-fRPssJWf_@FI`Mc8&~HUqF-tSfC0&2TD%U@^o%W>Y*T` z8`9(sh^QD|)HY}^S1t^@wJ5z;1@ad&e04tA_aKv#<7_VGbC1H$N2JEq8GkeHL3jy#8S zjx;?fs9M#(Kg@ogY9$ptrf6f(DbD1kD3`ww5!qCZ_V;?5hajk`fd1Wh+%Si1wY5MT z&IB_|9g&u+g&KBAJ$TpqaIRbh7)pVo_O^&Fk)|>BDBm9$BMdw!Lp|Q}^6WSzYX$nP zkW3w*2nv758Ps?_k+fv0Ds178{Y&!=DD{3;rm!{lp1Z-dH%JOCyD^I#4y_()O(O_Q z9<>=x^6v-n8rTh0V0U%`{I3SMVZ73R=Z$l{hZzT(0qp zMjU#c1#LqO3+Z%+P^p}>YG#*Rs+|Zd0hK0d$%l_GGj>nIEQIk&?KR1w}E?28~CfBFVON*Yq z7wsn-3-ocWI5gj&sv};6BT{r}3IW$$pEKLmki0F{QFY8hHO;uF5}m8YP)qQ|ihBuF z%}R3RRNTHxht6G7eS*u8+x(Nd1LF55k`vQDCDM}85*x(gk$1Ke6tdG2BYV;dt%j0V z=M>2)F1yXp+{h_dH+WOC#$3cOHwZJVyY%M^d3EzRoY!(z4l z)v%M29S_Ik5W@oPaz)^284M-usM)jkuA*o2-a7fsla}pyvGl%P1*Z_IGI17e8+MhC zhzJvw{OT*JFopHo!`$3c);}v0@Qusl;q(vhljpLqS9*g8?@WZ3kLclgX`JwO=gKJD zXq4A+3f7wnH?}59&Q5?NuKt8;UqB2ows_w{0_-lAzCP?_kVqSU59r@%g6PVPK*l!O zL@hKm(OQ@dl1PU!Oyfv(%laXdOd?IKzL-Wx+LRCZXos}|)DoF~Yh;0EO5OlMjU<}< zp*6zt$uKhs6JhCZKEm@_qIQ0f!!;1)K%(?6q~v25=(F9)*dG@V_sdvr7jaxJ;y*a? zrgH!Fa!Sy4EB?3Lp|!nQ>OM8pD%AL7UVYnGIH;EQQlzHt}Wr|KK=z%9I4@BIMXkh57xy64K8-UN-Eb8_o~43wqmuU)yh%x;LsTMA$b=TnQEp-fbPGoOJtjpB%l(wI0<5HaS6&8I;m;?V zg5FK?qp-vR(Y!T7Tq>U7LQ?JcQkD`d!%15Tv7O{}rl`8(v}2R9HAAlT|tWPzSIa<3SHtOKFp$2s-wuF4>Ci%$%K0n zh3>^vlP~r44ykcphT@DA$u~#}vS>!fjKrG6d*xwh$Bx9mg`ESdE1TvRof?!b7)(<= z;{Yq-ZyJWl?pcX7Dfc^34i7g9-2&s^^xVgMMP3Dowu~kjY?5!f!Li8LEJw&;Yqrt#V^4(#%%n)cumkB|V;Kd|+i~(m7 ztpT~z5UPli;(~a6-4d zCj-YCL`4agAdo6dtb6~x#S{k+KYd&jY>7!WDIw&U8v z33BD!d<=Oqt?LJYJfYZ>-NO#X4Q4_~R`;WM#dyy<{Qi$AGr^w@=7^-{IqFY|y#(n0 z$<@%t$lS@?*5<#K36tVy{xPdN+#@~DF0r+o2$=T)rMDg=xTzoz%r8z}SVSlx5I&xV z_H!3?ZShj2SrIp!37;TH4$n_@R~x>8lo^dS>C*PNB|A%f=5l6dNA{PMenSvra%AdI z5}~^)j822yRxb<@3K3iMCZoeffTe!$UfsqgK9cY1u~>BsXf-dQ1u}Mf#E8((dPcdh za71Wz4G;tTxV0I|sIbLQEXS%bIro!LnbCa|fo9be(;+7ESnX{5Z>Qm#zl7&^KH^l7 zG*=IHIb!p-M)uVicHgn(vaO3t2cu{k^SvMDb?;$#j=$HHd`b<=K&|7To%QsSWM&cU z?-fSEqbV&|kLhg>XqT-v2fRa!8hk!ilw^(FoaA@pe_fpI^=jh$hXv9vC5 zVbzf{>z8hODm&dhGcS*SX2_|KZeP4WY+f`^w8pS4GgCI}-Ej8BP-4|?!;M_~EA)~h zUw1SfEX-DqjPJK@?3-2grRt%BCtjRql0mkeNd&6CPXvgdWrRq7KeA-d^v zR)196t{Oqd?*T9IXN2qnkTp`B0?Fo3*V{3ZI`NT-lF-e5_j%|p$ww8vo+k4APrNg9 z0a*GQ!~xwJ=zc7w@`CG?Vy5LwN0=~;AeYK`t)#&Lm)8%RW{q|C@1JD_sK{+h~-~UEV zt0yRu5)mnX*{>;BM6?<`Mvf<2mWt{^+E;0XEI&z_i(jE%)0%|}ugZLh_FNtUtM7;j z3+^c$NPF4&v&mXh;ifsn1!;iPY~5Z!QlP<70SS1Jg8W#7(~{~yS=^RiB+VuO_3_HAQR2$(sxfif@`$B$gZqWPQB!A@0_h#&dZjWjWa`jnxo5Sea?U61%jWDa( zh>pG_-J!CKb&7=*bAU?CT7n;67ty@8q{R#65=p(}XktCkMWSNgH5rctnaEvX#7)%0 zZLZ{Hk$w`T1~cpL9H@g-ny#FsqsI7v3#C=Sl}PvYD7IXSN7j<}w4ZbQF3Kb`HI;Ef zJ$F^SWZDI&(HBC|a4wy0K-ijng{aHwxS%DDvE*RUPN-MGfQ?*IM-}3FmmRSE#DX}W zkIvBPPsBitk0%6RReXWbq28fUp@hA{1hs&&z#=^V3g+H0Kich22xdHJ-&uZwnKV48 zq&c9rs5ev_WDKA)FBhXv_dHaqTMC;83tj}Z6z;9%ccPjo=H)H*=mV1HHCDIwoqT@*uIn4y^g#?|A0;G z?EJ`ajpV?LwWMRUbVC4%1v?Y&ODRJM*_;w>oY3k%&|%;n*;(!pa%Im9%j(U;%rT>x z0djvUl7pL=5D^iv%A9{bBS_;pq|W*oB?@&C)p^p|qO|xbBhEGLP?f)X(Ddq~V2DqP z?viJu@+4}MAoATZcv%&(M(p)v$v()mMb{=2A(V_O<#C6n%Ec3^VE4ZLbfsH=Xh35?h%>?BY-FaZLd8*O(A=W>`Jq<7Y%{>&gUMH^>hHdxr>7dgg$B{6Al zAIL8W0aiAC4jBxKfvXPd*PwfjBiJLwaZ^`3qI9C!5V1=TxVk$yP%z^h9BS(HQ^Pj^ zXvAaW5WA3~5QY%>FLeyv0fG_mK7DaJzjd_4oRUbUF!ni_y++vsx@GX$WZm32Z~tM2 zar7fV66v%xcdR&8uNR(_mofxkK)y7AL3i8bW9irwl$){2T2Xc9D2$;G9nuU(V( z>RLnC)rwFS8<^7Xi-SqIHRADAtFverdl^CXzmq!1O0(mfLfed@1wJ5X7{3Qbz>g5C z`a;8Y#4?-^Y3A3cvuFs;tdgdiYn_@UPgs;WujbeCBuq`YKd`p+aIDj`BylpB+T?IF zncD1dHkde?VxFaBDhy4Jc>!Nk$b8{WU^2I&3wr{!UG2@l*qa9y6$^3h!EA#(sV~uA zFo<(+b53z}NZf2&^pI9=dAVE7Mo8K-+`btL7!_vzb3pbVEWEC!gY#6DXYBCrK@nwZMJ#1x9~(q9wPtw0A~S0d5UhCq!R2m$ zH62NK^5oyTa_f=~lBoSQ?VkD}GcwAb9WOw zra3$>*(cm5n_D|yZcj{pRBi_OXsQ}>!4;%5E2|yTtEJ)kQFr%k{`N?^yCMD%%?Y5z zt1?)r`dpT~CIPU2=Y_GMKv6NY_K#5MCOW;0cboYW9=Y4-V%1jTJsCCQr%j%hhm*%I z_w$1^A;c2pmqhXXFGO}6SCnjtsTLtk676Pj?UxW$s| z-G8^1WnLy;ylQZIXjrW2`r<1i=qk0Jq9zxSxguX3WDb(@sZ!CVV;A1%zae&=S5ndF z36MR0Bz^h3wFX_c1db>iAp2T~Onf}&Q-kS)s)%zv8L8gC!8CdDN835aU9bUhA-D`_ z*}HairZ;h@n|j9$c!4{cNxXSXKQ;Mtp-%5vLf!0YL!res+M(X4s|{&Gy|}f({a$Et zVeIl}f&CK2uVOlXqncD=?VqxvUQP>bLF<~n2tJW8|36YYhPa}bSH1#nTrym ziA80NH8nT^8UEHKqcM`-6}h`lO_Bcn;lje~WBVkmcgSZ7YZ5@(Zmf8APM|&32U>t| z^2@d4l5>2H8w1d~x#x0?tGW55f6c>A%29Qr$Vv_=YbC*I7BYLF_;B56XW}VJ1>Q37!KOX{;wm-}EMb&`rz)oDF^hrjPobogb#m7L!ALJEEC8e%<2HG!_CRRLEQa4Qx%4yi+d#@k25dPo9FaL zl|pDTr}O~D(@*wsV6%sCd@LA8VEkiDy`0pbLsJsmZ5!wm!k?Z^T~82CU>g?8Iv8U& z-D}531c+_Jf2x5OvS=NT86c0^KL4Kq$*M6xtRC& z!@ThvW0N@*fwWChD7g=j#rtHNT*p~z`tG=H0! zbQ}B_JR@?HgyRJQC(I)*fdrt7t1I50NpQ7^hNif&c$ANb^9W@Npfzkio8A#fpmHyR zR&-(Qe2rBdQQuew!IeG6JNi0ll%&%8w%m~b6Mnrf+5a;+_YY1C`6{S8|It84KiTQ; z{}m_7IoSTNp_99`zTJOg#wsOAS!4nDuXGHTWPMar;6XI}fJ-0@{yPvtU_oL4;yyh> zv~=TouG-1UhAvWKURWJw-fK6u;h;ze@;kl9b6dvY^KF!{O41CsvhwQluk!Bl-@e~( zKa0Lp=xp+lxJX%HX<!hJA&Fwax+&OnZGcG)Pz_>H(0)0r=A%-(}ofszIrl;FMJ?Heb<{`6T7i|WK`s6Ac z8eVY%bOuW^t=UYft7`TETwWxS*~1uoLk*GKc1LZ8iz35QikI-?TYe{_Cbh*pGZ3Wp z+&QkN(~CV967uVf2Wv>(DQKHW30k++1+{}}8?jRn)0~6{HPa7%w(LOy7>#OLF1>BK zyXu^*BPPqQKsKgrDb2Pul^hfd*gYe1SCLVbHFsvI>tz8J*l{3ziWf$d)u6NtsZc%y z;v9 zX3@b5trSD(&L^@K(4R@wKm+_k#$^Pvs3P+*`TGc(d!Cy)D z8V8fdp_7VjU+tl zjUdO!+!Kt6v4@YCjUX|{l@x^qnlu8Aa|*a)Yyp&G;N&}%4p781)=8(2$r*s}P*wtbB1g)} zn<}q)Ou<2;pDqqRpS2)(!<-<`&^l3h99h{fh=RSli*RdQH(M{$JTIM|JI6P^H(#*3 z__P;h1GOOuoPy}v5h_t;Zjxe!>~5OeqLy*PsUeD-PMOt1Tj z6!;{B8Gg*oP$-?fCi6ngwlXJGm4HW?0W{vthG3tN{o&IPAm_OmO~Aezb;FR33(2l3 z)Ww4ZHp7#$so6BcMG`Ejb!9u<@+%WXZh+HP&j#ZRpBO8S3akg7F=?{K;Ast+i+U1w z1Wfc}KL#>m1lev1;nN8Bl;sqFe0!?!ZAQmX#ItH7x*pbKmf9vhgKMm~scDhO6~Y|p z%>fu}p6id39Ff4EFv!0-At4baAUdfoFcLQpRk=|?GXSUw4StWa{ZBIzP$=O zd*OSSYf)v*AFa#Pn^BbS3_x0SR4LD&K!N7*+$FPxK=_LHFJLd$yI}&td{q*M-*P*y zYF^hCyPLtZZ@J=uIlk;EmLo9IN!*zPa*w#fDG3|!7#7hB+>r> zvX5OJ@Zc-KNao&AFN~B#bx4f1Xq!q%vqAR+xxm>Xf!Y}iOPuXfphN-QRzqR=)@*7# zfiiIn2g&<j(n-o!MhE494Q~S*pF;IBrf9B%x1hh;@O#L8scXC9uVK zK9Fjqi7%`+?*@RGqgw#GgCAl$7j;8E{sR3B1ZyZTe?}QXJ%_Li@EEItV+y+;Te$4U zc1z0Oc~;|pk3NOTcj7Hbn@!M!td31ikJJwX+UL`_Iq3G0Wtf;ia5YO1U3=V(yr1bpHBA37EKNl;i}@yCk6f2b7U|A9`0 zovrNz-JOgLZH-Il!E zWc4wf^{Wq>IlifW#xlp&*E6SUrtRUw@?hpyD*&CP+3+>EOTXM~34z}hr9!>5q`t-J zh+l+2nZCJNFpmN>#i&`SpbDfWq?mwAvnR_Ueno#Yq7OTyzqf-&Pe2QmTWARSc3B?m zP5iE>@eMA;HqB5M!uK@d4$*dtKuNti! z9SJr}+c%i79sGUKCGY+8~rC8UAdxk+ehTW766_GG5(p7TQX8>4WX)i^CyGfvh- zNU{hHy^RN0P-|KjJ~N32r~o`X*3@>jck|>neb1GZ7_!A$R3}Xv1IZVw>I=^rCM|Ra z(9mkn{9XlXs_3VOy-u1Q8eA{v1ESo9b_8c_TDEcWrK8c{i^WZg+n+psHygzjM0 zL>FL2!z!XoS4NB>RQQ~wtF~kKQyifU;f9oD4n7TV`KVTwwtbi43ntyN_^d#UO?t-j z-CO)U18Q`wzpN$uCQ!I7(nXUg2_|GR{+exN%JdjY{_Rdod!QakrZ489u6=e0l*VK7 zxdlc{!E^K2cD+8N^3eEEG*NLgDfx)vnn5+cJwz=^Cw|7jig)!vLQ!s=$~&ZNXC-V| zv@90T3-U>_0x}rc-T-RSvdyL#AvrBQt761%G5p`q!fXBZ6~z}XUa4`D_8CBO7Vg}= z{NuFAzMZ@ftU0@b=q_F%Mbicr;Dj{XwMJC!g-2HGg!OdNkY?|IV<2xSOKqI7>2BIY zs}E&;Y|wR8?_h9t21_Fh>%A2Wd@M2N8&0 zYMTlVtBkZTR2u@yUa*IoI%US|_<-#!-BD*N-tkBr{hdEr;GEtCcq_H z6O}{$S{8$L%Y?%>LIiaMa52GI8&!wo~Bw9`_-FJV}r_r^kFbd5=D$0RMBSVdHnG1twkR?UB~y z$0mqsvli4FfyqV=#Ua>QF{4bjQALy!r~2rTR>H|(Wm(4buGUqu0lS{Z6|Jo4*P8g0 zTNCom@8PeUKys8yIK(sQ4EGwRabCY?w2VTw6keOt39hsNQyUg;P|EfJXKu&ykh{$HAp-`c`TD;& zj2NtpAd1DO`3~2~Oz1hvRV^*f)@CVeDPGRvWsxO@(6J}c#@yFw#_)*<15KYwrXAQD|Fnzr;Q?1?ps@K@&{c&dsSVh= zzp4I|24?t)Ld^rR#p2^S(f)(f}c1bG@vHCnVrD$wmd8g*EySHk> zgze50OWn|GWbkKsLi7lcp+tm4f-+upt5EW&`2dnW&4LWTlA0FWswog+f-A5hO0^0W zDN+9D%TQ}o0K1~)k@X{$o+o=4rY3+72Z&7aL~*XE;E6r~TeKVVafY~;09rC%FTe^# z>o7o8sbGoG(8o~u3m;NTQ8R|TK|J|L4=RPTy!8X0x0AtN#FQDT(T7wZCe@9jbI?)m z!_iIRzl9Ulohow4f6P{Tf;=h$k{2oD5iS4D^3s&b%si6VtoXo%O)b#2)#8aDa49B zJOZ*um?bzIEYzha4A$6_19JqV3&O|*ClV9Pu3?1_#;)}g>$#A%<1S&vR6%78lwI*r zx&4>MklY;W!!G_XfK2a-e8lzie+8-DvMoje?dQ7rT?OvPx&=J|*lT6(+IITo2+tUR z96`pY{Op;hj$l`sUbE2uO=j{}q3^QMQy@uIu$C%^?F89aWvWH+oQv_aqq-I%>rw2m z1J1BV*0jTJNf^TUc$SEtV6AV%`R>hnE%`tkrp?=>Wx#Wdw5-*n4k54+{q2;md)e<- zs~7IQN(8GXjx8KA`4T2?G-MB*&_pyG_v3_yuOq7Sk@neRFi7V|b~=Z_9Jd9k6o198 zF~&IgZD_O(8+>UqMB0f$+2(S9S}i=vE&FHv?2Y>)>!O7~cmn3J1d5V__xvJw^pW6w zIflNi>OmfvQT?}W3*$aDt%qAEwbpa|quOg1S6!9}xhGc`qTb@z{N$Q7}& zG6)ySBl&8PUQz%h2!IQC?IDZrgEhAFeB5~C*_~&9PM!+bJsJ_To93LSr_ZT$#e%uqtZtS}wFt4|`Z@~Ya`V^Dc zEhfSJ`t?En>le@et&;rb!k4Upg|VTNkb|+llkL9?)kEq~UWzBqd?U$D)^C`#bpRv; z9aQrGAPcILj*RDMyZ_{r%Bd#$Y>wk2; z(!!SSAm~8OTDlr#(@##{9K5oVwoMCP0w%K)NQtK0&4-Ql1DNS79@XiT=TABx*}I6R zChB)l%z5diZp0rtjvhhYdD1?;qji=}BQd?lBJ=1ZAJU_IMUp=SINna-jvoF5J>#j) z=FFM+z6o1ApL3 z3B?c^t?IfJ$%i&U!>qDnxxuKAmq$DXq2=Y$xZlngLZph$CkO|rbJ#2fKvM!#7~wFD z^iye_$z_zbS(yFx_j*S7CTeHIr&c-KJL{_$vark}NG6x!ZnYdHg|H(Db;sh9j0p@x z>zPs`m7}q1*w7?I5EHx=#z>_eOM99JsiC<5b_w+T`b?W~Y?spL5{9F1rO+62X7mPW zIP!+SDwESjqE46H(6#LG7B+`=hov;)+^Cyq;u+ryOj5@dFvCSErS&?$#=S!L&3dC) zp!%*%CZ#Y{xYrCMGnxtKj;F=PE!3NH#*A|~I+zfdI_8BwP3py9k)jUt?0e#(Mq8Lp z>tV%uFGKoeZblWw=as1G&8Mjs&JB!yfva#Lu=EXOj@{EwWsE-B5l*}o8*tcMoykL9 zr#h)w>I1uCh2!e4kP-zQU$RyW+}X9P281ucA7$QreOQ)F~hR7D<;Pcb}U9s@e?6I z1FLWuL8qew`@b6pYXfXaP)leGtbzaHl-fS7<+lb1TpVo(2noV`ARS-Wi8wr$(CZJWDn+qP}n-eudiy3Re(_@kqvPuz(0vfgv8nRATHZ|I8t(Na4) z9|YZKFTR|d }PyS8IeB8WD#;LbeT&W%QH5md(iQ_vCu7GY>&hH!f)TXOm{YH?r) z_@=H(RQwbHaaw5WXt;pV${EKVv=JLR6*L62s=fTzqFJP{?GBy-2hqu(N69ItZ)v`O z2?wY2xY&>dN&Mv3?v5+sm>}v)CS&1V!i#0p0of35;3!jVG3ORLpnO%aB0oRm_R{oX zJ;q5kDW~5p?OyEecjv@l!U{E?m@c4xMS{mLm&%cX=n~ zDP9~cegk^1$4qdm?hV@&;NX6u>IsC(Sm^EKRUFqkhD>BQAH*m3HBxI|9SWCMa#Yf5ch4 zYnmNGUUDbHxidwTo-#WI=8Rcv=At`X>GDqgWZ5Np(%dC`^3)}85)JqqdPc3$e-p4P2j@?mSNifr^gq3 zL3?^_uV05X%a>Tim#L&{QaZz+7vaGS;W^V#lj_sg-GGxAC5A8POnvkIrLZy~;ubO+ z?6oe&3%$iP2+So$(hj`KG3r1(GD14ZMHXC=%IrGkud5@lfF;9VQ{G%6S&)q~$H7~1 zwj~6Y6cm(VFfIo{VQ?KHKN+eY_$N6=cf7J!4fqiJZ%R#n+MBM*wG8aJ+PETZs%J{b zVi=HBZOWmxZ6|6pmH~=n400+DS~~q?y)0-~3;$FoBKXC4by+Tr5DNa0jaTc<#@6;S zlCh?qISpetqx$9fS6Y zKRUJCTGY~1{(E`rlg%*Z3bF?3WV&L3nBnMt;4ZBSH6?2?Vg6m;U{ZXFo!t$`YPXx* zEN+RGuVA`%fm_KSh-8u;Hwwi_Rd_R97Mdf&@;|T?ndv}za01sNvM=_O%&vwNV>@4> zbmkDHX`6e;iBL%i#)lrOQU-hoU6uGHU+wO_&t_Wtb+g3Nu}X1D>QxJCd^?1NYt6K% z%TXK@=`;fE16SX6qmGA&!A8RPT&Qa5lBrDmZlIdO%Vh9H_4v-Y78JZXCuMtW6HXIb zDXe*GT%Fb8wAnD);Oh07pl$h`6*TZi6qS8dVa2U+@VO}LKwZ|U!yGh#d)vN@>u&!o z|D3+>=Rh3)=NTI?FIeZ+L7NxrwY8q+QCbGhk;uiEcfiOo?%d24a)b`X3*xy(45z=tsUnKcsi}!kE>f#@KgN4vJ~{Vr#Y!oB!9+5>C@*fT z)O?4hZawBu@n9%Ac0-(5ZXPt*|6oe?s&nG8xHL1ao3al|%e9(Eze!FWfA(&~+Yvu3 zUEMfFx9Fp9gQLpcHG`Z5WgPfYuBa0(*7o+&7?2@AWT@zt{EOe7CO}FRw!7XZrIA%N zOm1k4629Wo;;~!4O3U%hLhCh-9X9DyLifkDLH61v5-QX5o_oF|>W9ZW|-2JD5@!aIzULx`$Bh7VJrA6d1jv%k|Ow=vL z=I)U;e=xNaj|oupxM#|<583Tkb$^NXxU5xo^aVlLQ4Bosa5;_RtSLC6)YvRakypuT zEoTr|Dp8$|q^U4sN)pDAv8d5EA(l}4(P!^ewN6|70_$SJCdz~=w4NmQMBofh<=-~$ z@_v=y*)tz`o@$e^l;<|`eDhf#_fU{MRCht+MT(q3+W_tJM3i>s)|50na-ZD1k_^&^ zAQS}vG20TM^A8(P6oO)5%64%D`of+<$b;wX?+;6~8p-ymF!KsgB*l6GG*0yNqkFu? z?)BKZIe+MEyDs`B2Q*haC@svV}3;(tXd-)U5E+vk{eFOctGWMt=abQ4-8S5@OWccMZc?#VAtz1Xr-c?0f0UkYFht^r)lAMl~xrUH(WNMC< zskw||BA4$#PO?h2*o5eV6?_3|0ta9PY{~qy>N(~KrN0JB&-q6PyMha%JvXT7pv8d{ z>;a=AQx*V zKc$Ou7d{!GxLLi6c7C>`xs!6+!sA8VJS;ts9(Ow5)&d1ktzxeICd&_Y6@y zqScS=&GSfMgr)E?IYA`ReU`ZXyK+?awxRIVO$hYc{_IL(JkET(OJ#tp&jL{%$mklU z*A97!+dzLigr9hWT#75)5P4G1d4h7dPDWU_LIqYzQi9t;Htm!^dWCo}mmP*Pm3*;17cMW2C|-!?l>wlht?$rJNBT!#-`VD=-CWp4|~P!zlG0X+NFp1BXf{o8UlJ!Jzsw3m6qM1MouT%lA+ zqNvK_^KY>F9;i?u{UEx3Glq4GLY-1UCWEqEPv-CcK7ogFl;Pg6s0WPij*Rcv7{M14 zio;JnNbJ0UCkOHYHs7^Ias=wlD~-GF=gOjnLo1+ee<@09Dc0)BdH5w(g-_atqdvV8HSkc~^8!%6NosaEnD`p0Dy~gDqLQ zC1q+8&ZK4|0M(-Bx1E|i@3UWZPEWn&)?W6ef5N9OddmWR0lfXfvd+cE*y#eES{>aC zahO>n(A6Szt|n-Qvto|JsWqEVaA&4XT-fyx!D>ZwJwr8u@rsQWW7PJcwDZijqxNGsT!F-~f4pL5A3-%q7p0OS?&%d-jX^&h>4h14zlUVH!ZzFz}dG?el?i;?s@i}63kY=~Oe8Y>z7j$jCz*gKp5 zUlBLi%35;&3$TvXl6n;k=`MiWWY}3vP40+r5J>J1JXZvYv|ZAf=&IkjacL{z{b2Ws z=goi+#rt|1*gK9sD~mux7tDK^liqmZy~DASzLwkD>jPF7)y&MCRJhU&t(Z2w%S(wN zou3ixpApg#0tUTCbe$CFmR)iJ3>|z0Fu)QLUq=&o&po)%a?&bYOSK8~ zG3V?t_w2orWJ~Uy%2jf-RzRZdB-G(LSWt&nfYlNRzE|#S@)~8($cR1Eo>Aci9qtkl zJ&|0qdgYQa;5V2F>mj+3Gn~zHJ!$TDE-lPMf2vhO_307{=AU;pA-|l2R#v@nWORvetCnZ`1ZE|kz z=@iF@aD+$?Uc9BO#>JZ&oxJBnhVV!9I@63o6>XeL<@GNy#Dat@)qoGk(1E)c=U<2u z6%vJpV&a~u4;kS|ssF1hFhY?#Wrrc-9X!uNjWxy>iq%Zct|1}}e}2aeS&JP1O$C*9 zDazvnp2eBvG^PiY6B4P|8>O1BuhDfsch=S<{M4#ZDA)DA&wy5*k4P$W`q81gcib>V zuYIh|9^6WKtMI_5?(q0XEBTR#_@=8jWXPcBPZ`1SI2|XDYvHeSX>e!&DRhN4U5Z6u z>cRh(06{(D0R*k%7Pa8XPk z&w3xbyd5g}#mc0! z0swIS&-JbUsQ3SN_x=}D<|+HSzh7TID0Pa+ zgFZc+2?;p%AJ9!@C|F*zl~%rHMIIu**5!KVb6tl`>sIrIYwN$O8k?2t79Ek(mlr>~ z8sa#JhL7^Jm+zmSj~~C6x7?d7&pTj*>AeGA%i>(CM>=1}`B^6ms4qd>|1zbS-_`gm zp6`3d4_16VBYh7>Pi@k_J98z^T7BQ)-sLpBM&;lZZ|=?EzH|%uUMP} z(mz_FKSYIm<28IAX9(|++}~`msEb~JpFSK8inw@Vp-R4-)5^j_+uRkVGXDT}X`bK8 zL!4NiKiFqyIZr;^!@5EPXn$cV%g((L`ZFl-%O)g`H@HOnrAEOw3Fy%7QGIo&ogP*3 z&5`x{sA=rAVZdPJSG0)FS+fnQxXk!_NDpfbKJ`Dvi!!0I1x|Jj+Bi@lg3jf`Y((LR zs<>@Js#LRf)G@V=Tf$-8U6+kJ>m9GWJu@2zZB(B7SK;I}t~#h{7H;jeEnBEoGub>x z{fk>$LT&EYhoDTwbTHZ${4~O}y>5zSxjMIw4ob<;xr3#zzZ;BOLypzld-nLss&{~u ziL*Qa?m!6?TrnQ`5HK5j*0UPsN)Zl7S(8Fx51MuI3goCHt~OszkT0_749G%?XkuD6 zCiA+q0aH+sn*MkF3@V*8jtuG+94%W}T@C|qPPjTrY|3F>fNrdeF{emH3JJA8DN1_d zO=Ufd#yCxlQ{6$U~1%MR*9iQSBF52NoXTlwic$z=!Uv=2MZ-wIRiWI zNQJHJy-Esn4XK)W7b9+8jljYwa|v3t>B?m5G|SxEbU8RSLl+?4x6b;P88PpiP<;&} z_a&d+%7zuW#3FDIGT2T=oT7zy0hz4;!0`m2Y48+tDjW4N@h{WvcaR>NAREw4i%bW! z>j`8tgA5)u2YV|EcOJcG;_#sb%8;~DLlOM(A|}MOreUt5mc#t#Ei+inXaquRSWH_o z1z!rqDMGqvPbHPXDb%|^Lz;LT9I(v>LMbwLG~Kd$@(4^!$3-^N1YH{Jmra>hpN%WE zs_9;IIm2RK5m#wi2(S#jU4WOE-UTQxxA+7+?1M<3l%nX})J}*!7&x5;>J@AyKcn)i zIzmJfCP8Ro<_sTMU@=WCPj()HsO3ij%s)(C}zWqTK@EL)7SC`l2UKUABjEYWH0O?L50taY~5+2Zqvs++Pi zsyzOA9->`u6G^Oj#7!dV9P~Kh(=A)3y*MZlg2!#Kl`f=JtmGVdtk=Xv-c(F1uZT`d z==H)BS()lXuL=FCm9k|?#40|vPaLcAJx6u*q*kNTsyZT~^y!3&Pe1zt@3yrax>Lsm z)R@2}Pk4|Dz5E~^1ZkQn^WyoznA8>2$y#*aV!=@}|MD2>;kLt>(nwRD3)dTXZ776# z2jADy1GdTF<+5Navv*J(GZ5K%&F|PmItp!$9i=QQ_qyK$ECBIjJ7!=Rrd6i|?aN_8 zi5vzY^cum`vdAv(xm9jJvSV1}Fz=R2m^m1-T^Ho?l95NL{-9`IEXm?<+N6q+xje1` z;#?pTTrycyjzn|T?lp663AUai7pKjn-f>1}I(LMxTs&I5b7j+=m}ckf!bm4J+#_e# z<`r`KMBuKpgAi?#$5OpfX0zQbbK0exp|U#LCM#fpxv(A$B{u1m8aZp9RBzTegwCMa z39U2}BZiOeD-=%J_8JOxcXj_2a(6+UyWkAvC~!j4D|AZg7s!g)mzTV-(rit)0hc9O zN$0AGU#Ve|_}pmI3xLZs4OKpBn~cdII5#%^yRMh+jIl+1!{ z6+Sb~nBnqBm33Rt;nW*lx$p+sDSHqLs*k;w;d}yT=y?iRu!dMn(?w^@JM(55ti5Nt zR4>H=UJxxouaIe4m@JqF=M3q1hCbw3S% zxik23?v1xo`vCOGklh13tdNC25exN_yNi`hOI}`f>O>^p0N{9=gnpNSn9VodX{=>p z;Nm9c(1DBR5`(6h%{DH{kLj+4lw^#Ejm2 zRJ1&Npadg|Y7h38wN?6j!u&T9;d-5ztktCzprl;zLiWqK7H0z6=;yHGqU4x z;EntvCIuRKu_J+!&5JlcPP1HR!l^E}s#e#@V+!8m_4w{P=aJm7@{dw5AV)wa(!;w9 zDwTqdP)yn5LqsJbaoDVcz%eC}>ma5!+$6@TchX3fePSnXDDkt;mLJw>p6E)PNE_5H zoR2LEFi>2onrl50M%lmvTo;qKAqotz}@X)XSw68oMG zqw|QA*5gDAqleHc4ui`g#!Sl9e-Q`z!hA~~CrzNLx?2^g{fQr=B{X=z1TIZ0x-pAO z@=6Yh6P=6FP=IKXoG( zuw#th%-Wh|9iInXLf6c)guxTSPKdanyEF2v^DK!I;IhP%q#`R0Bh&mQ%Z(J`iHE$G zGkZe}>u7B@<>{*;0aY6EK6mw`P^PN-`3S<}@~!t|eQjHB*&Y>^nRkSlf@(c&B-;m1 zrj%w$QrhOQkg$D=V4yRbv(D7uvcM+IPA)T){zf`8lp$7L&CwU;SRtaE0n@7>Al?zp z{|)4IL!z_J7e_QV4?8L>B?*s&)-PO}MvUSFyj@91&hm6vesIn$$Z?V?Ca$-ut045= zQ=CumEfl5Z;w}q-HY_C}^LSo9L$1*Lk4!=Et|P{D$&s=Y)hXyDx`9y%))|qoC2cNR8E2*(K}(cv>f8yvyiXwsYu)BHNr9y+7+B$yN`}x`5O=7S&4_zk%mpDK zFCg_yQAOK?BJFjV)?)E>m;`TUt}O#06Skt#JtZBF;Uy#GId$IVP)MuIKQ9iHhVPIt zm3N2AUO+jxm_4aoo!!{C%GYE!e<&q)9Csh-BD%r;j8vaekn!bQ{CRc66r6_N0mYP} zf!`CQEUq-iv?;)7N=Hr1H`ldD#D7tbIOgogyjI{|p6_p-~*i| zs(|5xZH3#H1I=_Ag3WstHn;tR@5&RDe0RtjEt4S%XZ08+I=J-XbqfsJYZrzn>kE5} z2H^C^J|gaUfmcji>8~2JVKq$=W-ASeTwfxxKU=9Llsi?d0>hAU6%MpJPPhg3E$C*< z*upa&`&S4RNqu@=hCUr{+N^xrv^o0-d)pLpn>5F!G3lleYnBo3rXfOB0m5d);lDL! z5|UO{iuUH1L*qwso)UKTa^q9b5KR&xxZ5dJ#z(*nX%ZnC3L&qRz*Q2)t}yP>`telK z!DDBJ<2NT84%7~&!&ma}Pe+`Kv^&+&sL}o#(MuA%sJDQ?{U9PQa{2mP=9fQ?Ct=2p zNyZ_&GWM=8pXYUFNzt)N*a?^rp;f1kiZQ&-^)@%{HN+6S?J}<7>=CVCH;~06BNGSA z5&2j3$r4v>0<-4D&3`}OqcY)0SXgA&&T0CbsEC`eRNb+Row%rPWmgY$B6>6c_#l^f z^NV1FCBBr2pOp4aN1nTV3Gg}+{e1mWZfz%js>+5|Z21Coimg%PL^qZNTW?G1^2 zWI0SC9u*>-Qwep*R=^DD4GD%Jf;ZENKPA?0;Bz7(d6~B;YJ{~0AZ5X>4q>2;V zyYxyHQnCsfN>JGfG(40dOUnw%LrYZmiD0FzoIzDS$^mN6KFHa2NAyLH2u{sRNs}TM z^QI<(05_LyRP`7tv7hiq|n~g z!+4s8K>wHouhlCSJGGiyW2<|o;qDFet6iu)^?DS0=RKLT;tuj1BT_JkEoO5k_zJT+ z8}+96N}?;wbztf)mp-I4_ryMr349cBG!mGF#@!iO66WYWZ~yUfbr-gH%}QIQx>P^)bBNff+r7RrR-G&lvb0@(%qxuH=LT}oPKif#`lBXNg&#$FsP^y;ksQ~8hl0e3luvs- z1YE%F_uE87W=Oxo1ML3XP2coQ|1@6Tbmy8q)3*=uki{l_)mzZuv?SRrYFXcgP-U!A z;UvUg*T<*I8$UtLA1BO)0OAI~K5q^be;Z&u3E}4~kvuPOXm2M^U{pArMAQI+#O9AB zFfHE7Y|Y;KeUgr<;;wu0>Wlmm7+EHHqWW^gnVud|V>OOuK}jwBpzFVIN3!*LAVlH3!yX^zDzeQ% zd6EzUYDbHexz{=?3Zk94QfB&0TK&4!F2*Yop zA#A$o%y|-wg8fVs_)%UZB_e}p2-^B@EVt;EqL+;B+{5ib*Sipp#Bv2E?u^;|cb_V>BGQ>5>Z&4k_t2i1W(%F( z4B7h{mj`RwW6RuGC->+*L8A}9WMC+FM7n(p?Z`F6?V-t}=}XZtb`dnfn61$H%+q0A z0q(`UIF<^+aS`te!KN$FWitJLdP1#fq4&F@D!7sFy ze{&7O7I5N(nNBJoM*~HVk|=Yo#5DqRSpR|fFY~@lACB8`*o#ES7h> zL8kZQ1fhL4;zn!$nNh~%qY{Y)#P^e!i281s~Y>M5^^W!gj zo?O;q<#X?F`7@oWqjs{zxT*+YD3H82=Cw*)7j1UQ_l61Gjxi(eF;0Qewq zOf&&R<{!V{zEFC#ltD{l`kC!3%{Qo?G;$$aPn!ATu%B^PHVzE>K>%F0jh)vl$5r=L zD;^#%p!$F!U55h?5EdI_+JKCa?UMl}2bKpGNQgh`ODW-Wa~V`IQHZriZfo1d(t_J0 z;{p}*)=P21`=7z~&D_X0nvh3Jc@*eClky?c#vZBxGw&6$S$mG#w%%bOi{o&n>KEd9 z_6x~y`{fe_V)G7l*ntcz=AiXj+_nfYFM%@c@+NSU7oAviOTqTyzSFb?UM(4zCsy7_a&L=8*zjOAG?}+2=eo#B&QtSwp zq>PD0MtPgA_8Gw#up!GVG%|eC*^s`ta^UGXBod61gQ2JC3}waje@Q1`@ygB5GVA1= zY5g`KIWh=mqXI=Zk9jKsppC5BU~cp$`nYa3)a-4cA|b&)61anorGrhz#4hk1C>nOJ z1K1XoR|aW(TqJ`*>`MeM&Izq$$e>SR0csl{U7H+ych%olFh!0dvbhcVWxB?Xb8uaO z5n>ya4qfHl;-@a2me3p7%~d$8f(Rl>7s-vv=Y0}S!qb(+`otdVylN~jh|VM{M{zp!TVZu16=C07N+^R)@kkCrEWCz_xuO5n|%qL-uhU-PU${D`ElRb9oDdz!rtG*LzY|B%`o0;CQUyX-O4 z%H)g^uCFvN(~DG`Y~O_s^gn_Bv+RTP<#xP&flD91Q62xAWzYEEqJMLej@^bb_DF7G zk%v}?sZDDNLmvI0rvB4d2ihvSRALj%q|{2=Rv!NeC#eh8<(O+on@|AUUtl}{2x41* zf*|;exLrVp;QxBRZW9Qw8v20{phXls2T}a=`tCh&x;!{I@ctM*ar>j)CyN@pcLy!I z>nvK~QIRu%F@YrP&n`s`e-idE1hP+fM|_9u5Z)%uEu&44Lr{Y#Vi-@3U{WARU0jzX zsIph^&vO}~I=f?Sk-N9hgJ)yQ=EkB1gqh@W{vjlIo$JE`c#j5LHBZ^@r zBd^ceIcE)$u0q)Cj14)lq^_21%rf?h?W5jWi_hTZwdADxbmP=@$Qjq8!GSpyb2U%< z+gu|}?TyxR0uy@uq00uzER5hHOyHZ&Lkk63O`GZp6n3jp>mW{T_COp?PI(#~_r>TxdrLNBV|Ry2ORvR$N7YK=PI`Eqoq z$PX)OtUiisFo0e2;$m^j+Pi<97s0(n|a<&^Off)murk$-s+sg(^t2g8mU>Uy>DGXJWdL>x_` zsbSj`v0}Q`w6Ro&Lmp)s43(_g3Cj>BL8%G|GAxu^G)FX>he9GQtEG?L>j?OWa-V$e z(;!A`R<4Z)8 z;!TAY<4r{#K0NUXZZ2{PZ!Y*GXN7&!a-zMXe97;*dC=Yv-pvmW9tY#)=0*u~i%y96 zB>52E7~b6vl^(;#O3xDGcZB#b-yGiS$8L({U_ObY2?8bdz)KtuW5sg>|C|ud7KSw# zi+s~+CWLs;q!?rI04EUWj$$R)?bCG8yuoe{dnMc)2yMoH0_*Kljo04c^rU|N;oB!0 z&%LAaP5jgm7-lFyq^u<+CR>i;q$n?EIiYJ8lX0J8W01caZ-<;noyP#C(Qju`p-|A zksHEC@|URsj0ymt{6F^b{^KMQw6HaB^pO2s*ZV(DHxX9@YnT6fe7#!TLtS|VTf&K9HRP^((Vb#N3fT?-LmUk^1p{jhp3+J`~-d>KFs z01zPdtsy*^3A9Ii>_+hF%UGMxNVe`}pN>b>Cof&Zoi2l8FF@!}-v{u;2gvJRG`yiu z;MY2SzWrgbyT5U74F0~_kGuqZWFW6e0J*5U0)KE3cbNiS>d*$!GaFybr%@{I}EH+ z@!m%g1l2#Tj5vP&^N8Z6ReQ9 zKxZ%@deP`ugI?r;3TBYFa?rJ@@#KLLR?H)<)|u6!wrF5B-`f|lQIbNWF_gJkC_Jw% zR7OH%W+TCXqt(|@^Q;dbc`{3l5Ha?Mk5$2l8T#j{QU-9aNieUkAo?mDE|nmf55#ZB zap(vCRt|?$s<<9C_wc{gMyibIvTA$5kAwhLW^!=II#fNycKci3y@8l+Bq z;ic;5o{LR|Eyd5B82Fr2Ks)0@i;y|}7<)gBkUf#pkUgO{RIh(#C|(I>s9q67O|*a4 z>vNQDjXs6r{3s91Abn#_P(A^Bk(2Z&UYk#KkayVtKUIJ8J6^K^=pb*sx`+?p0v=+$ z*_GfVG5i#78DkLFch-X>&E2VDz{%#3;hso3uSj78*QV6e_wO~`U6ddqYYEX}gybat=KW-dHZ(SPYW+n9DZybbIVRsr&H~ zcVi`PM@e4aJdcG=Z|Fam2cHU>8iSeIcm&ir6SZ_O{DYubdnidjw}lta_v>QWCsE%$ zl(vfYUQ8IO~aSu<1tsjZxJD>yRONOqPM;vUDT1ptV zXlxK}%_e9L0uTOJ;*G}|sOXky^+LjuQ|IXJu4x!Q*GX+?f0JMzZkKTb^uO@8?dd7( z)+WEbHeNUNrlWoG5Y%qThiPFq4bRfkTVH2%QLQk^x+O?OnXEAD;5*P-$8-#goZc$v zjLKO^VXSd>v}x2ik}`|Cd2&xXN^OzVjgYEIH}sCmskm!LLNqK!0P9xDusJ>6NSKC{ zP4Bl}potC=Q9}#PE{LKo3mTD;Hs4Ib-`vF= zuEW7tuL_r*0_h>QTnVp`duKxqGD z4R<^IuBHI?85lbO9BGTaiykeK>_wfiM6c&0#Hf2T29sgB!_T*OYs@y1wuxD&!O}sf zxpXsAm5y1VX6S4eNU`W9n@eO!c)l!7JjA6ybdVbaqGp0+aQ*loKR~y2^!ZAn7hg8i zmU`Vas3tHly;*rsJG2vZAEA_Iw3%3!k*_9ifx0jria7$csZln}^{2EI^@3lWju?@M z*Ihm~ByH6f7ZJMWo&mNn!b!Xq{m6Eme33gQfcrZDGOZn6LpjJggGnNuu7n(C&0H8s zsxz1&xR`;UFlsQ9PJtS|=TP$@yX2e-W+&oh1 zmV4s8A?D77tUuggWSw$`&tx=473$w^Pl}edsFNN zfi0X9*Z7r&EuGT`+~mE3)=+g!^M`upY!}BD_*Z8ACc`2SQs`d2@?&wqH zk>MNledr!l$}GMgG)Tb5kOOjzehxY%rodhz`L@Co(&%l=Vj57^0L<)d!}g3#P2LQa z*mzl^_vl|0Tpjwwk1a0?0#$E+g#kd3FNct7ivlaZx^Z zkar)oyd9U{tt_Q8h97F|(E{Sbg~P*!!^8W-!-&JfiNnK+!^7bK>B6_1f=377Yt6m! z7ic0r-dgf6|H%~65*mb{p>Q0bWF#AsCU`bQj1+l}^D;z%3PCO!0WuXMwymHOa}wGG z$}axOJIrNy%wU{5tLPAZcN(-DV$)_dJ=-*&pb21hswiN_UB^$^E{N_c1b$+;c zQqL?w=a`l`sitV%LYxJcCM8Y&R*B{&Dad%rtKvFmI*SE^^Y|Qm9ZoTy1rNrelXATd z@sDD)F<065udzKbUCi}*|5-iXg4B_8VEbIEhWF#p`RTHg0WCI-xqVf6>J>e;sl4M!s0!e5%*esb0Ab+&U>RU+g=zsKX z|6d)Ol9nB!0P;5|Dyn@mTK&3bWkhnMYx6cDNmrgQ@_bRz4Nz2_GnbHQDCv7<7jIrT z+&6$<(%q$CW?uM05uHlr!cg~?{5r|ONa)J0j znGONQs?)yLfdt)Yq7qrDbWAxt^8G_Snk#x0B|%7V2$_ge(+X~6ZX8H^uerr$ z@*3|pES#r@12xJ-kwx$Xbq{h$ZAWeUu~z`Od603AW4vg9EmPaufS-$vdr&*yS66jm zH#yp%?8MQ(!ng0*1@v2=>v!$vO|>ikQ*_UqHC3W51QxpYq4YDh!3*f}67VS|6m+bb zK$1<@IMI<295Ai6i(O(_j<(PbSk$_)+e3C^QvaXJ_W&xB2G!1*#B1IS9Dc#BqQQp;VKPIwYaoRhH-5bFW6>Lt7Xtjn{vJL2eH zWy}`_06^(~?@poe|97QO)3HZVLH@RF7$@BT(e#CEQUoq2>dlA2Z^{Rx5x3NgONtoo zCD|gC8nj*Al8^X#{V1aANdnW$ipR*En_o8&1;cw%5yQhypT~IWSD1JB#1Pbc4Cnr%NR!Ta&K1_E%~6M=wf6Jhe6%~u4*;PbEf9RW%MS?=;+le! zGV3&YVQ>~u3{H(i1ytZ+WQLtZBr{3#l$^C#o=ftUq}Ky=7|Oz67+K0afCtWrAr2NK zl~f817lx`hsdHqJP46egE8G)Bl;7PXDAqY;4A1R4JYsa1P<3=)v?b6{wiqyuN$99e z_jDO*P_A8n{rqJVPEdJTwI$+nO`eMQWb=cgX~|2guw`metyc|M7g(ObF#14N;;2Y~ z7$z+fo`jENot;ct5Ldk?smbtuOU&<9XliN30k8BvWA z3KJr9PM~g}GzpJpT$+_(&`4hB%Fh5H(ew=W7FbthL<*Gb&xgMY3lqZ_u%M}=a-An$ zs5y6CE3WR8_19oiQdqeqt{}(74Nqc?FcsaZ<_I-{^%zAsY8!yZRHX5WHCQ7=#)xL8 zCJJB;ImU7&CJJPzxS(taCr}7>F%Gq$RC3yF{!ty5XP_mD%z}pDKoB#Q#-}?VcSZ<_ z0*o1_HyE!|NOHg)6_%c0_ou)hyGx52<#>ePrr4*b@zoq*VkgHTpkrWV)B!E>39pMvb^C`pjVk(fX>zfd&q5X;IWet z-W|crh4b9PMfx{EGlFM>>%zr>>EDTKtlfT@6G_QxDj43e%3T-1xk& zXm1JcsAa;r4%NQKNgZ%`_|Bh>3JF+wJANAADzT;FCp$uYc>Pe#ZT}nYPbPT9YCJ;w zjHuZJ_$=3;2XN??X?|?WT!B+&V$a}G0!TCbS2id+q5EAHrZ}$c!yJt?K67wpcXJTT z+=aF_=`>Lw0@OKm@uo;q1+K`B>pl!`!KuiT8QS#Wg@wyblyj5DGP-iTgCHx8d1g#1 zk1NJIiB$Fhh{4*KW-UeP&Q-lL!-mVkPQl(!XN$8&_5PC#N}qSxdI+ailEUojPWeVKoW8ebh`*$+if zY9y$SMWk})&qYn4d5#pzYA_q2s!a+RD-G4=r5bqu8lzA7qpFasSy=Os0J%;LI>A|F zRW3%5UkoF>7>2vk_I0Hl=t|nqlr|&7iL`$lUL||lY>D15MYl+(QR*q2F(yTTg{`0M z%u?sZG1Zo9wR%4xO#+gr#s{*COQ`CoKAWbN-$6!(P=1pR1b%4l9$JI%&n34`9kwB? zowmD^pgAUSkwI%L6D>WAV7;6C5BN82c8%+Nr194Ik>V0RCiY`uMZZkqc%6E zHvjIEof9h3s76+IUm4td@HRg;<$d`vd6DkUi2C+U-A#XNfAl_h*?F7~hvI?QXP7HG za9%4rplz2H0NvE?9oeE#^5*RazQzT>Rm$HYc(jZ4i&Z+d^-1QmTBiL{Y`vvPNksx0aWU zLD5tj5}hr_*_Zs>RKVKqBJ}lrd+B^ zP1=L?kHSk58Z{iY!V+ATnuYT&Bl4C_sdOQYig}jF7U|cP{zuhxv!t@dg&`{U;Q z<5fqQ5*y67d(xboha!D6hi>~*Mr94JO_3< z+YYz4xjEd}P{ekh!>y5NX+q~KT9yx0(43xL|MB_|4jQJIb$D|^SyS6|a(mbxlna3{ zu1_NL2kiM@%4#^QG|}V^^IN0^V+8;$i$)wKfsd3M7S)oh<{hm~p@sp}h96p~)Ps-{~G#_%u@yp&Z*)G*lA(bhLS+Rl?bnNP4FL1z_R zj8B{*sKaPrMH4r^7UjKXF^F7NBw!h7%u%V;D&GQu@G0LCf$)_;=G?JKPFPpjP{MTo zB@IC zO!awjrcdip;pf$$kUWgGfSOQ5LhXnq0Ol>CkRSC(`YyJB#7l*`vN93~ZI5a{J` zJT#>s<)$QCr*u^Z2v=Pw?0`HgFBaR$tylDxuoc786Z7xkBY#n9@-d3H*?BBQ5^JVz zO@D(LoQTOtMVBMaaXBs9abL9ctl3UvdIfPjQ=z;6h5&S`XxzxY+GOoept2sahP*a> z{E5yW@(bOLC}|Y_(hT8}$rvwvX)ZXI>NPk-Z(j{_X4DhL${{60?|==`w=y5D7wMF$ zH(A^MRa9%@vpa-uHdHFYtfg6eZp0pHueHrJGJwzGmFb&sjt@wljtNU# z*$;(%fGnL&X7OoTkM0*&SB!C`%t)VH*TOqnpDWpGJ2Cm`CBAu8N{o{RjTmID#i zGGkPEI4FhTbCY6z_*y~y!3=bP0R{}=vAJPQ+UR(dHz>70F>{(RDpOklgOfMm-Wl(1 zEq(grj7cv|PI$>A$iHd*4T6w85P|cezW;-@cM8rl?6$SjamPu&v2EM7oxD-Uw%M_5 zTOHfBZQHi9``c?(?OOlQ{;PJ?JbezIn)4oWj%$SZz41ni4?-kWYS!q|`JPVK%UX6C z!eQHIb2TOK`*4^8viNBRUZ3$W?Y~yGsbHV>>P&v4xlOg}teV@K&-k(P)2oet-ZjG| zgIrC8v^PumKRIC3IW^1e--e6rk{U-<-wneWD!Gu%Dix=)0bM2lq{it>p_2)Pk9((= z5d(l+?tkI$t-^U)Y##Of@gU&<=kUmaNxp!`Y%O$%-?16NE#+G5=G|O;gD{Z!LsoWx zD3Q0Dn>Fyi8CtM>E~{aIyHS3+%0;P?*CNmd<6hrHl>~n#PT6RgIgkD>3Zwb4jN0q} zh^hZ{j)ET&5sEQgs&KG-kg+`W*JPe1?CDizDVj{~I#$ur5S)VE8R$nNAToQv$&f%} zOyZ;Ss+Jf4`;0ZPV&3;4(i2|ukKw!Mi4WFd5rNj$UBg#4vZ1q%%AcJqcW8U|v~El* zZ;=&c%&HtQG`Ui9kP6+&fs)B3V{B3SoI97_BDk zIy34Db;qBCFe5L-oUvHFFW>Du0!w*7of#H1u(9F94FZy)fL1kSyvbrZ?^U6Rcu$4w zaS$UK1(}d_x-v(KaBgNmuOm*D@tQEb74Ur#?a&Bl1BQZz?z%K;bjKpBJ&=5uQ|15ck1v(#`;7o-hzYu?=+`$YwqpzGIK> z&qBMEf0;8i&F;{ToYk%dkptRq%cjbXZhTl+?vust@o+T#U^A$ng{`Zssyv{@S=BFl zXF<7hF9*HMFAZHpM|Lz`Yl~(IvnEh}!@{>G1xx;>8#2d>0IqZVQfJ!4fearf11(i9R#k86CLL>_uG<6mdI2C|^Nu)(;qSrqd1V23@*in!4>c2p4@pdC2@ zC3KoRxo~r4B`p*}W`0bNGxc?GB|+b(2QJrBIsjU?t;2xl91c$H}Lb;nCs?< zGBeeHRQGmFWk}xg8=IN()bk+gU`A&icKh)mS3D)#FgO{ygNAmlRg2QB&ua55&=kie zM8bB7MyC49+O^WOniWqw%?;OS)EPYcv7`GAjZd!h6K>q)x6e8sjkIB_)6FQi0)AeI{8oT6RSk2ZdT%uecj*W&z=?eOy3d|Q zQL}j}e}4O@mdvmtK&r=+3i6`b=tl@a^u556ik&!EH_ME~TmZXbV}>~i{*CAW!Ru1< z#TF~=xpX_57pIkSe&d#+5mRp~&;&qWSVYmmqzKGgOC7^?q{6h!en(^$! zy*24c`*XX$`}5th*LU zZ{J5X4p%~x-#`ARu9EVN7;XD+S1a~!SNnhOF?O~w{7>YSvbm{^zLT@Ve=}7p)y>@i zhq&LJo|AaLR%vLA2_+P$^b=|@2sVeK9wNVebh9F1zmTA1ZkK3B#~= zFRcqD@_YD6!x4lRL(+E~#XE-t7B^Bz)qAWEumBuHqF_A=HywhN0!`p^asaP~TCTpS z){qn_6K`8*hx+7)<>gv1T*H4ZmFQi-IYRc|=lf=U}Yc^v7KKRI%gJhe`>V@{aeQ^?l8 zz<$u~dcK54DW4uEX>nSaZam_8JeP3Ja&kz@PVQm7B_l<=7M1vSwttSTNO`>J{GVC) zhY&50t1_Xw4;tXLKU)DH4HQ48VsI`L8gNN$^8{Sgtmm_F$7O2! zDWB$&ia<#r`_B{0*waQcI1SR@e(+IAY0T+>x<f04=wyDsKvFAZsSe(qzxx zAXJnmE6>#<;0A`-o9>D@JsmRzb&)d^nQUon&LprR%no0ryH8AqCuXm!EcKx;%1D$M z)pu2lPGJ#xRIpJ~vZXkG1r??WTu)zYm6JOLd1z_Ra}gc}^?J;}r0we^%W&&tBN&)K zt~0w+|E*> zi%W*tr{@v*K|Jiqj#@Pj8_%Mlb`ROAbofD{{yX+avK&M4Ghgmo*3JyEq>-Llffy7^ zsFbrPP8(IdUMhb!JFU>|>{ezkkI)JB)wMpFH$RQtE_1`aE^Sc`=k!aULNh=vxokwK zwNzgU3s>f-+|z-hz)-P=H02}`f5b;Uw##;OI6fH~FtHOH6#I}HG+-%fp$ICTtuzP- z3_iHUqmxWZSGf?B8hn2d7`#3Vp?wJu_&ADC8N{EROp+MPAu-+`fUFN6cMEa6Nhig= ztUzlqHUiV9$CB48^sJg~MiH-9ClNK71pY)_H%faZQwr>gD!(E(^-|2V6P>Q%XpNJlYs^|S=HB_jhRdZl-HToXE~@A!)OkxjiaGWA z2OO%Lp1k0*U9~7a6)~q(dCBKb`ariVRVQGtOvVB>8swO{=R6oC8ye!cDVF=RhcGi) zS-FQ5EF-!74!=`W{8X38w;u<+3Mf&SoI48|Mj#_E$fy(cSg5hBrlc?_hw{D3AF+$d zQn~g#Yk`?r?g5=7QJE#A;uL@P-Y#T|nTv6LeTW)pc!>YCcaR)AtiSMf{^*2qLyo&l zR}gs;ejl|l<6mv?y%vq`_h1ZKSMYfOIM$}em^K@_+)51ylSwQtNxd{O8w)C3F3rt}n z3Z`3R?^-B~sxv(FW%`aCrnaxBTzLaPLVtV&5W4&{HeN*m7il zE09`n#kox-Kf8X62rd=~lc!+_wZjKJSVpv^nc3q1Sa$maq1qSun`mZ=jM;Mln_Bk? zmoMw%z6c0~2*wJy8d6{Zz~c>1Xb@mom?;^4rE-P-J-$IjzdDBx``V?xRZb=2yyZG_ z)f2hEDQ>s>$vl-e7WHEyarSOy)&ry7jjx9t9!9u4Kj3E@RDGPm+m-Q{3!`RxS~gk1 ztM!po@@cFg(%niB*b5rM-MMg$9Y)N}wL4FE$}Prn6#2~$4^Hl!0dY4N{C#unUS#b- zW}-mie3a>MNFGyD3HpAHu-WY%tDATSE^$vVP&dDzhOpHga#aZCGQTy(jX@cr10xhd z@&stkG!ZlA@#%n7qCGn`5!|)WX>r)EUf$d&G(Ey?+d8t3qI8N4tHVj}4$j~a2Hbp# z(qBS;@`9C#7(mM4w9~_7|K;+)n^(T!N+NB}L&ug-X7pZU4mR+Vz>rb1iWr-pzh2lk z`a^|r!}5P;5MSt(;+4y=-RmWvyOe0_6&%(L9S!g{2DBu7*T3j(hG@N{dME0hh6vp> zF}HjIb6TUGz`VUY<8(d3n(+Er#F}8L6|C@WhRa7IXh3P(_ZEe_d~>)-4ljBfL~b-@w?4MA=E-(DJ_o z(n{b@FBQ{9e$%E5nSaDR88j0i#v=1nlE6;dSdgE2g_A-VAfReoMs%Y+Mr2uOH495~ zXpLsIi)%myt%x~|2pCn+hDK5QOV2lp^6A4<$K|r=L)xwqiHPs}O>S2kG;bFE^(mdM z_s#pPd;azB_EXqu-cKB4oxzZg2EVV;+3zD|!p|I>ufo}!&x4|ShkNuWx);irFADbR zv0YYv9izF_Pn8k6$eMQFqq<>vx~H*9c|0?$<5zR8&)MnMi451r-7D z))rs9<2|nqK2;lOkr|fOEIMUS2Z|&pV+V>5CT&7xlhD%KjoHC8Y)vcWB zTYny1=yA8W4f%(oawv&qGfx>^qK_X#=^6gOgMH{x zaTt42ar)wnk}&?f&q|={mp-2tYv9hFC}baE`n;^w2?lpy3K~JFeT}P9eP@#V1?Nh`ob%U9=7)K}iUe62 zu$YYIcluT8j+@3BUz3LBtJCK;5=ZNp(aPb-MEzc+{J z71scy(oj}I>P7&-cIY9W8ggJcGK&dQTI6g&14ua^kAo3uWB~Ilx$5t6qFUPvh%Bn# z5~&>>C~O!;@vdzC$6ZQ@=$o7)H(YNf)`ledo5coP4JuraPL^(viqG&%qt{$BSc0lo z1+t^A!Z1>a0^E=WAZrLOA(~q1ZE zTEPPb<;Efxp7;~Xlc_=^OHJBLVc$aZZZr`oP_}CjL1eegcquZE@%43lNlK3(b@vc4 zJ!N<+h3tWZpTbA*WAY5U>QBnxJX$k?GHOnmzMow-evaEm2M@77!%q7M^!N4z z;lH|uvN99TPdc()&KKkvv-l)) zg#L{Jz@$z7hGM1B#35{_tr6RWQ_}~>EV&tFF5LWnF5A6HF56x9Q?Fm2y2tGEmt3}o z8P3rPV#n4kMLHT__a4sCW2uDqZSL(;-W%2P^$(i3fwm!2+^6DPpT2V8zAbXwx!c5e zVkx`?17KX_A{W0N?6VRS!k}K!vXmQ?k6BDv@aEjnub13nraWJlXcD7Sktl6@zfiT* zv!-U|W$NjRbQ$70+%rm_{so>2>-bR9QbKQmZs^} z>RFw(oqxIH>JGS`J#F>^d1ECPbY%WJlG~ahf&TiE<^DmJb$u8hvn%adH*~%9$~P8S zw+i=SHE?Lfa^LP`!1gceEPEv?*uj>|5DxlJ%FU(fC!01i-|QM0hLY%fCA6!+6ZbD8 z)9ePPgfIC~-m6fFA_qihdv)hrS*it!PSePJ0D@X3di`-yboz1XejH!$_{(PhCxBzjCq%*&t;jX(qhkng?a%^M47E zXRB=jnSfO=z`XGKfY;wX>wN)59s(PYToXaERZy{9i#!5Gakt(vX(p|%-`bWWGG6OZ zyo`&y4AL7Kbi6pB6_RDV#R(}iW#(xUYU^p@GaPdnj543=3GGoT(j=8Y%W<4M5mqfA zc;XGX_88;be2&yrvXct*8|hb_9A#+HEJ0M4kwx`ytS~WB2XcqR-!Z_RLz&@td64)gofW=`zpG4w>CGZ$nSm1z;%G-B8 zTTiA|N9fEK{gR<2X_I<}G-Zz_PRG^lsCq?us_N*RukV*3l-Yljns!C$TH8OVr{U6b zQRYY6IpX)Fv1~~5Vq*mk9>mstk0wM8vuHTISrT7Ayp>ED2%RBy`Z{!3>B9^3qXOuV zQBz>sdpA*E{2KI4aMbPZcEZhUUI6wj9M|;+T#`20Jm>t9**DAwn<|;E;a=pD{jyud z%87sCKgbubz&NG|@S~$*=fJbn6@1l{P%HC2EQ-N@$}7xKN>Ua{r!y^$O>&Ia_uZJ5 z?l72V>)*A+==W5Dzc2Kwg~SR;k0ANzI-pi03Mx=Z>jsccQ{P3e!lXN_;W%_OF?pKN zn~aY050pY3Q|P+zlcVs-j2|9cLJ{E}E!Ka@xnEQ{j(?iZ+26ICU$%1zR_Xrkr@^ea zOvZFLJcF_A8+E*wq~g$D=SYFEyYSV`QjyYe!mI?dW1nGaLuPR4e|yB(1Yx%c)Y>k! zf)<`u!tkW_?+IcrJEesNYd*1AM=e@Eaapk-8vNsz+dM{U*nxrWI>QIs;2uku`qFoW zobKQSXQ72t`f7DbFokBpZI_%hJ}4s3f?*Bk9m|4O>?KIhu+w7w7oYj3#}ToH=n>65 zua*htW|2QnPWXw9c5C95+c~K0p&I;i@())^u&4QT8dMv9KcX@kvscL<_>oJk0d|r) zQ~8t@YX~yiVFXd!4HWf_6a~%Hv>hr!*9tGd@-Cf94@BG=wap+(T}5s{B%3PVxgM3= z6F4=`l~l#Qi&43j8)^L}uI$nWTB8@W}#Zt@vFrO&wz(Y(bmsw4RsF{U~ z6gSZ99r0208yfts7XyYA-$jiSBnI6IE>t;s@fnrA$NSxY@}0t>nJQ!k+?fvF8gHCbm09s}W#>y9%#@o{#3d_N1V zHn-3`f^si23oa|gjn@ehM4y2Ltxw6{2NG0xWr<1JNd*4%jA*SG65Cmn7|$^FT#Er@ z3uH6UtyXETA9}n~F>?UNfkjrCdeEK&#xm#^@Oocc7PI$G9OoANKD-eoOW6;Z`e&Gq4N^mt{JHiv`;0?@?c4<$Jd9$19-v{%b!A$R6 z&@8X=mywpu6&5$k{elb9$kjS4zsby#x(AleWap>FC@b>HnVvAA!_VKr=mohXqN%@# znJp1gmCr0_7I*ZfE9nmj2O-+xP6}3j-1-t+DoW4`wiCQz5Z(Md*hpYWh@QoH1E(E& z>WYg7tNbOfLz!5Rz;FmKc#}MMW`oA#NRpr`DzBcbU3g_+XrJCg0hN4 z>@h?+_c0nLexP7L$X)kN#S4%Vzv)=;c|v|)6{r0WKJbnjW92HF-T$DP6dvvWqxz?~ z$34lf36Zbx&IhA7t>}a;G=4&pgA5xWW4sH*#KlfJN+qV7q;f`cK60%2xNiWtjjv$| zHd|i`O0E<>lg&=3j6p2xar5VkR#ZI8u}2G|wwP8xRW`Iffz!FlmlNr;5(9y-vxi8y z`@>hZa5Jc$o4|h%7On88kLl0;jl^Zr?pRy~q1Sgiu5d@qlF=0|_reOuukO#HU&~@t zH#sl#u8m*u(Nt+&EbkU?Tcrg@{TpA(QFO;m}^V=prRZfON z*vDf}rfa|u` zt`c$n`rzHJgPPZ5LR~gbKWyC%h%LDRWrV8t16z!t{>}v1DIT+J{9yj2+E`q#TZ5m+ z`3Un}bKbu{lDf+C2hNY*&xPN)hj)HSzqlwsJI#Hk20HCvsYke@opc6H5`ZSKk+kg4sy(Ct}Pj_O=~ih}f-r$dP$tMuxINi9iYwE*2P z2mRt#6t2oOZ3vyeGHlA2zjbmANbG+WghJL2MgQJa9$R64XfVE5L^DllD##+ehF;7m zYBe-p`Q|WN0Z0=vBjxnoYQ9y7{y@{3gOT~emRZzMoBwryr22dP35VB~j1F6Exl=3j z&n0F0@~(NAvMy5gP8iJ5d*^RdOQR?dKoy^e?!-^G`cGuht%}KdDn!Lv?P-3g`Ayk= z6_w+gxtqax=iN4BAd(uXWnSwTCh%ZLpD`id#^jE@*BaVv%oH89CZo7EWUEKF%oLy1 z4v+TO3pnWm9EWI#b_Pz10(Mct?^xOrO4c_WIG^}TRb{Jl&#lq7t^4iR%eL7>g=|IP zHz0RlYD76gy24l$-(4xukEKE;V%F{p(9A1|u0?5f<_lA%2LHe_w-&BH#(hYRaPu-&j~6#DIX=^T#6TCiR% zk5tcgGcXnUy~>+4q2bV%UbxL}Wo{T<%9g9rQ^z^H5sG8~s;XvFot<1!joP%bV}*E; z5^quGZi+e4`2pt>pj(&B6 zrc;xtZeXbG*`bq2WiHm&z*CK;6}eV3rc@%ufwHWOH(jJ;BLI~t?#z>eb_)vZgFU6v zHA;Erx%k}6kdM{DwkLDX!L zaK=FdZ5DGzr&N2lGJDeW^lELJCIWvK02ruc%l^w?$kr2fDW`idy?ZYQLvosx`H)aA6lvB_`7i%R3KJX z=a@Lak=SxCI&5r_BnwnOQy_{i#Aj+t?DviI~}+30tlUg}TOTo#Hy3kV8{mRr{(FYu1NjDi`aX znl+y+i$4wjnME<&XKMXTs&iL|%#-a6tsF-6o+=7|ZI!qPz-*sdR5l57`Yzhp+X0Rw z`f|rc&o3@I_>6w>)U*}n25K0yG4r-gB$7Qz74e9kBynapwM+7Ts7|H9Dg=aHi?e$o z2EBIXA|w80f9E^ffief<7lEAOe$^klA#8&^_Iv?jJJ*6LHgTy3C*oXR&Rp-H5nHv^ zg@PWGzwBa*@-il^CfbR!W+nkn2Y^SyM&-;-(_#>#LHywycATt6Pa^X?cXNU(6lil` znadMq;WhKwxrIV4?pG-~3ib~H#2?u;8s4LW4-q~V&OHR9RZPTH*)i~VfN{<%4XHU) zs1+>*=I#ql5lSQP42UbD;6Zg_KsLT$u-^V!n7}Wg`vNC@QBFMCcf!ue^?aRwVE7X} zbtCB!CXVA7nPN@eVa2^`{1)_hv}-*5*L|)~z7;A=N~m2bOVy z4+k~7Jl@xvP-bst&}D6jfNpFdiU50DLi)Zg4n<*Aa$%FQWX$j8w8Sr}6BF>lCwg9S zjm~_wNFHT=C%I1no$+bBzt_6449gswBi6?UJM@mTHgex{;rP2Lr=-#=y6GOBVo? z<{0oSx1w7~|HINMZzBCZbnO^hYY1*Yh(L29NA!AH29@Ha-W(G>vl7p6VUHxkl{47t#+lHWx6qKmPn8AA+x0f@gOp{FmJm zG)j)4#mNtmGjA985`40x6y)(|)ZDuhiqM9_o^RuVY;p#stKEB5Jyv=v2`p4R5en>r9O_yTXfUGKG%$5Ted z{ZM$dX?|%>T4|^FeK|ie9bG$>tU`Lt#5}LFd7m@4U$cgmgC)c1FCk?MpKI5f))04vRuRI$IaU+Zfuh9H6$yHV~1svEAESW+0s8I zu1G^{P;?>rfRY4?0Evlr6KkEG(v>C$a_r(`N_m1~%4H_(h_OeS;Vxhr3Q{$+ zlRQ_`aYj?O85@wwZ-*Sb&FZmO?{9*dD1l9eEa{Av4zq3AVX%31;<5K~o%+y74*CnX zj`K!RU47m~|1 z@>uaQ9E+qlFp%hys=mpBjukq%g)ml!sQ1nPw2DHH;OMnVKrhbF**knXqmDQ|X^w^f@6$!ToR-Ho6O!wg^$2>;^Y=60ZkalZ60m5xnl!iS+Yn3fd)HLh$iS$l!G zY!1WYiL{aTLo7%Cmt$3#39YR^Dx$XGfLPzH z`m52Ht3MhthYV@G{C3HZWmHW))4kky;lV1Jpsuyk6rW_RbN zW2;cowpS&a=Weq1nVpdYQ@1(p^|!IiFF-qvkL#`=B*AUlbZetkp6gZ5n2ff}S0)D>y-Id*d1E%?Cnq&m17nf$V$kVHDHo&+B{dOl_L{;l6Yq5szl;pH!u*Vy?lUFc7pq9nuz zsIM!*I%(j(XXhxK`*>e0B*OR=g6(Hqvk%PIYg03dJJ=9|9MO{t1ml{$({t<#Xoyr! z!TDQ>gbiVDJmRUM%saOTwGo13nt1iad{^X-*~z(RCG|_j1!*SiWs12mX;bnWgV6S% z5a)_MDKa5nOjs;T%7s14Aj)9232daD6LuPH;Z=SJX@S$IP5%pp5SKs`0lt_J0y48W zBL)5-;z2EfOL$y05uHL9bX3+HPwM9GssE8ViZWs1p9Fg%ka=8%X!y-uN4S!Votn_^ zXFH0fsO)x*H%1578RiFhBYyF0j?2&Lq4uaQwx!`Xq6*PIkIWh+%8_3bBglydl9A)& zjHKgO&xU^Y(9cex8Lsmx4NBs%0u_n!XGn8OAq}1`fn6xA-GH`hL$WXN)&tg^>Pbljb9#lMD8-`A75zF} z+oH~Ji-i)Ugv9HKkr_6G<^njH9AT+!@?vtX)JgH*_o?t{lfO3hotO$8lS?}aPctJt zoT)BU1K#V2W95`eVw2`^2Tet);Iyx-Cdb!aVxEk-Ix|iq-kH21?g{G#kk{V84Mta5NzO+|P+a zw$k@KY!4abDRT20^R6XME_g0!5%{#HrOL6d6k@Jyu8AUc?bF~BjQ*6jmw4x@L? z#Rlhu_O=4y z|H4phsX#y^|F4^-t)ae^n!ba%zJZmoio2b$qp-e}p|jP0xH2S+|J6+g|Kkx_cK`mf zh8yL=iV_dvhlCVsOl zrO|fWZ9WF-23PT;jKW72AajG}vpKEOO(^kaK4kxeR?%yD8uqgZ-`j0HX1n)#?1h(q z?DjqX7RulVKYT9+e3~B(^dk)aPdIkExKlcTA-&ezsA0d(7fT(Yj&2QSoPkhX8>cH1L{PlsGm=B79 z7}4A4ottoc`F-;I7Mk#!aKG!YnxNaEDq`Qv;V*YZm z*9tbud8zwOal<68?NxMYKYZNJNeuWX zR*W)M6vimX&EF=$ST-}0!Br|`vg_`ngGpt^=EX>Z7W1^u6cs_xt+U9RWgXcw9*3d< zaj|R_nDCo%xma7}>20a}47_=ol@=Tvm}&(R!6}wY@VsqVGm6frOXXYAH%&~WTJ>7& z4O^qPgp)W(g@N%4f8|QlONAA<;DwpB`h^T_n!YdURH{rb!#x^KBuExY($89k5+KNB zE9Q!E)yVr5g`BY(-EBS6w3J!f{g-Q4+v(~t1O2~Bv2e~6$+dMNtd|&{&l=ISaO(jV0PmF2GeJP90S9Cq0|&Ax$d4;=;=%O!3W?2g@&t0WLN=o2s%WA=T! z{rpylPd;5Gr=*-`N=qJB8IrSDr@kueOye>yHW`aZePeJ$s0TBNk_BI#xpDwJaARXf zgAYTuGp{HMrnP3qqvFQg>C(swE#Maam2*8;Q;9_6%v?4eNmbxNWZu}>Xhn*6=$RzM zTO8tSuo|IjnvBER^{kJ&k%>|^FjG^3Epnhn(@L0|jW1MbU?sqxoB#HNMfjb_4}&|1i@ z#q?{4N!iE@t)QZVTx~sZ5-aJ7#qYCi(Wj8#;!IH2D5ywOxucEMK*k=%Amkw7)S409 zr9L}$%lAHx*VM^>y4&wZl~_6y7IwY7cD;IJtLi$=eR_aCJNBbWn9AHXei};vFoX`o z<1&dLU)PuY81qt$0jJioraCKfdA77Y07eMxS+@;&3WnmRiQioyDJ`1-1PRQAqnKMzJ9M&tm(F zOtYhhl77At<}%NRaLrdJV4zApVaW+g?+MQI)!QjU=>62!Kjao#e|Xw!@24b!!&|EC zUhYiDsY}omlBcT#z%^V5m5!rOK_L`_NVd2i27Vqo@BA!l9OjgzYz89;6^q;kizQzn zkj#iHKUOkXNHHCNFmA&|8s1*2S6tw4#|5TE?%>py+`w1Qa8x%w%NTTpCO6}OYLgOZ zeM*uxN1J7i{q}epR)cHgnT5w!RM)0v^gI8ceGwji=V8y%+C&pM;39Jwz6(QM26&+; zUEcO*m&`>D@G1ldJ*7uBSBTaPS5b7g0{cWxGaQ9qDnHRybvmhBl>c9J|3q%(_hU+w>H zLts^T(JdVVDekfG(=R{i%KtxA3qg&$NR`mRc>J5ZL>ig-K$~#2VdJTB+oSZ)Z+X*m zU$?1KewLN>rLEc3bvguao2M&0i7P6Ebgb=6uFBq(psbsKhgRWu&97}U^s&#n4TUt` zKE^}%Q!m1%EXCRY_$o^sPS!Mj(Txlmz9ddc({J;Ut4mQ42B;+5=|H9`xtcF8n=`>$ zcfqc>ARIEEl0z1z+dC4r3$b6wa(pQ_4ek^~+4{#1wF*`u?}M@OkBO2lrpj=oC_2%b zo{RR@bl7qA*!{9TZ6R0290iT+DEg}{Tx1BbxmcSe;^HkPqN>6?;#QUE{H{LJpl}>c zeZ7%Y7L;L#)8*4P!QRzr(B*<^k7;l=?)P^U5BfDdnGe`qJtSM?mTRHWW*72uN=c*b znna%{@)>QYLYa!%8MBVD0TZ|AI9n_6sp=~w8Sf)1YDo*r|8VE5840);triTW zvNf%?^0o&WT(ODJMn0pDrFk>&nKfP&s2YDgPF8K)=d~M|Lv@}-t%Za6aR#QH^x1P5 zIx^%Jj6eWa5s{sX-ek(@v*zTnhscX(h~k06=W2f#h;lo6OJRgn5LOKAAVh}{-v&h^5kuDKaWeIV7$*wBFMZj~m8c!3nARIW(ciwC>N zrDtzdjUpP2_rsb_?TKbKM=0oHSIk??FU%kiH%K>n5O2PzP?&%_q8b81#ntaD{fgOC4+M zys?AVQ2gbFsp5#o$-0%Y`?nSP`s+ScSL_P*iAz`;jfu7}>P9j{fx=oW_FXD#6?&K9 z_cAzoAHy;!-vSU*@-tXfaH3ul!c^$9$W<;SJOOo3Q^w}XC!Z~C}S6L#x-&(-8LBA zHmaZIK~8`J7KApn8(0uIf#-BjMxh(sbl}1>pGy;x^Xp+>{lGj*bE|QE=0IY9yj<<4 zgZl{!Gms^K{6%@8U(}u+oiu98QA7JEYcT%p_#$c5`!}=Uk;dtgI;6$L`&lr~M!41qWV|rIEE!>!>{RaI)>T2tUID8QH$FJj2}8U{ z(Mt)dKOx>PVGh|#wa)6^+7uRU4{IZ+zzQA$f0$+2A+=xJKGG78rr)~-`#JP-=(HtG zeqZ;>b;67LZcDU2lJEWd^SU;T6cdaIFZ8+hmu=^8`84(uS!1-G*bntB!PS@=CUX8J z&u1Z#3GtiUbB68X>6co)L**Pni|F=~KC_vk#yzhc z;g){)Emx1}KxDmojb*K8$TE|fRQ0l>sxq#x4ADuxFUa^yJwKl06N>de>6>2|z{2)f z(#-&^BF{Xj=lvR|jV5dO@n&$(m3YM337rdO{QY$C5uE0I2_f{=*n| z52;wbNOA2=PQaye8#uv4?qJyB8hfqg2wy}S=7O%sr;l3WE>G3f;X+_HL=lAO79pGr4bFY9-y&`VhUtE7M9H59d$xfFGzE0I=iW>#L@8v z$u)=M?R9pmg&)m}QPL%Ib8=~mCkvX`a(WG`*u+!oXH2r9YWyNrI<_Hl?pK&1DpP7I zhqn@dwb+fO`z`0cXAENthc!2*cY&v2EvX&a+AYp*8$-DcXqWYD3vKZ{Hqv*TJiA1F zjJmOn!|Re$Dij2Hwar)D8v359KiMq5nd-{x~QRS z$&xt(lGyydU1r&lf4$q-xm%1@QQ!sNb5;yy{CRDJCEdd(mzDoD%J9^n9h4|@cgpU za|ZRfI^m2828ns{p!?mtQp-1cGlT`VM@cToxAjT`HwhXx%(>5@yY$E8cU^KXwN%l zjYHD@6v0MB_9XW35s#hRg+oTO7rpZJ!PI~GZ%hj;yYw`oSJ$p{NBgYZ*b%LeEi{Y< zVCx)=Dgpye{{^qjsL?YE~JD|+nF&Mj$;-K3q+4rf0f zkUe~PU+7!YhH1XSzrWu8zYLI(tSeyiKQh_zKMK_Un+N}YbE7L0t^Y-%A`O49msp+? zqIwAcAUMHQI7CDN2YH8~gHKr2y%;PB83dnfNk$2-dQ z>oW6L<`ga5W#af29Fgr{qMp{mbQKr;%!1t zfOA-~Z%L_z-#+FOF3Zx^H%!R*(ZBciSs#O&ZAj2^Q4ff{&U6txV(MVz#Oq3Qj2RsG z^kH55K$JnrSW5nstX*w2t9`FF3om=Jrr3})zue*dy>DHAvuv16!InkTq@p2v%o4ou zCt6FQ+AJRsrs^%JHFdUzf=X3BADT!9a9GRFJ}YS^6}?uVSR}q;5NOjfo5{1sLzmDa zfhLwgyRk>K`qsLG72@85y(U7T^b$JI{ACz8OLiq_K^JiyN?#jsu*=lP95~B17Ff&X zf+S_lBaLCZXDT6~AeVw9iU@hS8DRj*oJ!0agL)u&?iRFMxSbo0Wsbf6BETi1D}X{s z&=#%FCWA}>o$_z}vYLWYIR;znV)RLfBxpN-3R5MdfIn(E8OK*z*gkSb5~Y$Ef8IYK zbS?is<*;Pj7rmDsARvH$e}~fl6FE%E+1gIt!Q9l`=D&3@m062_3BtSyvgu@1vXQ_> zV1`8!TX=*#Um<{$0ISKIkn}oXGFGPTf3bE>(U}G9woW>>Z9AQGY+GM!TOHeG$5zL- ztuMB1+fF(;`N!TjXJ7wgtgCfdqiWWyx88bYI!r@Dystmefe4Dv4cG_u?Nl+XYN;ex z_XOe8me*CAdCI@H%X=)4pEs@1Kdw^ahC_MI#B4Bc{Yh#b1eth|ns6XJX&2n>@u_p7 z8x;do)UXHl1v7H1inv*gh)Ltg*I032pG;^`L!~*7SO^Q%hWhyb&Sfg8BVx3ap@{LC zXds$z;43l*eW}qHYB4LIF28+xpI_!qE3!Okk^_LN^->+Q)%creE87*RY z?y}iK@tDOtoqsAu@&Ye=g|yjQkoc8BZ9Ch7BuyCv@QuaYy~XRGBg3&F<0>|O6BbVdfU*vvO7`u z9iX5=JkY<0g(4jXrW5F5bav0a(Muag!ttB&3i8N4s`e{UljMkfS1ATIyW}jrfQP&> zC9k73m2cUB4KE9b(f>KlhT9#JN$vN4GJ3`47lt6das7qR|IK-(V(4gY>ioa+Ox5c@ za{Ohh&B=*KKO%A~hS9{3gr;9A23{d5Nkgsz|9RNFnG|&XEyff=atql1_$f*GQ3d?} zj$)hF9IB#4SfXX>xVgGMar9)TXaBoBKX(H`OH<7=lntmR^{;bv-ieHK&`F+SgIHFj zZk-LtOasLCGC;IoY#0J;hN*E=#*yV~?&Yf0d4UV_q30AhYpH+o2fHH4uoqsX%Q{c+ zSS*}UD6*19b=7B<%w4pp=9`rlF6(QT;PBc9H`&(foPl@2#tzEU>kx|QDLKve8#=o& zeNM$Azmr2U_sqJD9H(7FpEoH=!lk3AmT_VT?3;SWr63wtQnn6azzy8p-2XvSh8Fy^ zOLlBdxB0QJi^{*fBnAA{NfT4H+pa<{C(cME|3_q++we!kU$sJYpuQ{Kfjyd>?n**)M6NpCaSviqa2)3 zgcwn)pb)*fgD<13H9q+=?nVb%T(miCObSypB$+Xa{Z^+0$*vlL1_GL@f)rVD#zmDbOPFg!axvtUM+tw5JH zYfnUjfOJ!9iRfzQxFV1?T9x0Orj6yHG|BNZ0-gmSi230XO29OeGM?4uRkATcX>~pS z&EzCz=*$yDw=wiRyfUX6SOfD2uu>hM$*7Li`Sp~Pmze*yLh9QjQzLQirJNnr&J}tR zv2cQdkSmdbRjROT2C8Z3br1UXqn=ZWMKXg6T|5Wq zbL3~o2(o(&vL5n+OjdEa+GZpRC)6vRaBwC0g?3{c1#*O+=m}oZV*D&YqmPgmh{7I> zQge)Npq3Ld@6HZJosXQLoj24i?MO}#^wfUfHEWx{^Ng^T$7ruGM*~BFQaCHTsr_4g<8x*##*vyeMzlC2tEK1NYV#M zTBp@PZmpr?n!br}#$oP#j-~G#)YDt6>~L}N(C~e?kR`JgK(ik{G+{FBGsSm(ywUMK z{I};5affXoAHYNe{NadFNU9ONi~sx1IKu4?g?>19&lVB_gG{k97n=-VG*uqSeAEft zNr`lTG=;RLrNpjeDJ@MixnT_Y16}@O_Hj@6H?#f(19WK!A5i<mNzIpe( z_$Pq6RZqmtP0qaIv+fG}Gv%DC{SdtP+dJtwotpuj9|E;mE+Tg|hl*|MN&L2It>xCC zb$;FsxN8w7FeJmawdD&^>+7IF|4dqH2*GENUOn94bx0BnbvXR{YHFx&hj(q7^zOXO z;N^OkA>0@8laXrenybj;YbMxjGi|4Bs(IKtoZ&ZKIV=1rab0 zG0q${nJqW3ZHzD`{zmUZZqK2)!u|xm$(|LehsZl_Sy;l2C-zagBLp64e391q9PJj+ zjlr_wcwP}I@#`ST3N5GZ+s0wEddPOZqQx$#gd97JlaAK~pR&54LhU}_gDh8Q7fMN# z*|6c0S$HNa5%d2^%jA7Ar$kmuJYUv!`_RMz-!~?>` z=8;l#1j2Xt5R|>ui&!^?-hYf|N67`nAhGHU6Cl83VU>OegeB`1vgru^3PfUy-r;r9 zJ6tRcWezpwc4of+#b4u#Q^ySpM6KON&*BM)f%!mko^n{*|xlJ;{vn?X)uZ<*8* z{L|s<-h3#Q&3|Q2_$T@SGKL3@*Xj)hN^NII%8b_<;;%;c>j5EMb&ZKri@ZfpHN|{` z)tvazuP)%!Y=`n>*5PkZ(cv}cPC2SI=LDKPUg-p-o`7ycLj8W&{MPVP5+*r|K>r&liy}lMao2MV(DOygqR8H%ONxm|6FkA2xVvIcL5+<=^P{9|%H464TKKegiCNX>ibH85jCrU@Qj6mX`ygQl(#Ro8U``K->6!&4RT4%IispM}TFl-S zQeBHD;nGu_=hC0L{7UVzuj0R>VdS!5vdhd58amegxlUi@BtN|Wo%VycVeBM-V_J)# zYt0yt(n-AWMOD*bzq+n}DrKFgOPHQUS!um_qIiQXGUT4n|Y%#SQ4lT#$vID=Z z0hMc>1rOVz-h9>JS)`Ri%Ws|(D~+5hjSUsHJFR(!M=`3{CFho2xhlhNJCLNSCywJu zpxcmbrA(bS5l3C^B$3AAoxKG%t!-c66>(Vd9$;Ma3;h{2m1J*q0mU@dny$f{p;Gm_ z@@nz&Oz!GNHQQfo6MlZdx|5;^U6K3e51DYvbXJc){%0GTWe%~sxtMQHWHh+v?L{5n z8UIp)@nUe%=M)4t9xX$gtFRMHJ9aJJ3Q3yTZt4r+8SY)=Gigq%IZQipf<17)p(8MG zzi3;@zT;&~slTAQX!gNXa6}}>Tf^XiC@4`@jupi*v+4G{a~byd>v?krw(CxOVKoqJ zj$V5jvOC?;u{U+lFK9T!_JaK-KcZwb67uqB_whqbPuz(MLfhfN#=(~!SXvq@tqFk> zTP|3@73&~obUlGw!weef*SuXzmotYDkYr>43}`ZgAO*O}+J>QtLxxa(nNeJw0Cq&| z^r1N=?7dW_zsYb(mCJA3MHQ!%;sHhZx+L=wiw8ozLptuJprJ0FQ1$JMDQEti4?ifS zx>at|Y352?g65#py{XVUr0TU4V}%!`pvD~QpG(Cs9s;XdfZTLJk#d{0twM_$INRdZ z!Q?cH+qr%l-$25ORQ5`3&Y;c>MOY}Gir?61IEc7BYY2_mqn|uiv&ew%XE;LH(zJ2t*K{wt_hO3=1!}#oBh*O156E1>4|!rat^OAzt-*E8 zeHzkLIcIpEYq}W~2@5dX573&I6ldEwM|tY6o%5VKAIP_1VNYVTO;H;%G_+cMrUpFz>no}6J|=h}b+QTUcqkH=pGE{>|(<)|5uERXcUbNDj&O#+$mZ?6d(KV)mU=&8u`+_KKqRLB?2pO zyZ;KN&dK+XY;)PIl`9r5FVjw#XCCHC$#r-VJ}#&4lhQ z3f&`#KUmcF=RThwK_5>eVgj%91MxuWEVR7tb;&gO+asmHF3XDZ$54bE@)fvlqTm1F zbOm45D~*0vzh1xlM0Nfv3jQDaC;1KTH+2+qw|6vk`j3kLuN_p`l>a{Ie-^KQp_hhJ zQ^Q8CVVa5&>m|h*gaDKxU{W01}a-5#txtv_hy1oD1!1NMKP@xR|0Vi$60Xm~%USR(oWCF%Lg*diqV~)A;zSg?O z22?RWVDO$=GrFXA$e{?G4`cVgX^JpSB=M>?bcM$8ES5AM_--oCxfWtdJ1$qdt16<+ zu^Vh}6`JRewC_kt{QNax`k9f?xT-Y1+i@XTt&DIXaxw>J} z>Mn#Td`)BQ@g&z5!WLadYgamtmeVwb-y7eBt34oXAZ_Js(us-m|F}$fKBh+%4UYaxtI5YSnuU}b2p*k%! zGcbB@$FwK*W=oM`^EPye48~bk6V#jDB+;ayoTY<(^btby*pa}8>tT??IdlvdqWrp_ zskN0NwHpgw!CR0?%Ahm($8ZUrQV;6nKj2%SIpt6SN1$1oIgmu1D?us&4G46E1jEuFC!h@)y|ZcV6I*sJO(f+2v-I{S$#) zG@m4i#)}4R8V@%_la|K~BI)~oPEjwS6S#eXARx{)ARrR|jV1KImd6LiKxJ|9 zrfqy8GrhACd8GC_cr+LUR#*dExK|L9929&*_*buB+CX}+m;=^x6C<0(xx5ZtJ|yyT zd2ZEWv$XEnoYiKlfEE!#!n$neeOcddB;e*EQpDj@Ogd)=lOY?;R%yKE>bp zmuv6dmnglZ)SjgNrMvBtTJBG%KVR`Vot3xh-aEVR-tPBQe|pX!IQ%A|Xg*x5o?foW@x2KOiV^Msz{^su(F(*sF^o@K z=@*AySNM$J8IGX;awk4&kY+OAe~g$}bLB@~^}s?oLu>>^Zk*%bxH8ht21r()VO(r}A6O z-(Tv&z37Vp%*jjMh>Mi(18tlL>|E2sBJNRxW^!2y%W$Qw05q)E9WDoEj8s5?E+fT& z9xWr;Tt}8wnTn3kp#9%g)Mv1$3X32=aS{DXv}Hi7b?J4}9=F^_7WB4` zj?-1XM&)NE2coWj7>id_S=~V)D@Q?cd%m7eAht)YM3_jZ4!ojHys}Mdau>M-Yf|Km zQC9Ox17t?!l&@K4n8eMgQ zBK7aE?93&06;%oKP~6GPNDFsUY`jxd460I_iqw zX%oHLIC?l`zilG-m{O^_vz4@nbTt9n$dQ%Ry2{99F;(TU;;{Yk-zYRCU3Gx6KAsj% zBswc8bls01CXS8-(&yIY=a{Mzz{T-%;f0@SD!S_PD(agWL({&438EU!^;Igc2FdzeGe+20u@+>v< z$Ga@L?5>>+0jikA=;Iw}9WdZ2Dfp#ubu#*8M3q-HbQ>VcwwR|XXK3U|y#18k%}1}) z=b|3OMHbmAN^!uN(c36-g(<_aKUkaBlVYJGZKGMMVc9~hF-zmI!8-=RXY299fS`~n z%OtW*cu{3SrOoBtd900$DjXd&O7o@I{8RxJ_)39&mYWpWZI8Pn-dPkXL3@S#YY+&A zVRKW*YONP2)d*s^K)U|C(h%d|)BdY|72yJE6a1^mspuxG%1sQ{u;g{_a1RI*X*n0; zNm>dAWBVT>A|s^u z8Q_APsI+!k9lYpOzg8&e9O8#W>QX|l77hO$ok!OL_RHVaUKeN2nn=L;hBU;lzJfQ- zO>PC!!KX@vq-IVGSRl6#KGAy{$>Ky(Pzw;-sOI$d%b9+O&ieH*;!oI0{(zJ!NEBMT zvv2|DnrCm3Mxu(uDXbPZyg<(Sg}OE)`Sv*DaAd90918loT&c0u4-%=q=?kpz0j+LH zLAbZ96%CF|gX6g+w`=1c3bReP0hT7N@#HyY;of7ZgRvx8PE#R^a9!OkZMQ3%Ski<& z9hhmQY9B|L1)!!Xc110Fx)bTIv>5;fCFabCm+AM=>>o)wB3WtkWCS}!hw@l9C;p0)s31UOakf>reIx=vOjqej zpZ8%8w%F3lk4{%(C;DU!FOeafeBf%>)L#9YjGMhJc&swCc%|GOeTw)!u$n@(Uh^#!Mnv(gXc3lq3lGKy^Pa!-;uLs zRcZFLu%Zh#`aIy9_r3gB^688-URbs;GTq=~Bt4T1)u>?CvB#NKXZ_7>P-PsP0pqjQ zsG-=KT_|y<82IFLGl^F4RfFr*hkOvLS$y!_RG>?yOdTTXI)Oh7)hxtZlnLbd%g?`I zW#i#mkKqTNxy?+)<@%klZ1R>qJ|aqm61_{7H6a*Dgkyk=X4c!2o~xisZ`D~Pi$%B_U~ZFom? z-@PCn3nL>pbRkmH2WS`eofi?@SfQU}59+33t1?D|)_AxM|2%a|J#|Nl3+_x#sWtab zI~eidIEDhs!%HJ30bgaC%1L3%W-@kAxUehO0j4kQIG0;{N6+@$?M1s91*EoEw&>lH z7GDW{jjplnu22LO6db?B-*Gy)e)O43My@_8D%ZLGacrm}c?G^~KrdR+RWi44`B+0A zu>1;RyKScEs@jh#ENW>8`azSJq)5?U6gU(r{>(mb&Zw#&UOIq;jVwop3UaPRMcy02 zE-u1n)#k=}izOp40mG)oz7T?kK}1CAl0)>Ht4}QLCPjbP=;JmQJwi zevEG1h*7*EZ&Zr$#CcV_%5bXJ`Wb4$2Xifw=791jUUv*#XtrkV-7stUH}9AtqTa* z%*qHiw%eShc-4gg*ckG<_?Rb1Ts%dE^yaz9v-UJi1xx3c^~X={Fg7SQ$t~=KQu72q zwN8?Yh0=8K`CxNRGooP;xeqzCYcfg%zoO3Wy3R604*OtB=w($2wG7gJZfggAqh4GZ zq`X9|{PIZj!=&>57`u|gy>Z2Ip_GVAjr|LIblV??S9+Pj$m~9=UqdvPF}6J_ZG>aL zWI~y3Zf4-Mwe|7z0wUSuP7$0VG2?TG|D9yN7>Ki)>`0&yX&lKdmRtOLE1bmZXfoVl z9pZ7b-5Pk;Vw6oLz>_sHgE}p#WReX`AKO)cYian|nTNoZ2T4{irmaj=hJg1-IL1U@ zPngDft}4Yf1Q6$2g}_i9HjP}es4B8nFCR1G(B~xy7q*VqYA9b+kqD1sz0GSxV_C;l+;v%$de2Z3u)cIHE$wii)p~z( zHz%`b=iF$eJ~3$cQVQcr2HjuFg&L>iyk_VnV)5plg+ll1^r@2%-P8SzO6B*(YGUW zY#-tj`f|YYk>4F1uv3h4-aMybv!%rr=oDh{BJ6nSM~_FFqnl909njTZJ=ys_{HRke zpI_5Be&r^Fq`DYH9HZr=rv?2cz2Ht; zc|(1juN*W_T^sfKtUK(mT0ER70Tm!aBRl(j+`y`(E9+$xJYs|uY_RY)TF?nKUa6yr z;G6|f;tyzhOakST1fv=g`A{*I3kQ4lMRAF!@cF*x2VXfJ?io8~m9QTa&ZCtP0;yaVoN~s$B^Nu_6Eso6?+v8ofs*NRS7*)<1~~J`3S*H1{oIcp24!K z$|BRj8w-@&k51bq`~~fnsN9~qZ!(Fc9Cx3A3yFs00uK3^OrWl0dE~GE`ejWpddA`` zjJ~&T`2~k4bp7e>SjXL8sDYyw^~c)PDAVwadGH<;ZTpY+E~ibZ^U`HWeW&JgEbk{r zWy}{0aX4*#{DU;N_ee+}tC^4FjKU#%-ZQe+wu3=J>XTa#W*CKoulXw>R{C8Q#X6@K7aA{UN8ELJ;|uuitVKm6@17YH z7)T{p(!{Y^7!(*(UpTPV{gC<4#@Y_IbGgl4sfoHCOy%a0_Z~A4b$rinkh^fso-}iE zb`Zp081xg(WCWE+j%JvnNEJ3`S&@WH{^BF#F%dXdl1`lxuZ?eao;PAKTUzEA!Yx=t z7;-bIb8cDSohy*3dDu-bjbwBQq{B$1pD#^eWMNG&8>e3dYYHF(=VUOz{}ql7sR`%}!-|_w)kVwQ!2Ew!nEcv)PCgLm5TXl@&wmfxY+6qG5W; zH0C9`&(JXd;Y-K-CozUp&qYDQp9>Y;r(~Lpi<0qANB^nMUf-GqnU$gp{L+T=LlDS< z1a=-)F3HrOLu6gVWT5ksEH)DP^&m6ITHvlloSQQ0`3I_R+0^e|h#RL@k9K?#hhe8f zYF)@UBU3_5Z=UVq`}evyq4b*Z*@p`o-;}Ap6wc_xA`O=pLUJ*)hZ^{jF+jBo)2B$r z*f@zZ$Ih%=sf~sR#{@>WLzC>9{d3E`ac5VQKTKlsi-)Wpjf^GZIPF>l~_pxNOeX?>!3O1=ysZNR(qaKa>aS zx@t3%-n20k8b@~?+?m3nZT>l`sFCFd!n%z4>w|Zk*#bcMSxvC*iu;mRb~(n`L#1w! zW2DX$UyJ2so9W@!Pj*iThOhye@@eILf!V$C3$J9}EtJ+S zI0;~^ZGnABzwwQclsA&?9skaL#xtsqdCOJLnQ9!dK)W8xYNS@c7==dDE{|~QR#RMv zT9`i82MGPHA9il%g-{&BMUMX^{eVK2VL1>uy*JoylW@)&A?gC!14zY#p4yda+il;V zj*nL;Ak<9=^{LGrm%DS;%@M*DP);?MLgzR}e$JtroTB_ucSpukX%kN!&U)Jk0*v3BtwxGa(P`f&2-V$3|m)ZG%4^A+mwAv^7zKK$w8 zf!5TKZt;%kK?hU0?%bVtJ$HC{7el_nnL{b)({ffBC%0q*7nJ`+h+%9w!sFtR+NquT zJmTfz@%JN{@j8|I^~y9gpKlRCA!W!@kl-|ppB`nPDr2L190~>kN$TKTO(~w|Kt5Zw z_O*S3qT6Vu0IfkUH8u`gOK$M@*+1&jCY-Ow1h&^SMj|RPmeV*e;xLHEeQf%-Yl*z~ zn4X1%4F8$ncj?F$a~EEIL#AeHt{FySewfu=PP%i{ZGi+m_IjC?RcV5M4l=p>&(czo zPXnd8sjijD(rGE!O|pUW*%9=*zx0Ir#XUQ=e@hAvYEV5%5ij#&3+y59X~B=B5NwfA zrryEMYE50TiU37$o)-;aqfOmX7VDA%0N=xgc>kOi6T5@x6h`zkm4sIjQf1>6Ux8}4 z9L!G3KRw|>sEpf_zwcJ;jkUPE7=rGAr`TZ;&cqQkb>>@3;IoUOOmo4h9g*BuB`R4H z{EAQ2x>kOs(e@S(Iy+#FDYDZUtzrx*B%Hx)v zY$7B^-Bj0S-zKoJ0<4qht(FL3yR?Pv5r1}^T| zDcq?L!tBkOy|KHl1u{c}J+kqPm+d1iX-&qN_!xGrNTlvDl=B;jz4sR_Ucv6j*r-5n zB6L}Y(b2|UK5Zd}C=*0%yqfgb?N-;6EK&IgflC^NloozM{n?P}*{}(FuKy?l4awNl zRW#@cFdx|=bFMN1-^@hN0&e^`S)HDdqkCB`-$D;MUAe)Jct3oDTw#-7q>%iUlAHQm z=?`bmav_&_K8sKp?I!Qai6h~$Tl%ZOT5BFRE?gdAp0NNJ*cDE4GKJGv2d}jCcA|Vs zKl8AlZuX%Iw*P_p4}^>)Db_Uc7uPj{VFF0c^9j)c(_GPV!Pq)(r6_Wj&#zSv&E<}q zo4g9b7%n8MIUqB42aPg~PdFtc|C`ak8&_;4_#H=&bU!}}2YV#0*lklc zm|UO7URavwaOkMi^giUt3wddK!%sdBT{)%GI~cvwgX$CBjVm7MLh3ya3^5imH7Zw* zb?Q@ag8Rfr_$+H*9}!c8Cu)k-p9ddn=!4Sh*{ri zO3L{vtOptZs!b zV2}8h$z%DjZ*`04kC6kq;U^^);+B@o{PN1NC?#@cu7BKy0=eMEBgZRf5n!44RSMQ@ z9{*`BgMPfYnGlKzxn~BCXCT`b>GbY#i)F(Q^|@NQa`tdbMb|B@rMPl*yUBMI~? zP3GY4&tYXKO=w`51R81ym4KqIX7)L;~gxr0R0K!Oc0;y-i{zDL%NIR$O;|>)9Hqf>K4vRwtFAFr zY$cublHn==9BL9h(hH)YK{0rG@~{JdJdZY+jyzANaFXZ`(?PPLTbl)w*535kGm6z# zN~NAHbd{r82P{KbRIujv;v7-CNPgHMFsFyEOd1S9f1656)XkhA4*8Z%Mr=zjW&wx_o6M0jR#Hl)?e@IQt1wD6r4V8 z?jDM$R{&u``k>V?5)HFpUgH!$d!d%>Od#dcx*}SHcuAkbqG4#|`;)i?_MdHXhm1uh z#Ko%9o#*+tY%&gKzkip0pM@RcGCvN#Y(4F*R0Y3lt} z-x3RyVQI+EZ*&)LGt9xTBNm~npF>4a=7zJQ5NMS!wJrw(|3##Ff%>nW%jI!gRRvQu z(`B#F4Dgi&zQ$7u11tqzptGXp$T7?Di;XHxGht(1qsCN<)LHkN1!tP8FB9w9E*ubM zeTV7#%AviLEc2)vx&A_}iz$m)SPC%$MyDk5FusIwjb?0& z8@Mag=FzJHvB!_-?kX`0qrIN4y`@! z-+X>MLLULbuWwVR;zOEQSRG-9Y_!;tz=xCR`R~jt`eJAl( zWe0X96wXof;fs3TQRcmOOiiXU7(BQ6=Jyv^jP&34@kLs?t zdOThkSN~_29=cZ5Cb$ELf7NO?m!U}%9(`eg&mE<4JjBT$DX=>vJAHSSk|E~AU{8Vq zD{v4u_|ajP>^KDtT7Jbri%7jS)69 z-7Z`y3RST7f5vOCz~UW1AT4&DsGUuu)gn$b!u_IdzsHYlcz+w_LKKW`?=Pn_x6w^X z=MF(G8TTCa-VnwHW@EX>x~s?=s&`_a0hCF~F_fwcmWhK#Ru7yfQyTHDi$Z66;Lu_= zLxU~{@ZQ~F8@{Jlv;~Gx?PTz16&}{dcboAhb(@ zA$#Yi?6=i?MqcOAoGamQ$-1LdW;I*+#wj-SwpGX!ZlQx{fFea|AvE>23kc z$(FEZSZ7_OJl*>K=(X#~(OYO(E>d3v+xrt;=P$$%ny=h$+K3C*g9Pt;6KA$l>!;xADY&yfFIxa5oC>Fx`RbJJ$G*co+W|Qnr~vw^=yw1#!T?slYjA_(fqR ziD__cdd5`DNEOC<&GD}*gY+S~L%&vjY8K}|QsqHSeZQ-TiZind#<(!4EZX~^sI~#iyFwxx5BLm0=42LT8B=z>O^-%Jt$M` z6dl7fH)If1l3G+V_yeSF4yY29qBg*ts*MbZ|1fVbwP9P*0mK+r_GRFi?$A0lDU@4N9&V9)MmJDV*aT}XnoONjoVHBTb(s7QKK!jeXGHg$m57aM~T zgFoa)!a6XcdC{GhOgo6^Gal5}T82Ad&F=DKrvvp4E?vaghozfWrVoldpyil@n}$~Z zerU9_ePfuY=NL;SoScTLR{BGh9V8!u=<9e}0dI$TH`k$kBUK`>MV_arkOeQMOg$K( zLxyJ}rG_#nIi}R1F=3w<;Lyaj(PFeDFEOlUwG`K!+TR_#a>WM(!%K#8xd8mJr3Qo% zC-5$)6;NmkCJhD>)lv%9-ED52(AfH%Nz;!X9dk?#NtIGaoT8Ji*qF5|-K=&cPHY2Tghs; z#9*8YNPV2mn}+AFa{sLXd5!@7X>d<(VmrFlAM41uit7Pm+HpTFGPQ-sN?nBsH)rp& zk@2y@>ap^$R_6zoNHtlB>*a+cjS!)S8(5GT0ZlBn(7ME|J+nGvC)_x| zT?bKS#RV{)K{CI!a3ET|- z=NpmfmWb;YH@iV7Fo>1%d;}hPP0Z=9_BWxNWaz-n@VZ0D{CNlV+66l*{pcX8n~zDIcR9&Q zvo#AwvPX=^aEXyxyl~q(_N)N&!WZ8*(-^MdqR1rAw}ZDkB$Z?%TDPy7wIt10KAy1m z*1V-ZYfy!&_}>ds0imT!^xxmHf2XRA1fpX7l5xMRT>mihd%@O)N+S{uE6R)oD)eob zNs5v1)nG`s_!C0!B^@#hl!A@bNv(pXGh~wZa3uX4Enfk>*N%&U(|w?4L-t0Ep=fXg zWv`@nhWhw@p;J0L&e~bG1zB1-Htq+F`Nl@CWvEC;FTQ}`S-bBr8E5U!jD4NLDgyb3 zXZhMPr05&VeP)KUn?+9ww+RWs;Gff4F5yrXOJ>d&Fn4KLSjbl@{@kzfg#X<{FGAx5 zidnner}RXcHJ~TV_y)#Wn`+3i4yE28(1-a%+gQ823;INvfow<9SS={}4CJa-?a%sv zhU<%MIA58L7uB&a(TUX_jivQCr3vs_lVg%4EYndmYSyQA;82QGP)AT(e{Mr;GF+uK6F-^C+S+gEK{K zPF}Gv=T?T2;C2lExRHn0sR4A7*O^^?2C&%~BMmHf%E3&1*Pfh4=IdBop!bf5njm!c z^b#@xI(a+w7h!k(H8nS3t+nzH^{X*1mnMC3Z_!oo%Hqc64Zsz`K&I_M&>90^>capj ztC%@*8%QK7s1zHBXysCo+&36Pi@w>bF?qUAGmOoNmytD2C0fU1HZ3&JJrcO&Lak9L zVJlHdhKdTmYVrU^9?~R}Y}GTiQQg7}abZ&_nQC%&yss;4(z1>!EH6J^2jt<+U@t zXkd05*YPCdaS%6gEb2g?gARO+BJUpw#ckcC7dbGhV@4BZwf}ZX)cd zeg?n4nz+X)1Z96%g#6n_Ne`(F<%^U@3So9#eXot;3$0JzeYd@Uc_P13!rcJo2!~=( zcU&9vh||!-+y6FFp!48@&RAvPjKsU!aeSiZXUU>o)BoOSv-&Jl{TDiABgm={GQE-n z8YGOp*xWJ2sQ!f^B6!y4zLYX{sek>OnKz#uFpkX55y3D7tQv*+4)ZO-OygO8;iFei zQ0oPw^TYjmxgeUk?J?~6qwEsOwrz)w&~1Q611C#XR8ybpnHahbQSxgAl_Pg=DnjYQ zoAK(5H!OW_Ic*`M94TSo8Ze1-&IQz&7G?8Q+fugIYMB4q8wPf>TYNNiJhi^`dl4gU z@)!v2TWP$AcB!hsiW4IkKaw1Ts(DTE~al zNhSX`JVQj^a;-3A$Wz{&DLLy83ueXOQ{CUEM5=9c{nFbP*}2#&*C85xSpIH!w>S5Z zV=7J)w>$);JW$j?{?LCYU4A029a65xEnpAd#Zw%mjlgqKinPoUbQjGTl?9)`;(Y!p zUO%MrL+?3m2HWewHeC;?PTW%iF8CcbrsyyOa(%;~rQrcZFNg*t27)8#&)G!v$;&w* z>?33#*qaz&K4ZU zn?Vn4;!MJw^~oxGercA7r$UPmIKT;nWm{7})+uh=bw>4l+3^2Dy zCdE$7C@&<16{6Ek7}Iyo+YL&wA+FJ@5VHS5S-NZ9EA3ls8~Pw*d^k-V1ubESCt>(R z=>_MI7z#PMZn8?Vkp-X8f`AdH(tk)$KRDCOdadKW9t!*zk+FE5!|)q@3L3YTDd0$E zeS>?ih&aZw*Fi0MM>fS`FQ5uiiTrG^1f`?9&S$74{E0<{>(!J}$Fa=x#K;^p&nKCu z(@qg^RdV!b9WoBP>HDmAvvz6|&mch;s;mPZGmnB7c1c37HQ!WWF)EmwQN zneutWyxBd>FuUV!0X7ZIpHHm+#cNs4Y?*YI`TZDzsO-pfu#=M$=GBhld2{a|@H2Sp zgS7B6$`Dp!JFP%}qDBzGg`#p_y$|Pun=|$cpYd=Rh|!7TJ7nk2@L{$f-O|hMi+8cp z0z~tsyVzy8A??A_J9;kl5rDkBB7J}u`EV9uIH+@*1-byaZyKJHRQVxjSGQ;4DE79@ zl?VBsKyjPkJqr%M;wQHtcE7;#IXh_#K0riID65Z($X6y85z>b06v zQp{}cpsW%FO#QAW0(pwNOdBqt=v${}8k@z&&8nV_+U5e(I=fhjdz2Lfr+U*;>EBSH(XJ?Qhj)UJ*wQcpQ|eQ$CGRuEq%`ss9>Yod|j_M0RQ7uHWToMzo>!q3|mc*Z)PS(f)cHj<35Ac$XZs5%@8o6qCImLo*$bBZ7ux`-T1)0}8MQgp&6%9X*D|vXibH zblaH2h{)r46<%n660X&s%;`4pb8`p6nDHN~SC>wxoFaOLl=~{_(TO6_PEm)oTWeC-QaH{pf7O{rP&fMIxY%=? zIFI{}D5n-p5;|-YW^ZDkc1JQtah8I{7OVoCMN|T(_FN?EU@hA1#Kv*A>(#QrYom8v78_q)uiy|;DE zl)zd0ev!>9~BbMjBe3( z#cdy-cyf7c8I{M24XC9t6qYodJX9Ru_84n;9@8d+NRqX|+pz}Etke_wa=S+IhWN~eP zZDk&FUdj=jO&L0PB|fWJJQtu@`wI+iNX1gDtX&>r59@heSn^s~dHrly%=MWiV~6xv zQ;9*#9y-r%Og@!<5din}fWxJu&TRqe9)jAi)A&(s$eDvg-VlSI5CjeE(Y@k?bIb|v>kUS>GYLQ8kU zZg-Dv<|nYD*TsXKRvcLm=zRS<#Bbtn4JAqqonEPxX?lWJW)y>CVxY2+FTA8s_8<94 zSM|$8e>AB+D*%l$BWHMPhaZS2<3bT>f0XPyCl1JbU-o7TK9WLwNrpDdcj5cw^fVuxgKWg;qYSV*saCJqzY%WP`z9PP|Xrsesr)kDHK{ z37tLf;wU(c(AKy|WURV?JtKA{ow2~ZUt+Mwc`?;OEoqI7STSJ*?*0Xz!B~dHksz-I zM6yRhuKb0)dW|e63LZCbK zSt6WXYdfG=yUtY_obrZue(QDvq{6RluL=2+)1`1oM#VoU}IF&j{Ko1Uq5SE`;%r?2jA!5Q-x;O5|3)&i1ddC^QY48{G*?@z%TcG@Si;QVn#m_v6a8Z0!aK<+ke-QPYJsV}~i!9~sAl1V1k!aIh9qid(+N?*{WRmp}oYqp^f}_Cv3KF%vb;u?yLrl4M5~w{C z@sM0Dg*(4MuCkc$^Rl9wThs6h-patSsL2nc!UsEUyt>S4YfEZ1g2VkcZ*JI`J*Wuw z29Rwdax9200&Sikn@5bsnInqqJP|ifsErd#3beU?o_m^UbngV++~DNl$9kntG@?UL z3zRMq`izozdjqQP<{C%VCsgXoTIF(&;LI;t>WllF`i0x3`nxs# zdaepE7Y!HX`9gaCiw@+2*H}Zsu5YV3A*!O3rr3N!WDC+e;VUxSExmdj2ILEmHO*1Y@S&P? zT5Hn{AzJNV-Fqv(XAtj$6RQg~SKYWsM?g^EwU4k!MiRBI#?trcu#G~LLMDlTd{IJ9 zW9t4xA*;1nSNv+yt)Q84_7x{e(Nq}<<>Xw|qa*}x5%jABJWLAzcoGGJ)-|1CsX)SP z&?W=av=LloBDY@SCCHQsoK2EHoBE@_q!C|-_G1W*6X7N;*su!+iu;(sLl!RF{TKou z5f?&lLgzy(KJdyE&xmP)#~D^K*-%}EGxk17ysqF$f=x(XpU)G8IzDj?!I>iaXK9^< zbLjNJnQ%N_IPRsNj3bDi5Cn2Jkf|4ia}PwnU!^xDhIN!69$%g*z|#POUvMH|Ek7!6 zTwW<7ddb$3At!ITN)hYH;LV5Ai6va;Nj4UwmxyEB`cEVq_RFId(*ljJ@oa!jZMhpH zpxai#>~bUFkgu^>#ex)ertbCtW5nZP5+$pAz&Q$Ao*UxVNWoky;b+cMt5Dsi$p|zx zKq{G3pIukbiCK>z_}YEzV5BC&)}*NTAUI7SBcDAzuS|v|rSrl2k+SzonE8oe^O8y$ z)V8(czPvIxL)EA)8-z|1xaXJ$gJx=9LKBYJ6m&Wn+MuAinRob?1!xTuNJX^mi{A8* zPfKGE8pZ&E4fnD|Swfmv1ZFkPup*o_VYOO!*jffPANZOy2)r9tpFhbNdvM77lvTz! z%U{q$9=Hi_$fQ>$iR3ZiRGwd8lgT)SSp#gv!Eai4ioG`QW0+Cs?P$hEN|n+W6+svYU|>7oJl-65Uv3=Xok>)Hq?Z|wLv%?SYQ)e;R!fKLu?^= zLgWi-v-#*1lOd-lpt66)WGt*?EqB%sAUA#w=SjHjKdX{b;~11X3wc5XA&Mdx?Wf7c zU3_dVy`IE&Q_>ZEtscjsl$7q-vD$FC5xoe*1wTqEq+o8K)op}xmuZA^*D24RQhKyv zq9yqLRHr-&yQOcqA+1a|$wT#H4^lA37@?2{7v)Jo;Y-D zYu3ez(M}9)uON^YW#WUK@IX$wzhIEc{dMvn!=aZ8+VWsio_r>hybXhW4C)ZpjWC;v zKIHj;^wAaS}S$h$W#uNvag8(Z`EW(i1@__ldBl-tIb;$qm$ zt6j`F*A0|3Ar{~+XG%!{N1xw!P2qA00pt7_2yPsrpl!}G`v(N>?`3?_gqM;jyw8{B zshDA`3Wgiv+PKru)q%5P6>>U72NS=qLthw^L3S0CaS=HdzZ?&HrXU3&#x&V7m~9T= zY!iMkCj$gKq5>ysa=@KpD3QbY#^knRHmO;Z*jDD3rW|4~3C`zC|1z_j?2Ny@6720o zSADL)S1*pw%Lt=$L7fgi5kVG5^-zM^k(GMxr}pRBVO5vR67v>FL(fX5fFVo#ln^> zoRJkC1N#+3CV?@B5{@x+eoH}9&?}c6P11Ph@9bWOs=VQ3=FJJ)ziE%{avTA&-b63E zNka8yptyS3dnCm(VGMzsAln*sr(|pc3)wC9tLbuis&u5@hycgZIp#=N&WlYmR&vD2 z>1KwjGHDrl*H+n)_2rB5do#pYk3DXY^bhd~MEb3c^8C}bvL#!}7o#edm=iDucebvi zlpz`Z_JRM_PATm>Ed!}LFvF+u23-}*Rli_9yxRXOS)T`e4-?(GTNM+nVqvD0s@!bYLt7aslMqWzRZMPwCiH6Mi=DE=(wz_&$;aIC6&@bZdPsYs z{$~j0;rA;k{w{)=JW}PRO`4`UV7F&|I!RV=^?o+518_%`Ez_gZvjp?W{um<>&97*=HC`*r#=wIJR1Fdu>B+-TJ=?R^0|)YaIIH-v5RL zOEoQ+V^!DX8%dm6=x-LoAMzJzN3i5Q%y76ZPwQgC2xK z*qitd^+gKI=ES&=9gv~;ZY z@|CZNW z6cjuDYodn&FWAiz?9cv43whjy7rc}j(LG7Zq|p&vVgudWWTE3P!qWpYr1OEZAG7aX zz$^cQUGT7*QMr@fjp*gg-Z%CnMsn9|!%Q)FEA-2o_j@N`@Wl@FxaZ{+MeW=8C%~!5 z>(#&u2)9Blb#rpD`ntZophgC7?Fz2+x~`j$f!1r+-OH#Kw{K1^s=+2Vu$??KbtYNJ z3KcV{#QuUIW09-24FS=f8)aL-TOKU4gVr)(!ha++y^%5$j_nJpID&&hpGx88Y)i&4 z(UR;^oPxh>n*(fePkK+oBlY-T^Wut@qyo83gH56ExdRu>CiIt!P`#+H_L?#GG1}zk zwBYguZP3&+-jnMWY*_nlctI_^VfU4B`q4X# zly2UT(Hg%&#Z{id-*+^%4)7tZR|IXpv_dC;AXrVJsEFA3h)DQKN@nym47Ekwex-BE zCk2kl-@DaF^7bWT>(KX-gWf_8@So#j!Q`?sc^KXKo zDZgDPZnt@HbZ!vm*Lab_+MNx{x#6R>1t}cQggd;OgQ2OeLDkoxYwOW@)->uRp%!(_ z>}!sI@tbA$HA?}DM`U-0Zf>dGG{x;>oX4p{0LJMV5#tBK?|M!E6|y()V^BHoB*+^T zl^dtx9O~cR5T|(XHgDkK4ss#*SCX+L@6F_AmJ@^=l;#Iqh^Vb6*6X@k1-N^)kyGwB zI^>9p7bryyqK7_fQ_a6Pn6H0ARu)*ST&=KH{T$?eIKnSziK($YKRu6`G4ylnfAQ{p ze)7y5yAM)P|M8TXhnXU#6!uK~)kiHM*La|N(xkRpga9h&>41JExmRvZ4p3)F*dno4r&Y2@#ctd$`=^MArDY=m}TWyx6St?wZCG1J(t;*nU7MnF5pl_ z>fHE%glt_CPD8%H%c05nhzo=#aF6#2b=RpLG>01&c(dYA>;A*33$bIpN9x6l40hcyuIQM~BhE8$^)@B0k)Lt-o{e^GS)8GaSixQgeq{R25BRv36-n2*nekOM6ZMr%qt5KmdNi2oK++5cZT`(KOd@_?EFOU zECHtH@5$>8$*|xLN&+q~BOl=TWI@h$0}{Rsy~3QRb?!Y-9)=6%@4?&#ME5L81+#a3 zZ%uWcDJKs^<{Rr>FoO;G@eDK%pQy~=wO6%s>~nTF)V2E0@IM-bHR_tn_{|@8IFP)^ zctO4Y8UhT-Wk#=HJch+Q`0Q3J-GW*p6%n6lz=Icu$jeGfkX(RaazkwF*>N`KiZar38g7cHR#)9 z(w(-A*zWAxqgOFS^q0YLv;F-2)V(NG%y5AHVEAwPUM7pL*;4oA;*5!o&)M6DoZElT z>s$YyC+Gm-J64-PNK|sx5C=w_T2n<;)W1i4ZURj}+r%K6kL8)p3~ne-b`<3brp zo->q~2h*XJV;{Y8KyoU1zPas|3;1N%Sw{BDF;3eb$I4P()$G~zF!qafx6wx5rIuKy z(tZfirP%MjA_WxoMOW&hT{=f+Ng{_Cuq?`n4Vlp&Y+8ar6liZ(3!diOF<)P zy(j7^%?j+EV+C49!aJ~cmK#oDT%_k6e|Bvw0_s}&uc1=@3lwqKwes}kB6r*AiVjfc z@uag#(2fn_qkB4-&CtdpO}7fW5+j|lXb)+EOm^O%>Z3QOUQL4`r2gbu>+M7e*Y2Rd zT)U&(T)QK#xVM3P%Cf?%jDbOMM77W^?2TONRW!OJsHJh(6cy>qhLQ-%>8pxjqT#fO z97q$pVtg4dp1T^js@-)%pdFW5Q(tHt^UIQF(_3iv8v#S1TQbF~DP=PvdPp=lymhC2 zXCVUSj~JjY?Co&SmmqQ_lfNB5Oe$iuq$$+47fm;6<*@Yl*5Rh8r=pC&lVhRZO^4`A z#mGvhgQ|rLi^d<85`Fw+H?30L8}v#hMYd6P3Cnay3aU*9YW4>AYZdR#;+L_VawaX8 zC|0e%6GvGEwVrEgbCaTXWN{gx^(;+sm3XddF%yzcn3eP#gS3-2R$A;an;4n6FEiO` z%f$E>E)NwYXnE(HY&7F|GwFyuyBUe8n_BI=H@qVMp6>_K`5S&~ z@)0ELQw-_V$Gh+a0Rn>&^o+KUAgtF1)#|7Xs9PJ*8~YWaM~Mb{LmTh&5IbKW4s_b{ z?Ed(?(hvdnNC{<#M)&hRO(s&^VJz&9uYRm2%88gY(1lP(j>9y6M3556BXotUmHhl# zB_+DRFW^uAVEsziO#xDbH?q0Cn>wOn@d;q_iLmnt@daS?07CSM@{E4x9U$?Isrn!i z@JlT3NEm_ZokVb)y(0W;>lvoI5gnp%GlqX%2zy+Y)=YmVP%ZGRL20?PrmS^!vLz>S zHeo0%6c*LGt9v+tn5)+pRq>1(SH^M&S9wA30jpcN7|e4aFg#vj*5Kb>VU1P`)NBqV zz_d;iG3D?mm_^?34&tDcq=-k%8wlH*&%BYLIJO4E#^+1_Al|`DEfS4t6*T=IHTD_# z>px@b8%zv9E4#;?-Tx4kdQou3r&I*EzHq%92_zuDc{&}R6P)-!CXQeilQc_6>i#Iy z!*`?>Rx^*2pU@A3kiqc};XT};WELMpOydap|BYG2c32EA`gQ1sGjuPo7@xLb9kY%& zJjVY&LA{K&exIs;9Mk+ij_E%i=hPFej8=o;r zk!ZA$nU9U^o4l!qNu`w2K9-evijNP7)I|0N0R1DDWV#S^B6n;Tb2FIFUS{6p{J*~M zk^6~xSQ7_ik(Jw11o=y4x5d=#o!^t$75ifXxxu1fe4~Y;3^EGXv#OYirSx)`Vg7_3 zr#QGLSEv#N9e?qD|#mt?%NcaX-7ax_sKo zOKOh?Q2#shVwqUlo2PccYfDejFv4_ht{+zvR)G&Dq*Mf1KYl2LHpp?&oFY#XF;Tmo z)pJ9Pww4ggF0RF18;-8ITd&5$dJriHv0d2OXM=6)z84iugb|;1MAmcfX~|tc!lan0 z=?>aw$=r4I;tyopFU{>d3M%ha3qCBwdMKgjzDJtlV%y1ePu zJSrQ)2=_dx!VKxY6zS;)J)G@h99aFT?~_a`7+PT)Az72=LeRg$R;Jqob?6!mWaa7F1E>2 zZ-{3XwV+}f011;+IKd>RxcNn^px9N=&1O}B!#vKxA>Mu%0PGOKdV~lEZrb#GKm_RvF9o5&I(<^>1tezGJTm7nScH-iR{=c(ox}`{yd8QABR5kfBRzU zwkkO4n0`0)Q~gW|5P`p~Bnp>lx}@1;|I83|rv1kuxxGH)Ujy7CaNT zXz^?H*|M&@+kRF`8EIN9WLj-nwD>*8e;r(KdVL<@|2&^pj2(HK_d&Xu%`F@|n4j)` z@Xa~zeeAyWer)&U`9SsSeHP#$PTz|i$wskr*~b8RC89a91z@KCil_%(wtZ}n0ea*Z z1D^%259oT^;_dcqi+GLjum{4vA}7vScb4}d@X2GRnUL-P#>^3xm^ z5#d#jjosY?vF1VKL+8QlD+5@>rN9%0d%8Q~;eEu0&9Cke@Zk2H0X{(74v9tlM2FBs zz2ox?^wvA2Msw+-oMCqnhH~Pu&4qJhXN8?MGZ>|Ks;=6MuA0%6W-i(CG1!u(0RsWXw+m@AjNBM6?Zg})2rsl8l=Im*(v?fNRKUIfFe2drSK?()U; zRnPKVd*+!3_OXi%!?MK2?eAr*q!`CeC}X|YVlW^zvl3)e{Li?3x2<86WY$of6_RPA zKh{m_sCX87!o$ZEp=gOcW!O&s2sJ?zi5@IXu`+L89v-$tUvo+>&Z-EoSVr;Gq2x?4 zoW^NxM&Ye>yzN)DNyEe%w`QkvQI8DjR3ZWz|G`+7Ur_Hd3}>!n`44s|rm(?~Yp<%QzoT1Ymv)0$pt`>h z;N%iV3i=(sLFbJis`eOP=mGhhtky2NHbMUiT;NiPiN zqqMGEg%R#osg5x_Q|T%eUvxb7$%LWdasr97wH&!=HH9YArw$pO*URugTMRvRHkg@w z=^9L9JKhh75SQFdPh>=%Oqjd}mJ`~X5VY)zJBOU9SMlXa%M$UC989}MeNfS9xaNkP zN}UhyL2@IaGDNpasmJyv>HcN^XFk48iN;%MTT~XQH>}Qi$zFmZtW63)Mi&W44Y5Ds zM(T~ejr5Qie7y&O%TXlWlJ$p|K8EYURj5ZD9-EC`?d4_7c9YZF{ z{Zk)>{}`WlcS?!KZ|c#6eI6lo+c?VJ^O0Urs-m5vKFM`KrQg>z2FsaB66jMKQH7K{ zI!E$}vqr)bY)zD#;=oJ(f(}Ed&aMP%NOxTCM|r^cDK%V2$`ilm5V9u)!1a^=r8mrpQ`g8oT;geelrcsvvWVAGwz@^5M2RO2%R}JZ0rR9xYN-cQ!w+{m~?^ zCa~`u7ANVS#U3o*CblqL&T85jv{kHi7?rbo&IxZBML_MV3biqnIcuxTD$b z-X9e?hWXj|W!VMnyQa1zWHPbz?z!kI&8KWLnPsVr%;O}o5waI}{-uaim^CwrUACd> ztliG+I$CLx(PE92S23 zlcj3PpoZNZt7a<1ap8wBg;-gU-T;gE1boNN{#c2UPmWqTn{1_gCV5ivSiG2G7WK3i zzeQkx-xIskl$yIGbU<3KU;-&r?heUX0Jn$aQvmma^jToKtXIkj3|3wP3bKyhkhi;4 z5pUxNWDlIpJ}U@H(9`y|;2tRAD5wL2F60jr?!Y0xE3S_tQ0{31`x(pJhT%>?Ag9_#|f5GFh&?cU*L**NYb+wqOQ;u^FDG%6uZj7BK!3FgZXk@zdqxu zC8oAXVl9&Jfno?Z4s}asAJ@s_R)l7*D?WF-kqu>*sJ`8d(w};$mey#^2*aL|)^t4( zXFyRt!T*Oo!EswPC-(U5ZaPGLLiC2csa$Kl0PWI`vh~IxelG>tsLGnicB+?IAeo{Ji5>>j7b0V=kfyqQr;VM$T0Jt&LrQ0SP8Ytpnj|BXtk zJweqb{fWMaRSpgN!)Lkm@AGSIp>N~N+D+M7ID^C#Z(|KpaJgUu%k@hiFXXASAh&6rfElI5{W?fanDC%=$ z)p=%&2_##zwv)k>$5(G>;SF16gK?6!j-Uf5FRp8I)ciX%r2DdIVp+E%t*wzE=CNa5 zlVQ>NI5DVSpx+D>uofu3*KfqGP_8HFXATOM{dR(b>iG!pP%<4ft-F}{u(MFNZUURZ z=s~k?5|@3`1G8098$<9AI`gF^i+md=BxDZ3tZn(NcQzTMGr_3FWDoAor>gVs&vWSZ z1bkma_cL+*pjhQ)@{}nO*!_=JRzw(n5Wo>(;fBvc2hEO2rs#VX5w5o~*#8q%4BZe;lYoXEG)7CT{OYd8-4`T!4`#$`gIOZJ9Z+iMavTn2gGV_NV``ORW=P&R8@^bTr zKy9;*9AT_3&+UO7m>W|CGY2ys3Two87Y{7P_P>mIj{aCv{q|E7c1*=bi7}J$7?*z) z;J@xf%J28r)p@W&wceCQgsAZVUk(IeLT`WXR4kBRWvDRu%{>$~$?yDuR_KCvh|t57 zw5b~SbDlzO?(_swYuwh)1kE5i7LG=(7QdwJ&Ril2vwb7$XcMOrx?{+>>UaMs9nk@9GAt+3dQstr zAg%~=9>&~uzXO8DWP24w>4Pd-7ewO($F$)ZFG>Dx!@{h;L#2BAS1>M-{k+3 zoGgHGzS9vp!0k`j*)4FFaefZ(T)CSQeAZ##)Vb&^Xi;e-Rt30iPt1S^gV{=X!vWKQ z{R-aIc(O!_ua}Oekd2>o(iJ8XNJ?i#dw+D0K$Eaelu4xVY++`o^8=guh*~}yZji9X zidiOHT-r+5K*w&Sef? z#qhiW)Io)sh5~EW(ooqjY_``svO@NSU6I&c?}OQqrQ+&HC^22~zWr}jRN;hrXzE|Q z?f>UjLDv6{H`OgUWJMI+GG&ZSxRU&UAdP{_9)e(Wkr03Z$U)oW&7bD!;V96*e!d=+xp#R zUOYTNpt&?XQs9`bHkg}ewp4hRX`V^hq=$#F7n9eN!t5&%Jd|2Ffz$^FdQRGh`IVFGUBlsMc8y} z1l;F4Io=(ypjJqt8z?{8E$~1V_hyk*uRTZF=2d8Kr&cWR9gBx0kWHB5->k{Zr_I76 zy_!C6by&L_!#tChL#7xt;v1GW&noRAlNEdLxb>v>g5=&}tk9_L3(i`6sk()UKFF;> zrd-CD>j(y~+wjED-$`gn^)xYu9g*m0dPXC|)X`x3Es?Cy_5#(>!GE$1YxEVynEoW< zud8}4JGol4#Dx@iI8#|^hc_YvZc?xEZ1kr0^?r<=W5j;G9em3=^wy0T9<1UPf98lH zJ$q&uHgJ{FykS3%z6%_PY+Zccy`rWQEFvt@z?+%x0)sF1ygRp4KXh0Q3}BW8sQ&7B z{kdKd{JNSR!^4{ocMaidZjcdkB@`fMC6a{EDv}@$W9+P1s30*OS;W+EyoYHxgvZ$X zL&?+(ADlSDGJ0WG!JQ$y&z6(ijKoNS!bBTJDdVolei1{Nd+1)jC_iir(0TWY%uwOS zVwS*c!g8-H+8$AD<8GG@h%$^=5H6TAXpPn*k53Uyoi`?J$uU018PZRC>y~ixzKqu# zP2dmOmLGrX-jA8-Ebr5vlL>k}9}J+=4kuK;4Uic5`+66?m4CCfz2TJI1hDX-F4})a zqu*FMX$!V4Tv&%d{`!=L&mA^h9C6~kK;}uSKb~KzbY|WLQhQVkqPM>Zb?P46hHCxM-%1uH7;7bcfcJY&pSCA6@Y%49=+lyh82sKk6v9 zr6t(BJgDs-9q{eRkuond!w1;-%~Gcu=$bV_KM!t64~sj0aon%V0|BP;rmkk)6%_i!u99u z^EX~7e9&|3pYE~%GcK;t;9PGtF3-GN*J=RUr{}+Te&9d7gRdS2*cYQMK5gJ`4?R#n zzY4zp4dR}-FD7^KIPgxQ!vFg$zX5KO}(wP+eY8=0pg8Nd$jU^}f=oy?O1(6HBZY z7~EV{fHNLPzrS?6zj*lG(g8f)H;;c{uYY3iJMqP|G)r9?s9{UFv*64BUx}NBke#w(!3A$2&Zs)Qk7kiBu7z!1)mD;w6$lk?~Bb zCC0Q177Y;|BuqSp4H6@PQ8Ye1=xo3uPZ8DY_0DoMzi*0v`N2Kzh6aeJgLW#I6|#m5ecEpxA%3dsg)T z`DN>3Y_vQ&76}N=yOh|jA^QYA+lL)h@hGav!eJi$)H0T-V&z~ou5RF1Z0lKB6K&0y z=W{Swd8w_ZM;fS$ER$8Vze?q@PqQ<{HVw+!kti0Ppg}D#e6j*Z`Br zt*H4DAW=B;fB?f?j@K-Vrd=;4>F;i?`tiJ zkFXJ1qq)#o*x6XYsf1h{vt9A~jk-Qy;Ai9ojzw`XyTL;+-a!t&P#;t|TYC;Sre@$* zqZv4dmAwvC-4J%U`g_Y~3%9EFt;Kmo_E8%H3E6^BfDoK9M!_Z7kZoVIZyN=+$a~cN zA_*{AxmkHiCH|)l%4>)_sfed#2ECu*G{gp)Ym_qYf&l|&3=qzS66qDZ5XKM$LHu*l zJc%Q*`M~oA!boUZ#XgTaR*-_t{-H(r5R-t@D3E^?_0FZZhfysL*1|cRzw_=i_W1QN5 z)e50_N?uV{iPv<|$cmM|6g?qJb}%xsO^k33(juAcGV;JBN^2g$JvQV(>dR=6SOR18 z%p!s%`N>6ABUr?giE`q(r7#QEN~O+bMazL3IY=O8&6XW7eMIF%xmez>bDbQT968Jq z&hAV%SlM6@^_|?}w#Tb_++eYm#fVDt`RjhK&xtOBIWgyiWR;mPVBSzU9c5=vU(uCG zE8@cyFZOrfIO5zlS0qGw14D^wcQoMOV=?_^?&_;#kY24k@`1H_SaGA?O$7~8(cv@^ zO~iXr@q7qC5#*H1VJ?1M57vN^Nf8eJlwIZZ*E98F&j?giLJ1qhU{ahgGH8c6=R>=pj9DS*sz*0VZIA7#s%Dm z8t9j~aY4|8YBAVS(EG3oSw+&!jEGrsAMv@UvjCGhosHalI+Gav?9jj)l7Jg^>bYW` z9>_Sy?Ref4J+4BPZ+M7UwGS~P|KEy#B9@$XIjnV7MI0mCpoZPxKtzV+Rs703@oz81 z?V?ag1TnVccny;ceLRTsYUh2wOx$o>V=Ad-2(HL67VQlG{_APXzmT!vm4Z!R%u5!H zm?^(N^pju(jZKCJimx+Z?QKmRE+-BLv&Zl)UqZPh1*b3D`WqgUPWE=?@4-bofxp%A zAg$w_Ey)guds&VeO==8&lL9DY`+6#0m6yEAWoM>_3~%9dzdXGaq&>4>pS5`A(;{nx zirg84a4I^I1GRD4$unX{v9Hrc+m4#~pJXIghaQvNV9}8Aer``Ma8`aY2{AQ*jwqyq z&bV4?NN_5g+0zf=mCOG~G$-qHX?J>gR-i_f3UN~)<#ZVjtrmZf3`s$|Ds$^{skR-c z+$*to95GyfKj$YEU4(1 zqRPQ#Mzy|RsLt-J?z=$NbXQcEVXk_TPONxZ!+!Pu*=X=P-y~_lvC3M6yBQ)6GqAPsVrc3>Uq(dV#1wTD3{zxX4VS*z}9l zoOW;`9Vq zWh*dX!GTtS6xgaJs@bEz*?})sJlnn!TjVbnvWj9Is zM4~B77^d+FhwPq2J3$tley<@eV`SVm%MzyeAXAYGXTUFIkuGG;)&^wP*VT-K@1q^# zUg_fy351onh;dn2nrCl};K6cN7edkg zLiA@TF3{RIf%d*PYk8SAe8DyEh;+IK;NmuC1Z%mflv;0jRemkEKaS_nFGX0wIAOOa zieUO_4Ie(dom6$lKvbLLvaWz&!U#G|!69{Mt-CDhEz$mdz|^JorT6i69b1 zsWQr>P}~qNBOOs77jJXA@EMSoPSb!elK z8Uaq}nc7tz5o(Yy#czyPY0x0Zd5bf~sF@czm9rjt3+d7>+9@`n!XFK%Hj1R06zQN! zje=5DiE;vKk~3O>M-B|ZwT#`_)RIY|N{u{$34>9PM&7bIuNOirt0+t@^#`+7++a9V z$?4B_3o&Ip87-};bl?aPqLvU5_q48(mzGdzNHTy&ho!1AevabMMU5R^?+&@NWv$6M zTaB_%5^d5biCC%BAf8najn5oUA9C>M>nnk(7 zG#ULMPC2PBh~kh&#U55iIZz!DQ>=?pdkF=x#q38dFO(%{bo)IBeh-sk?!5d7XEZ~m z3ec1>eQo$qZP2t)6cuo(B$6XjS5LAg?Oq||Gm zb93W|wB{N5U0>Pjl-+Zp#3hHihmb~jm{r-XqazsQRYKG&N~4h)1k6t2p95+)@dkQ}oPf&sF&XQ+Eb;bHJv(;gOu^C`X< zRB@~VCFE^ss#4Go)WjY~kj!Y9ssxguApS!?{-&;mfif zcr<|u72T+f@E(d+uG8PBZrEPG9BZ&f3*b294|stHqrM`>-=Kpt4mc0PM~_8NhHXe# zb}i&eaVR=BsG|0i%R=PG4_qf(t}d*DtGF?DiJJCQ%cH_u-N98HjvW$qdTF4>M+{YR z2w8m~H|FOdiRT&jsc!JOxjWh9$RgmYju`(wa|Hef;V6&Dt2jJ6lvMo`;wfna$0YhG z0i;m*)rc1El^SNBMJH`SEkSgrt*&NsfkJ(hIaALM6FZc zW8~q>ruq8RSp8fW~-l%QWsj z{E9K{pv5O&$y8<5=2+3#*90H&(pPb)d#~rDlp_qXr#`%=`a=03gNh&FpWwo;G9rGM zR|!CWr8wNH1jGY0Q1c?>Rqrva5&$|r1yMzG3c4^GXNSY7)Bn~hqdYR&r?rA}GjXht zZqub*N+Eiqs#jhr>DY~=ly>sex_}YW(p_e#q_mK}4D+?@!Jwp7c7i3wPw1S{@|AUa z{0)=5TFdFMU3>>#NjboZ0iEzy#57p+RpR6u#ivHDjAT}=1c-U#@-V4rK=HR{~RvtHKTQjvdCy$ z0pjRH+>p6P;6EnVM`5w(KdN|`j&Kx?V|NJQiC&HNaNWc4<{(J)7;B6e+}Ukx@Ttzv zDk1)Eqm7L`L*FZzf!6@b?lsjq@6W!4l#_tEZT|=|PPo zGNPHC_k2r@*j+B6Dl;-$?<#*}%btHLBNAaul3dc22vxJjmT>Ok3}#05cqsl6n*9=H zl69jV#*_`ceFEE#sAWtN+Zwm^z;!b}kYEk-&Sm_(z%w6JnA7tbz`F-&toa$m{{=_5 zhydq0?%Pu_>U0_zyx_<_NDwkwd8!)w+#Fca$Jh2fx0?TN)2`U7b+76>gCX3MScw@4 zGg5;&(3C=PU%qad%L+}-DG|7h9^wNy_Fyj?KI@>+!S}&9eg#~pbrAIs^%kQUfcPZP zb{9ACl%XOUiE&=%^`s64H?Pu!tsj|O?h$;8;cZ^dIcd2|lEo%480jcou-wR%np%BD ztspQ5WwO~+^;tFcWQH2sl9&H5nnke=d`UYSd2}b`@9C(UkHNP|vQu2LDr8oZSj`%X z#ZMu)$S9g$CE}qP2dWAyy`Z0_mg*UFphfkmR0SsAW&)@Y`CTGZXP=Y(OW zu=(*6<1#E+X2xk|{nB4hP|9vV-^aRkK1f#{=$d9Ln5e3-Gpy8C>?|y5s%zPT9}A|q z)tJ>~TiYG6s82x9=^&er4pO?{GUB+^$XhSQ)uk|G6FNS$v6+FttEHVpH;rxcOzW97 zoAoc-GF^^km@06FWY$`N)UOHVH;|KHYE+t9W6!vO3@FBn;Mn=CeXPGqcT5xfuXGwX?S8GP2CGJVhxG?j&EN&)AlmKfXa-#Yp5p=->1HvP%g;X}XQ+ zB3_bhCFyb&jV89V7qc(0HF8j6M1X=B7?V?!tYbMG%Jr#dIBLnT#jsH&OwhvNhA1<*HB-gFc9+i&Z&oiYDXnZ( zTIFbc5h?UvKUtUiRoN^av^l@56W8&+PX?28l7c;nomNv>RcSx^d&g%{W4F)}M$~o2 zW>pGmGPqzrZdT!+V06}6b_!oRJJpy_ zdgv0zb=ivfeSG-iUohCp?rb5VT@$&-ag~29>37N29n5OnVCpXn{@~RZCGZ6eC@jcF zqacG-3BbW7Z*1P9V70mz5MIcbHk(bZ=DmibLT!6@gK{74ws}kPcefm)K(Bl{=>;ag zXl~V2L4#y{caF6S!HV*~R8>@` zYwHqiUM`+pf8g{7+UuA#bagunON-2$U-b;u(>yM<(v~@?y&y@RX?N|-%tQrIV&$-_ zi5sYA(;vovY01F;kqAN&L;YgUB!?jc`CPJ(qSkgTX%8!O)F}EKsKK~<9>^@O?5SB$ z@CZM|ibEDR-d@}p*$3mM@=a*X?c2t-fd&vIH`9YdK#gHjB*!36HS`^93Mv3uJc#>p zR3Tzy;Vg_AOh@(wU&t6<^ru5nCdhhg{JP7YXguc3f*p92*d&2S;0$`PT;%tF>JMu| z?AN5*4HW2kpdaq)V1mK{5M#2^Aa z4|`D$9X1rp@XIa;xIN<^FhZiimYD-gP&ZoNAaTjjEoW#0V{$m-!S*X&L>!XglfRd$ z9bf4+3q$E-Xa}jV(l=k&q}g6QZf@CN}i zm?q1|;*0GYTN(ENsH-WtABW~2O;_vTuM-in-G>rgh(*01IL^uE# zMvU;UQ=;vI=sN}@N<62t(tOPr8%$n5i%CL^m7p>ZhIfSQpfILV^wF2Wb`B<%Xb+_L zK7piMA)3CBjXP?|MVmLfTFFJ7)^yrLo1B434Nj!%K%&+^ssta*hEP?*=9FLq(WC>a z)tiLmxg!@m?l^KZ##6L%G|2(h>rl9NI>ixS4%ia1Msr*Jj>tIXWer-;+m=W=xK(Qd z&jCa&mV`;1z3^lSVrcjQkzEAXc`}=z7vTm>jn0$A;RaUFe3L_cKA+SSm!X4SprJhq z&3nrlu|4!fH$6c&`>Gi5b%X=<>&r`k&apYO@ays&TOHfBZQHha$7aXr*tTtSZ2OLF+v#w6 z=08hi-^RytmXQli{4fn z-3nLS9Rz5Yc!BNwOf9=iQ&IgDEuoS|{z%bWKT1Nwjs4*SQQx(U} zfqucL?iwmV6J{k6z!$CQv?@&WYW;*vWCA&zbxmBBg3KLXz9939o`XJ@Fq%K zKcqDQ33e#6Cn&C^Xs#v6tZCL7#*k!^D+pnVO?-oPB8pKC54I#S7DU4N0FRiuVO)4h zCsElSBpmN}w4eM@{bz0vSl&2DqN?}_QLS-pv_3>R1e`a+i-i%{>C5( z=fgm7Qj~m8b*!$fPccQpRA6da!S_I0wx0i}-WIjf7qwd=$R;LiK1EuVsjXYMnbBi= z!-yU%(g!eW8OFi-r22LjX!(4mK=n=L_q>8Eay=#;+!m(v8;{LT6;CJbZWl2oAp4~v zSAnQBH=org11oaa_$$=>hCbu6Jbc-><$8mf3B#RLQ|2lK>0zMGNDST zuC~Gul0LZl+oybN&bXguI`fziOSLEOmhgdJ(UWu*RSbtdY%HRI1r(TLumm-*8gbb+ zSeNk=&P$z!pW(UJlG^fKa4owNyjMcp^TFKPUiz~3FILmXH2z2)I`%BD8I@*YG{V$T zVvJW}bVhVbaw*4blEstnec)R7=Gm|g{OADpqJAIg8y{gVgeCno=9CySxyni3mnPkp z)?!Y4pTR<~o?^ z_&s3|IY#)!Uphrrg&mTfK%Cz^p)sGT%39b%hC3&&)f9E`7Y#Y$vhc&qQ&1 zr2cRZ{bvBScT82nqODMsK=Vy+(K|}U4^65|9aDGB@io6R{j%AaswM!}j6JOd&k+CF z6&=I+PbYV3odFJ$JO zDD!|z_nhZ&w;ZaKhfAI0J8q!*GWuDqvq-`<&1?;$Se+L^ID7d|!>ho>?$@1pOgJos zciX$SdZ-pBMmY1{Js^b~&oNUhpUr&Gkb1B9GrQT@?}&w`w@92fVM6?Rqkz!a1pHV zOr$q~*e@?vbv#q-(6Gv(W`ooj{wbvcv~M-6bF^on&Aw=cg_8XSXlY0&g)bXrcVM>H z_LX2=AsIz`49Z8i5empAZ4!W7Qn>~dGKmLXWWX(S;6b?S=(xq1z58AIFF>l6aE}#|VP2)% z^NyslbrM(6v}LQab|d>1Nb!x>`2_Jvp2*x)X9~FY{V{c|nV}2pak?xcW*59A3|l%f zfJn8>$lp&b^bq3hwVR8Z$p)|X@U^_~D`_+k?bOVFKrRCH%o)uNY1qODp5jeKz;>1r zqja1}w~lC1@#TqVR+~=q{G-keZj64u^IPBs{5tXleSIa1xTSVhiNR^1m~B1x7Sa@y zpd36xU&FLGOV?dE)MtXhSrff9UW_fD*LINpL5n_?=H1{=p=`JP)}49fwemY?b7G57;7?p%S=Brj|aT11?^?v%849{ z@kc1;0FeQ>s3$YsK6zq7lsS3@GuYvh=waj`I0IhS6W zySp~teUo9lED1q(2>A6d>4uJjx+Tr`2Rr2{yjad-qyi-m!d?Iu@_{>k_@tl7pGNNJ z!I+0J^sjGV4HK_?6M~ocOa`7K#Cv=8CllReu2?K|dgR0_T$l4WHxld4mUU03D&xA@ z>Wm0Ne3#TgB9zD2k2kdU5__k*!jbrTK@#06*7Nkv^Qd@vt}4GyBMOP^PV4=+!F;_H zFh?YaQevRH<5V2m#hRgz+qu72sN&7i)S@_@?D+5n7(Ikp+DpNLJ}JOQ%RX{sS(pBO zB=AM{wbKa037y#BR{*9>2L+1MO^y!TG#5K0l$*Ed(>;p+A5_1tstP3Uvw~}4Shz?R z)giFCQRYO0$R=+hNTfoadhiyscF-<(6;E#1%AXSVG+3im1tE;#_+f_~i6Mz;2-pj8P2Z{j`?z znIQAw?BVHkhIeW0w68lJ73;xXZg; z19Cfz65rGuV`Okm+zE2iihKJE`24{RnVUfFSD6vwM-Om_J|xJ_Dbo8KxD~*}sv2Ug zf9d^be!P`@PE;7V8ZzhOJpG3^8VX(n!qP%d`mfOW{(Ts{HG}t`2U9ICiAKsD=r!Em zsiPw7f2d>TzX)|oXZRQ~z_Y?};Vr)BjZfPNZ}3I3nKmdt5o}J_HcH&?Gq9*TEiU`< zj&_c)ZkS5K^yVy!)Crm(DF5~)=a*o&ImHJqFE-!w`hxfM*{bz8a1&p@Ll{=7 zlC~#UE_+Ah{qgDCU*2FSs!+Q5L9qBh7#O}2CGaKSiu+<}#v1{=Vg2-h!LVGcC9W|v zo-u~-iRor&>5<{Er*fh*bx;~0I!QsibfHR$Y0DY8G)QXlN%~xqMi?UGQ9yXaeAcXj ze2W7Ai{YaSrCoA?w?0G&KkO|~`!Ev_r1Pzc`zTEDD13?k%nA3#%hKw`V|v2GCW3({sNs~jaj0_in! z8cZWouWG1~IVfrTSG%)#lBqvy@F&;xRBEj?PXVM z61s8SA?ZZIdhZb?1F;fRos2J2FQgeJ!J-OwF$Rs($C{S3lh#fy;iNyz+|j0U61!Eo z=Ewj~oC14tl-r`h_9ThArPvr0u6d>+9?5D!Ul3UQKE3*7%^{DtPrHKxuU7M?1X!9@ zeECfg{eq^qj2(FR!2yH|sy~PGBdz19W#X^T;u$T3Pz*4mv|G88(?O~B7-AW)Dsm%S z#u}%EdraFh3lO<=?6@GWSrVOdBLiSPvaZ7~t|dMCm|qqDEh5>lXC_}xn{je^2-@4k z+{q*S|8(TajK$EfaPPYZ0IPcP3e!X_xizlX=t$eARP z2gw?m^W+d`@>5(T2M zwcnHrVX{t^db8V|yP2cZY|LvuOBk^sYYVe)HDBDpK?x0$g08AU-0g4QM7DLb*B^*| zE%gY53nm9TAE#h*Umik&hE;iOXO^XTCnm807!$=9v%18X+va)saf*Y9Hp%4-lfyc@ z(hV(iE9^t;Qz>BWrx+#a!_Xg-pZ}LUvbvv6EgXp(P<=g0q$0#60WQ=SRTgfnR=*$~=r71GD z5LlbY=V{Pspv%>YWk}S8P2)~Hy@iMYSKQ}{p0NQz0l&jl=Wn{#w%D|brQ;|m9j<-; z0fg6`J03SvUk6Qs-w=JluQjn;xiR|&*%*T^ICA(l^(>{tWuk*k%y{AgVW#y%Otr=& zuwY3h^=^o&IHhh}R1pQ`F1FHx(Xo_wU9s|x!~Tk+OuqQ$+4HwSv7Jn`@W*VvvV6Vt zVNE8lu~Q)ljEvYy9JAW^OX{DvI(8Qjd+m?DP1(+c)H<7c%yl^B>Xm$4UMXhiCYhcQ|< zohZwxPxT33-TtBrWQO^%a?l-la5qK+>ga7@EMS7 zO>eW%MnX+BRDXezTkEwzlf(|pSNE8yXf9Nend4mIPizmRTmr2yG+`PnY0M&Bfq27B zr48tw zp5Gp6_skbym(xV|VPgGX#=OCpa}$$(!fJjK)gK3d7@3QPn`0udmU}yUomDA-XuPb! z175qS=a7D<+(ub}L;XrqWyz=_n<=Ley}3Guwz@J8A;)&2k$UU`0obz-Fg|0p4z^@pIJ@kwi?~NZ z)3W;6fUAD{dPq}x-dh;9OtsBuS<@LWcOvn+QjjuhdRv)L&QBq%DK8Id`uPwuI62IP zJTPG3`E61hd(oKoM2TL^dVbV_{l0wsnZQk^-MT%{YcsozZ=>bDnk!NmV=w{8u z?)V)+UT7g`s<)h28B#3wD48tEcUCi}8^mdDzFwN&oG z>pF*gL|?8`xF)u)Al&v73xyH$4bA^Gtl${U%@3SdVw#mJdK4K&yGqzKwDWoV(6Ij? z8K?LWoIkucxujU;47w%ev3tzwg@~@eu84ul*yAcBa2*D8yU#!VSv31f?3JR`mdhKp zBg{09xTb&3QExD*8z<~LG*^-KmJ$bBE1P&2TQ1vp2>Tb&6LgPqK_67HP&926IxKuB z&(cPGKJ%Iuv9N*^`p=LgV_{HvzD!mW=^)1#@WT;GuJpx9vOkR2YdDE`a~lr3wMo+S z1(b^qm5I>?!^<>}4_D}iq6HxxpnpY&;5}otS+KvbCW#fR=?q-5dj%;SaX4hs2k=Vo zF!c077?lYrGhL))7Tt6R6SP>s;mA$Zo98^AuM*m=mqBlZ&EvMh2Hiy1UFE9!>z_Drd@Qv+KKu8`wzM~Ty`pA z3G>fDBmyJ}CocO!tqs!TBU{ZbPjaCZLJYMMgU***=?2<@_c&Arf#L;uEM&c~kOP3g z7qJA{;O#*q8IrB^tz_#cxw`+$CA)h_uQwWF*%-9W1h)#nDLA=)$MN8rpw%@vHH#YK z@q3Vb`-Gq5uRxUt%Ity1(8bHo&e&c1pOjJ+kweg!5?~~ZPs039t&e8m03V` z?k>vk3ujNm>usq63VSzG99C+yk~1h3m(Dmu0idJiV75?~1pwx8+95rwbT8e0i3+0U zO2Yw$EKB!EeHLYG*2jP87ALi{L4#GVsr~wIt#N1IQD>>?YLBu?Dqj+GD^6-(3T9rp zr>(}4GlKOd+XS9hv+aVbjs|ZUR28lU`L2b>cx{Kp+`Tn2V;Xs<^+ro@;jgS4vH+C< zMtK+443F#*10r6B&SjIT9-F`BE=$T&-H~T>Sv~YC2`nhp8}c9rW=e1tMVmHUECiod z+Ytm_+1}im3-Fo=U)~tiMq9eNEPhS)^Qpx>0VxF+a#irh9(9SJq{PfmmFRSnCbU&1 z{5gdDL&nR@S@PIl3RkYeBX`MLdiPfy_^VlsSPw;pFlI7=+7;sw;nXq^2A-IKtN{N) zT$@*|L4NmT*S{8)dXb}ULLG-KACc^qvfuvzB?TE-eD3J6y`ETt$Me%cYkbSwj#&Tf zB`1_LHs3;1Gzf&e^Yl^B3m||yHdb5@c=rRfI*vCNlMQUgE+iB(|%Ex9T`P z%*~^l9X9>sW&ICH#AiE?g(~d?=`q)tYVh3Nl0Dm=tcP4pTlQ6m7yXrcbtXFC_Z`#c zn4}i;i=4-FLni57ziImCN`Dc6FryCd;iqdyIy-)u859QNc)cMP3g_|7usha%0Q2Pc zKmK+=qhOJ=Q+(PEi@nK`=oaz_3vzClJKQCCr&#TXr@y_Ei9>)mSL9;_++rCyr(qqWbW~NM5&^Q2jFLN!0vu%J&F(X?W;->9`r4zI4k?S|M@VG z(2!&2g%oZQUWFzPc7t3?-*^A!xc~eZh&RZ$?U7OziqexVPxow8p>L2GBCX^7Z# z%#sKS6L`S;+ugAiV2e zc(FvIuWrN~p1LfAmPVqBgr_o@M{pZcBOFN$F~uKWB9)AvSlHAz`-=>ZmSaB@V}D^H z1)lKsJ%sm-BKyol!NpWW+omrXS&a!;I;YSy)D;RvFo~sQIB^>eld4yPA~ggtX4E0I zD>(Uu?_Bo&pBW`=m!>w#5>}f5^5X|G*pDC5|LqxN?rP;=CJwN6b^YhBCg)&kY^P!D zY;9~}XD()L@xKp1D)NdOLdbqucF;};%+$>#^n~FAL~8o|B~VI?)aX)yB!MJOdc30h zi`t|<)cclBLMh&-NV}567MyzJY86sr>p3T!4i>pb?cML6P<@uw;N^SrU_Ya-y>&-B zdA8veS@2aU>Q|?u0w)eo$e$BUN+ui5I~H4iHEW)b)pI9gCt*JYP!&xY8(^j2$cD~m z$lMDcj8RcCEWzyP@w6mmF8ZTzX}zEPqHNQ@X(&T{0G+nCIlVQ(Q*|mlh|bX9RCHZl z5I^50l(G)DrbgzU@GD^rblkzB;mKf-H5tDC&fq5EoqF#)SajYW+|mErS@e5f3e#Jz zdT+Vk$zPf5k^YH+(Cw8CDA>~XNSxx%$OyofB?hGnPZ!}IV#5K=v1A(Q*(6J`W>wnx ztV3S)_BIVXU-w3;>XKsLu7%Z^p(;v!m{7ijxea0^E>ewEkDY#IjCdYFpt*|UEH`)u zVjEI`WC%?N;3P89&N5AYoEJyuVeBU0lJYqR(|C^mQ8wl=Au;*e<A%*$jVgE@PXEUbzWX0Pxc&zpu*~h_Da`#)E^ZP{IgT zhLO+U{A8w#_(Av@?xv2D9Ol_e_1&54;$Vt+#x7UwYJuFJ%6xS(^2G^ zED&YD_nt4{#J`uTiPM~1fjuppt9n{`o7YD1Tg)57-c>hTj?Etw;(;%O3YG^%V1`*o zYT{NRka~X_)M(M(fmo5A_9r|ZNrAFWROyw(aI`QBC^b77V3pvDGvdj&P4atUDO#eQ zg52{ZT+6Yvn4TV+sb-1XD3lDc$z~}C(MCjIyK*F@=ZZ6G0FUfC_)=#oPnp6A)O62j z>NqsBB#~o_27L+3b!tjlfxK|1IicW=DEb5r6}G1{CLLNP6&I%_)c6XwR+KiB^H=7s zUM%>asjZv9bGn+`Ybd|IX2`m?+*aVCh{0-c&VDE)Ks|H0yoRHN!kbcVid}cTqIB7$ z;$*HS0+Yg~Ce@qavsL?C;!WSQ;$(iX#Z^nvV_En7@1svagBbR;lG(LrA*Tz=>_wA3ACt0(DhwYk_CM|o$4N?*{R++4YdEK z3*BSvTE}0{d3@6sW7u9hU%FntF5rF9Iad&ICDh-%2{mnoqPr6Ac!B z_0-E>O{D>zz zTXND(*UkBPJTL3V=iOr<6fV-SlIHkGnlJ7u9VSI+bS62M6ZmL{jQ_S^%!EINb0&Jm zG-U!2GUm|G^XT4y>3vH6=vPsKRJOws^;FhMVp@?o_y1W`-}FFPtd} zr?JxcQ9MKVrDER1KLIUu-x^$%&CguM6uM8qRP$uTriuog9%p=I>xeOI{frW7ytECi z8c^xo6Ngi^FU@2P%^Hp{s+zRAhl4P5jT<1NYMQmiOv<$EQigHpKRsn7-&>^(MUc}} zZUpm;a)n+L#N3v~r_RidACu@?XMkV=xAA+q>f}&)W3!TE=m`(tqSR6V( z@=uZpmov;e`{Et-km3Zysb7NHpjwe;=GaYD=5Vp!C;09e7ed7plMdyttuE#(y zCO%ssoR#j=GF72eL2$Avug2kacMtE<>aCqDCgyRMPgZaT$XNd$6?U}5Ko%2{Qjw=zgIrI5FX{hsNn)>yp zL@>}^Hvm{9SRx2Go1H7NTzEWr^fP$o!x4J!>dDLJSG4TCKkq$8dc-`v#r3`=!8a%z zoHWo1?MBr&tBcPzC}~wP%SJMWdWtPZd^54b(eyD-eBUg|9#Bpfd{*qiSK z_9_uI(dc1X>0w#HK%{a)mO*J!8EJB`L+F;WLy{yIiGi59)w`MhF$nxp-%AZ^yuDWq za;vcoo}2_tSTc8cZ3-=rDrethA?U3snx)YQ*U=58hmHQ3#|D+9P+_RPj22N1-^P{_ znt5xin%_{(phsv2&5e^r{!Q7Y_N;hB|A{o3M9RyE`EO9|7-<$!>ZfuZ@_GuT zOkH+-apejjo}O@^ zy^TJWm7S=Bw&@??!26lKJ`Y?Gc?9!?;K>b~%G;o}9s|X+@{9%AQU;-rhcdY;Ee&;T zePNN>YJpahuf=v3+k|ZD2ei{95lTyl%yV36B=6ZQUI*c&5%)#KT`M$e23UWeh2 zze6g+$=s!1Euhw{BK;$8)NXG5&>Y8)3Y7p)HVV7CKY`9k}pgf8@Od~MSv(R+Q746-LEAyJ=&PeC7fDoK>u}csm&>qu^|rF zYSxf`a_rJ3hPHNR4|YN9b+MYx-IWx!AL)8yN`RkPnxKR{0Fe`J^J%MZ3n4_qQJ=My z(E4wA!4}A_c%-s%Fns|}vyna+aj%PTCXS6hZX|doytMyGklJrXj(FCVTN4>s*&k24 z<+UJ3hQo)8c<{B6*5c+K;KP}NtMkp~ejX_25x30?Oh2rk;y2BLyhK~g;;u73?!g7U4%e>A3*WcM=nysv6W=3w z8tI+@)3DJOt``)ds<@K71sRAjl0a;H97*7s)OB8#u4W-95k?o@hPJxgNRUm4nvJIz zm5v$Um!Z=}s-yv|yuoR+QDUg3(Sq@mv}Q168jd@$+~M(jgrgX&YbF^=R;$rJ+jn%c z)3ZN&l%cun7&g_E!e)}2E2^1U?QzB+Je7;st33YE)o7yR)y;nVb7Gj*Ph3%awr>>H83;y> z#*2`k|HBXh$VQKx0*s9DqG{8cn5l)q0|VM%y6?Wp1OTU5WX3u4-6nG zI(NSz+{C26mjYAOKb&Rc2z5o>x@@^^xC><$HJr^Iid_~@N9Hn#JVq7<0A}kzTxJ&! z6>6`m;wMJ&(#>q+IDb@jcxBfSo(Lv%if)2xmj`kE0U!FSqBD;jVNF{(qBvVzE zn5XBWTFe(3f+#8BA{M8sG-n|7K8ADi{D zfJXm^YynF3>Sx~`zMDD!x?IAr8><973OGb$=P#MS$$*aX(kx%Agb_#DhG2a298lWM zZRrkNe{a!*^^^iBgwZ-~lD1*l3@}VoU}It(fPp z*Kp`X5hLHCFvJ}O)X93DAL@cF_GHm#t8Or3`~x}nyM2?h?e)s9wnJRK1jt~@rk%X- zC9=N;I}a0;O?o;;9Lds|R-Iq>E^9kxJ?kgVt!1kBB{L#~b4unsyqT2p!~&ek&L}g1 zrb=VPg;De28u!qvO(csytAj=3hk_!;)Q5c3K> z?Dl6KL-LsTL|_#@&iWpTm#koGk4#oHAf?Zk2Py{0#2;JOuB-q_0B#|w(X*7pu!sW2*4DJ^Tbt&Wz(g&tKN#`laY5lh8p_Os8L zI{ImV`Wcjzr;ce=39z=U6zxTcB66O)ZN+UEcJOUzMSa?1Uu%n{auzG&1QgVHh463*CzK5cHnXf{a%ZS9&5x^ zNL>j%-1z*{E$l?TCgg?25(G(&iG;%2z)Tq8_)LALmoE`yAfJdXX(n=b^3oPrNhfud z2BD5TDJ`wY#j8?xc2V`|SGl-|>;CT{S?XvDkzZpH3|BK`hO)^0Z1XAOGRQjR*7Ba{ zK{2kJn?F?_wo-KQ8R4vObk@50YB~owB>uF=DlS+?UM_8s1DOCBH&*@~$V$cBT9~kG z&g3^3C0k@_g@&j$)&SDSRx&8nADN}y6`g;tzL(%jh|+r7NQ7Km2=4!dcjQBAB3>24 zPHI`75rdL3NEG^?{ai2f5Y~U#};tkII8+ZP&9zXG;L5l>MUDNk_%osBNn45KuEa$BUXKEWp(4hSe|-m z9i7vp^?^s+Yf5J8=f@ea>60N9Cr&i2iH^y@FGl!qtJ>z+!?K)#Vb8-khO=0yKO(fL z5Xe%ui?3d&f6M10DFlMzII{-i_x0dvBDygXcXsK$`Q4*t?leqoKQM9@=vQ=O_G=(l zbP@j7+qIU_7P_r)q;x@1ooiE%^f;G4=I+52K8iXMW_kAT2FDXu9u1NMjV(%OY_crO z6#q8avAhCf=aIny22x|Zb6Y}F=;DZ#nFTU`LiCXgTABP6C-nU_n5#VXm%m63fw`>NnO5*muSQ!Afs+>-k+R<^%w8^#=C342=izB^B*M(l^;C-;iDk(w=KH9Z z$@X%FaoDcqGwM-1MY^Pase`OrG8Vy?#1UCNz04?7(Hy#Ii^qgHv8>>iRKLhUVVEVZ zYcLnpRmta-hkRc)FYQ=91U1I`H6(H7nxueYTNdTGEEf!{Wgbu~aU*`Yhgm1!4+GPH z7!W%VVhF}M{R%Wxk%rbR>5={D+J(*B{R#3d1WSHCDrAgpSn=brAU9StX_D5kh^rJ5 zD>N{Ov-_66{Y!i7{lz#nuUT8s5C@=1YHpHM9r$-`;G}><`L2C2=qf0k2rn2nA#PDs zk7oiW7@2cxw6GCVR5Oaa3^rF^J4Zj@(32j4ntP{dEy=H*=iApY%X(#n6k9VdC8R$s zL!GeM%U7E-`mi&)jZ0ua-l ziM`W7|4gP=&X`dult?c5Pr|Wgqsr9CvRFYr-dBRWyi=Qco7PAqbgqwPGx+e+NGq{p z4k%qvG_nOD%FG#=yJCvH-yF$;`3f{VvuGhi`!zK2Q zl{+`X_g`TqgAb3pqKJqMjNMPd75q6DJ0Gn<^nqGd|p_v*Gx)YVdE|EgEA~i52m0nj+mB;r^(-AeXnq~6^pgWPX61N$L zN*8b1&@Kx3YZ~tr;;?2Nhus1|8E!(jR;5t(0A$#-X;Rt{01A9{>r=q_cjFR^TXDtr z#`1SjEzxX>mUG4r6}HZrfU@ru4ao{qfLk^X2V_=nf0G@v_eznrQQ;kIqgktWGSBiN z((2pG8&a!x2+y2`%aTFsy6SYp(zZ=*&9gEb8rIr)e4zjr>#7(c(Wc2c6aWl(EEy?1?ugH5z;$#e;JgXz?$GN1(^YKAYYwl8AFhr1yY$kx@GP&(7FMB^4~ zz1o^!Za7~te^Y31=ggZ%JawIL3odraFDll z)fASHZ<=7YKVr*Rbj@#Qtn6A!5Zs$S8VfeX_`0Qnns#yu7~w*kJL$i#vkMvHhKBgK zdRUm+71eqF#K*YJ{A^j}x+L8=wM1L9VTU;pDCz~kGsV^~B`y5|z_UTFRY%9vFQH1? zR7LlfG7oWt;SAah&N8&dm^|m3C+BCceu7^BHmJx^5G;}QH*82Fr>UCOGR{eK^(~|8 zV@Q{@OPPTj5*4Fsb~)ZA*)x%!yjkeS5>`00xK4bvR41svO4uRkWg;=ydN+E8>`>%A_BfqHZFh#=sAYmvmXN10UL0L8k~dU1UO)U*IV z$Kw6a4&htRhi4BDnJIVRsj@xXh}aPU6#^Qi#HOmNz)05+Q*v32wJ4#+x4;!hkZpxa zNvzOJ%--4{_c@^9F#>~bTvW3i!&7i=jOno)MsZ5&*5$3+P56%Z+>;~^=CkKqG{bQo zL$uYiqQMzIQ^SN~XG-oi@xC-_M=BQM6WK6PAihk7C9c-Ynn2;A#DQ87=dX~*XN?>4 zIBs*!LB6zC3SFOslUei#4N0D8@@%f}E?KFSO3f z{A45x-WqY1H;n;Qlfy1!UjPo+p13Bta_suBTklwdUb*;fOPVy7R_qSs_V4KqF4HNh z57m243w_#N9t%O!-@YI>xJJ2F*f`clyhi@EItA{Y;PZ%@2q*`5{F%YL;O*L=j-uSgbz!>7w|o5jCmz2b@=S}Du4CFcE^=CGHv#_lD;XGu3<%`sMTf>N37lHxL; zfi>e=CmmDiWxH<1hQOMd1lpy}u-K(ed#}^QJvGF1MjX_Pgtj`hImOk?lzPtKG7i^k zqdYU))%i5Mk5&>!nvh4e1pUL);Q2iR#rnL$?ThAnR{qR#Tw4y>aiAfMfQ>-HPlP9_ z1W44Psj`$r)b=!Mql)bf4H_^`cMAj zy(a}~kw_Ylm?HTbpvP}ABFLW|waPAjp;HN5&Y_h?(g#X-Z*XjvR3Gq92~b@qmG1Ab zeGCeTWf466!>>ae{*sVZIrAbQr3@0Oni2ha ziq&H9(*tEAH^b$&)m)*Fen&oqHXw#?Q?J3+zV^6=<$ib9D(lPOyw7PPNu_s^8?O_u z>l|NVClGu0fG4ZV70+kR!+lqt zzr4B03s_(t79)aMI3|hO<#&o<$Ge96Q2`qx0dsie!=Bc@mLDt2jnQDOcl_)gJ!t2S zYoXx}2{C)lp7QkkKsNvSnBerX45LE*8_c+(;n&+}$H{Y{Dpaq`Qk$&kt~0;G`mY#z z6B}5$&mE*tTBff6?st6f#Es_s*w#Yq z>Jrt32&gj`7!|SseEUZfg5l5c70F1X1U<3E(sp-j`;NLS%{zw`#k4n*G#ov~NuVwr zUEL!EF9AH2ojWRbvc-kc_HM}|n3toaL8ETY%k)mqv9kd+1qM7Zo%l`yl=X<6xx=%x zR>=LRc9n6&Jk3N&VfxJcLc)>QT{GrX3&Y!5G}I5f$*NUXY|FOirGf7DEmWf|%pfzg3`;ByxZZ;E4M`{fc?}*Cg#>M-Kt;pU2t{ZAhi#hTUsZ?{4{Yo9> z)SDAYjBNC)J>s?rIwnT)oUlk82Jaa`3;7dpE^6b9h+IcB|d z*U#rHhii_z?7?p4elTEV7J{=oiPqW!^pepjn;WI=E6wi5?5<%M9NX}Ya1X>2!~lQR zDpnHV>h)H*3r0qaMDrWUdurMv9O6{j5}H{NT!i2ka0lowBuA>lt+v(&n^y4=XLZ&c70dxg1Ci#eG_;kajHq85(Bf9VGBDqV zCQuqv3Oj^QGkzcSldMZnY3=0I3B4+J_#j;_U+4xu@;G=TK#c8=ex&!xYiICNs%N>H zpF|fA+nn7YyC7a=3^7ehHLl>#s+VRya||uVu3`?D1mhGDA3pjQFN;^w4}WHvKdV`#+7agtUnUES5Qa3SlS?_r`g8n?TvX2-*A zYZ&;Ytoz``yj{DVq&;5uQnWJEkk`?E=175~=UTf|+1iw;tt=}Ag3m$>ftg+}uAwj3 zSP#y4Kg!bb7;|>$j-(i2Id2pt(5A==@XSA8C`{gjzG05I{9_Xzf z7H|1oPn2~MMx4=|V=loR%hjak#Y;dpCCU4wkZl+LDYIdCBGHY|xa6K_qj^13g~SYX z0d1y8tU+WS5A9D;1KDZi)1XY6oU~I-=Q3cTSP=i-<05ayy|-#}ZVL zx6Umx@AnqS_t3=w@^=;O|B{_T0>9>jLgEfFsRzqHI+HH&5Kn&cHx$9IE>*I8E9|8)L>MHiUo4*Em3Mw|6Nr!`Zo(juf>*AvtP5Dhk zR?=7~?A|q50Sw{{{5DF|w#Vw~_ccDY)^sHOvLo(Ei(n=XpId zM6$3(Y)bTatGf=$>A1y!n=Bl38he-FiXUz1Pi2az*Tap5-N*ded^I+rBr%#O!%$PK zOIOy`Xa2l}ao{V25x4RIUrB^9j&fGD?-k+|gHQw<$ZD#Tx;4u)p8aL%A$^i_hB5p7 zlY;>Li_$5_+haw>oqulkYlpUmarSRl5D`i~hAZCU|6uH#VnhjpEIhVt+qP}vj_tW) z+qP}nxMSP4ZF^>CH=FG4+a{HCr_%Y;51me>s?Patw^1Ull$k4(0}&VlBfZn%`PNt= zQ`n@n=VLZdRZ;hINWTPwPHIDEuF-~(_Qw?xAJ1BxnCp?KW%7u*G(}_@r zS|0X7fx0J@Cnc70I~t3~H~jf_(&zeo=)@vky#;+@77@lXh8zU#f68JuhBau{1UsHk z0BIlz5Dl#{jj7T?gUY6>iiJf>9d2!s-!iKAKxF|VHpHFX842ECD_MD1NdTPK8tkgc zNFbPx*;s4n6O=1?KIoy?QSZ+8h@ABInol`^tLxD-%1gS+RE(dHQQz=puFYI5hVr5o zmDfie1*94H)JgUNL^y z2kQ=M4s^ydukhe(1n@6Ole9d_#dfA<Sd{uCTST%cST*_FD~aU(L=rADaqt%^3;)*y zp;V!5^$$*GdcmhJ8OS@~MbSYXH1-h;e*^HA+6=j&O)DTbq=$8_`DW#55{k z*W#*~v%9H)V5xuP^bI>4x$W1;aejMKMGB`1zwODCbj@oOpD4cp3o^mxq?O9mT z6lxEV5gN*jRbQ>vtJv_Ae~Lt%5)G9sVdpp;iY3&%1d#>y^)vukhkw534$)Ve2k8Tgn6%IJ3Y!H^-;a8Wlp3qYa5{P- zCC;Y~^>)iV4>?SlJZ+eu&>Zgu;er*IlnE2L~Am>EfKp{&XU;GtJ z>RGmKk5;9xm6NdLRSl`2j2WlRu^#eF1Fz4J1}YGAmJn%X)_4~`0Gv-~pD$$lHXPbb zH%eMFfY4uli2RWT*B|v7Sa|~qs{`Mb0d95|-uDJ5fzGci*{vopgAFs950zfpt*{sY zX4_}X;|Bok`8zltTTH5eM3;hs~&P7&%*)**)R;AL>L_I6kJgn$(-#boB zNMzOoo*RML?dJo&Wx^HW4j?p(n$OOOE_-|{-m>4nb3=R1Ni;jOOht2_P_a6(%ZGsN zJ&bc>ZdI{;yn1A=5 z;i5sDbT6I)yZ{-~Z=;M;SBFNjJhA8yLaAjSr~`yD%-Il!`bIwjzaEWv8EKGfGnjFt zFYT;*8TTo^&d1oSltOy*GS-@jBNn>*`vO>)_~Q{DM9I&#rHup{H<-$}-+ zm9bUPN2Kcj%x5NMu!ba=l*LtV9LN$XMF?1GGh7ArK?4*KA7Wq7tzAT*W6c6mSS=G& zW8z)*lAtun!2=G|4pp<_CJ5*ONlgm65$ivym9OF&*Dv>fo#S(-4J?4u z8lgeN^sqh_Wd5v1`q2Z1V+UQdk^eNl3+=KPwkd+!K5K&oDaxetLme=UDb)>KJFsGu zYW&B>n;E3r;f^U+grgmRX;f{35|c9tiZ)>kj?m0iZGxGTHg<82T-yG;fokV(0WqKg7Tp6}D&qyACxz}hn_zcV^1$>cw+x_<6W>-h0Dp&f ze}a=%P>kb|io2_v74r-~84wo@7>}x{8~LgjP|Bp3kq@{vLq260oU$sx9`M5UD-^ID z@Z$Oal8?PyhYa0!g&O2|*`iOT-0(swFDl>V`0s7GHG|%y7cntj4KzZu0vqek_f2AL zp7bz0^dL;SEYkOPs8z0$$pkXt*%urIL4S6?DV8DrQ|`cgAAio;RV(${7HFO(zHh%b zM6z{}O#^UsqFNR{wAYQ+Ya!%XRA`!_9r{iv3`81o8$L;NP>3e55H%?(Mhv5 z_=zLx2iBB30U0Oizqop#MFPqd14322x{K5XQ+rMC@l$^Tb3k>~Uf%HUSoZJuqxsEK zYOnxtT~K`rT~Oy1&%YM?gARx?z8Rv=6%lC`sku@Z1DTD}&SUt+W}F2KJI{9c*9>g{D$lmRPlbZ-x1O2*8n zOqd`JZCJ#^R96|sHa+}|@;3{9zy$jxG{2rrZG{{0HO zjfndzE{=%A4&eFOpKiyY3Yl-xE*Rb-57$o@gU`*wl(=qTaiO@D51%#Wv3U^^L%j`1 z45dz^6CDe!a7ss`0b0m&7X>UbA;3{)#Bp(tVw*`_=+F6Uo6-}^lbJTPRPfEQnOTJy zAq-~S$sp4Ja}c-s%%RJPwYbsn*mc77OEuCrD^#>(ml*B}-&lVKHV%O!MMVFO&ni=8 zxd}pA?O3Z6*{Xq5UGa%av6Mtcv7Dp-^@$(f2%runw=sQBZ8{6q)2_-KwiXgF9$_74 zS?&@=Q#)N<7f#(t2@tj<$VLX#r6EWMNFSXnq>B+%-QRHil_DT)kveJwYhkIn6HfgT z={iuK9nC$QWm=-u3((62r68a4a)=Jl`PsQgMJ=6MXq?xR(g0r%s8_S10AI3xe(1g2 z=v(%pAYVbMSAJ>0pGlQlda>73f=_A0ZxhNr+lV1+1?p-L&8x$i7w=g zcRH3mt^8V#U&YEx+b#ayF27J02x%pWivh_YlH_PH6VrCgPP|}RTt*!4silG37#@e7 z|5RKcIXVSKn{`DA$IWIncu$yOnLX5pz8<(Ux6&&_QaQJN(?8_la>b^#j;z6a7}+Be zV_k?!;Tw=Aho^`jh0JShO3pg}BOHJMmD+bWEu{vBc4RNTXkUKUN$=_=v7~d|Hc%Np znWKrla4}Ks3?X43$dY#q%TeT6W$*c7LQXpL@O$D`=V*^=ly&7*1dLxorIK|~qyqy$ zH;4R2tOb(CJ`ZeThC?7rY1$;ycf9-bz@N8-wmgvg)ZWVOD44E*evf40T+&@GL(F_# z@u|_7ni1yYTpgkz_J9*|EdGtEXVHrHJRzGt?MXXG2ap+!rd0&_XD8%-?GR2bDtofu zC1iusk)K%bO5G*}UnsEC$f7q$j`)CZM>|8F2;UF zdXb7UYDIs?VIH8@{$9GUyL9Gw=ECs=9bs%eCju1QD%hr)QyY`n(xO4Gm`I%S2{!L> zDL%AmYUMPyEj1=dN!SSqvpEU#*-KkijyLsirhyfF3GYYU*c+$N9*R{L-#dwHvyU@4 zDWaVXyad)!q&SqvVpIGH8NRt$efrrumoq}{STN`50;Wyhec<3EiaU7G84xFl2$t+& z3tls!N{bR?TTqIUo=Xp_&?b(`+fIgBaJG?IDeX5VB!f4+cfng&R!L*)VEii?TuyE{ zE-2HUJ>t|#tvzeF*AIKx+A21osN?#-frVsykeJ^pnLjC6u@7Vz=z-FkEr zvH^MJlGfYB&?7UZ$mzD`Mxl?aP4U4+@lgR^VB(~}N1@2pBK`)=$q+~pn?P}QAEQEH zMl`Mw*3bZ)1mEucBaC#!23|bPOLNxJ`y~6`(*nhT?)tz#Y@W4gAOi=SWVR*2<(UgyJUE5;GVa zZSN+qfSe~uDp)Pwg-r-dE809$8|(ac4v>C$iA(x(s8e(Q(Gnkmwm>3*f;Y^ALEkD* zb9>bWjKvTc)-N|cfQ|k(7a~Ski@-VLvKlNoWFtd`RxcS0+B&ei-!HTilL+lHkVCrVdEt+K?Sn2S|WhSB4IpSv|D@@CT=MUyZ%j zx#1*h;c7BVS+9&Ey1b7flH7rI@%1rrL)Wg8Gd%kbQPjNG&VYpI8(fI-2$oIjVzPK~ zjZAp#jM$$2rza7VJQ=CuWeeP}*^8mTQQ4p_yXz1VH?x zYeVTC;4_TKT4P=6mFs#lzF1)F!?HG91_W&+3)Ww$N98GbJ>0N4p=?T@frhH^>IXGPy zpf3Oz#^g#g{a11Od@+MOhva^<%E7nMsI)=+6O;)rp?*YqadvjW&8y^su;RSYrj89Fv%?}fNmAXH$~6Rs-xW^N2We@ z&c!R>d0!c0&)EyF4Ig9e z8(FO0mlL=dVl&@43IgPHjjuUoBcAcxr#a?)JnN2ha%B8UOogq~|1?XAkw$Q`K9R zYJw$w=>;MB6IC;&vf$Ra;qJ3`!tL2kWM0@?$D7!ONACpou|#>4LCmz-VwKESC|Wqs z-IqvP){%3PWyGMnxSMtio1V7gjDGm^c1F_JA|;Y$fp9`b^jEONjdaYGUI;@k)^Ht= zlZ0v=+Os*NV^91h){vWnEE2IVV@raY5$ zkcQ8JXrc{SaLXR$Sg^5o19^}FBy$6ov@7g-`btQCOZ?P|^vnoJ`x;y1q(BWIOnvD^ z?Nj7Kvv)1rW~>ou1|2Ii3$n8V;9~%HulYd+x zY(U?-erg}y{w(95#W0~m)0*G+W+`MKFXAfT=L567YjrAHoKu9f zNF$1V<7WqzC`bdg>F^Z>ykE0Ho|VsOF=~ggW-n(R;-~I;DP-fo{T!L$f(3J*RwDwxt&E0dN(SvrsKTZ*g+kR{slQ+>R?Ir%4i&~Euxe$#yW70j za{UTnNhT;VO8n|QapU{I&%7|?vwCFBi1#qZ*4hwxI>c=CdmI1#3m(k;~I zIt*#ZxdVTb3%1iyZt?~Y+h-^}YQ_0!a#3!8%#YH)81^*al% z4n%pPG8ok?JJ4gCEIhdw8Dz@p)#YI_dz5-otUt9|_8Itg8pRE$cpzbyia#hs$f?xD zP%~P4?3kCN#ZYSb zawhd^l)sc6hb)xhJC2wMCwbgnt*Y9GJA%(ncfb?1f zN)}2ljPS*tw<*@%C{!``Oy*oS8fUt)FBbF^vY5|N zSvHrazGyZ}l3i~cFJ9-X38Ge@P1PTK(p&UFrcykC&wIwULwr06j|CL(%|3sz)Kj^L zx2t{!?iOdwH^>oF<{UI(1RZ8B?{y<1@4CAr@S(ok6c`C?#J)@+VQm18!)hmo)PQ4FtULu^uIzNk}*?Q)(jKrd@SLU z29aZpkuHQ3l;R({3@O6H&C2v?7x;ACX%9sMpuUA&9L3Q7b$ zd#1fE?%vd!j3$(~BVq zUC75->3kJ@xPwO~jM`W`ND{TnSzsxRp27J$C9Fb#bws^!o=S7QAE-;L2>uSpRnwv_EB+zU~PYtnKMRmDgvm$UHm z4KrMRQekT2VN!_(3d8r@BG6QaNzl6T0dfw3B(TKh(TPu_^Ca}3hTb9R-Zz(Q0<6}g zBazl_b_hR=64&(JjOzIPaQ7VcK`Qtg`02DbE7Ce8tVIN5aH+*hG2mvk2xV;pt-WC% z7gqk6pWnW9C9Ve&-@Odc_Ula&lK7@}Akk1q%Ayc)s%_1ect zo&Ap1zc?WzJSkkx2LF1JWzg=K4QF9uaPp9tHf^iW0VJ$=71e5rV>NEi5@53TX(iXh!L z+G%F|Cav7^k!UBQlFpOL(7toXl1!-i)^jnsO? zgky(tRWZik&;-&h#WA22={HW+8W-h*{;Bw=#tYfNV$uyj^%r}gB zBij3NeejOSoYgjO6;865)zAuAR!P(`W`_5!)$r?3r!TEj4LIT_@m&hub1DTY z*HLP-!lEVG6XAC%{ta^Q`-wGkK1oS4BRBs9P~!GQ7`kRnDz7QQ16PEGK9)Vim0s?2 zku4QHIh+ppyM*}LgoK)ibuu=nj|`kn!W&ysGmt;i+8htZErb%4jO( zFO9m&wjz{ZPTKNC6<@I8`zmTT7ka<;84UpB%<0jCPVToxFLU$fQqH+Swg# zV2W$13=C3EI8x34Bp)%Ua#Go6yJpHGr)c?dw#H4S^~8V0v-?HZXS@AZ>=!{cOSh)VXv<;rWPv>6%9J%)Ak{IGa%I9OR>su&RER># zxKa^GxmKGkRUZ-ivo}@DVyP?k(ICV$@e@rqm zI83Er%zP^!#~!IFbx$Y8QiMJb*U8CvFE!>gjb=bzyR^s=yi`ts~>uv)KFHo&nIm5W%-XkyeDKW%hZD>Uy3_vI;?u&s5gtc!>R4^}X~ znQ%ToGGb|6l~Z_P-YP6-5yRK8N#I#Cxm9K~@WV*S#v0;p@~Q|Qxd%}Q60&ET&bWzq zjc6WY(Uy8_zO~Isc8r*%uNtFK$uCedR)Ko7la?(Fk6Vbls?8=p?^CQY$M?Oze>L98 zO$h88)1mBWLP`Vk+EjPlIDMXo3n9udjC(U6DY%Qav7z1{!BYpvz;ievkd$j4iq;f zX@*DmaZQrBjQ|TfQAQ?Ltf__+i(t5^UVRGCRCd@gok(NzgSnhsxM{FRBR^?ID8dw* z$b;R<9EehZMVvWXsYoOE98ZM#vkTtTeUkYv`KqbrX@-)+QidO0dZiJj%;I~X$em96=8pF^^m>q zO@!>^TATr1=D(ci?DG&`s?eSN-dw7^NK`o*D0K3`8o*eM3!tkUk%%@yaktflbHA9s zZVT#k)`wPr2TQs{<5tocSB&z*V2}d&?VW+7@U6w#DwI^zHjZ{3uGYR<@zir}a5aA|E@~^{AG~Gtwez z3$sUFg=P?6y}nE_)bh58^268v9yJAQAct&1L%UFwzBP{!I5JY#gj1D{G`y81V5bu5 zZQF|VvTW#=wWCQ*E$A1ybIGvI_<gv1(IT;iPWn zix&nOxeF>4VJOKGd{P}5-759ar~lB9Y;hpd?bg?Oltx2|~j6 z#0)P7NMp%zXMJSwgKo)#!~l0!Uml(#M#c}y`6=R69`y|ajptkFfWx$;geicT?$PBlP&w8c^CbB8GU8$lxU3nUY>E@jN1>Dfii zkVVZpD7Z#M=h|@<9+8wDnUo%(0(_f=Bc>fFxTwPm?q2zYcs9!EbqQqoqxo%6pqG*# zjE~)XB22W8Jy-E}LUoV9=@Jj{0O)RPYpG* zdW%aQ7awp{7UdR_}U^wz5sq z@u|Dml-SpA5jumCdaYJNyR!C&c!VO4RjnA5Q>;sk-jo{_>v3w~U^w&!+cMeT#-Pd~ z8YGt$Dx#{MVH^t_xvEeA*q+40qV}IT%hLZNR8TszzJ9l!XrLG zwy8`qFas2CpD)3cr)E2{T6ZKoO=>}a_898niClGWheO$O)IR4q3CqCB)=2m+cWF7p zXH(*B>z|(2O|nl~fmUx``kAXSvcJK2ZtdxM=j%m6Z#tYma-ZVdb%^*ZWgVni?f&wE zX4LHW&UqWjndW^bYWiafJ8Ga=`cna5l}P=|)_<&Up-VNcufC@3jGe)1mrwkHXAjr9uEl=_1{Jl* zn_IxyID!RXiFD~!t2Y}s^Z#8fTOYM}Y3#J%+K6O2bWk`C3s7PM=UsKGm*t8nap$4f+jx!40g}6YWXjbcsS0korP#aL<;UBXM z$U}9RYGmQSVfzgTxVNs0x1wLc`U9t@A}TM?pJ~B$&!FSjBWuRtsPhSMNsTRb)lSUX zoS3l)otdNN^uPYAK)#5Po}WXY!K-9ii5rJ7>TVZob@e&>@w~w~d^5iod9m{QhG4cv zY=RcIg@KMO$yE$jGwK(GYOxjvTgE9qrR<}tBb6F2{Hqam*`&OiK%5M^BiclClO1UKl^!&3njQ2mO`Ul!j!c`OcInJCJQPTDPH3G#wdc86F~31PTL~=1X1~GS(1j%IS#5FVp82VHl;LEw5)4t>|9$X9-Je5m`9HM zQgpe}*MPd00sR|r@Cj@SWMOY>>nd)*}KX--dTGf{R6Md z5wm5{3+yXTuGVubp z8tJel?D0-<^{qv~(%fZjBU7C~lGHV(3%o~CUWBg-SS7C@(GzHW!Hl8r4d-(q%`yB= zOw+q_P+e<~{zU#b>1*#S_fl|{CLk0eAS6T({U&W0=7uE54yWBsqt+Urm-f?0k6 zm$D$@j6|v)5;fg(fK-(>KU0}8x;HaLsEwHbI{1V4vDL@#?i7a19i{BHjA5g2fF~A^ z+^7ld@lW0QK`>e@Y^gib?(d+_`oUOmF%dS_-J{PeYXOa&fQd{mLrwz9^>VVIsM`iIq zPzD?Qc`TrDW)sM?<$ke+Ps3~=J)Ku>qnhWd>^{n~!zm}!{l4!W#qY}g1U5L!)J$n* zZC?BJ5_~mKiIpxv?8MnX_kJTi)$Hq^3V1%+45U?~9*o%C@k`1r*t!2-Zu0uOyXib@>MBHbkUKD7fH?sz@a&7 zvm?r&Up$U^i^{O#9>@K2IR|yj%e^o&K!46Pg64Vfrk`m-WOIsfFB1uiR^S~O>l0ik zlg1DRDcg^Tq?6{^p6irtsE3hD(D{omcP4*e!#?-PlfM*?-Y7Qzp@JQJ=?&fdP*r?p z?99Z4d<;z;b~`%M!~xRkHKHC9xwO9 zV$>}U+ug)!L{)f%V$?~0zp0#w*7q+CV>a{vr+8*-Bmho;Xu$y7B)5HqzUOz?uGjZh zaKE-(!py3N*2Zn!`qw7OY=TwY-iUf&%-!69bh`)XPcGaz!RhsmQx{-5vw~7TA^-mrQmr;1vc@7|q#{u*Z>LzC1t4#iRsN#C(+jvIzFvi4XJ9;kIX z=C*M|c#a?|9!1l3vOUWB45vna;Hz#Ypc2tkbjngbQ6RiIp?=Txur?;akhOfVo$K6O zs%?uF_PbEuh$g>S%y}~{V;{Wtk)!a?jtk6inUtNdm{gs>6mL3~FpW93H1szLuAOqK zuN`vAx34++JM^6NxB08J)MuA%^$VN`Kfl<4%{09E@?IjsWDmv(r6l4lWM^yd6k4lC zDE&POEZUrQlm#zd?^uez>tpV&N7@)L#HN;Y2>fKQP#Se)KF!hMf@eElEWA#ZPB1g< z@Jg!wOo>vhqHzvcEp0bi$OYvt`=|D{swZ_d-(9og$p84r;|F#sc$o&@Co;np1%T(0 zewq%K&75tyoNiqkb!yh2cW>MotNpOAS-?YNnkbq~y%Fi#bJ)pyeCzUWlhG35{u$B? z>PM>33)!p4+Y@SCXQ3)*gmB>j1jQpKNHC|AYc-Zg>;WP4m@wdYvFXYnR(hQ-*}2y! zu4N~FGx*|FWl=#kS%lOSSffrkE=xu2{0;WdLs#VC1qtS^6Q+>2wyjLI-6}@-Dot4N zBS`qlgHxMPLgB-Lrrf^laFI7bCOhuHGj7iv-@~2H&p!uVCkfVp0{c*bZ8YC{HlMojr5X_AM{niVRm{+er{B0bQ_(3?uBmJFTuE+{p^Es(;i!a3K!079WQtoEq5>xU< z*kMmn;|}i#dL73VA4UwTyJ~m{V)`7@wws zLB{19aGY7ijm3^X=D~r+^hoV$Eny3HNLej@bk3bT%Z7&LIOgd^snk#HIBS#>-FTtE zt+W(!PKk4~+nw{%a-c04>Zkj6c7hHbdi!LUgdu6kc?%q2{O>|O)T zEBaXyj4@T@YcQU95Z5Px zC2*Mc@ppyntBQx#;&nFicxnTBzO##PA`@J=(_6od9Y5wCO(?h(U&S+0dm!}s?226h z9L48`fZjVs)`>X^!c;ELri1`RY$(*Iw0>?*iqQ4iU28cWlj? z;*l22ell)kRv6yRyLlP^n4G7$--suP<+Q};Q%r!*_8i?AI;W$gtuv_njEdVh;*8rM zGvJ+!2v4HYzI)3k%KG@^!bkO#;Mmagg8uzto$Bi9i z_Y!3kud+c_B*qEh@BzZX=d_U*doYbR=N&Dth0%>vH!L~Pb+S~-NZ^?cOp7bzxC7<* zeGp0mkN8vL3qcE_SLdlkuj%bOvxUS-tz@y#HW)|;Qs$`6*a4lFN3J)*3mGP3$@U5M z_$w|WUBajrkOmi#u$efj&e*FOaW~ne=oBuBgZHWK>N$o!3 z=|lydl$!D_reJ4v+=Wkxz9)S7pF33Al!cn2fA;tHityUu@HK;gr9!D3MV<_iU5wEr zY3rou=O*aqnJ}tYQ+2@2LHQZwdCS=O0#u!}Q7@!yqn-&ccB>=U&vzl-od)=}t9_M1 z#5hoHqqZ+x2kL|?V%m@D+K=FGJ`Ivs&aUAFqX9X z;^HG#_>P~r)N{8LI`Lc{$y76VbXECksdCkmXDY`}RF7_z@+)zectWsI2Qs6v85e6R zp3HlGLjQJBCjZ@>0nuDzYx%O>pn$Evc$K+OtE?A1JvVLhLdj@<=dWiqOeV2wh`At9 z_7L{QDMjMYF>}~@^eEKcpNCQ#q)Om)xo|*iyTA2+@x1W3t&yOcCb4Uw+m~aAPj%eP zic*d|-+XmhTndWz%cw?#qUuS}$a2&@juNbl!g44;%PQ-##JI+F6T@q?gsEPK!E8zI z1+>z_Mp-S{4UmqPJ zx7&Q3Dv|5b zZ+q9?)cJY8Tb8mp_|#XzT2Y8+P^}6IU?ynfRsnx^M#=koauE-8^0KF2yO>yB@wCsT|mMB~&ypJ;=ywb>5;86Jm6PzMMoI+|2xk4dg zGxX4GZQ8TItBRX(Kl(jN)|KHP`)|7+ga)xKpp_>6BL)f^e}^mj7-oHN(+W&Bz{^mY zziIq-$jiG$F0vn)J+%%vv%}etMM5v#g85c8E8K8nwR3N_JJ8QWN!YVdYtRb*(Bqs2 z(?rG-(~X9FL!Fo{v75`(BO0TbfdVD*T_j(4GUG>i7iI>5FB`e~7mP)gcFGwiyfZg| ziJpYDI!nM}$eSu@GF!kUY!uHY|Bk4eSClWuDqnirmn_jWu9TFLXOyMaqi&Ag_|%0{ zu+ZOK8x>ib=NToX)#d>Z*qfQuc&I0G9F;Sl=Av)06GGfQe@W6&t~y)xWs|6;H4+!7 zk9n(J;<)z_N zd^Y>_#d#9M9@YGseTDlH;q+PT?A#A`~uVP03%IsnCyuTuUzuV zm=f|-^E;?o2@yGJ%*}Gm0D}fZMjiOGO$t)Sm}1Q0a^-lID;=ceMHocTQdVI{Qw*>A zSF%8UQ3s6mR90c*ilffDWp~|D3F9>yJMToT46o+^A2abr%jTk4$()TEJy|wO$Epg&-584L_Pp!H5_{ial~5aWSzLgB2ZIHiZRv5gIDpH!{y-iWg+Jo)v4mmyDip zrytYM3-05`Woy(W6C3Dj*KBp}-4;UbRVzgJwu3&iHlnj9%91o{6k7jKF4W=La5#Y! zp1oZSMax+7W=~n4psAUyn65Nh6&-nn*83|y7kevAXa1{_!T?HYjhzGMHR&dYkN`K7 zc65;oPcwILz$TtLFUTXwG>zRm9?LSl%=GLJTx8ciyq;52_-%n_9_u#u=as+)7F}$% zfR?}RH9^G|Zs$sGQk?g9Yv`?ttpCs^iFZPN-gQ)-D;BlYoUn*1B!CT6J6agEJ=@^a zEM!(@>Kf37jS9=TBD$CK-TGs9Nzt>C`2w4Nh7lE_7&*a`trBYbI56TbV3bfZ$ehZD zBR>{6DZUOKCBd*m5Mr|JI@Zq(5_*MytKX9Qi0gb<)d+#}&dP_BvP-I$lf=0}{zMu*=V8laCtz`;cwpE6Q&|>QLX9~4 zVr>!=;xDM)L5EyI@PYIFks@M#1%nQx&zE`k`~qgxeg{4`&SSp?YxM+XZFd!^aKbN zM;y2^77P3SP%mkOCVvOf;kJL=AmTv|i7Ou9&?$FxUTXMmp_A1em}CN9ja?w)%~WfI z?#yJF<`8Qb`5H?3gE*7UHKMxTCLB)(!gWK9@r6yxLKU|C73uQWDgSSKfxh|VE9}I( zaWu@JT0RUPx z0RZ^_zdt{DLrW7QCt+&?M@MlJ8xx2BfhuA7_Qwg38H7Wn$sqL; zLbz-EWchJ|NehqXwXUSJ9XV-K-1x?+x# zs24+9sSiU)tJ-D9#hPg#jkC5VWOHhD2VRLeEE98J7>h}CfGlum#F=N%>C+}t|NXUC z^f?X``h`&L{mQz!#$aT{PCLxDvL{=n?DnL%15Fy*lgD!$khm9qp1177m8+!7>kie; zw*LYi!Vg;gR`BeQM%IN%>}qF2NLZ;=YNh$hOS;SQ>YDQ{<{}c>9RZLkmfhLhA~`$# zn+bjOh3va(O4reB;F_tQK{YwOoIpySOeV~I{z`1kM{okj4g|cLui}{(aFm(!@e`lJ z2&;K;wRc=`XOxrhYZ7Wbsb z#=lk2W8b|ZhjazW|CZH5n6fGOiX8X0>R2x?-Gf4+E@@^%QrKy%be0g&2=v3S0^O@t_JxpNc>S994M_=Eb6uZRNcOx}Ew&x_Wt~eg^Z=R^^&eaOETDbUnM;VSUA^ z=hRpkSgU&o_1eE}9*08M#CvX|t9J!6CWAs8Q*6PheF5Xi<=qI?SKIQIp8*SzmYV6x zj(MsNI`pjR8m7%jJ$ighP8G}{3=B0o%Cee2eA(tMB_0UqJ*A9CD2o7RInb^8Q;fL} zWmn5dm(C$K_6rAd&hJVO&a)|=KQ;+JPScjVpTeoC%rX%mOiH35o+G#zeZn^P2`eg{ zF@vOpxmbu!lRR~L;0B})S}7}mL}$@YN<)i#EfUU<0AJd^^+?%I>t2^Yt+X=t_`x}D zL!;k&`fm1PTSs|jI3GKv$FzjjiY7^J`zQ2F6j(cpWPWF3h?Vb7JQ0Ls0bZS(r&`a? zcrqM#Y6e|w%7BvnO%5}O1Fmv$7V*v%QT~ZAZ^Y>0X%+BG(m}JIw9g$P-_IUZUU(na z-zuOkb{+^L!+iOZ_)e3?2JelkX`8SlQ9>!cUH9-|psnaPDXo=xyij;WT-U==bh+sr ze_YEa9F};++yDdLBU*#Ou7hA<4BCd%%`PSc_E}6JAheGdUE}Vjaia;jTP^ssD<&qvez_&>ral(J!tBoO0va7LMFY|D_wPILu#26Ply#?LnAde+0GVBG47jg! z?5ZOnq$=fYkl)hI*f*GbSv&W)L_w6@x0u`pCVIAsF!(mx zdk|J|b{BI3`L0K26n~g+d-~o^!V*@cK9Th{&1*ZGr(kbvU~78A3XpIhl?#0LX@L+P zcOZc{->uvtu4&`I063EYej!ix*7!h3%!Xj^Dc&E@XEaGG`b~Vf+xWcN9CWd*NUsb! zg+Rv!Hzs7*W(8_xtVgCbBErL2cMEH5t_=`tTtFuEjvhE4WAr1#Fabu##F-QG82Xwh z2YhDw3@&cw2Utd0`~+sVkM6t}OW2RmOXfazK!B9(%V?W~e7llx&rko}3yjA{ zf$3f2Q^(zA_1^2i@JBAh2LdBDV4n)nA$a@gmue)T-n0H6G9TD%o+nPv>F@0|C;KEs z-Hz^>l^7P8JXyzOv`;>UyIpXtFNCSjV(4+6Es2vV(tu@9wl~dvT}}DWG10V_pHY;%of26-7+^P^n~KheDmfwnkp=akx%3#~&C0FDj+%vl4jT&0eBb_a0v zyUD@{xEQVCVSAp|M=&7ZfOQ*WndwwD0ilRDP}-6zv0EAgn>5tW0v~q)cuaiQjm~4( zukHDy35A*h-c=OaXO;Ii)RE-Nb9>4}VWYp8m&xB@Ir$9<{P^g}=cOdl7L?pzJ!*UIZmPj(dm0Hsg7PGF8(R_f?BWYQ zfW9hw4zEp+JqPJ1!ft-E(`({y;xWgKj?9Bj0sb*l9Kjruuh{Q1nG5?+3X_tE7Grt* zxl23CC@Inz<+)-*B(2Irl$EM`G?jU`r2c`SwPgRuTcw9IwPpLIqf|hEY$%rvcqEwT zv~Vv88A@0EsFbM%5KS~K=+UF&c!QX;BcmGkDY5?L8WL5L((Yn{$SL1}xg|u%xYbAK zR#y#~qI`q=G#fz41hv?l9)MJ_S~nPQseMsa7o0Jl*`TZ#h58+Yc*Duq)~OE3LYJZs z9#MweBGdL$inh{4kZsgT4Z*Z349Wc3^w^kUO7c1{$V^ z9^P+7m^4skM)1nr0CY?DMP7+e=!T_~^B~F4j8xpvwhMaC;P0uT(NVVhq*NS&*cDMU z2(?he!X+qmMc;!{l?X^9DauFDK+1p93z!+Q1-!}ihvWEFTtG646rA2GBDmMFyNeP9 zT8D@M&W8CZYqwYW_mUvx`jIFYbBVj>sr=X=DqO zI&xx>=xxMAloJTu$_8Cwwq=9K%IvIDU-~X z3Jz$`l&xArJg;#PaEWxiV)iwZs8F#1vE{E)UedvKw`$&x5@_7KF+HlcY?QqIa^Up$aq4x1wR2doo@E^(a(_6fSXn*jRgg=4Q>4y?j#Boa)v&oDrI zOZFRHvm$yze@b7uy~_9Tz;)aJa8>U>ycC8~gG7c>*KBq7*71f2$LTdHr|^cy z>h^e_8GeibM>)QseySph@+F5_?r>1P`9H#0=c@aKIv+Y-{{`GsyaM=41`uNM^~tHs z5paS`VjTC4`SB^;^|UJV&8Q$C699;WzJBKJqa9Z4Qn;x948%$Z)68?R0bg`tt$C-u zzj3lv3DvLyv)ss#oe|;LZ2QEz)omDDap0GC6wkGd0Un+kJ3Jb6(iA_z`9OgAev0G-P)4|1QJ)IKwY4M5 z5FmOVVPmGA&^YX-zM_~}>%x{g9!3`V)n#u#@h0GwQSJl`^J0ZPPKX-DrjOzFFeYf* zGYo*DIsWX9{o(MGxa0zSL-6~))zeZ_IOEvMO-%;dRMVBRyVl0KoZZ%$cS=-b;sxm3 zR%$3M&&P8EyF!f-P169IMH#PUvm0|X03d24dL{{w5sY>YVX8}2Py^imEAijnK!8d+r_77 zZW=%{H$vSY(y=0{_w8*m!TwI$1j09kavwm*A$ykBv308yGOgQ>9i{TIAP-7qd47xV zmKYmPC=MCIg$$mGkI{eB_g9%80n^+-cx(nY$J<8=+sj^Knp}xd4l!>%x8ZW09SE|1 z7{U2Y(>`#@-ZN0QcoDTc6fl4t&FNM}?+IgVaSk?(gW_0qB>)>FSuE;uRfJ?HeCavl z-9d!-drKw5s5B{cKPm-0jt0JbS!e~V<3T8DMGEHwkoY=|e@SG9sE+a2GMbu+dPNy( zQj8~^q8fc{yb}o>BmkU2ZpYW^@%&?KN`5KTk25yc#LId9%0aW{J#lM%Rd&bBtdhDo z-?urr{V&^=UK^JI$*qJ(EkNJ`PKu) zzk1pOj{^=mfi2DC5Cm@OYKPTB+M9;TRYGN{zCHoeu&GJOH=UP=ZEVqC+a6Y`3VAtR z(&h0e6#Ieni+N!a`PSz0McG5VW;4{+BVBV~?vvf9!&Jza#-*1#!^WC%0|f zG08_Q9Im1J)pP>Z1{UBP7g_|6LqOW|BRAJh#p<3}@I@G)PW+Gh9y8H6|2J~p0$bdR zt4~NN)Qt}XPLji#f!Pdtsd?WDe9n2jps;NXE!_M@zpjR^32uRD`}Grn|B zs70ab8+Kh;36Mv^7PFc-hI9Z6Q;B zjWc<_h5GVgnME2$K{zq}5B))6TWDPtbW$j8F^1P?`gXK-pb86PA^#U7zxi z>EccxJj(ZeiFM@wdCo>JL;G%y*QNG|| zkB-%1C+AHA`pfceOm2`)`ZL4FS~s!p$NkP}&|3VxxabfXpPhgBdt!Ta&pngQ!Ikil zd>~?nHhKWb_@E}-d;G}`ZxZiUhSYxQzZ$z|nC@5l=tdte4p+?HK02*CUJZo4yL)>q z(mzRMt|WjjNZiu(y9BFv^Z~zL^hvID^c_$5z)fFT!TRQ5mTl-6e51!KOpF z*})=qwbLL@`movYQ2T>#DW@jq;Qdy1<^I$U2Z3uCDZ=ZPyp#9Zxt9YbqPPW$lJ#dU z+K6Rji$2Vo4{6<+QKnZCz8~RGNA19lX^2iya?!=>tJnmcws!*^3|rxnCIyq=ahdT_@~;4bCuV_BVzt3_n~HQU6pIy-%oCVBu3*t5}J*Am923JkB3i z#4XqOe>*aq_HbtpbkOKiey;j8G4U9)9F!r`T#DE?{hkia^5;6LtbG-v?MhJf&$A)? zL1>o*wt@lnWHunRnSM9^bH>B8?=t4LsGV}4(DlQY_^A*Ww*Yy07S)igc%n#V`I;0G+ zHDw1zWQ)EEtoWrjN@erhKfS{I6CGg4G}WDLaz9+Dja4h|B}d6R{AC?{&q(bEt(Dpl zr|z@$YKPa=H*0I?P}YC26ducFFkeBqJ8^WHfZAzKD@_y~RmoVL0^TwhLKPgU5=HeE zLL{NxueLhKf!V00v1qyV!xDv@8OkP^HK=|@`a*_28op^B)|4Wj;f_JeYf{!Iqn4WW z!w0wrnT5ehMcGFc%CRHSf#oB?z=qK;nISfCo6tc*`|gHQH> znL&`U0MOmAP#+?e5bd^Gd02a&PAge76=!fodZ{$cw!UG*=b_wMHC&gy zd%dj|P114`B$!8D)7qtD^6+q6wQck$IdPDNN4`&_Xkrox4Mj^w^*TSl-j9qXrD#Nk zkEfP6udQM1%by+dk`PTaf}4;Lk2FFsjj&j-o18Gec)-CBlp3BXPkItCHVtNCur2{0 zk_GtyTAd^uMOZhQ_A$OT4ZSrr3V|)HE7n_k$B{*isvQ z!h#d;r^F<7VYRk#SVP1b{~anlX5KJz!^A)ozKdB~r?OQ?l0Zr4KrziHSi^8nBr?Lr zHdO|%;Vw-k=BdBluw%nyMeLGWP=rLF_B3mLF)v{dA|DVtz|s)GIDE)HNgdF(4%sw< zl3i7Kz^g7QswjoM5gSt&hdBb2U8C|(k2(ZE9VB7Cf@8G`5!H0tg7dUP^it5z30R3Z zZU*={=a>)^MWNdLETC8)1Yu&-m?W-4+hMWb>r}#KW8YXDjka>bh<7gQPO+0FAH!-g zkuP$wXP)A_Oy1tCki3+_`aR|p*4&s*3r^*hyq-1dRI{=A@Rqa}V?)SQc2%0`2BS5` z_!Rww$yDtWd9#_i_Oq@zxyxY(_T(rEPwl*jC-N4oW!^*`mMTFd=L~7H@5iG%DrE)_X%atNK5K2p`-Hmji>SKXo~LUvc| zpOL?xMBczHacw-*gvJ##0*VPbVDm*|wV}qXk;bc1jGHoyn?l`(1dda10~Cfe{>@>s zTBHi;LCszYxeG4A)k#iaAAhvodJNO>98ebmeq^5v*>JSu}R2|In&*xCZF3D+bu;`B7GVl>Fpv%&f zCV01?#Td=L-kb+Ay9{BH=?$eOQ87cIg6mt0rX?BZgqn75RCl0=bzE^@Y=ZI`eOm#R zR5j~ud%tnNPrVVDgfI~T3`0%xgOnD)n&jug4unk$q_D>>ziTyyEX`M+Bq7BCB4v=9 zELnF5YF9P->GBg5SdK}(jeE&b^#)E?*Oq0e=%cyEy+lp_ zD|S~k2`!Jjn%|2FQl0w~^bGXq>BI3Qkz%So2xxeA_t+BmEqPkQd@W@XBzl)#OfzYk zKF;_H#R;G`#eW>zn6(gpQwe7ooa|ASw5hv2HB+Rs85vYj*!I}dNr1BNd?p&h1It4A zGf8G~oEXX8g+-gt{}~VAVz0-h-HAn;C+hD)leZ(DhQx-*4(dpsD4YmF!VWubAJAu7 zPCOy7oOUF*EJ^%;^y@y5UP9D`WOWp+AWUKXy`g%rfg6peGyZDv_EqRSNt-XLHZko5*xR8zmqjiw#1 zwFypv9&$v&mXJOTnV{*|8xdbSMf#pSxZVCe6o=S5w`XhF8YRzy^qzJl8645O6lXD04n(kFR8z71G8d3! z9Eu(R#wTn6Ls>>VJ=EP%P=56>H_U0jMhQn2^lgb&9}kV63T5`S^ZN(f~iC{42}Kwj#U-) zDoxdImXp2#b?yYzd{lb`qvgu|B_QQ$m?!_9Ic;%1R>Ac)qu)`Jq^<+4{2h2|!4SBa zWs_wWXUxaK+m+jU+I z8|fu+!$#o6vK`d0=b8@mEVCoSg`+d1=nOr&Wl-%!J4K+RML26!J~T`w@5f}ovKmsf zhM#Zs&d^REIND{Lkr5k{hmO7;oRDQ@lN3g1lcg)Onl@?>Gp; z(@++`ll(0nH~rl?R+?ptNU16W5vQRtd3@0vcv}GD!sw8A{?2q<3y%oS7g<+i?V{nZ z<@d@nX@FNsEE|kVgy(8Mci@u+hPJK9N(AVrx!jqXgA;h_&OFU4&qz^dt;U5tr59KW zBRjSrSe^X2fhvEV)*5PT=q8kq@D(zhtNXO-KYjOdmx9>v5HH_NE=(4GD>u>@b$$eosf>V?na@^9` zx#N~YN>We=j%TXHyf06O}&sZaVb}xr(OFG-AH7O3$){B>jkCT*MolD^*pp>pr?)V9G>X>caQ zlDTNWIuYquL2o~%gt=O;NUo=LX zM;NzIY7~vsnf!Wk;*CG=UY$j!e+?@FeaHyF`n}jE-K=wrNEoM!KkdE%-R54_Wy<6# zjBCbk!t4a#*Y=OJ7`mZWs@G)vw@_9-R+3bZh;Xb83#0pGd8`c9pE+E{p~DBtkUrqpVG!p4H;aP1*Uok9LDlABu($JNjmMS??d&`7*KG z%q~rZFWJ(T)97qwp^Nf@VOh7{w_K-4nqsd;511WaZGk>~G|2Rn<_(r<+!It8i34vn zgjZylU{8ZormZz9td`+o9le~m1pra<$Tf(TLAF(z5*@rumkNWxYR*h}Ki4Eg6 zofOy6djVR8oByur5&D%h9#z8eN`w9R6OH=k5BvY6s>jjfzsY&jp`5UmP`;NFI^#Pf zvr61CpbLfL5rJiU{cr*uvhYX3w?-Wh(a5pf<5zU6Qnr#M015b|q$pO5NmjyVvBh=j=qH}u41onwkYklFe^`SdZ!SIc{%LtNfF~rCq%JVc7&{1qaQDdt|YCnLL z3_E$sFKb89N~|rh@G#G}Mr$yUVnRDcZ74pUOtoRE$gDQaERUy2I%&)|HGQdAW`;t~ z_=k&K%m-1=M`B<`AEw}Ovm`yGx>YapM<+$b)A|M2KN+4Y=8=*s;!lGCz

{eW(y}JPAZsS)p~est%?aTzf-(F17d3tR#|d{*;A9p zbau2o+);qVR9Wl9&X@jt`a)J8NCsDH7V^5Z>J4;#5!}7M9ri;XoO{Lr8TaECsm0A! zv~DR;03&?!C})rp=WwXOqM(Sz5?dYQ@8`@dGA!9Hg%k+Apn)jl53?P{&paL=xJ`}( zL!pMHzaZ7hhF&TanN4j+AY9bDTvJBc@Q-lQwYWJ1;eld}Zq|!zpv9`tCnj9(0y<7hx<)VyM@J1PGEl%F1f<4Lf&7q)j z`t42dxchccu+<(4%^+K33Pq8qtSYH)dHy8KV3vU_p`Q?OS;@H3gJ6Xda~-Z9mZ@A} z??S&#?vur4gd?YGH31C$?~nn8_#6Nn2=Qt-Ho=}X`A%bMIH;M>JL9njaf8h`5tfFf z0fSqLrFeQ0&^@iqiNXyl`wJ8LSR&4{Vt)~blPnjl0R!^JT`liCPuYBH6yN;XxK_xu z>?v~@W)pGwp>+G=VER7bIA(@V>61KkC=VG-%keP^`0Y?4o!VtUvlv>k@IaUMZ|qK7 z-vuP=0UG4HeMbOtGo`o_QxpJ*-L4FXZ+{L-XK;&2XTh!xt~;D>-mNv0T76~D_-keW z6MMz3w^!~it#$leUO>-5R@4KePdF4LtYK<^gfzt~_y@9AlsDsd-Yy~B4^k>JcHaf# zchN56chxSE_TI5l`QtjBZmG3JGjCCK&hV!I1WQng_-<0AZ2_>)=-hFuu|4d?^=D9b zp^Oztg|*V!@SN~PX`H%=#d^M)=;do&rtgp-;WfO9KlNU>pO`rYf6Q35ZRILC9LKwa zVN&C`>QFA@5q`QFl&A2< zxQBKo{OBtdyDy8JI6ctH)KnZngGMdCO_^;|&B&i8_4t=C?WAq3`E*Lo zvv^m9hI4+?4;q!hg~y1(9>GH_Vq)dFrOG9Cha6!*C3PXRJ<)cs8$n(3=O`AR- zw>aU4A7|Wt{BXTMEczgwppkZR!2_CZ+Dd__d*t>4h3;jGdV^DgdZG`@pvzPDLx^`n zE8-uZBl8drJ)!#t%q>{}J>*611`?~qgu}2c=A9k-mI78b)l^bEqMqAg<(qdiNX=9`GanQbJ4ICbbn)>kSw~3S~@BTNtf9LouT!~6Pf#% z0w}WY*ygGGgsS`1hS*u+39LLey2!dp)5w2_jNDmibOk*I#-aAgkRPgkk-8QEo+S9% z9wM#kOq*B&yX0AjnDenL1(j}6}( zG9mlT7VGud@!!lunCtB#BIH{XYVASo?>r}uo`Os!>VnWPPl*DTo0)YgL|y91e|F%M z(k*Q3cH!~XJ1Az7XY{U7b$B3&xbI#mZ!M}0FuR%vu+0%y;$dn3&*O&JLwec{=HZnf zEh8%Sk``4`otZtxBOy0fqS~9~X2njLpSAGnP?}xQ4fbEU+y8WEPXt*#0@z>OE%~26 z!vDt|+TPj1#=^Jdq2_IT<7h3}pk%*y~KOggh%dvOQ%?r+r=M9$k=NlOx@UEzP zayWoC<84$prFOX(-nX#M_u}Blof)ZE4*l&I=Vx1ZPj89zC&{;D^tYzOS9G}C0n_=t z#^1Lr{~ngPpdO|>@^4+9x6bf_S8bkdKVNyio>I}DQraKm{hT{G((WL-%vUw9p~0TL zou#7+$%7Tu{fpO-aJzjz=+Bby3I_?EH#(%9{sz)-t-E*De=7VxM*Cd6*RD%p{6CR5 z>92g;CEz|ggAcF%1oU-6eDRh-mZgH05ad+mrRasfp5evJ)j+92+T!*~vr)CopmoZGG<#A3BXABNWDu@Y>C&NGjIt4k_497NW#yjS2!%>uCZ3;CE z$EIA`?9A~Fj1v6Q%Lx=xPA7LVPs>YZv*l+}r(o-nOd8JM%Iq`gli@4~vW+zy@JkeC z<7_5b1A0B@678DPVh|bcl1nC+6X&@pXIz)-Oc(YFpV92m^`Wx>qE^$r5AQm#(9Qig zrA<#iMw(2U87D{)w$!Izo}@S9K@`NHZC`}9?J^B6y!J}7bhnlmFw2&g4TT2qBx_I;mb^8xM*`^nunARfQjoTiuU3vsO{+?XzH@-L4$Ia)*suIpPNB7ah-W;Rdxb0{NErJa6(- z*C}(5cs3YKuMJXQOg}5lM+Z^;A_*quEC0e(xE=*}7SunldR6(FFu>N@Xxy88#ESia zxa8xN^Zjxr)?WKQVA%5c=X4^Kb<(HJH#4&u{?JLSR(%&pCdYARxQ~e^^YYTIE#2vO z7A=u#w0&}KXofh#&AJooP6{i}aqQsU*ZxKZi;EEeV?W~P_gu8-V6jp^N`*1BGt-Wg z$c;lX-EQBU^Y=xpKe>T6!DQJ4`yi#!_*GG<&D;ZL+45 zXrii9BDrdL++*a_S8qW+z)|gAwzfG;VS^5gxZZX+2e>N6pPq-0dUIV@`@d@)_>1RR$x!`z*;%(IYiZM*a> z)z}?%lCn&wYD-K0qzwm~XXS1b$GLn?Bu+Vc@6=)sT{eVf(K)NgDE7Dv+Wa(v8cMp1 zlnd2{ije6ZFfXAx5{sEmZ~#w%7AutNt}Ij_yXh!PiZ+!NwRUeJciE@>aPS#53um^_ zSwJS;`%xMPC+GNgUK$n-OW{;PRRUw-Rs!sr>6lA!)^Hj@cFJX3HY%eR;mVloHEa07 zT7pjfM#2RJ9os0XkZq%J?56S9RQ*bVw`HTAE#sjdNlWPp7|ogDi%^Pl#fO2EN75xi z+fu)XS8rSas*`OAL5qfC$Nqe4ZE#be+&c_Lt+718temU5V~HZ8@mOs;UW>7?Ec!j8 z)OA7!&7|RUoF{dg+TN9xD9ujg0izw6mDe6o1zYA(JYPYE#V5=G?#3yuQQtsFngq>q%WWDFr(`RX1O`(3e%O&IstgwUiq zHkEw);U{fLXV^9+ci&X3yBvEY$>l&&`TD~Ix!r=-R8XUS3fy|0t|Phzc~|O^F0;A3 z;*IQd0M7`%WqFMxW}%^g%Emc-lq3`Vb{UT;7ci!9TuLr3lfLF3 z+vQ_&o{3V_Iq|?GWt$%rl6yJ6lsBz)uwEEfbg5&#yY_&e2M*!?kRS{(SewQndw^}E z;5$x8Z1Gu)q;;101O&sHkOGzSXg>RZJz(zt7(apBPkpWYa0c-fG~=f#DZ=1?+lU(#%i zZa1n#x9KW>UOB`$VP=sh>oC+k!HWSd;G1T(S0GcHzs=X?s&S}%PWn5(9}nE2*~USo zX4`fDXpr&7a_mNX;pGOX>N(RGigP;f$Ip_vTsFKwbcxl!i+4l?-PT$~%pykX%@vUD zprM1EQ#PD(d!_iFUmftyM075t54uXIytZ9Z_pv36_be_yAJBGjp zX2zE0A0AA8JM(I?W{v)FjY?%p>T-!DK7W;yM$e@3rB0v$`K=*LP9nt?0_7F}*x@n* zL8-BfdR?)L?4XMalq23Ftk`nZnSIo4%y)N-EFqopz;>>Gq6^fluH3=Jh#6qb$+L`+ zO<8rtK}}A)VzAM!MrI!c>T3NaPOVV|syv!Qa~?6KbNQsK=0;xr8Kb$G7Kq|o`0kO? zTBk9p?Wd3))=)Z)p^b0}k2reQRK13KD-n;_jV13I&_BGPoAUkO9G zc=(TjQv5l6UkHyFFy5$40FM^iDn5@lw%ry)qB3*MgQ5qsNI_!z`<;< zt<|s6P+G-X@=tGYsS=s4%Wq2hC)jvf@Tg}PI7Vsn=w3re$=KBfI7Cod7+Z$?BM)%Xj#z_gh4lu4E2`^(R#ro(bf;yilN1 zP(r(o!iFdo&ksQ%-f`w1^z%DuX)&SF0{^w=3kB7s)a0&cHU+)BS7nNc5mmJl>c70f zI3$b_F^qhYA7n-w_*Z?D$UO$g#mVDUWBCf+x53f!T5(2qFBbR-sUMF$>z5LFbmlc{b$ zj?$4RmURL7^fjoRsTYG_KhE;0Ht(fvJqH-pZpdttbIpjhhrAvLp6cTkZThndK2fOU%w7 zUH4k65oUFGJIDT~T#d&ab?0`WsB7%8V$M%JYByCrPc4hrZDYL!iY*M>YFQ;sD(4+$ z`d3&GMZM3Bdc6dF-_bW>FVyQ^BeAs$caJ~#RGE+ml&UQysMh;(sT5vu z$mh2suizz=kR5+%{$yl)NS@{xwRy2wmhXgO+!Gk z?z@LrfBgXc&k^)Ozjq+%*Z2Mj{^yU_|KkWMZ{q0mJCK^#8krFP=7s#n5nA+Xg+~@c z=9#)?WP}a~L)ZsGF?8c(po=DlgyKci9YcgZKB8Z^goS3(dbo`rryIk&RVr_kL?aAW z9H?&05e(Pg@?#qx*XtmS>38|x!&+H!H8&H-Jh`fG$~Ob zt^Fmk;(B1z4?+? zsz!^{L>n>-gQ`)LxFvxdiIkV;?5v9OTEk~93raimv=WK9u6IKVAV#B(C42CQn07F) zQ8jFAkzX0s59%cIIgG|LJ$xOy+a?rm0ZM8<2EvNGRb(e3F)ps| z2e3Rtd|l#n_+=`6wo8aDVjiO|IXki`4<=EO4p_o`qQU~h`8vCW$&wDvZnLEe-|@?k zB^dJQhl8h z*MC#zI1wJt-yio8GOE)7r5@aBMsD}@0#1JS9&6o%S^>NC{_J|K!pG&&x>zhaS_Gq_ zz%YjvDy!V$Nk?jF6?Tt@;ASqEoTPJzG2$}88By+-$Kp*wW0#+XV(OeZlTM@a54V3J zomM~DV6|ldatB1-oKX!(ZDJR*-+`xM*`Kk6yYX}!naV82pB02b%DZAoKBf4S*bJr$ zz!Fc(`_7531kG_84+-UbWgM}8q!SL15^dO$+rh9{Mjr^aDvs?<3`s}$Q6mHN;2_g> zV2sEvH?y`af2W!jxk8aXjh+3_u+B9#mGyv z7!<&8j)$G<1$C577Usb+G4B$n;UPSvokC)$&c(7o@1Y&uC3#&KSH_*NF;+d4eHP$X=Ls$_(y|z+G9kQTXTUMPhYetG=x!MpG1obkUd8y$ z&AkQEw-goPEt~iVL|GcojJ)2Uo@j`qKi4exE%*z~k}fKCZwFSe4Q&@fvA5+E@OB53 zeT3P1(Wm#gt%NM5_i!Y$JJ#dBzd`>E6d9`*n)Elj42fTp~QDAHB)?w;Q+7;NhW}A`0J28d4 z&*g+M?m|giBazT!BxQxVrfFk3ZcIHB^o^LyAOl4a%7zu*jdb~BUVQj7N0O%x`l({` zm!8uETS*Lo9c4*jszA6S0nJB}=q#PJB#Q!Hvr62yA@TQx^Ms1rw_bwg;73cEnz&=? zQa@u3QiB_W5f)@gboCD{WtY+_r>5#V#|V6g$WrNvn=c}fg812zSf(mY(5)cCO?t#aNM7 zuEMS>yWEQ|EUYlxRcQ!jA(}yOw+g{B@1{e|6&>%O$~A4wyx6mjdAQm%?=kw zhPoZ*2b4$Cps`ppMZuhs4=i=Rcpk9@`;wp6;9z~;h;!Q2yEwq$F(hs4kQXLCFzKgt z>w`nTY$GHSFT%eY6uL&!DVt!}#0!sQPxO$6;o!^#nYV{$%}uSY(WYr6<}Wb%P-9;C ziEZuCK_frw9@!9SPyPritr(zPN;ADzBjlam!6~xMShTCwR`PF}Xv2051gV^+nJzI7TeeiL3n#p2oK5_I{mk{4dttsY|qOTM|v%wrv|L zZF8k<+qu%VZQHhOTPtn5^4zmeyS4XIRjcn0%$FIX_lQ3Fh&W0UL}P~}ngjff-PKQG zG_E4HwB9d%1mLs;zF#ho6<~Iv4?(F;FovLckYrw_v61hN8Ee*Owc;rA8EP0cQZ3!L zs@dn}eJc~Z)h(2}-4?k}GF_WNde`@whkQw-Vz-2I*9ejSh1kNC4aCS8RRCV3gsTJi z@27jRF#j(a(2teR54HJ6PWS)t8l?@Jgqh zf*O1At|PxQVW+t?Y^jcFc0sA|UI6%@7-ba^73Whl>P=0(Os22W)^vM(KR|A9&e>>a zruL$vj4M}LBRtt0)SK*f`qP8JVcKZR3fD};Q>bBsCv6syV4zvcgu8aEp}Y@Sg~CxR z=9z`@oMaitkUQq`ZITS75M)Ou14f2o4-?Ti~g9lz9ncY6P zo|OH;XW{N=N15(mlFvT1TsQip(L)If+F6;0dC5y2QD#A?!rd7ax1KWDEZ%R%er%$T zrx+N&Fsvk35uacmhc8es-|b?!EH&QC3lO7Vnp_gG+7_v^j}4iMy)?V`*QDSs&9&I8 z3m|tp+m!_Jakf{$gj!H!pvkEL9hiVdLbfEB>}K;dE{wyZq=ET<(T^#fUFl~A(WaGi znqW;=-r&LggP4jXpT2~L)$P8YPEwxyJ;i65e&TU>(k-uha#wg-R2=IXrkY7ZoKfWi z{w^7Tg7fw_UT=Jerz#5(AVq;-BEC2(Atx%~erOSLo_y3RjcPWPA{HN&;zHi9!gRHV zkUQ&x|A_`Iq|~yNuaVs@!3vCYv;R`RFc7qU?A8H;&-Z_GQ~9r3oshl>Ades1(f(LT z{S%a$>AxFGld7f?jtasT+y#?91`r_$pdk`rsTMPqArhc~U@yRgKb`+B?ZAb;t6th# zdfL$8nCZ8_Z?9UVVs7EM@@a+~pBZLUi!PCmsY{!#B=wQ<)T!f}lW$3foa>C&_w&jQ zKx@#DB+D)RDo=MZkWTS1y;}sFp@cJ}%-no}x}|4>Z+mGoNr>Y#)pa`c00&AgZPI7Qjgj zL6Q;xIWv>0qthO7O1jYGUwPb7d=NbET9s*j9~kL073Ot9!!2;Kssw+}l$z+%o8)A@ z=8$Ew96iBsUPMm2qmiskYS*S~m$YUr(gB^9z&ujL6Ev{^=ZfX}(PkO69GXENST>uo z%tSi=1Wgh03=Q$-Slyb_uo=nu(3{*i>)5821x4!_CYJP3EzOC!@}pvonZc({w}6=h zOPJ@cUtJL)RdA$Hqq6z3HF~0O1Y{=ZCAmr77X~JY$YWjlT>C@*7G77*_C!jZT1TknV8!h|*CaXQ~R;;ywnA&Y;w(4o>u=>>LP`87JsG2F>$}a9i#r zm9|V}yrDCJo!sN~gC5b!itn=eStaPz+Ht1YYr}-a$9jQyex|?|hw+Qp;C6+AhMrG3 z1)fa~b_Zsg2P07@Fm;Vd9e>*%p(2E#A!x9*BzIRQ7S4A-i}pjm$?xq|QUi}T(1U`| z;^PDddCR%eYLCCKZ0ucH9>8=HmS`^bF}UbreZnKBkZt8Amrshz(`aa+fn>qI5EC=* zkxlBW912cAHe0VoNa1pc!xkPVMWoW=QXp`U#NA%x;^K0}Nw1HQhwT8ny2KrqyXRkq z_f~VyD~3t=C3U{H?JO*RQ0`T#WnHvTCdhX!)x7yoZTx=ER3CYK$jgs&=RJeo+NBoz z)PUmoB=$Lqw#pWD{1Rp!*vpL2)Ko71s!x49aVCUWZRZlu7^!S~{Q{NmjccrHry?qo z>!02WZ?KTK~9n|MsPykOAN}3`}axc^9pBAhcr5!vnV!%?jj?AuGO2CYtdYyLW4Kj+Ppb5<;+n=(hsE~kq{LUS?!XGk z{?%ZVS0OjEYg1ZtF2r62BE=`pIgM^Sb z2?UGar0-29>`x3mjtxIF7H{1Y`T+lxH3jGv>rFuW7T=oT-DImxW~X``d3uoX#}Bsa zeiQCB7xI)a?dCfFcQ#R2y~my;K_t#~*kEX1+Yv%(O&FMCo+0fi{8qy6vOL|^flzTX zl3J=2&G1#~ErZrX?&{hPiF|B6%Gl^_f+A|%xf`RaS_fItVqtZQr3t{Ytm#X-A}*kb zo?ecucfNK2MJIiyg`9MmWr8UjEX=67G;gP_0txi_hiGha>|yitjY|L+{XkI@j}^ck zF?ily!S;6m-k~-6lkdRQ`!(-D_HBa;faT)4Bs+_@)ii!m8dYBC1+{vN8|)!&^n6PJsfn zy8LTS*`mreV}gyQ?ZV`Wjbdle2k3hU^PS0g7=HKpwkP_aAw?M~Pi~y3}s4ldh zK;5z_lT?Npj?d9au23ev+Zkv?$&rT3^rUdu|m> zs63b2Kd*)&N!@aJf~b9Jg76Xs&AE2~7EeN|gi2wDVJ#d{0E zqC)W=HHO`}(5oym6b1hC4~=rroh@r*<;3df?vt$0;5kuv7!nl%aRNw#uS%+n_U~iF z8fYZap)CcaxxSNCffR=gRds3yx2RUy^+4jo^0O3a5|yM@3|yElyV?&&0*C-rr{t}L zUb{{sH+jvYs6iQUcFb)z81_0~_Jl(q7$Pg}>VMxxK2gjFFdvG#UFN!>V!DR|o$o_?Nxb?XHmhhm*kkLq zt@h%g>u&7Pb>~UbT38z(--<$OZ>9`)!E`&{;&!{<1VrHXF;jFv^lH1grAWRtS%zHT zNrMdhL`E3oNLT1#{MAK<8W*3=c+Zm zmm?7^>pax~o#S$(aJRytwLF?=q{gaQzt8PGYKM#3a3J`Bcou_Ufl<4?q*~tV8{*d! z7tF#GT4#85-<~eyQ<-&{83x&W=n^wl2}ks)wT~B}+7CmgPf=y`?R-X=cs(VzjOA)= z;s{r};J*zI5AqHYlnrVaQL1Y@;-qIQ*|{Ar)SF9v*QFVR*}4oZ*foT!X)eTVn(Dt1k8U z|E`ym+!E3$?zZ3HUFQ4>=i^kIiM3~VgAszuRQiNQ@vn1%@JgrM%>E#Tx#%WISC3&bwoIwMhL zRY8y=vUZm`8jad_U^lBhMxfp*gKkd0{eYjqOQ z24WlB)8g+(p-YT-XG7)fu|UW0N!gXZz&7OL7e&N2f!OPtcGjKaQKPMeFu=wye+SR& zTy!SBD#9Ezsa%aHiI?uS-j(HaFoK7h(H$0%6U>L*HqhFX<(wGMzGdu95@^BF`JD(rQ7l{;er6CokGGubx6oyo9Z1JxZvDS||8uOrU zM>ic-6uDVh|5-p${lb<;b{t=jZOjQ0DKBwJN0KU?gA29|hU;Q8i!QW}k36i?yzYIP zI6ru(0VRKr4173b2Y_XfP~;Ekn}NhVNw;NCa*r7IRMst6-zqr|1%QPrPjo7g0Dkum z^%j}zF6islkN$j#=;kcqmmu$#riVgQnM^^Y-9_V{vg%VR2y=nLC%McH4irk{1@{wi;&Op9l*n92 z8}vHNiLYjbbePLndspvIe|!)9lF3daNd1wo-WC-V1j(ysdIiD0#ap|>3z5mig-x6O%{530}gL+v)?J=|1TCsAn zp9B14ThQvcIS#bJaEIB+6v>LjQ`$Jr%*(g9pG8a4cq$I!Vna{VuD5 zVGYfuB~@!nx?R2(`NAS3{~-g9zToZ`maQxsG8>bD3*nQUn-q#rfB5|deo3B$LIi_{ zqTi{!&)1op#|%9_zTbd)*iAqV>J!9K&fqanqx(P8H#jyNE2cx2ANCbPs9l}OXoO`? zfblm&F|5GnS?dIiKSsP^Hqsj}%bvE1N6|!DlMLp{Pf?JaD4WE8P?UK&5B)jt4CI=3 zD8Z#Ni988W<976(Qivkab@ONDh)n#sY%E7YE=W%DGjoi(#0{poe$1B&-iKtrY=_dQ zm%{wKYw8=haiN-m`|jl>HM|xS?QdWk*S?54=E*dd44%(pxa8p2OLqnJlb>iF9}py} z7QBAxB=&Mt*=P+X^raFSlF?Pt#{YJ~uy_*{-4@Y(6&)Vmz-R1W?iy|!zEtoFT4=5D zuA{&7srV}0aXcRXYTZmKtjVf zThUQXKRG18Q2k!SgCfswGodipV1hfLcY^<9p4546+Cl)yV?|4*q{2EwM4pSSy3{ z9nkfsk2f>$dg3?lTu2tSJ_2Q1>%w=u=5+nLy@ub@?FD=T6Qj+c?qs(k0tS!6;}ka# zA7Bhl4Tr~gxcr0E0R2jJE`w1UvOK-ia!FE`j?4DhkWRdW5_uUQJp3P|^4gimC>6uJ zS{DUwN4lp&F|k*KMSm0$D^q)2Q)jawSN({D+3*pcUodgT~%7gw0mJ{TO3W)F>m6mnkcJ%j2NjQ{+wB8LJik zb&gNxA=_o@Y#WrMJ*1jCyzx7<$7RQ$LeKtXao@A`Q(a4VVd&|_$7^vQmBJ3Z5uFXI zt>NdbRgTKmBm~!Di7HR!&4SkqPPb0hGI726qie;Vw*i}ie4q^<6y-+KNv6oh#G_N> z`uO7F1+u$2&yD!)lODYI0akY7jGiJX$H{sKY9oW*!zVYElp?b}V1HSn=@sL%0MxFt z*v!901{Fns%~T+qT>v~%j_C-xKXTXSF#0&i{x&T&RnHbn+CC%Q9L~zl9MF#cJ$ZnK z=8^3#9Y+(JEc9L14YaxQ?JV5lhK5| z<3R(`-cCyuD=JI;-(H_^yO5nsIMST0+QYb;&J5b3h|F+ToUPY-$$70LNsSXQuWM&; zLi#G;{&_uQNvme~S5OU8uEcjR%Z)PyxHd)xrqlk`Wne6sxI$c zHxDiW_2FxlGTt7`OS#A6G?M6wz5Jfy3IoI{VbdfN;yvaw1IlXI79iK1UN5;VE{ zcGVTXGY6ng!ar3nuN~MBSo(Q=j9G0Zud8#q+WrGr2A6gM*ee2omGM4cbojbo4cs$h zLjYiys&u@RXBTU5jFGyFM8T#hmE#P^P#s7jH6p9gtrF>2H5FJf*4p3>a^ZRlApIm8 zO5PjZ{K2xGN)W+1m_4zp=pW*)T!8FQQR8XS5JgDGVBX!(osD!QK_b;zJKi@dqmUgX^caC43sNF zc+<%I(Cf0!F#C8=>0LB3r5TVn2{LWU;>{cMe+Tmvl!L#+kKoDv0krx*uyi18YpCyJ zZfhfK>}cpeLw(oJDI~Nda=Km|v4nG+UECrK^ zIrWA5OwNsg4DSPge|)vWgK;=Pr@6wVy}ZNa>GJakwHI&sUfj?g$WVRUfHMNs?;$B% zJXig40vnS8SvmC@Iuf6v>Lah$>hxWqZ7APxL4VFk#k@QU%(`A?E!z5TIa#8Dlm_a3 zO^W%B($)DSb%ESykSc6f9Z(Q+)fk@GJm-G4$QGrpz zj>3Sq*$O`gouH`=!ZxtqBau$x8T=u3scDXPybI>y)Zl|UJe0oXqHKkd@jeHx16drM zzp6#2bFhvZ)M*JKnt+%v&|6==z1_DC1H=?v13Wt9^3XZn4siVH6x)KdyWd#8p}eqf zzaW7|#t4HTMNz9Mt6!^3L#r;y3-Qv5VvkMZntE~1uiKsh6DE`SIuv?YVslrMkQt^S z$e3x;WoZ5~u{J?JJoL(t< zDFxO%Jj-0r$kYHca1`NHk{$PKpiF)Z4^(ut5b$Y@X&ZKlR`md`K%p@sTzTQz{F9oJ z#Guu(2i@MC#B{4tW!$-nBtbN$4xQh+CuUvddhXecCczxuRMQw&hepd!(jC0`<&klx z9Hy|h9MLSzW|TRbvY4026C~>q&T5{n;4L(0po;{da5!MrLu=56%4n5XlW1#^l*i2^ zgUQ?qYtDs4UxuHQ8s}VVNV0MEHcy{NQY^0Yw<|+5(35p;x|}^VuQAZ(<$R-qiG@ zqo9~e*O*HdJtplx#ALdVQ*}D{iVapY)3{}sVcb(Onen<_|$~zj|~Dx zK8zTvTk3Qa?I=_RNIc(QlA}`1miS=!!*R* zH0%Q>JEP#aP?q>+FGcJ$4f22(%XRT(A%6pRhQbtbBzbN*s+UXlKub3UY{iQJjz;)5kpmX?4g7k!9F5)yuD|496 z@ie0*{Slkr*w4El>eWkGRX?e@pTs<9l2h5?4+6t!wQ}2D_&HA~Qzy<)I-~s>;`F}r>|EjZ^kY375 zZQtF@6Iq*^R>Wa}Q@Oc>)C5EsX5#)J^|K^@{F*r?Vf6KwkWI;emMkpoE1WH@sWnm* zX`6>k0c8Bmo4SX(E!aDHie^^2YN{^JJ*_YAm+roH+FX-KfQQIGcC$UFU#DHWKi{X@ z2Bdu5pE!xym3j5&t+rKvC4I;S>=?&K>{!p+x^;AF)5iSaaY@BFjP278aFy)YDHPl` zjvtE?EZ^M%b=xqsTP$S4W8fUN4*-`*A4UwQVdU(SSR*vPi4A$k(bc&;u!P+({-Z{1VoaRLM^HI@miETcDflvS2KQXp!uV~#Ws#_*E zmwaEjeXgmEp)F&xAu!zwg`_yQx7vK&niOp#BhTa2_)oWDg}ym4{8XD>lOy=gx1UVT zvm@}MvjKcxfid_@ulhcNg)TmeBRPpru>jtZcrlnCt&uzwF>)Q^h_5fTW2_&6V@n^H z3H%c|%&!pql!m^GwN5cuUXz7q;IMpznLL*B zaCj+kGw)$95qv%Z!QacfJ@>ae87O6MFh}ACd1@ zM7nnvUQxe_dAdpSKbrq|jqF~x+wi#G6^`v3Tw{O8&3`rYeG}DxuK-oP`SpHfNEm*U zcYb#C@w1$os3Uxb`qB{p_~bt+C1fAvK>G2szoLKO*Wc3{KdFIzYjD+IeqbkD9a#r; z`uSSk_@+DMY`tdjd`;|rvz_|PeO}9W^k%;X^vTS_y}!idKdg9vkSA;pd=tNS?j3xy zzRZx}9mHh# zR`Tdh_VHx|rq=FQx{jWfO2@@f8=4v$8E;0PECT7=qsnQ3HWoT7?Omn|>B-Gkmsv8n zRf3mSy-j|@W(3PSs|k|A2A|a4ABD@rs{Y0qJjLZ4hFUU6+f~#A{3)mh04} zjcwt;3oMXc#;LbDn(2vg%wex0!g7yC+rW_!&AWnam1!f67L@U3pV&si2OD+>d14v* z9ag&$En|cg^@pkd!hj5VAE+Cy?o&1*h?f@e?|KIx7>n8yM(VPop)jDkhA+8WQ!0OZ z7)2Ih)PrSz8Vo`Dwb*<)X(j;)8YMU~4*HrnMp-Oki-|gzw&=vL)I(aIHkl<q& zCTPkK3jt$B$w=#SPUH85dlO8F)?-3}1Z&ZY0LhM2)vj z(GlW@*Wnp25B!p*#;YRD`2;3SJ>>H-$HVn-oM0#6mCqSECo>5CzK)5=T#;(#`N!qL zc3}{;!r{q0!0oN?Ofz}Q;zDy4`7{PpQKcPTZ49@$icmF*BM#!l=OM*4{`#`;k`+fi-w4DiM$8p*_0$S*j zjxKxbWOD&$pPMqXG-Zbx_222ho?cQ!@$=pJV9zF{Ll>gb#Y_obdPF$lK`YYYL`6%` zWF>LGmG=IeXh=VW-7<@@DSLZ_b1llPY-53LFFJ3N0+WoKmGVvW!x)Xv6P>Fg8C%uB&J_oK z8rJMrD8wFBd%n~el}DAn7jRT!S;xJ?4+lf?_^nB!W~?0+d*f&$>X5)ry~trbFmWi1 zi=W_8$!M4mRg$im1b=^)6wI}<4BU9d5J&{oR838p%mK@Mj;z!veF?G|DS=_!c3EO@ z7tQpCXlFP$JsQH0c&1t?P@==ur?Ok6uT<$Q&1Fd+`^~+CZUPGgSN$ST$ThGCLt5j3 z{-FlVJF6QkEhoPdAhNu15vIC!QJPf>ui=PsME>}9x#X1bnNJ4jL8n(ye$qKmG8g`P zKs@vz+Um5}}7`4>c7FZR_GA6D%+ z*xg!7UyyD54w;&VpBr z(rSfFllU@JPGi5DlkDn-eCT+gLz8=h4#t90U0i{C_k>Bz%1|XcYl#sm&(6x)#}zA3 zjI-ez2+%;bV8#Cg>rr=Ow!E*EmvD3`KkS>&c!s`7ws8dJoaQw_0Yzag8`8@NW2)y ziH?>p_1S9o^D~;?+i*b!Y;27_4|>Hl=n_z8l^vlB-098rPLxP?QyDF(eU*)?wsa*j zwoeh)^GF^nHc|{ki7A5rD^$lzA!w*_`X=CV(y~a0I>(eu)u-4oq=CI#U;!@}5nc`` zG(^*_KICm7|I{J6Sv)3>;H22s72GtabSLGNQXa#g*@Q8;Jzh=!9l0O$^A4PVca-%S}?tJDE(Ji#owCK46i{>5E z?Td!9ha|LRR`y)>todN<(Wl5>2Cshhr+sa|$*U;Orp_&|eMzyUtW`ueX_Uv);%gm8 zLR3*3uB@d3J{zZsi8k`m=Dw_VV5DQvuF72!+4 zXTaJ;ltALDdT4CWQ*zivXs2QEB)5CN@LC;dKw!NEd5!9ZM9@;~%7#*I6md*vUJMZQ z9p`y-Z(bP|f?Fo`11EOx5j?T>!vHvDoehLaqUu)kqspFWH~$Mq8D>I%8{7iM?0!Ah*>+h}@l2%Q@CDIIH9IoMWqKe|=c6 zF3GU*ig0B`NX>i!Odi+LKS~~z`Wm;k=suq_QKl`MY#IkmkWF_ltmB-qM>iBDS>s|z zU9XXo;P_HK8e-XFv!52eWCsnH^>yS)mK-clpyaKR=Xq{7lz1%U4!bZ?vKxPt`>U6U zy^=YgoOYmZaE=>i#$tefB~Rk+$~820$CDg;{E+=DYrOXTd;!aN1xsR}fr`5H+Pp!7 z&WSn3^{lLE+-f9$=IDL90o@Z+Cp`o7ciRH8*3{IA0#aAqK{Rpr5<6+Q`e7;@-{fSl zAj$={yoUWweM0peCM$^R@o4|lTb2&3e+33Ke@ueAh%_LJj)(Xfv^Ziq5&~jex)>-- z*+EK}NK6Ngy@a?PZ^>1fZDwolVa5MmPDbsCY7#M_TKut($T(;Br`Pqrx4W$SRlxdD zb2bv6zF3MlYi2_bv)kn_I$`jzANM=#i)2}?7x!#JW<|)#98+EcZuNrfJSf|05)USs zY-!O6rR+B<2lBF!=2tO7@*C`@wM=G-!hP_Vx)^iq{7oD9vA#t?!k4s$2^7>oxHLBD|r9*Xy2HO^l*rl+j2qimx)j+;yyyIZphLnL@|6y zgPbOTB&FMF^Xfp@xQNMdHbuE3f!$lQuuVUu;aTuhF$Fz29bHO_hNIru-m;R?lv_xD zT28*a)K=43Tc2v&zs#kMXEq@~6x~}vmJP$V-J*KS$Z8=-@@k_*1I(OTSl?V)n-7p= z{1&@pIg!-XElj9v1+K58_wI{qkghg8@n~j`-E2_wl(AQJz+LCzMY=*T4i|buYO^*y z=Tq<8-NFv-Z*0F?KRB#7kN}^7)LkOm!SbvV)D57WDPsO)MR#Y#2?uff;G{=d`K8B@ z@k(YOo^vWEC!+XW5Adk_c-kAOy!pClC$ZhNZcujN;?6H}LNB=v-hjCQ{w|(>B9}!3 z*u17c3r9fxExE=*`7RUA6fq<2yM}x+zA1T+LMa(yuIya(L;q=Rg!{y;aIx$x3JkK4 z9VL^)d3em3ozx2Mm=?*8xK0>BUTKs`T&%t1Rsow0U)KM?g#^E`JgnixT*ui`J|E}1 zpslHCo&(h(7+mmRGs!tSln1p}`c5=e1mtrfL_Ot7YMGY~NZ-$dHbbV!zwVkLs?1vv zQRQJihU zh_vh!+8Xbg%zCt$uN>^;U7tO9K6r0^DZzECK74bQd}ExLyps4I!!yi>CeL|Th=#>j z#QF{_RDjpytKBwQ0lc}s5v~KhA%0@Wt4{{~x!0$0k6cU{9T+$`Jfqg;ki43HOGmtm zjBRQ335-l5KHN}zCZetIywY!P)rqk#)7-0e;%`|z&^V)gPWFVGiq?O3)A@%WnCTs3|acd04`2hPNg1=)_K& z=UEzXRYYsHBl2wSlKdwLuJRS~GZBU`+eegth2KMZp1v{o@RI)|MQ>gX@D247i@Pg$ z?)TwcR$-A5#sfCnNefupjqq-F zmDDeu6czK8;Zrk~Bj%gS@Babys~c`Xfq=S-H|-t2v#Xe~f@uB_je>@5NSU%LPvyIZ z;C1GqSm;jn=Q^Npja8!!uI%!Fr3yYVIbfb%QA8EdM~@7n78~p$=oW3jPIrmNj07%g z$PP1ax_C(BVyVzgJ;5O17QOVCB*EG^Hd9P0&m?kMoB0_-241cJ^O;w=;iG{^H+Rju zIz5}u)p`bFstmi}{x4q1sTecM+TA;+S-mq8zJ)eGP~!m^xP{iv6H~^ zj5O?<7{}>3XMK8Fo_TR`Ag=XT_LI+D+Qb_DUkueHj;V9(qSx7(An&GXh4#{dVw=G7 zTU&ukh$R?Pwk1J8I()lVzb`EYPUHvEsB%eLshg71ivu8p-4X+~y_@m5WTD_85m|yZ zFeRdnwUN>%0@Sy>h_?U(Hp4}R&b}Umb?%rDi9#1R8KKQOR2EybkQ|3R<%Rq7;xU!UDKLiAyVI$)FF{(4)D;d|e-t#!0~(xV{`E-^7P^siBt2Ek zc;h9Ok?xU`{XLiO{&kijozfCJq@t$7zg%|?#He?I^=^nHtsDhh6M2b`HM|o}A86@$ z)V2|^7Z5el&@(xCMx1MwlEdZ__(Zp)zwaFP8FkZ2=A#FBXdRt8aA~1!cg?l;x`X4k=jSO$jvt(wtp1 zu{3+HRg{M{%>iu-hB~w8j&U)Gyc?5C%!Pbp$l*Yr8BcB{?X2Qi@Sr!sy%bppHUZXA#U$Ls9n7A<-qhn@=A1-;*quKc{+*npFvr z>u&6)c3gPa)UW?DMm^E2>XB3PXuHs8wcPkZEq0QX^;iT;{7_wFBhMyyP3Ic2NtpMw zMHt)KPb^sR@XL$3^PsV7#`lo|bl6->x2Q!g(!T(t^riu#hDGcAdZjh))Or6#xr+7+V=8d+N$}&nH1t^sR2Mv@Et_utd7h}-FZnjA zF?~m@$_v@~4!kr4UOv>e0At)eDFyLZZU z9{)|qDg6f`#)DXsL@IpWfJF(6B3CK`{q}1T zH|9G6zg}Z3bNsyiY;vP9YcwD~BgJL?(&+Q7@KO-Npj61r7O-U`?Y*)Go|eUP4U0tV zqivzaym=)X8lAn0z$MrhLt+$u@ncETn|c3m|2A=p?QnnA5cB3pZO7PgYHvq<#=%Z_JBX@%x%nio(DoOl#x(o_zFwzjCmjbCi=SL5oeaRTyLV_XM+dVa6z0JNQfrOU>-(;d}snACR!a;iX^?b{e<|9ym;Jd+RoQ_|l(hgNN!?Xc0+3+K31p*9GcML6qx`boxL-+E8Q4 zKqtO5RGs<9hnigEkE0u?u=R|10#_R=LOyE(g zJb(a;{AE6-_abI&H;16%GjVz^XHM%2)a(^?>DIP1&Nm9-T~j0cR^_8(GA$nnMYkL{ z6}#INl>M}`CvnAWfMQ_Xq?1yapLNrpeIuYa;k~fdW>#o&pHEGolmVR?SFaWI+VWMB zycb0;t5pA)u^rmx;0ZDY}W<%q>lkLWN5jy0u;Ia`|&h_bV zs3)ZE0}<|qD{IDfUSwPZotj;{T3Vw-vJBg?>(3b0JhwY;3!*U``O~P!NF!S8yUZDz z8!(82;&ic_Gm#S_QElV~;Tp5RuglH7RK3Yt|F$nNw@AD#f~KtV(C-A7#{&g1t2+Jh>XT9(Mb7rJ8fk2 z!0P9dZHby=9yz5;y~K#ZAupmco`Nv&#Q;V%08bV;@uM z0aIuj%mkC`k+ieNw@~NYL!Ucl{6@je^E~Bi&%yh(*91~D!Qxi?@ncZ)t2n1b@(n0g zx^c#Eq-6BVanStQdHp@}$*07-*WA`+om94C)Vze#I&JF)@vHJo;EAUYPc0{3k_3m7 zkJ<-L^t;o?=kh-t%gtly&O3jW(vA@RFC5F|9c)b<^sVJ>&25|<|NB}lNlnugSq0^* z7&eVWIs*Y(H5`jxT!JDG2+eG$c~-EtFsX4!wNsir#Aw}!Y*JcMq3`{Y{N_rd`{s|O z#P3m!vxc5A(kn-%G-X@BEnnx=NWvtZ)A*-Z1K`k&;f^P#B9ymjLRruxP~8lfs9 z$6&|ot-|0I_hj(n{(_v{V#hekmHYR zp#@unv$8rSvTHPSrAEBOln(B4L1h`0Ta8CHGSJOZjo56SRB@`@N>eL1UF93m)u06F zYd==v&0|dY5oy`wfy7$~W2=Q`5!waP=E6tfrb^ns>(uT{l*oX9=doHa}2O`$!SRP}izlChZqz0#gl z`rX1fl|&g$&Cn{Cbb_AYW}5POdzzT0cWlOT;kZ#(PGj(wTt143U92#Zoa3YBYb0@q^=+$V@D9*uE3WBS& zI9@?%Q@6`tYZe4{4ZX(YmplM^3_cT#Eo6I0WFN)egr|C=dN0nO=A{KJcEEV_DRC$I zgl<0=2CCv2tN;g=Q7f_E0nF(J?PEV&lI!aw>O7AijzHq%$IZ)+Fu*}`j%%R5aY;PO zckq8=_iu#Cz>vCrM|O|MtHyi7&wU@y@@ib7j!N_&e)AA{pJY@o5rg1P@$_foZHkcGqsKp+KV%9Q z2$OY1`H8J=1n43lN$K2RZQRjaUf_UITsP^B`^i8HJM7cdITG6 z*B<9v$_cJEM0v;Dc)?eH(IpaYn^0b4H-MCEbK?>NRUAKZz)Aa%P!EcN7oVSSW zKU)TU*;U8uu6PLpzt;}nxunh?3sM_20NoKe_z^_m63{EC zy2Yk|9OB@<62v~i#kx|I0L6OFTyNmpyGoVB`yu>9)v2OxxqaoNq zFQ@*`tqqu7;}YDTB`SPG006%K)2$6fV>>H-Lt`OZYddo*V-*|o|LF@|YUfJGYAD~< zNa_&O^ngR_2DH|MkRa4Qo=cz#D6@BpH$(B2`ub~y8K8A?mOIzWwr@rFpOW~%LRsdQ zDO?Mu{K_NiUspEb5Kd$u+*#6BSKFRfoIUs4$J>tA*D=0tn7x3TcZTF(k_@4F1{b}x zk%;_W7&rF^nJ1j|+oGQs1E82GA2$c}k<>mE2CC@8|BJMDiq0$w({-z&ifvYG+ZEfk zZU1q_wr$(CZQD-%n3e4GIj6@s-TS8Z*thF?Vtw<)BlV5pGAA<@{#W{!EWIK>49ZV} zQRLYOIz@z;_7aQ9^n(E@B`&tMNmqu9bz=Fzv|dWC93>b%>35U_h4qTe)^+TzjX7@I z?nR^+E0jrBd)ed6w6ythEMl_!8cs~dgAy@wx`kBhdM%?%#X}?O zF~5kQdyc8xaDkbMRMs4Ygxp~(5?TSWCFI3RXLh3tb22!6GHAAK6`NB$G^>@iS9MZcc!h_VuqNf^*|ot?_K%z5uUj`BSCHpOgD>usoB2G5O=e zt+X&oTEmV!>~R|!;%)~a@Z=1uA=pA81n-9twKcdSqhy?(Bt^wVd`3mqUVhA+CL+z2 z@7v^`-qVFCk~+A{D(QeKLj)(d+yNFeHxhd(lb3VRJZ?*6Ytb%vXX!5XF&Y?RqJvJ~ z1wd#x19?4%q~t$X5*lkvWy$g4$gA9~->~^jWgz}`%B^3ZcZQ?@IKe(KI6*`)d=AA6 zY7XTKuHNX2B2SN^Ju)vcylwN3@fXcL_EyS0dAh$&9*QjoVC6F;url%t`%m_9haD>o zm!|Xz=r7V@F~8RoI>~S556~ZIl&at|sC@B>>+_CkrY<&vxehKfFzy9kmErqks{K5z zKr=*|r@lTq>$^L_@71YnA(rT;9-K3+>W9_#>1DwZEbDXlxK0Kb9mpK` z%2f}u&~C1&6XC7B;v7JUMz>#B#h6oKCub5otmBWIP=57h<2!gfmMh>}Rp2Zep{$A& zHmJA7`4+CkrgBGfjyuW@tE!>j`}St25RWtQaE0KINh(X7C!@>Q-SLpGH%RQzZR@+< zH4`V_dyilsxHix_bAbY)tM;LD@n-^7h!xmZ7km!*ZbvNTyP8&6%6BAAn#r4O=N^g(%s^I=`C4-Y>!QIP^&HQy_7?H z))@hj1ei&2FNSe1pB2|nm8xIfKc}(hgwryxv}>KW>)Lip%CXqc|MF4W8#X|6iTj z<2#Db8gM^;X#D=p$^1_*9wl=FM-yWuBRhMOZ!ClL|2vE{X+Qy#zwt?SrtZuM7?4l_ zU?vGemj!=+BL5LY{>?@bhZPF71Z27whah8iI0lcDvQp@1-Cb9mRb6au42RJdr1IC- z`Y!*uST9$(SYKDIXmTkF%68hB5JYx}U%nxJ&UU+LGvhzW=H~Hu=?49A9Bv5C4yc9I z+7cgN!?!)$Bg5|=8F+SYO$)h-bc^`avd@O#^Ou0?1*zt8H;I6`Uk=c^6~JA)AT*nW zuN~F#a&OB{H^Pgr9i)SA-DkVKdN8cLIU!Fjq+>fdp!<1gO2z*;yY3DBOq z?}Jdh_ZJ4C)jswXo)QpylH-opH-q&Cj(~6rWFYVj%+tFAQXupgw#P6&a3J&?&+JeD z1pDl;-X-v}?{+PC5(JW8yM7%3wu3Nu07$<2nYREOI_enBdAfCl>IVzHGFwwHX(d{Xq7w|)ho7_p{M zdQ6zJDUU@pbSoPeu~TiDbcV*#xYX%INw8s>DxQSFQd$#J6V{4yP!J1V^@=9(4qM_W z8<6X$52RD`1#nr3=rVy(C;|dP(P?2ZE2T)(QNE`=`^}S-ROexom8d7cqVc)K!oDW; zK;cs*D=}hU9bG=_f`J)_!MY+#Ebl;AIvepOwk+me?jOt9#>H~;)!%Fb^>jG~Wm+x; zr0C_#niA)c<&W#4Qy6pkQ+z#02x1vfMl2dBt*--93Q?#ejgx1WGp$Hu6Dc7mno7b5 zak()4!j5shd4*AI)(x=ly+B&Mwb+LAgA`Zs=W=`H8e2P?`GcRT2{NcKmT^m(Jd-=i z`CH~t2^Hb-M#pw@`yQhNB*N6y3m?Jgo3(;g2@j|r8UkH^-k{IYHKR$g%n^U7!631U zG?<*on{qumtv5kl0UF+4tBWnV@L41sDGb4a!f2NpKg~}xAuqMJV$U?Nn+)~WP5^eL z4A{7A)&mNG#UrdjHIcTFHoxbc5=d;$b>^_y%!T3|(-o)~%t%m6?H@wttz0br9O zXAI2!=@k0S&$+ImnJ_KubUqi$a{^^4S}Q{5L^1l&LQcdeDZxG@XbufjE7f6l8$Czmk=6BTx-Y>1wMdb z!-Ybvp`5=<5B*ubYlmt_66~vghMAGYtQ_Sq?l|_-c5qSz1Otp%Rpj=?=;}7Nn`Ls0 z&GpYGDig0?xY0PSU5w(|qPMU^0RFQ|#=F-h0o#KkFH&U4Mx?s72qZ?ide9Qp2V`mK z!JhPDh54VNaMqXl@CHC=o=hJ%Pfx|}4Ns5Q9%npA?mq6IJ3wz}&0M(-J>B28^ySAl zh9)?~cVCZ+Z%~YiZE zD`!s1)6z|Z2fyIYqzvCVWv)p(3-&F1eWE=KMI!jeSPD{!fKecl${tb~NRh;=v5Ip7 zddvb`bx7qzRnz`v?)ETKd7*MTm1rE&pm|(%I3tk|S{hHJNCs7>VJ1~H%7IE8siKNk zm`ctcHl&dTgY-XsdpO5%fAEA94c|>hQV2^CUzQz;Jf70eq4UDhK4QRAw@apNisIZO9* zI1Z7|~6FwH+wo`d=x>#;T5@uH-Sc z&6+W~LfPqABxOHS@)rmw;FOc0CUMS&NMn(;$QcTHz&R)+9fLa!v>HOxKS5-UVpUR5 zt27)vM6h^0F5^-~x8$Yb;Z(k!Tmy+#sb1@n3jJh!r22=8?FC!5nG0TT#Sw=7$QZec z*ywZiSXXv4Fm@(8?RMA)Vz-^rnk2DXg5y9m&i;E-7aQz+_mT*=U<Mai7K5Q~m^RG$U2c`9m1q}ISJqyMN8880v}onAr*^PP*KqwTUTBvna9BZD&M^%$ zC_c(Ui2Ea361n^DE(Pl13v0#x`Nv5>uoT@~)PS?W>7DQK?}vC9KU`RS;6Vx56}cwk z%)p=LQb;+400h+F>y*!B#0`OgQAkR_l1|3%3xf?HAaFS zgj3f$woSA26tBW&JPjTmiW%2<$h}yPX?FHSO%`)iYoktG>_eD@yWvmePk&$~dzm6E zuBgr~=hlk1o8YLoQF5^bABc9K?`4)ps|*?9tZ#tP;o*KiK<9e?xd55ZFXX58 z^pSaEwC64Pnm8oJ$QaI60jd5hG@uOF8Xt6o6cKuOEONVQshL)1TD!3RO0~#XAGu^4 z0eUNr)eYTe?zATm?tB3RYmR(U)07_+$;@35N1KCAZvda|`Id}N!C4D$JOP4*%2^(A zH+n_KdoF{CshdFFmRywRO&U+~%r;#ya#Vi;f5V-h9aSjR+BFR**E$~ujOiAclx3E+ zDD>>NSR%MgVPfdFsnVF>=6%+I7GM_iuX8INe*`OYViBz}i3pdM$OxC0=?Irs2nm-L z!{ESX#Kw^h|0u*3|IXL1{#cQ85uKe!>c=#s4oU{gzXT(9kR$It8)VD^6rrRz=jX^G z56y}aYn25T8bX;JZo_C{@UU>2uS+*H;FNu<9jzUCw%e3Z)20sNLRE@3OCq zlt&s75=I}Qj*U(2Wy!)^<~q$8IA{rUY8cw#ul??lN;j0Y#bD}SXs(W_5hK#^t7}!f z!!^S{KRP27%0!qeSa4S?fw9lSf@oeloMUm^b|FKtTe6M37V^_4rA+SK3?Jo z7&;~R39ggM6lt* z902mtga9&p44F`H4mL2Ku{Q1D86ek=Vvx(%0o>z;A9Asjj^&1j%BAsaYsBl zq6SoncSRX>fHW_0qp>g(67j5$`Ag(&m@!GF337u~d^_XYQ)VBNPbq zp@XYYwtDOtw4zEge?`%wh}B}2)0CqN-V`I6dJl=Eau!FH4}h_TnCk{uRdKz+I-EpG z#VSSM4|_!Wg~TOHRJ&e}Yo`a`J{CS`)Wv=3muA4nL zkpSqlnQ)SvBgWJ7DF^+av382-gm~U1)Vl zO9f*0j2^bITM%2GaztU!rXK*?mdJT;@%W~TcA}v+M4^Ekdh4zJW0QGT7SOVj#ZW#{ z^x`U4l(!)e2VTZOTm=KuqVed3KxRhc=<*%ORnJsN$B&`=7 z`Y*^^O&Blbqol8#NfI|lrv%spX?^JR+|mSCE`LNxwZIL5gH%(Iw#Y;Jio{~a%jnoM{ z6;K8@X}y3ddQXcg{WjHq#Kg85cZ9Ln5L9-wlAH=?z(YBt1f5MMMw=bCuK`Z3;ssfv z?1keiKRrYM-EmW@9aIY8E{hclI_p0cT<gAK2`dp=D%PFUyEQrGcGT7M1Xo@!Cyk*DJHvUNYDIeA1N$8SN*=!F9c{h zFM5=^D!14^#M09g0G&P!Lhceo3C2xXwlk?5e z^I4zua3pxglyh-WUe0bJW#G9jv+aYim@7^hpGNHNnXK@GdQUb}QPz#K=~Jpj|J%MQ z0{N7>^~QCVD9J)L6Acoiq)%e4DWRu6sd4>SYM)|j$xVp78pVU(Ve4*~g_BlNs%^1<)_Vdaan>=j)!OKndV7_qsT@(ej@%hB#Eg;O#K5t<_KdL-%`9TtsRjqzG4>+q_A9TQ?-hDZt|D%)dHe zFAC!ooiSAU_;K9dU^{>@WwvZavB=#go}bh`O#Q24r?=8Q_!FAL)|7O`BO(4zvVeG_ zRStO$)ak;>7es`l>xMJcX)z;d)3oDE5IKGf#&aYoJ&}NX`7UXKi3+7C)5le^g;0^T zU8;HiUI!VqYcvHoEpi^s`UDldG>Xg0HO`E3DrB~BqsuhNh+^rwc7SMGw&d9_u7}8J zHpNYM4b(IJ4URlq38uaeJAl$ux;(ee=d$QoHLTggXstb3PF}1KVToW7PpmXa10#e0 zy%O*io*oi(XwG=mtSDf=O;a#0b~jj>Peto%UPhhOP8J2WA012z3!C!O1@pF4UnFLz z-*k8tbsLacR@LVRG^TAAy+T zJg)`ZaeAI>_log5WeDb2eX}ypx!5?1h#`*J+F#SexxuW=D4=<1T`=2sWM9&_bmq*} z%i=UStQ;TnbsZdR;0~VKh)M%t)>MYpOhb5d*=dm4q!|kge@w9X!sIMk@&93$@YD6j zk9oI5od{%=Df`Qjn7=l>IR}!Kwa{|u)RaWN0f>|c#)=h1c~H3JbEe4>!jy+Y$3c#; zZT+ib%N4*aux$tFf~Z)_iBd$D4v)oozlY)I-34gc`OyGwmhwE!s7~dQIm0{+LB9T^ z`#X8;dHre)AU<})N};G-UJ*3y!YNU^dO-_OkpeI6$|-WYavmDCj%bRNGx#g3$=(K$S}+y$4Pe z{qAPmXuaEFX_}^Stx7l?woPG+l{0OV#RI(MN@>8OY}Vp*+7XK|Vs&;I*6Q2vaO6FuQ8#tcJlH&FH=>;O5h9)V7<3UFTD^lR@s- zVk{F3cHerDE1#pxwN`@d2xHsYxYS~bk94<`L4dE@l z?vrkV2>((hGP9XXmFwXS>Yc5<0RorP&rh#@E$Zhr`k;`V!IHwTlc6@;%~!ChX#CI% zVp9@p=p>WM&x^xb<*dxUl@E=4g;7a2wagh;m@o9av55p1o!a_Dvae`E9Vdfg z#my;;dDyDmneCBC#Qb~qeWE5UAif$k8-x3Toz#ZoqDWL@8dE+B0>NHn_88NkCf$!l z^&s7cpTD1x-OcvWBYJ*z!ob5iUYRC3!j)RWRyuP;2Vgrika-Yoq8xd+L&-Tih2TK2O934E5ufa%L>zo_ zFm4^)rWiTk3zESd@CA<&DDWN<@j~vb4~*=y{%{(At}NnGvC-iKEFX5BA}ZJ1mxU{E zl(j8hcwr|^nMms_RpJUgVs1{+i7Bj_ zQAqM8acW;yOz&06*k0?MS8xTr36t@x zj;jh6dTw&TS$XWOBDJ749;MKldds@pS`*2gT6k$uiRBDf4*r#uT@2JOGt&XU+D#bz z``fhUs9QKV7M1!?K6f61m7%r$0WkXkM?!MdZI_4 z+9&AhdBwWU43YE~tRkcEBg0rsS0l88W(?TlZ;5{Z1UO=u7JY)PXsuKH&KSPnjZ?nP z9Q=MfmT>N~iq?qegWtSC2y<>gq&LdzBE9{C2d3{#zP)eQubf(-Gcqg9KoLV-N}dD6 zvmqv9tNXtW#(r=7I%Fgb)VG?NzdgeMdt?`IeYdZPDpXtdM(G{KAZ)gn+tyYn92e};<}tS&AWaHdcN}@>CYWih z=$@1KLu4x`eT1gCVVh+`?^}5R_DrY0LB#kZXo+Fv#q7Hu--6&b#Ks$MJsMR7LBH8R|8ahJm$whE0s5*G?dwJ`Hq#Xj(5M{(6ieR!2m zP9H0~mrHeMnuozXs*@?%;pNjCf=R}D>Y8(~siUe^E+TEtfX7^m=3OqNs7O+StJV=# z%jwj8F?^@K+|q%6D6^ZFl(RHb-kTKPFhp4O%0TMzRe6_if=ifC40!gTNtjKc#QSQ4 z??2HX(|`6T2)jA^AmFoM6D^fuX3*RJP$wM?Z7`s^l z5Wb-|t&g$hvx5Uwa=Q(f1)t3Fu#9oR5m~Qh$NXM9pF5K+dIWqwQwIe>%wlDK2V_XV z>NAAl!=4$e`Y!~UZq_px8nF1071BnMGDJhcw9ra3WnQ=xgoa3hF{Pu#b)4T5fqG8Q zGSC*vKVuo5O)3Qg=qMb6lRINOLr0pl$R)?+q zOW}@a^iE|IM~-h_oFb<@51@9;8D(=ZwPNL!m=LnC9Q^_Uj}=_$c|FHWQ>fQAbZuJ4 zDH^QdUluC{{8mN4QLH4who=iJfyP$Xu;|r zn3qy}$aKeOd*B4b^C2OmjmsYN&kMF6M*MNLvkPAVKXjJ#vG7{*kyfHZqjAuMd6T?; zd19z~A$}6k}V zPSBz($5=p{1Zs*|Sv(`h6~`|;`_K~~M-!I~=<WSO4psXka4{XJ;D_>kltG zbRizd}ClU~+kuIrpkunjK{A?|<&vX;$qVP03eK1_JLja~#}tp#koL*wxXgXkJ2>gppJN$Ezt=LAKi zu#@GO07R=zW{q>G&1Q}S_3I5Xz5gXp!K5dAf&H&1R-l$r;tc+|C_>z`AtM3;n7wzd?n%VT&Nlc^`qM11OV zlEw9UmIMBJ2*N6fPMj}ac-xoujkcQxXn87OBZ)ywO>SG=G_4$IwIq!6?0o&~1*ci# zFgx%(((Q3G-K*Ct2VyvsEJ_ z?|E!Ql=}!FOQP|cw|}-PR}Gg}2)FM48h=zK0VQ3p#%OKw-g8*iM%t6Xasd&^-3a9C zmiwhxiTtLO)%2)ILexg4f{F}5O?#VW&mFhoO0A1&@tD4%?7y^jySS509u4OBuLgdY zd`-8Vk{EQRcA}h+9rEOEq*0jyr4lP4$}hacxT&F;CIGx{(^R&7e?`Po0W703x4Gav?xX@Jy;AzL z$b>j{Focw?@8j(vTbY4r-8qAno-h}_lZxS|o?xBj{VWR@k06685HqXWzzh<>i`#)Z zcU)e8!sdguRZhv^E~XkdO(zfyg!y8W7TlvSFD7SY`PJ~!+sEJtpWR1^vx1xGu+GdZ&%8alG&4g=Ggd|5Ay>fec69rJ&Nn7s1Z||TTAV+)nqyn#Q zN}5s9Ma8bpl}rzO&>Gik4@hElQve=(n4f#MHk(oCT41a$8V^4$cd6!eS*Meu}0i5sO zB+K+7HLu_dS+JxV(mT{xeiPipdbkdiPs9tI2h>uW&T61c=7|u&9w9OYZL(~3J>VUg zv#M;S&Qsi`pAYZL_#y)T?L1~=UDyIc79p7>A!`UobW7ylOZ!#wxsDKQ6RJl$iqy9L%WqL0;ddr&JFS=T7tm}~F?Hr^NHza9aJ z4_C*Ym_L3%2>&0+761JR`2Q?s-2Y)QTRPd4J~q+Sb*+c}og0W4|Hqh797G%$nTZf8 zHjuP*$lwkcA~=nMSzuQ4xp}#{d092DN{4pYQVJQN4gxGG|lm7<|h8C)J{;nb#7@g9dD34el7aEg88Vr{` zC&I~Ht$&CIIF7P*ScS57)Ja)vl+}wvfxisZp^+0?^$>wPC3(*~)-Wjemv;2aH5#6;)z`HH>`*%)5O@_{jk{wCExJ;15-ckNb zU_d+|t2t34g}eGr(e3orXE;QBLsi|RZjP_%Kw2 zn##gKnF05&Xqj4QD5$7Q0HC^FK3(50Lv;4_t!JCB65v|@)+F-=E$r>wSY6#(wcwk; zs|}ZXL&bCsF8KU&rTK5)AZxVM<0C-eTi}s?vyt=ZsV7FouY`j;>)ZP0X&JCRS}o6^ zet{tRXg`Zn`WW=~D8Q>uEprMbD#Yk1GES~uZNNH#6NZy6y!JL-Cn=qKcmVD3d>%(w z6NAdNfj?J`36Xc=KA`aRcjx38A{~Tq2S0u9%6L4tn;+Zg>`aV;%8UR?3TpX{csuK7 zRyNl+hw$^%GLSUsiGw}l3H}w-qt&1f9ZZawmlTTYL!iqFa3jmRIpOU7Da2=|AHjZk z;(4D(+&&=}tbOZxCE9NKD*Xi6WHBxky-_CS7WBsMF<{IKV)QkS#W$QMW!b=}8RqvGL5#<4W50j{QO2zC6QYFW?}fYgRM;JI*Uqk4QzXq&-;k`ZaS z8znB#xP%mA^*rYKe3_C90?AS_e4*Z#qEd`VIO4?S8MvL72oq7e4kgR9CB=nkSUf}B zAlFSQ?D|6d0sZfAGmK;X8189e$;wET1~$=8T~O;8e#q@fF&yNffqBvQhk=izyRr3= zJ@+nxSbkWj*twXHr!y0Kvl0(pE&zK?M1u?U3E>T@C270$BQ9!h_V*B;2ht+F1>6%h zh2h^FN9~7)gmTwQqMtJCd8vH>ROw znIT{>9Y!ZP8l_W%!cKf@RXfiWr9*+ACC=@tPKSRm@2bJ!O4?pLiwPOlFo~?mSHag_ zy+y}D6KTGb#T(_NkS>C}R={4$z&-0Bm`C8L;!}0H?C1FEO?^vmJkk{mzs>VE&wCqR zi^}O>9CNw#G%E&iEJ(x8Ej0Jt1U3eIe^N3g1VuI*l^n?t$wLXoIBd50sx+9EM=_-k zCH_x#*&k_g&#piHhFYx{;=j$_1Cr@Zp^}vm4#_C6a9BOW8_DTh&|a zG{1hKqgZ}bCUyT^8%xkEbp2hF_T-QcjkVA?)GDj9HEO?_ck`@7IvSy}Ly+VWu#SQa z|NfxLzYml}wf^6(h{;G7XmqH+}^`><@3T${WJojDm3oWTrTE##+IKbp1NS zkA}zzn6a|wM$il9W_^L)l;%s`#*g%uGk+2Ki1X)P29m!hnZ7W?!4Kd;(%yuEG7jZX zzZ3?`?a{M*kbZ_c=3JTN?rGDk)FNY;zpz}aJoI$nNv3~G= z3Jmbiqw#<~TiRo2pne7VimJb{eMk`wR^O7ZYd z481v^N@2zds~3u#1qhg>VC}M71gs2)a4}h?;N0$lJ&s_}R(GUFt8#7>AA!pSL~Vd4 zziFBYDC69T6_ARONIP9#gss2|`@`>8;;>e!h-6XtjK=Fvy6oY8MEWSot8`KCFfMtZ zcq>^Rx+PEypp7E(LL;a!Pj!?X;a5yZwp<8O-cpt}|G?pPHKx3@BEPwm5=TpYx*FNt!&saeLfN)85Lwsw!3bF~E01Hu^1 ztPG<$l#t9U467;B26<$ZLti29t!YKzK4EGY(j;-9$rYO+a{M+}UTC;|Bt;J3Q13Nz z!>AOZ7@r{ zdUVrEgU}Fspk@~Nx-8}#pk|ie9$joNobH0Kr0*T08gb83C(5!8Ou*;mVq}|E6h@Kr zZXjB=%nu^9UK-?@D~3WClAfbmuOIY)rp+H19#11u*^u}Vrby1;)>G8HM>{t#pd5O$ zWEIsojU$B8EKoWo_g$}D?2l?;alTJe=DMqRR8^JH8E3bs$lG1(OR3E9cqW;-LsO?Ut08Bvm}bKSpFw-&1WosrFkRcqNN{{LZg_z{ z8l2VPlEcW3C6q^4&tbuO=3cEgL7EVggTeGCVKXaBK&`6t3OiixNQIp?@nw$nUQ$D%fJU*s1rQ zm|%ZV2I`#gcX9&L)`u>kIZ)G&icAoF2QFKM?rnn05^(bmjt(|wH#SEm@rf3aI0z$r zE}?$|n?5RQKsnH4H&RH4s%|GhE$c2`T_C#E>ih;xlI=jqLFAEHQ(=V70qcag9{j>0)`zQiw5E=&xp^%+4wVD;hNL6 z*Z1|9Qq)Q>UxlM%4)^}b>JmgnKPTqtB-G5x3#L0gDP9ctyoKW(* zYPI{=eT6a%Z0z`^OWJS?3hYxh^)&t+xGN-KsI9lE`*wkEr&}5n1;sUEz;kP zPEJDURo_aCgq~$<(~(h~l(dy;X}WX0b9eC(pcO)56i0_EHL6(P_BC3s&n;|RSzNQq zZG94$^1_{@+TD{xGIjG4&3Sl%ZQCWT;irjTX3ojJ7?POfuv`Dr{1Y}PUPMJ4pD%}f z0?@8iulUmTB}w*(G|l@vD$Bw0yDyx~8@!io;z`W-4Cqy;zfuFY~0SQV(#S%@n`Ke!H$)wLojifD`dPICC$HxfS!mVHnOf8v;#2X z9B=)bY589B8SCfY~Dg*+B ze}8%dG3q+A^uMnvReZAeItg2^Su{%a-*Q^ja-xYzv&O)M6lYcBPLpjRiNMBn>7ZQN zHAK@cR@_WDJbU6FhrP}(n47SW1Sj+vdnnGn5l5e@?fm9#{KC&UjhnvV*ZgoTbC1s3 z@JT&6Nj*Vco5J>bvG#g}?;Etwdin1I_9300Oq-OxJ%Zm=-H|bhr#@tT+i-eCCwc`P zIK2XDKTcGl0#0#)A{FdsO~`b(Vglk@C(Fk<;H%3&*kb}DP7r7O2Ixr#^#7iEOP#sRFH~aS*$s#tRKjo-6L$_g2ZTd%i$kAxS@KPEwe#{h<_-^OYxIe`|$ z@-&>r;_z*~dcArQ!yV9fyVx)({v0VFfFH8RDZfb3kB3%yf4jVrc}*C*`B~I(su0W1 zyTMB0Vc>-*D$-ctj;{i)jSsLkcjR%tX~i3Qw!!Usr5m{PBqmy_O7j`AIRERIKd21;Wes#V=!)W3PY&Mxj(;O zU7in<71Z$diOK{;?pe(8gORoznJf(`@p#b{Nkl-56%=SGx2V>Q*S ztuJh$kwTsXI!Ixz|*9b$hMuerR=F%D)o$W#9*tX|UfDXc2$2``H(1|P0Ug2A3~R9Z9t zhANE#ZiyJxc6EoEP*BtHfr;FaP8fE_EtfiC3?+vPab4WY%N>XjCV*5*=F#Q|no@$V zYM|<$#Pu$rN^el4+UDAy73`js^kNm}x^sdLS5UHS|M9zphh}n9n#3w#aEeEt9bn-T zg1JC@A>-QRR)<8`lYYUU4ic+H#~)GwVzTui)WZ3MZUK!pLHP#|dBW5NYJeU9Y&-v* zeOe%QCr&mg!8>U5Kn~F7g_#d-W-kKB^NFX62>%ixzblg0&o4sQNpd_ekss`XF;J2J zQx_u&7;Ij&^N1S8Mifp<0!1tA-0!@fBg5nqqiu;nyCk-&K=FJ16^Q9YusP^L{qX}o z1bKfAS}s;bGE58N1JOs=A!gaw8fhV^E|ggW;RDl0&`J$W2dGr93~?C2_JZ}1S*a~q z&m*DCWRP4MQnHJ&8|ZFG`4OLOBG9A`*^_U7;t$skE~liZ&=*2KPy@c-+tNxX#ShS-n4JvL5)wxA-pmhM7OSA z3LT#UAcYD(t&$sQilc2(V(x$Shq@MdP1S{AijHN>KQ=C?Wmb>pJGg^iJTN%8k{62K z`Ew2IUyEAa9U6tf{YFtS1Ib(fbc>~oB2~_M!c!)eTf)0zF?F80GvCkymXW={6oobb zWSdm~V&z~>{n&HT-_UtXn`5LmJ_7b|38h~3fLfPHkgLR$&)@`%QcxCR>k!4B&+jOM zbe%u!qi2^?snCSwKC-x-sW_%Dnz5Ba!Ik3`&QcL9pp^$qSGSzk%4DP(OX#dLs% z`cEf^?jg$KA(%pUCTwE0-q3kUX-<42`a?MeX-?f@$>^$158S5*nH+D3ljk)t!arwO zj^>O>s2K9WZf5OSJEV#sd8<_A8)Nm+{oBG<>VkgQwZZ)tf)qm{3^+5vD$X;4)nv?b z;&bPwl!umB(+au{95bUDr(bQrekHyDOfq1rvrR?iWNcLoK&kf4 zE`+KiR0iOly5yLKz$yn~9N_GNz^J^oZRs#|pn*@=BiAW-gjnkQRJglm3RwhH`h_^A zR8dO%JX7qbHSFF*=BU%--U;f@aL}ISSzXA`WlWLH1R4aAXo@Ti!VNzxB$4$bf(;r8 zG{tGAp+&#n+Z?=t3)6~Wy|7G@JCZYl*0#y}o6={e5pq^wHCIffHioSFQGKe!4$p!V zRTaO>M035?mJyY)bp$IT_cZM@FAGVaf`t_o0HFqadN7j=Fs>Xj%B}P_jabn-zK~kg zk0zvJJDpT??^Miz>9A(mtbZWC=f+j`wY0LLG=(jnI(jgf#rL~Ol9s=mDkwJR8VB># zaD!jxRf*cM_Aivi`X|d8x^X7%p|mX3HI1xfbqcT&RwbHPnSwAc!#6gaN>OG=AqezG zP=?l%U|;}=hXToq2kP!&D9yn%Z@wHAYxnS|*V95QR^u6WrQ@G`Q6y{0@2kEXM0J{= zc#vnMFs$8ddS;VGGNd7iTN()txS@HvhaUb7xrHlQ`lLnVV3+P_<`q2_1~E@34pCD8 zW3D%WDrk}KGVrk`iMgkSS%;MFn2aa9lR2VgO(k`P{Db|jTP11VB1t#C3L2YX;a`8` zH_T5pUO@i5-b4{OxVB}A7gk0XAHbS00*i;9Rx(sH9EI@Ni!0yYWjLkSVgSPL0#CsY zh98dvtl+r`h6j$vkXchBvs)8^vM>JA6`eBilD2?FnQfUQ*A~Xb^UaHm-UwjQ%B0ER zqf(P0({cGH76AMTu5Ek^s(^nfoVJgBrEOSjo|sLsrwzh`STrwH$FUTr_rw4HoNAbQ zFm-(Y>Q2^w2b-+_)qLE=)an0GnmRTIR;*Xveg{%Hg%=~9FjhK5122TJP`FM~P|c8Pvo%u$PYF>QV!v!iO@|?=G?{Qg zo8=O0_X(slJ=AJuvHwnpLt-L~*SyV6;A6_9@{BRFL2V&XyFDA>iihbz#O;(En(dm1{wA%S+H)5)CO^GC^I}^ zU+)P5L$S~l@)|E1r(l?~ofOb(?rg;M5tA#1RmIdYu|nASkb71*55*^NWXj>fek~29 zDlDaE5fWmK6e`x4QwS&jH9pJQc|{Y8X;Z0EB{?5}(KP{>xXsnf&qAqI7yD4EewF$l zQo=a;a_r;IvE9;(f9on?!fLpj%epnG9BBvEqyd>`+Ls$ReIvjihN{yJ7Wutzyw~A? zFkr}8D$tZT$oqO8qv&SR8E39wb(%xIKW*e;)Xe8TsY{JF9aH1*n}|gBSPtG zCEmh^cNpE+V$|ugh~%EK$oHUE(vj7Zl+SzMe#YKk9hz>LPqvJS=XEg;>^##faAir{ z7QIq^@!quP3iuGg^BZCHvBbc)c503;)fy4cyXjUG!gC-FnuKkjmRP`0vS&o3l*KT& zh(c1Zp4|ho@rwe;czdf=f@n076AX6cDALb^NJC zh@NL*%Rpf%?;nCE8@ySAqiLMJNxF-aV8JjZ!us0#&tOs){3QP78w}gO(;>39edN7)YJSR2iE4U(5uhr^XX2?L zDg4T`GE5{~41=a=E<(co+NMS9Inw4MM6T{T{w9Mj=7jH$x|s;;_bW+)z~+l35*$tb zN!?cpiBXX2Je8JR+oEDs)I;yU_MrC|a|aC&VWyjkJ_+xyCb4ncXBA$#mfAI@XkvAD zU-7ip@ae&Q5>{s^=0u69EYHlJ1IVFRg(oWK`a3|QV!Ko~#?V1X^u-d^6pi!1He#?z zVK6R+8xw4bO|8-FFgaqoW$1u36KHtd1ovt};)=j!+rIf2eD3W0hk=H4VnpG|ghFTg z*WNPN$Ro+6@8^P1mr+RSy=diFd0fj#KcNN-(D38LjdvY3K~mfz0XF=&jUj|Ryi?+h zA5Gp(R@OO|giRAOYVfq;kY{(JWIh3eQ5t@bf)}&gY25;hK)!(yq8cdTNV-XzIG0Sn zVkM@pB2rvJwU_9N5{@Pj7m(Qdk7I*d7rh~n`MCVTL~$JRR1-#tgWXugcqY$d^M!dG zunBW$)A&*k0XIOzky5ighUC8Lzc`{r#|BE1@t(qoUFGS}v=MNxO}>%wQn0AOBQy6L zBaC4H%%X=yM?p<7hx9+tj^OByl;1pmC)<#SnK^`abefE?KEeLa^iaMG4g`n_05J9& z%FO?5dibAM?$(0z(NS&x@gJADnaa*ACQNse3J{daG_#blM9xUDvlLcdY+}=ufnGX4 zVoP`XkMCM=AO%%KAVnZFEyVzEg$8IqNfOi&stAhjh4xlX&*`J>!_w*Eso2N&=@vJ$ zBvuxt!pFz+j`Qq8w*9;RBxmBO9Pd5KA2-kbAqG8`yIr!0y8?XSw|1{jAzr_7ydUEO z^*8;*YW>RD-L$fypY%6cKl8m;zR!LU4F9KnQsh#H4Uxt_iy}*fQ%O$u7zfXi!Qpur z6^p0Hz49{3Kt}IH36n(;EJYh}SkE~n7E2W?nq+9w*;dQW-jEe5F3)L-&|@^YS%rKv ztVcZbS^lskLB~F<@2nS&0fddRG)d5?gbTUCWE7QCtn)QHuw*G?JQJlQ^eH0%Hwh&G ze5BJC^G0JnqQ%QYQkr=qJiqG6Wv#Lk{VKf3CF6SijiT+NL!{vaq~%q^F(qVdlXSw= zj5Uf$)zgSSRY|H+w3W++i*c-EEfZS6v*qsUYH>`$E9q;L+kBd}V0zXEqYH&@JA0{m z>t$M*FrL}rWj1X0w&#!IXsqUygqt$!bhWRN{+tfHGV6rgu8!$-q=-R6DMXZy}au%W)>@)io#{r9fGzM;Pad&J>TPSuw zn|a4ANP1`!BFgPs7ZWifiX%gGcO`gI#x0&TyhrM(%GM3cLlS^9iOR?7rZI~wsc1Pb zP_Ah6DynF?60h1b61uP!F%?6l1Vhuhs0Fz!PCfKi?kd{uN|IYZI-$b&)~~psuDnXn zQ?Dfv9@38>>d!d{VLQ1w9RwI!396UxO(Am)n81B=6zv6Gl~e+p(?DoO64 zNFJ}lNx`z}I@%0<0XOAvHD#koY$^IPp(FRwnp7ksL8WwQsxy5RXYdGcvLN>4&51C* z*W?uDN&<)eH6$h%Ub&GxnhHZ1R*|X+l87hcv%be((@oLSt_!?pr_!^UpdW{q*tl^r zmD}Qa+WkzPc`}hf6AC$InMQO}V1ySnMRcbh@g1X}2C38V4oxk|mdZ2ytB}Z*Px4Ji z5l$S)e=ruGC;y=;NH&W=dS>XYgYdj~Oly+WhHZzkpiu+#GTn-~YTZu8j?~lCksK7k z#g(N@bUW_TFJ!+8ceEn8`QvE4Z#z0o@8>*Q1}8HDLEYC>t>w1$yw5qjQ09_qqN`+t zLYN>W3MYe*Z?=r7J1LE5^>Xw~<#ZK0Bra)jGh>wg+@8Rck#t=&s#0`DQUL~Ul8`RJ zLoKYyBdI%-Jq@~y$HP-0rj(UDfire7oO(@hQdK{movA8CGtCuAsFOb_6LZ+pj&jnzVoFqUrG_c} z%BVV$jagfSmflyQJk(bq5;(VXijaa(tXkA=w3O=hO0eL4?PB(~S`N%Il6)~& zX{&2yC7xuG0s6x+IT*2PUI;2HwZDw8lQw+fPq6zT8MOU?Ysy-~Xq+H)D}=IwEZ?=s zwBoL8z3~wrBgP6%P@?yMGwn!NcNB^{y~s zGH*<^Y;i)8u~@9HYZPd?c2{V5aUCyD0;fQ}$ZS#?@Q)O`MJ{~lFzj$PEJm}3;WYWwo zeOq1G(b%9Xo`ao+`YJl9U5A3LS0-ueBsk4`p3hhUJT>bsAvow`L=7CxR_G`#=TPw* z3tq#KT>E4~?3g%hyEdQMIImMJAc;Q1Dp*W_3o+6XdK@_Cfakn)2 zXI&=aV11C&gR#&sp8ATX5aoRkY3^`g3J;|`>b}6?V8g+?CG>7$CkXSSHy0;Km646z zXyVigY5!QCCL&==3bu^m{vj!5BKtdx4K+T~Qps2WAhmBozZ24^nF~ z#%5NIkW?;J7&rKJMR zm65MmPdAatgk=iby=p#N8oJ*CNw7a8gxR92Ba`f5wQ^R+6?ttkyMxZNcu14`&P05m zKO~zs@v)-k?D1l0m_VhC&haAtxuPa|0WP?59Wq@Nw^d48aCm6JT9s}E6dpSNUr2dy zS%ydPWN=5(Zksr!&sWztd(V;-O?_yyrzi9*7I%|pu#dr(s5I#;Wp@SAJUeaRHxXO_GOYTio*38Uel+RrgE=>%e8-9k zkLxfoh-i^XPKWePwNz+@BYz+bl}SZ8-KETX1J9t4t&k^Lc5HcnJ)x>bWEg`ajMBl}XumXyI}zU4;uYAb%4B0`I*i z1_%b3D!PZ%lr4Ca#M*U~cvVuoy^;b&$)u;~&V(x4w1bp&^vUCcy`X!W7_~jTh$3&- zvL})!Ya<{>;UYd(d&6;j=`!yyC#w@vqjNZNy{J=KH7-0dX&skOg>bJ(XX5ccy*uxO z#gp9`Ci1O$)kPbIS>hTq@;;etwbvJZyoU~!{2j*-=aEmqgPn`(yY6S5JpVcI@2bkR>6YHryO@(8<2u31B_k(5t8z146qPU|FoKGe45(K_ zc_-QvK&x4q|MNs#cO+VS>aZhi7?@!@C8F;;DcOR}E;Y>QD5TcNKZ&IXd|B{IT=u$M zu_^P&;NN%nxcq_oR^AI#J~y`Ixtz;!xx$KQtCIi(znZ)fp+m6M>GrcdP0{tu?C|y) z`O(qiE4%T%#O^2!8vP`St9!XX%!yeT8c@1s@OLbhZG)}KQJGU-0_4V(UI%W6CPqaO z=d3F$GveVaxZTiVk7;xN#Uh`{nXnz#@cmT=3BK{%4&kZ1vFxEr%f^o(ePO2QXv_-Q zMGapd0f%|Vx?IaKaoVN8eUG_k*%qC_o$HV=BCsnp=VnfoLyXlk%l{Yb7IChOOFly~ zj~PDt5jI>L;-l+esMuA1^S=042Ox|ArU(SweBp+;#~s|{XU_4F-;& z#O~SgBija7mY!b&`K~+7xq4Wg^~mP89p&WlW@p6%X2}!K@NG=N4<&EBsOW*68Cy_L ze@qcIGE&0$HnPVL^~mh?xAoDJZXHlh94MQl6VvtVutj^V7d3h<1e;^yEN+zhPVTEz zP&UJNz8afSAl;4Blq1@jY(K>x zTBj}QPp|zI5kPd(#W7Cd4F-3w^PaRw6keSEa8~q+T3L-SsMGcmw5l|Ev2|kgL~#w5 z8XZAS0i$#_!WqTqD2{@TFxQYoTZIj+vw~H_6Q7j}OPjz{hbm6T=b6JmFQsi%76xjy zmkmOwX7vqxkHk5`pvM}{OVZrjHA{?@dMsuxQL*I+3fMZq*vhw35PLSozZj9_3bTyp z1=7W=56w9J8#V=5VI7*?JV$1qC*~9IZ%g&Nlk^%ue)>DRh^)7F7Rho>e=m2xFIGO> zdBoZ#d2)!R8T2XzZkMd*Bhs>Ow;t-vg1|Od8kG$Sq4&+9qid!)qpj6k7~DX~xsc0<$k3Yr58?-NhU3cw3UQ0@>-NM3GNS)HJ(UC-D^-eCKhb z&ZWUR+XK^Fm1|P5H+yMv$+lNm)lckM1Lt22wNCBF&E4B(H;m$B%%KvXpefF%)9~|$ zigoZFiJIG+P!CPtS2UDCX!vqUIp5HEGSt!|FUS@W_7 zb4FNwLzgcq+8-XZ18WWOH?cd|t3h_i%iiLDJ)+59ynxT}O?~c=Pq*`V{C$d_$T=gi z9FO`<9rUGv>I;HN313AG=&y(^fD7F*0v-bWnU#$rZ**{p1!pUxu2nCNwekoE#~Fp- z>ko0Y^v=E3kez>P1hX~8of=~tQh=U)-sF&9Ljnk4kL$!9b@706f#eL61CsI5jY0h) zGjKr34E~N+bMqV*5U8E1I2o)x&vojZF^3{=XgV&jXi8be?ZV)!Kp8N zX-DT44RiWJzT@~!s2I%tE-V+w)T*RvT%Q<`pLVS?w{?g9=>Yp}^R6BWT288N% zS2;WvXu(@`m!-IQD`^GJ)~O15p)X>^D$z4DpvTTr`7OX1MyX6?S#z?)w0SyYgl2L$ z@sItqo^D-X{OpE~>auT+1Y3Qa%jOP_{7qYNLBbxbJ12mdHT0N>JvwsEx^X|uvh$M5 zIp26GupQb{>}5Ljf4wyE%o1E&zfqege(;1w{qFdCmx6EUq-&+Rc2AUy#3O4+R(iOm z6LWnm&f`gz6}XR*Te_dhv=jl(JRIq%6hL-weKMdGPRn_srGv0?U{k%uG+h$F^n>r% zLq2(CIDH1`vts&0z5&peEC=PdHeDQ70;}1go_lLD3`S1->iYY93b{;jVrTp+WhKTc z6OPc9mwo)?T26R;uq-7cP%nzX7K`P+=Gn1G!Xiz$QpRAaQV`h2*4uQFZK$d@ry zhF!Ao=B7^;@jO~22WFRopCN7Q8~dsW_T1suxTqD+|KNzRFMPh;{5`evx~3~yg+`QL z48{G4RLh}Cxz1{Z{p+}pEMLLDVvJ6$Qy;-AZM}3xo8oRk++IRU zmccKojoY>${hrZ)9bG$rz@*`FdrJBGaBoIz%=5KO+bd~!leF1-G&%x@mByHBw4mfQ zX@a}7T)?iW@$Du;H@>D?4PIU+Y*Z$K%TmCm;>6J8r!A7uXbe>62&*+nq3iU}781~n z6KO`|EC=l@#br_qf-pa|G()hDl(xtw9W~VU5*v`<9d{Hp|B{>1<|Na7Ds-4;bg9i4 z!sf*})XwamG6+XA>D-Kf`NZTGAThes%e@0NK0yEvqFer0rL1<|lg4PtKnivb9IWq& zg6)}y7r=qN*Y0s*`J?vCn%whroA4qJu41J56n((@=!SmDUU>$n06`J8Yn|}K_$M+v zOvtRxnuX&AKViwBY+7PgKMut&?Kdc7sGqK-L)9nipTpN0Kn8`8V2 zzGH^e1rp2bLtjH|13X&a8O0Taq_=_rh8Mc6rm624*S^IOR9#{E22Z>Px6+~x>I=24 zD$+`Sh8=6CUnf=ANp`f%QC-e{3GC?tzA+IbrSFz>6|eeUGg8pEkmr3SW@9I$nl zM6s)kGm)7U#~RYlGlUG28^=fGLrvHdjc{8U;Er&RTBupmC)AFl+W@`&0QNLyC*=L7 zt|dN2P={QYCd9ZcaroL7X_T18JvSbthf1=muYETj#*e1(E&VDLijzS4NDKCw8)98t z$6}Siy~@DVi53voIR0O36(vpJrBlg6E5Z#n+N(z?L+n)xB^F)frEz#z$8$-JC24)!xV4-(9P{%yy|^&G+i934F)mz!#}B zsEtww#~LY3%F_0^C5Z2O@>N=_L;Cz-0TrKAPruM)k&i%{w%*$Quh}OVa(Y{b-*;(3 z3>97`yX3g~aKS~Dj#-7VviQ+*W1{pt*8v^>8Cy7YRu_nh)4=4X#vVBu%{C${)rpC$gFDGGE(h{W~fvElkt z`>dz+V3S5q7-Yee23+_7)l)C$kQ>Cc20;unzVQeDe|J=cY*JN1bnQwjjN%Hz0?1+RjV~1Jy4D@{eGrz2vrLdI5{4Z=P+$>{MZ_f49Uj-RT4+Zqmz!O z9&Lo_bV};QpzoPw#kseZVGLI%yqZwDenf0Nz)>E{fx4G#1a=;Wl%i}E8e#TO(d#SU zJp-xi>4CnEr|v4;$)d8pRRr2ozO|Tq$3)y7cthQ&D~8@uzXfdV0iyB`#X{W~^H8MX zQwKud*8LOoR&U3FnhkY2kJ|3`<3shL-|$jvHxzh?LsC6bGO)R zE-$XM7gn}AYHO>T&$j4l4*)@Lyj}`>*~+xOEN84JeEw0~H01ETKWiBT9?=T;l<4yB zi^6OXu=U>BQOCw-sQQ`*ZcfQLi`+y@#~EyS(y%4LV|~F?vvp1gn9AU_;56%<$0bY^ z*lq_U3AgSfZsFW>x!$%H!GTI|R*@%x*fD zq)@6C?%S0S1Y+Khojk+2+O=NWEaCzxzP=B`+sxlA@sXqZm(9maSjWV^UDP`$>}8|i zez4$9s)x~X*0N(Cmy#gl>s7E>FKfMvmib_F>VT~;;RQw>2`$sf=3~*vh6MS~(H6A( zl>LoHffto8ApHZT)cVg2BXSI8ycG14$*y;lsjaBpw*v*2Vx~yG878><86)$Bt;aZv z$Qv!Y+xj$aDHS_j-QfpD0Ig#Jf~zeMsIYNF(#t4v;+CR`hapRXi0F0<9_L&>d|R8s zoi|aKp3LwZF;~)XC?+E(ojb3N%w+qwh|jIxIRTKYkY|EI&n0WM#SG+@)IpMKLl6wf zZDUGY(jUhuQOc7Oj7fldb~#@6aF&2KL20zIKTjVZ$P1<>GwYO6i;2I``+_0YWb75H zH{fHDXbG8C*wn@kho2pfsNg1^zpI+qOF`0e=Unw1paFK>t6}qZ(aO@ZGGJw1Bm=M6 zoC9r)vb2Y->hf{ca#Gbvf0Zw;z4GT$1?>$@sskwR_fKL*?()MJ_dJG6f-I5vxYMD+ zXbof7X9{8QM7*{wnS+0Sy08)Qt~piEQKOSIs1;f&5f&iGlpe3mp

(GxAx-4#^;MZaERvp{t zRXYtjBNBOLQt4%e&^x93P+bzj+*~YOP^)!t@|Y}GT=T_y^jO~!Dos9Pz5romml)9zm!YjOeU1Vxp4BP9h&!e>Wb?eVpahWoZzgi&;;hb-13J=7)%2 zAgegZ&9`913R;xirwENJ=dIU5P#F_y*z5Vxxkr&4hf&z9n{2aD%}wQK=o}Xf=$|(Y zvA|g!1KJc2BUyFAOF<``r!i$|5TP|$N)vK3G^ps5hL~h^XeLk<^TVZFEDNMms0+l9 zCrO8_!WHXE0+p5`RWmeb=_b+oqXu)yR@%a{G^E%4OXKV*wK;5L z&g>(8(HL6wG1ndugF-c!{C)sqfFH)dDsV%_wn};Lpk+JjRQp4Xz?0_&103Eb(ru>h z?p`(6?2Jy?%!fiXt(rG`6x!<^NVB!nlba>X9m$Gz;vtAlH9lMmP;eQFc$WWMq^Vd; z0T%$@&4@r(fSCI;u$YUQ4Ad7<_fKqr9-l0pIB^IS^mo{vW!JBob{g+m+I=1{8n`3* zbBtYRao9_dKi9i54EW23 z(q8ekpEqQGMF=n!G?i;~3@=-Sx|_6FX#wA{_{3QX)l5jvpl;*ENP$&%-wTU1EwdPn z=f4%egw2AqFqxGu8$DgBEm51>L8#R0IXLKescCA{(_s*w2n+cQXwrksT&}`nOgmB+ zaC6I+#=1j_x8kJqwLENh=9Wq?fX_6@pfN5-YVwDj)@N;b?HJm*d5WQu z3MP72Y30y1lKF{1a{02&R_S?>ffum0mz2_-Cl)F`yeab2iq&>SlQvI~hb%I})uYvp zsb-UmZrB_ygLrWX;@!~1RKO}z%N#LYw=K_7W@gr8;E_*i$;TijEniLt7Ir9p5w3}i#3pga<{s(1h>j5ik%wt8hSrQe`X-F zB9+*RES8SgiMxYgw!NR;ffDZU-{gBcMP@hEkqe{qrM99vw(~HuHD<{XJyM0Lb7?A57)TpBx0+E ze!v$X2)i5_2HsI|Jy_B7v9f$Ew_(%@!s^9xbpc*F(6SxavK(KtymNJ&{%bU3v(T6BflHY1dimJS~S>qh`~h}**Ay4CPmg9l(5_+51jv)p6kr;DJsDL#qEInPr^3*N{xM zBe%Cf%S9!7Ai6P}Hd!l}sVk*E ziL{c~RztiYOJdNBSYd;Z*bNLCh30UZS!B0OsBnTlPLew}k03o2o2)c66Zjh|9b*d6 zQhJOYA1!KZ!U-t3RN=TL6v*B2lC;c7wM+ubgVGSpCm3v}fBKf>>6T1F9?|1*qDUxSwLSQENAMag8JEDUrgv1cv+`|xq;5< z(5HK5ps1yaQ>!c4@84V$Ydhh; zeGD~8C`??|;`K9?pSJmZj_jE29RaT6m8z+`d&$h~s;Ohi(c+By-)wlM$EWt^^Yv`V zP%+}y2$g7e0NGE%Y_s_DzVHnpydtOkQ+xb@$sS1oK2Z>US-u~oSJV8&LjE%cKhRts z?p1vL1a3b9Y)}QKS9JCQZv!EjYhAPQ3 zfyw7IYcazf$U9Tbsf0;7-Wbmwk(&-ThG^Z%-k@!<{S{uggif^QyZsXvhyO0`Wspy_a8UKB5ZpB>Ov2mMA@11HC|R4f%oSywu>p zcBEUqOBU(zZ-+l9^hCKHRNN^OsDVqQB*@DXI`;t{Y#BQ1)w?GBy!TD!7;^uPaK^0NPwk4z`u z#7+n|X^V!H z>)XrQ3myRhKTD`*S^Im8Y06TNPb#UMXZcbriG7w3Lgyl>O;De&ATfK+InDV6{I#P$ z4{v$qY;LkX7Nfy;Hlw@qv2*IXbFBAu`B=#g;DfnZXd%WHu{}mPcl;`W;uj?zHxy(8 z!|AT_LS~wnI-LZ>45fP0sGtxRV}vm~=G&4)N9BN$Wb&F8XfJwC6Eo_u;p{Z~sB{$! zk$F3D;%(@|FO^J=niXMGtK`&_+KjdBsgFf^=0;iiQa}}ivZ`i4eu ze2Oc?6i$4hApdH?{Y>8|&hvB-}1_SOkTeA<> zWb|Q2x?7Qlmfb}@n6|rA3#B*^?rWYwc==_!QEtlY*^@nZ(3-0^6YBrk6nH?rD{s%P2tAA!7Bb4VI&1{vQn7S@u@ zk7B%lwLi(r58y1Dafc5@6c8*^${VTVKt|9U$Y9~?F6L(Ai{Qck<1-hB8nOKQez=ii?3tBY>fN=xsKUfMU6&CqWpX-f8qOCs3DyAMXe`7E)as&7^tmR&>Mw& z@=r}Rf5fmUGKt8nw!Xedk&}rxH+LTOB+eK3Vt0x8$OiBU>!5fxYg>3+9hSU?z1JHZ#MdnTDNqYC8(jC%9=~u@L-uOx#Urw zamEMBRD}x9J2X~PZ8SnJNxm=lQ!o59>W$0 zv9~0)@>fkWHD@tc^T6(c!gRPidi=QYt?lhj3ql(y8Y38FLK~EBMC;?bnNj!G zYx6lC&L(#Y50@}yyg{IHZmKrtU}x2W+83vi6(=jky5GFy>@{EatWyl}pt+ay){vF* zd{PTk4c&EHewQ^@Yl=I`SF?vji&s}QjyN)*5$PpIleji^>E|Z+vWG? zAL;|x0S6FY(HtoL51v5PJ~8ja`r!B^!Zq`qZfGy}-YrxE{u$h`Fnb4zzj_W^;EO=M46eEtM7BVC3$BCrg2Dca zSNrbiOHsYw1pL0@0>U1qcl}={!K*u99aLib~sC(*JU3W<786(e!U+>(WSkl!zysoGYc@8zRx~E$W zw*Vo()S$A?{9HZnDb~7mWd%lS-SsNn8@Tx^AqXgRAc@(XhSLrUr8MT!?ZV9Cd2Dk9 zeb;{knPV_mH3V!!c26)^N&%M|eslQNIO&3mb5~{SxXte12Laf+g8u4dluIZ)p&-7v z7guTr6Qv-I`82qts<1a9%7k7&yXBmZT;q%oOfi59Tw!cpM$A9_F9(n_SD|vZc*B54 z_b54sZzR5CmTOYZE>(Ft;Jk8;R6$uMJ%UILV23sGzDvJsbKf9xsf4r5x1Tww(Ru$| z{+)JK#Q8lDKb`Bt;yxYL~pD{=%t z*VHW&(7}EPjsSWKS$!FgYV0JwkT2DX?R@^IIw?*R*X;4lkkl^t>4<$IM=UuW;3O?o z%s?B(Y=1yPe5&J#2l(&^8`}&}BrGOCGoJUC3Es6(9ef?}=t>P!@NrFY*2V761~>zGl;aZthlOx^VAU=t^l^Bc~e z{2Ts%H^7B||6rW3Il`9V0RZp>{tJP;x~ZkPg`B;K>HqSJFV2AW(N@Lm>rE_axAj_e zt6c!Xj_uxP(pEPB-e62AInSV#?I5wm1Rl}#THT?QmP)uOhM>sH<5?=VAtE9WUQ0S1 z@qj}>KmbHV{5_8I9}qx1r*E@9WK!asNBw%Yz43P6{deDIE<4G6pB}ky0RI?E3K58R z1jJd|rHPlQ2MUyc^Emv-N z871U6Kom{GJ|9s4))d|ro*!a#x2(vX#J{ULP;<5<;E2bRmcLN7rMk?G0tIC4gqLoR#qDy(-HvWJL%{O9v+u{74%t*a|0b`|y3E!|vIlZH_C;Q3cj zdEzws-7CtbnSN{Yw7IP8K3KS3plp|>9L6Q{pkH7L|30Iw!hmDTd;|Fb9xGjIV>J0Y z@gGsi7ovX^zmc0()C=KURSXH;B2Y44*1(c}lxa0l|sb8*SMa(~AU&Kh$ z8rTLVo`hjHs{!(1@wq|Pe%1xAa#xy45-soIdAKWE6-+s)^?H^Sx|wQS1exMcZRJ8I zr|G$km7*j}$>PLnDJJUDM5O{%$<`1hVW-dy3PRFQ?Z4$m4x>UvYNpDC+s5M_YLcXo zQd%*2ym0$S+Ru*Hw+vTSgRdWs{MEnIZz>=(;ss;-k8iOci(C~2z`sRQ5 z^Dr#oAKM5aSWwvl6|(FLxss*Bn?E!YIN*?T`?o11d;i^6?ht5D@}X5@l!q$PCWwTL z!bAA#%MMXnCNZ(>zKR|_mLVJRL8P_~qTHDuy5wv%nV&Zj2W?5Vlr<+p)tBy5-ryr! zpLb!J30Ji$)XcJV7&L@CmziA9T3(?D<?*#y4=yE4&f%dQcLwu)~asfKiK;CS{gy**sMUvy%pP6U1&0nCpe((qlZV@Tj(jYnb&4COL&>n;Kvd<@rxc45$Ve zg;;G`wwt4d+g9Es?I_XH(QI8QI0@_z-mUAXsSu(jzO;BT4}#K!-K? zcA&>dnGjiO8!JMv@6$O=C3!pV;3a5(+1Z}|Lh+H7q{cB}VxiJD75aJc!F{Oo=zZ;EGzWg|6wz%7ZYHn~q z!wcD6qilQw0=ki_+XWf72AhrGezY>93U+Wsdh5OdZZ*`!K}2$FOV~!lK zsOEa*z>}{aURTTMYz{A7{`S*V@HFbg+@tJrD%^=ls&*9i@g4gNn!(6myQ%^aaJF^Q`4G#40Lzzgn8lDa78~0 zDAYoyoNosCuTNOeAl(f=RSf$PnIWx#Z zCvmwCVw6gzVJb{Yfu{<379yo0Jxz%1q?z)=wH21vyXf4gogI5bdxr9bg4JN;t_epl zNO+wSlL+y{gw??i5lILXdS#}*wV_<=Smr5lNRk*XkPG4CP$!s@5G7FL_l_Rz5JeW{ z?oDamnf7doFi9wEIyp5+sKqIRI8{i<5toLUkaFr0BuJ?Bs}hAL62_02G)shxOX7Uh zgI%c%hDo3xA&z0i;{er0nn3yDK;1@~P<6@Y9ZFB30xf#FiD%_=zP6Yg5hu{k5dP1Tu|&GEXYxqyuy+<+D95Sp6vUK~Gdux1wvhpBA`PAtg=M zq&h^uI1-zj#kv?JioBW(ITsz85^DZHpJzroZFte9^)iIClYp3mB4b6fQ4U7(9XSqY ztPj`2S251#l+oZ5w72P&yu0(qsPy{0AcbB|RAj6Cogo#rwE+HO-Z$bbk9Ep^fwpd< zM}Vn`dKUSd&vA&eyNF;!zHq8qF^7oO5-*a07|qgjVEX!sGCg5Q!Q+24OS?&u68j0y z8pJhIhBI{T@6$mrzyn|r;GGIJ4(M}yng!3pd>qw&a80vsJrbRA*LXl?=9M3tQS(eq~R5k%l&)q|ys4H`rv6 zG*o9oT0)IWp%Dq%oDone42oA)$sjtN5oy0MbWGU#*P1*r&#X3 zbS;uS;YS&sQD*D7CN!iB^NxHRWs5>{&J7+&k#n{(tDgD0xXfigSX$1=b=^`aY+#k> zziA=`C$+9+V28{&vZ%e9gRP!+!4IK%={*^*0?oiJ11cp-)a8zSdS%<7hvnZ5xHGt( z%Evl7P^bGYy!cxCK#`=jA*8krU~>G+(xw7hu8A`SWTpg_=}X1wBuvjYfnVJiyQvyY zVIgRUBL>~;yNqDFe}J{uvXP+u{mfYT)%X!`#E8LL=AIN~YyoYu<@U-i@;C-MHM)-L7tka63_Ytlh4ZLVoakdLeph2)_M>_!U5U zK~HM}b_eeGgYefIzS+b4iy(dhL|8DNrZBy*rOZDtU${NF6imYOp}A>ylbD}SZ{<+r z!Vt9s6*4my9q<>eflFQJ7v7Hc_G{4Kp?AI#oyfVNEvf8D{4hIU=AD)OuCqS*Hh#lL z5#s}@45j_7?M5@7QkALZ@-;ja+uM775O@3ezu+EJlaHR#rfWG~8Z;qTzFaZw+<@>q z^DMU61H*fqsi4z*P`y6e8&RHp{Q>6J+O^8weZ*HNyMOctIUihhuJ-kQ;totX5kb`V z4vvLSDmwM*f}{;8gUfAOI1)NwoBLc@Va)a4=nlMA_Wff=U^=uwr$(CZQHhO z+x9A*`u5q~``@QeMEBnj<6>UStBiNf_ZgY_Wb*7|!e_e9CesYY6G1d`VP$&gV)nhw zODirw;1V#MhFXfu30fZ?j1S|TEYk;5YsYFI8^N|B@Hh3f9O##Nun+Fny>Ryq+)nsw zoA52&E~v?_j_zxU?RSISVXRwdjt=nmPcaSXmr<|}h;|c8BTlG}9Pl^h$JSua@a=c+ zmc~%aK?bX>n{9)|6jW5`ioYX<7+NQwP5LMs`aO+|AsgR)PUdLQ$EXjA$n=!x^BvK* z75+m=dDX7m(}5feGDi8Y*A|Yf+MoO%7xuW42cn&$$BM}q&zOaAmXFczU4CRAK#T{jicD%;X${c!qi1{JC!SmJoBfX*X)yE^f5%bM!p}R@(-kW(FqLAHlco&X_ zd4}O}L?gXX^3|^&&pm_UTG`m+SrrbGepwCn48+tNIgWyTz+%jVeWc=X_&Gpa9*D{u z3UX$CjEZ~WxG3EdkvR}nyu~?X;^d6RaEBtP&%{)o2+8b;vf2{H9E##jhGh;#IL`8& z09*9HWS|dN|vSgE3IK$Vm}EqCx7Xj?|Y>fVI2s< z=Z13?db7I)KCy@gIi`1L`^@{x8}`uz7VIa?*cOJlw?+(Z6CCCn?7w&dp&y=r&43nt z`Dc;-0~i2+_5W1q(6cr${c*_I+S}L~**lsUIYcGO+pN>WkL6L^M1h>))RxCfFvigOt5KXXVXHZszK|0lx{vE-C&AB*6NgZTaJ!9ZBcYo_ zC};D^PSj>ZClFpHEj0Ryb4u!_56X(mycNnV&jYgk^6O3#1~#Ge(M83}WumbyJ`*Cy zYO&vPhkQ7}bP*XXF93-*Yx)h0xPxj%{`NH8;MDySZlf>VV^1iuu5Qwf?;On#kB_$v{R7RMJ&Kcq+5wQy`m79QtLoV-sDV+Kg-x98l?;E)2 zXR*Q;=W$!C+J#MZ{Aa+^3*vRU4kDhQy3Uvm1X3aImkgQM7TFZ|VW(JhdbjbQpF@L1 zYO+!dpTh_61sIPVYK61|B0)Bo_<|NuBW#5Vj4$CH4Nb{fJ#m?9w|@)!%z;~*d#;)e z*80eg{qC?@J`doRVcN6S@9+4-1B#)Mm@{e`RbA>csd@WsWJADv0q6*JG81YOyPv|j z?$PkTSk%yObgVN^=<0K6-2rIsgz-L>g6s_1M`{+;s#nujIQPOwik=LjUKF<(`4WlW zQ_o_!hvZ{Vww(JtFd`Z)y@_)f*;{wH0d*T-u_&eoV79gKkzV)1JJ@Xac}fqa053$4 z`tB8_P5$#_^@LktjKi?IbL_Hxw}=C`&diwJ7+zS>y6S@?&8UzVOT$smksq#(=;E*; zCM`6nTe#?2q1Z!x=zzk~p<4jM3?Hpn-aT;og+iAv^B^zSuyJP&MEw})kS{6*x+Hv(Er7K-DmpS;FCCWo~^ffSsfVk(p*wfeEweEUQwRm^Tp!> z*h}-O-Unp&2ZkrwSZXLvgcLSf(aw;8#!!R?rXN5o2W2=r#~N8+kzS*MS+x47qDEzeAhB=`pd( zpMhpl1t924_<`U;tF~-ww}f_%34_~j^nv25c1coPiVmRQ3fVMnLDCWQau1k^+h+#m zCYY?{lgGd^%{8HU;?L}7@nF;WY1`mnf`R>khBk`pNGOZoVFE5`+|xs_F0lSmHmg=> zHn=qzR=txpCr+k>T1S_4=gL4mx6L|7ra013>K4Rpf$WYj-jO-9VYBrCc*%^vX-v>q zlAd~|-KnwAzvheekG7+|3TtqVw;QuCmT-7R!qPsT0sqP!pz0?GQaABmHmx zks;AGSZ`jHrY16q=>*skrRLME$wCa7DuVT-Bg{DS3&;=z^{UlEptep?jym;8Fe|bt zCW>>C#gs^<&Ack4dkCrlS^1k>oG6ed+bs?IP{@3Vnb@RsmrLrT+fr$5`Hegh3?i~F z!0TchBH8ssme)3?cNN^Z2^g)4Oh{j4p&)WtaXg$YHGV3`w5<1_GY47 z*-}?LywX?r#1oAkE05}vQ#U^6mOM-oi`%&Hn7goW*}JsxnLCVu>?dYLlmT>khHzXI zmjhgsgVh>+0q6vMc7G3LY~h~p1Hy1R;JIzc(2tM1hq{tq9Y>DI(b0!x$I9#SencMh zILc_sPxcQs`(Ba6KChDw?Q?uYli>w zwPHt{?HK=23gIQO9vrgnO3pA7*NRr`&s zO_%SCa2KubkMNhwq|x^`6hz}a@F!rO9${(xG?Kg41>qv%CB))92T5(qTRF*egnB$G z9ih58Q9KDn8f>5x6QJTn&8jV`hQ*=7;-}x=0!h=?3PlTopFG5OaSb}B7z3p10!Kdi z-#eJ|Ey0gf=!uUm5+2QEvoZWno2{Y44?+R^N?DX*$~00-f~{1c zCnWBWKJ7-_QCbnEoE?a9$ks%A$8dA=Pi*O7Qt;jl`SZ%yR;U>a(ocrSLK`?D|C}g- zbvC|ra>9%n3TPMb^kp(yZ2s0T{7tnH#O9Ab$OkYW48xwJjJPlpcB*<%Ywp%47e~)^ zna1h2{5P6Y7^zQp?T%#q@aceRwX_80X_z2ogH^0=q>;eMnO zKus#pLTtVBLFQWhC>1nrX+fBYrlJGs9q3o%kSU+3LR`2iGJ2F+O_(aN5~X?jJP7DF zaXP+HQA&^-(*j}MA*M?#zBY5eRQH)&A2UAx zMY;OtJ?n2*$uj!WC8qRK1jYUDy=Mi?99_&DjQ-_KtDyBC2~R+plooL`zbM1QyYWe` zBjghZ;g(Yu7h?)nU-Y=vU}>4ACY64DkCP$ceE@!uAFNp!$Y~EslX{#?ww+|RIrw~h zJmd5-aYr)5csOm=7*s@q{R&f+nb6P8Tv_(K>_&vQ0Z!&ze#MtYAJxw{PxThi)sw}- zc?M~2@esXlkb*M1Z^X-Fb$iF2rzm{6vVNKSjeowHhp2XKRPw-%(L2YFqvcH+PNpnuiA7l^1PJ*&T}YFip);|{AJiMT%S zainLdNl@Q8*%+m;8#KOWn-xsg+gfeNXccRm)af}~CPs`TfEcPFxj%bEv}ogQhWW>e z!{}%g@0d#w)0DmWp65`H*@HaEs$mvudwsGFSVE{!#x?Ic`3LVBvAbPMnmXE0y@Fo* zx44rfB0Y11gfI6o3&<2nljD>IFbl#^&XeYxDpfAgE!0fBF}8G(_ec+hZI)^3S<%^u zNXdW^Nr?+dTeUDc1<#F9wmw1r>(26zwwV*3SNTtpo-NY9|Ag4sSQ_bB|JM^zrTXcC ztcv1o3-9=JG($c1)v+$wSe z&0XzK0VOV!z<2zsySfzG&|`R@Dx%>>6Z-S+gFK?_$Sr3#{$2#b5duP;`bOvP_qk2)V`5hn0%uMN(#+LdY`=jo6A#OSrn=BydvT zl)w|UJQe4jjhuW#Q~q+sVun$K9Hp@t%rAj@9wH>gGuDx1U21zOe+%_Y|I9%#BM>`z zuX)SU*d^?p9r{fHxPjZJZIDGx{A2?kQus|$LP=;SKH9HC6w@cDPPFT*O3$B~F0wCk z{g#LUFBHyzVsg`@m~IP2P9<2En39t?a1WDJG$eNpDx7dKPe!|v^rWm(dpK*@%w~{4 zDlb|OUh6@z(KyBd4<1<#Dyeh;F$gc4WgO=TcII~&q>t!K%=0SBRH_MGl|Yl0xxRqr z!NR9Sy?J z-q&rs1`HU03I*69IUM1l5a znC*@RL-f`PW2a9On#=rscP-^s3q$pe2V-Xh&agA;fic-uYgqJ_d7wJ|Yd653-7-Cx z;x`}VLC9-dB;ElI247zXqc^1pA5u(C#0cHs0R?vQo#cnaaD8gAZR*|_=4Gq3G8M`| zqZ@_lUh%$K>z_ju2HOEc&E_(tO{v=EmfaPE50PP;J1+kH;J}>;R|xgs0?mqY#xGwN za$~6clf&Y&)D>ke2%khdq!v>qD2++;foG2_PCHQwU&6r~6c6O!0ajCs0<^o_VlV6G@KG#U$j z_Cy*KrgmYDx$Hj$j0hBGhO{X-xY$L{N7S}W1L7fd5&WO4NvvdAqJ}qx>}aqhF7h!p zidB0l6_g~vD_6gi1Bk|^RB`*mN|Qava(Rn1)CGyfm6|+5&fWtyiK~k}sj#?>)wnELUzIn>4+i@xzBsrj1A8xy zVh;al+IQ^uThQdU;L^=s-v|2w=!U;1n;*ZOXCVW#DlHrn&~Zj25od2L->*ZSgx7lO zJPTqImr*c7;Vyol5>xSJJF?v}D;){?^kAVSjENs8>b7)zxWv;AYZZz$&>eCi|9zg1 z$MQSXdxtxW7#()D5~7Q_#8~8jCw~O+PAlJJzB9?Oj4S?FKHUp@iuu>!wvZ{vp6x4a zx(x^T%q)|LP3$&nIuBLjA1%+waw$CS;=t{m?*2@~6A43zO z1)RHuAVit>Sy=CUY|A53^fW{~%zmNCc1Wm+^&&rX4PYOApklVu9jf-YS6)YGC1Jb2 zT2wHWA5-YA8(7J}(v$6=DLPT0OZxCsB&l6G_elVp`|ezKva+QnkXz*b_ZWcvqG)t= zP$mH-?L7n>$9xMA8+eEJ0_kbVeFWfcC>*vr)T^5z&#qgzLg@Cp3cil0r_Dmgsxw|y zg3Ii)woa$l4P~>3cObTdRrIslWue71?jKtz=MM1hF|TZK2F?ii`b3B2hAh|!(}aQj z$^pa3{XNFCr&P!4vBOeb`-+K5$q2BP4zVlH>v{8cnJ~8!Z=DLfHOP?YFRo66i@cP2`+UD71<+uI zl3C|6K;BTXyL0*rM_zOsM}fQ7Gc<;xNtjEl=keT_(!z3+apKBYbQ`Hoq@yNSDDr_+C_owz&v&&moj-ImO$vqhbR++QJs59 zt0JU8F$i4DO7U~FN)PVWt|K*qe%q?XXeLDgrfuul<5k0tT)&-yD zAaEU<*;~*)o>l*n!#f)5{@ZR(qg#JB8a4+)#`lYrcGd?t=b7pR8i)SQg?p|k{Q4rc zzwt9~BNj!anzcZjmw$!# zkK-b*V2&?v6l^?DNWc6ChD%-e7P7cqMg~8gsQ(~wWNrV`8kH5>Ap^t!FI+QV%?gK`w+ly}+eb2_B8N}l=PxA2w}x{S zpv6@WBkvvM54T$Y{~Sz-e`m{ZkFB-CmEH1^^3yoME@d05O9-!6K!KK;wP~s-)>X>k zK&35KU)5=(dhV&cHg1-XZH+9c;`Bw1vZjDVLUpuCZf;oYK&vL+qFtZFq@(CJHoPAV z-EEQO#<=blrbUp#?GqQAI8J@G=OZ(wf8 z`*g8jddfw1^6tnmpMUk!3an<=@Lc);{CA-DQ$|58sk0H9n9OVv2({lbNYu1QtH1ocIm^B$MbT>)?*?GmhA`v>BtODY_q zKRzGcKz32lFs7L%QD6m*P?lBUrc(}CSx)RGu;%lMM5B|aw590h-iDsO@(nc6Y7Cu9 zGg}NT4Q-qR+o)O%CGA%1a+^*C^txkQ=e1j?L-_U9VN2tpgnHsVzVz@z}x1HOz5Faqjc z!&<{G!GMh@IHl&~!;L98W#-24h>w*V0~>VyJDxF6tL<<s zPdu~!2cClBW)yO|yC}MSM1n)V5K$0kB|e}nt?iRI2b~ScguPfmaeYGsHXD%$fp+)A zULNM|+P+>rJ-}>(2WX!=*x^@e6Ct>9ci3~p*(NB;w9QDB>F<-v+az-wXOkWZE)sqS zwu}UfV1NWo-<71NhJ>NC;?&8^&YB5sW{? zi%cqtXq9E!weF0SVH7^fw{LtP1~|0R9H^%|9qVL{MNo+B?P@BEuQ%p`+GGignaAdN zz|-HigER17v7{oXm;DSgv?~ux6P-wonwl0zjpGV=(o*Bx|}y{MmDZoeY0*{ z(WFawmca+;{yW~<3*LUIKX?oL=pp~^?ejnXDE>HOiEV{%B3dRUEo;5Zm(t6(5JbjyRMGopLQBXPwEdhm*9rI*?+0>BS3lz=(*eZgo zPcSdNGrdtdF>p@LL8i5um=_pwtn>7|o|Shz9MTfMe|M;8iqm^7%rb_5c;y@d21H6- z`Y4NkgC)Fg51kKSXmab67DoDgx!`qiNZx)}E{_9QB z!xec6O6*v&F0 zgg#a^+CC_Ob>uGTCU#M)aHwWbt90lF5qbYf{vYoUbZ>0{FaFLgitTRnel5H89UO^HBG=!O?doc;^|L(WgQ|hA#fF^93-i!jL~R28j?sZ*QNh-0;npscX}nG7*Ez%l z^V{o7%iW}F_A1~z$?%p+j2IC13M100Pn^;% z%8IItP838{&K2%j1#=lojDJqn>dS%G6(IJe{~kvdS1%_a7OOx=Es%0p1Q}CcF`B0% zuZWYY|9FKvwZ(rh-m8>WZR4kXpgbNM9cd2j=s8g1XVCW(qB`wk^n;>_0$wCs< zE0(@kM(LP2jKnA~p{`J$7GD6KXVpY8UbiG^S`#*u!=zw3OAL%!q8)(ASWN5^-jXX( z>vF14>KGAoF;6zeI-bYAR54kf4|7$Xzwnh{e+~t03!$iEE4Y*(zeeL2fe=5=JD8^= zxwz7t6jrZFg|uQYBMDRp-kyPutL*~d8@X5%>X$RbLPV9()%z{ZbH+Urj zxZdJd-y-|Fq{&-C@v3NX$=X;jrR;w26tNugHqtf$Bn_6Q%Gz)bzv4{4bDmz&oOA4N z|9Ytw<+L0AQU;Bw&9e0Ap{a_2O)`s9?2^ro8!f5XG-FPbl!_(pyqBkV2&gkrEPTib$#;&57NiX88U;HR9M4Wsk`as`%=Cz$NGDGdmeKWt!`)3JgsgGFit zVo3>mU_g5i6MCT8QgN)^!#pWzH|S?6?w<>>?R!(Cm#g?NS)Iu6ME|6X?wG4^t*0`y z57nTBz=ure4%8hO!B^;!UMH>Zc5JQb$4Yc0-yVFz0PDJ?;zHVaonA|@k`YmLDExNf z9qQMl!yfX|aulKrPRJGL)ONmlc?PLu%)UYj{SEj@@KdhW)j27tbaqB7P};H7_sL1$eGgg^<0_4lzDrhtO0K6B>A>nM8${?Ry9&z-R9Rg z#58A#GYb~sJ7N0JLxMaCCvAhQrKy#bb-B%%K7f(*4I694B)H~5SItL{^*Muv7=r#i zHYcj5lL6k)#w(Ph31fnMN0L<0250DWi$eLI+Rk{GYlg0I<0M;(Ae`*210cy&)@oN6 zrwF7|c9EjLq1^Y4{bLwX!^iTKD9#B?^UHJe%w+ZD z1#YK2orG&^P1R=7R7?9#VqL{%OR*ZA?&fVpXO(}Si}Up4HWU194YWm#Z@uTGSgumM zSY4fSlugw!n|3YApWfQP<>C7a zSf^CZmO3t%A@@o3u0>}AN%+jr9u^ow)nD0Vj@T~Duu=9Lqurt&ypmf7{nXk_aWd`X zm?1LT{0m6PkFoGq1_OY|X<>PT{bG)gUwZszyk>kh9G;BnmW ziRXMn`vYQARJjRxcbDMaMoM0DYxceg z$^3P>`@{gOK6>!wxkJY~YI)`Nxi=6APTZ^sFS)Uh7+yk1!cyNS`h_Mx%i8nzpWhic z5F(VJ^BvK~sVJ{)hZQ&N*yXuG>j4L!*}K8<==vEFFX(a5a{UV$>==Lw>FFulf7)JG zL!suRt3XjQW&@xdANLW_SN*AJgTkov_RJsm+#e5M&0Tb#S*$%fSHoIZ z8@cW_rLnRi4M41i+!|4OHOxzXXMgdciW7VV4eZs|<3s$4Bdm_F6OOc4NvP>~zd_*X zHEC`(hF5gXZFPq0ioD6)QM9c>N|B)~ZV$hT&JPPW6JYQ?duK2}LshhktRQzmeke;l zqcr9Se?>~|YA${TIQF%29_m})L#O4ZIAQ@e;9~mXyzjwnu=}Yuiz*$GW(Q$AfZh~n zZehH#E&yvOh=`lFD?!SUNa?}Lsl>~nHt<3CM3xW3M~({Xncp=1b1GC2@qQq3pg4$V z`f!501;>HVv3lpCA+0~{J&7~e`2Z@HhCWya#BROnhan@dE$s%U)rNM|3iojP8@BC@ z_dkwQ!UK;zKXTHry&DB=4a_8AiYib}KB^E>-GtZ}Q;sGH?!Mc`QEKMi^rN&0Pr|Qm zHTzwXVq@G>$p^t3(#L`z!HT!smg5jy{KxM8JhtP2Zf?FuZvj4ScE=XDty6wk{cfE! zymyK~TY4~$_!u4vJZD+$%ZhsgKFP}y$dTob&8sJSmuf|E1Hyn$&h${}9H!8>kRq!x zrg!+_dr3!BKB15Wr)nGf>YQo^^{(J~763DI>t<{FR7+zkhcwIk)XC|IL!~~$8*t2- zgkh+{JAK%wHQxU?qJ?u@$9gg`ypzfX*vBd4I;2giuBkdldfr#3a1FeqHQ94NZm)oq zi$t5)m5jqDRJSg#$4=;+6(igl%D#v)H-CHv|J5)9c#n{KJo15#)G?|e#P4wel_R8z z`!Ho3nM!K7y35qT@&|Y>Gid$q;t#m6&y>M!vOKux>{Swj>p~uPGK>3%_+Q-p{D~4I zm>pR*g`WBqf(ac1`tP;)Zn7ySrwE_Au>3f!f{61p0Tt zLthFK<3_Ix@u7MmLal(lp3ODmMLg$at}E{EA*`zpyd9`q$j*uZ$>bs+_~|2$naqph zf-zh%I$QkCw2`bsoA5{Yh$`B<;r8Le_WhA+TRL#wM!)}sDfnmZMvt5>`u(%Uo&(2#R4=`F_Ot)uKKqd?HIUy1>~*oI(>f zOE;0pNSs`&`Q+L7BJq?)2$>YeBFTBo1q1dmGc%v7@7eFdl9HD$3`0FGS2L$*W48yd zmyVsk#$RtaG2fuO;9rOV7z07T`=e*B*l+BC(V&sUuH=LClrt*O2AqzO7v%Z^)vKH+ z*lwEfArTPe>k2bT_}SdA5S0Uke=_1*=*9*}$~-uPCv;L==|*lPuuYsUdqaUgM4)#T z@6fwZMwZaEm+WA6qW8Ekf7aP(8rX^d%-CP{NtJ+BX$ne*7y$c%KDJT)Vnqlq> zj?@Qe4Rnndv{n!C(qMn+mewG^1TXaD2{@m{nBH7X)Lt9f%#8CWR?hx)ZBWQ({$gzWLs;uV=fh9 z9OZwzktIn;cY{IWKZztKo-3EXL%CYacczfcuAP;GkESBO$F!lIuY|Z_fcn;QG z)ln+gtCk_xRZr^hBV{nC@l3i9=p>kW=Kd#&s{xg+oD4ZQcxz%mAwt#BGx>LLQdwv= ztE3)m1$X%E$E$=Z<5!$MZ;76v+gW(*6J_0UQ)}4~E_Ez?cxQ>BiCO3ownz9-_1-!p zdsT9;26@c`4vcISKZE{pnry)#Cy0tyj-c6JwoAz5yehq!`>!$JrO|e*#s~dC9;r@0 zl+r3c%PeMxO*;p|oIR;M=*H^N?8ug*@V(f%_bhQ#i)4Q}53Gv233hLz-Idp<#)s04N>H`;0w@zjg?RudHO!#i|_pI5__qx|kn zn_EtRUm(?;1^d|dFHfFxXTXn5HBL?S*zfFO3vYVw+5Byy@5^MmzOfl34 zl9YwZK49yEKXt=glGcZ>$DP8am)xTnCs5w*L-&R+WaBRuETHgS`tU^=BY9+&?S{!D z%~v#6F{n(lA0kv#TERReR199lWSTLq6&`x|^@6bd8*$hiDsv|oChMJUI&QU#pz;*t z?8v0Kbda~Rf8Xy#^W>r%y2QP{6=_SL@{7g%rO}zK*zjb~KwyLEz@;6Ti2(IB0YLBs z%^A|RoxHQgdA`fm1dZLKY$2<*`mJ=W=jj~kf>XMWQrlhTh8lNX&K0_?5FX;TxKt3p zjn}`>Sq>-k_VyV0cV8`LW=}oc{KzS3IHR!N`XGLgSIRExL`^ZgbqUHYCO4fOaRq!( z;E#9yt||B(@XWosBWhh*y?2v_Ma67XvQv-miNMNpNPC=;XWn@ou*MIZI}S_J zoh(-qOc4i0hL9+>I-?fc&GU2C?M=pzLu%HkkSlp_UZ9x*SUO#@?!lN-v^jse2FMWc zVMB()Bsl3tnGGX6&Iw+}d^jEAk*v#frVr6XHVC1Muc*JLKXtXTUgIU+!z8z7IzmZx z2-3vK?SAFS7}|^5zoj2YJFD4q_WO<7=p))YN_)ag+Gobw_6#}md}kVXf9=ZD$3u!Z z?9<}PvnVq)0DN%rYgYiUjqeUC?9wIoZ3r&A%MD5X?iqsb(gN&a=SG>eQ+fCdTNeyD zvau=GgY76mh}aflw#N!n;(juv#QmhgwHGK7EdyO^*Z3_?yQq6 z993mE^+T@Fu!VUqfcSuDxizsIKtYj$(K-6I`2PQRLs}0J#3Wsp5wx z*Eh2Nm$^di-RB=dPsTYxJeeRS2!96MJaGz06@(v#@GuBV0Ch|dsOrqnMJ@Ppx{|43 zFAqh>^lC%Ztx-eMs%#k*oJEd2-)n{UE9Gk$@2}Z=7t?_8RCTOn#!SeX$4sU}_m0PO z+sXFcf{)G@c(3t?mWYN(F{TKNVJbl*p=JOoH^Bg;9+@SB-5?Q}0cD^tJMmyh(4p8; zolpNK)ZBAvEFXo4Y}kmZz@jT>pn+>KD2l6!R1VU@$&KQvDhafs!K-j zRcSC&d9i~=ktlq8;Q8DY5K-5`9WMqM32?`fqlP~s6ZF)%Oht3m8X_(>eU;MUr1@~) zq2Z$36;QE}kkh~%wV<>plCgrMnMmWtq~R0ul_z~j|mvIKSCG>T@6M3F8bwpMn4Yd7{m)t_;k_qlglA%nfqJ)6+L`>-l5z6yn*Posh$Pl7di|Ceu<)j4HF_+D_;qhoEDG42&1&r6s9(i1jffhI z!wM(Mi!tHuP=yH)5brjy8gjIDs#VmO3Idk-gsRXRiKgI6x&^UF#SBAUlzXq-t~7~4 z9F0`iiv3$g=%9~|7&PmAhfe|J zg=q)Qinf?LinS0ss<))WZBNFE8wVDhL3re@SbR#g2rm+9ycKny?rhgh)C-8#tW>WE z+w!-x-j%y7A3}W?A4+{5t~z})w|@RP2WX7blDX4ZB3aSrf+66Lax*mBwYXF#`}ZRG@#vQp`A;4a(H%cAd3QytSe# zAbI;)_@4|QUXqt?Q9S)0q#87D?>;9NGDWBl*dTwhhQ(JK=>f7^A?SFrz6baz_Lei0 zaNf6WSFg1pU`S-r#0r12AF8#-ol`4Bv{ltmZZGX@@JuF{Nw7y_uma&lw%+`z@ zA<(Rrbb&okI!uHckLbESuPRt08 zw@Kg{OZsB2p@?B0bYJc2nd|bN>#AI}p$)=#>dD01Np`$;84|MpIOr z(UlL+|`^vjlA_f$<7$%s*L05NK!a4xi04- zIeD^0bvrgQlV`_S0of;eRXbi77gRm(*pT)jd&;5ZiQD zxgol1{E@@8>_QQVx{rwQYn-vYJ1`+g@;0h7-b;-o-fYVHj@hQOb_U)3jYeTd_582W zp+Kdnm*Tf z;$g2$9$$%QfFc^~R1vxA&d~9P>Z8wVRU?p`jMu5QwDiS$$GI??{Rmh%{feKej<0Z+ zJ~O4Pi4E1Z`srUAy4%kLmIcYRS~nY@{r7(stU!tZcN{w^7KGK=+&y1ABmQQ?9k-s+ z>%;X3nU zA{gXSl|oUA4#{9}eG}0eHrkGQ6u)EdZlptEE-7|p2Y1k)GL6qR*={pO&6+LNRJ&qD zRO`;I3X`v_U!>Gz_wAc4PAgP6HMuY~#L#beK%TN3o&9qNWVB+m0p{ZcNbOR_h#vPC zI8K?=mE*@g{m0%om~ly>M#Ky%eTU1rqmL@h`!0 zYo&SycSf`pjk29_o0dgU;FIE^`ep_@-)X-f1LQossxqDlS0I)Z>U*8YZ#b34pU4Y# z+q%d7(Y^uaZ$Rlc?;s8r8VD|5(eI&?A6vBh=hIhY1@FK!`^J~sk**u3vuMq2J}1k& zTzvqSJkKjz#4GjYHUR@Pw+ye0+GuD*XigNYiNNIBRw!Oh9A;;vqTzs`@trCkvTiIh1Pm+w`QU&}&g$QPHrup^n3&N`^he3o zEuuOt(oG^$yMkm@s2xUPZu8$DZ(TxS?(n}wsl>6kXq{ZTU-#K)ie7u->+Sp{iV;MX z7F^QC6jN88op;<|={-|-#xn!@^vh$=&bkUM?KXuzIt8Q126@q#6)(k-msWjwu2m<4 zC06ihzowZUGp1wi)!uso_EkENB#~T8pr$rZo2HkBmViNFi$@-yu>Chuk*`9LpA7t| zVcYL8h2IH9e%r*enn%A#BmvWYJ`>D#9Q`sY^1Yn@eS>iNY47)(@{3am^OV|eItePV z9}v$pW2d!z1Isp9%mMBij^H7{t=UTXM6WzzLxn|i>`Ai^A7*H$-F5=nnh%D<6|}Dx zB(UH9HM$nNGu+8wzn$I@A`Z$lJHi8P%gWY8mV`3V>Z5*FoKT6>4$fx)dn<^|)}vLyW;XSaYZ-4eK~K{8BxK314gXLOdk5pQB%e7zgn_+|HYC$a-Zj&WG=YXda^y_LIhz153DxTmBsDg?U zBHo?%8J{##KoT6Zcxe?eL+zAz&%R+K@VkWenGc3Y7`3xOl?fZ-Z$^06`>|JI_(E?$ z&DC~Wrn}Myk|2$^I&I+XI%!&Hx6Q7stA%w4x^J0diV%tLO!W{tT8B@^62pv$my(A~ z^@H4v9C0d8xP~#8({x1G(1lyaTJJKPA}V2qr$Wad`~o2$${?&&L2qK))rDPr0ROvZ z!h{sTFyKc)H^Th4XKq=2b0dR)714kC7L=AyzHSWT*_b+s695r^h0)uFr85|>k8Xrg5Xy@xTH7^%6JGEdgwkr*7!CU0!p;%fqtq;*UFI+EIJ2y2o z(SF`|*aXK$!q3OwbU57nDCZA3N7)W|Gu=Foz`h{)0(~Tqyr{pCXrJ~NK~livLa7e_ z9spL3BoWkW9StlIfWLWE;U(B+ZFXyp_=6#Hqx*uv1+jA@_EK|Gj(ys% zR_WsVa&i~%I^16##Iu*`#(Zp5>^|JPi1qP;=)^A2jrTPlLzLC;#C)>jhmalJlC-@W zad=3;>cmadjIS*aAn#h(zg7C59NyJH0W-B@tRPxAF(I)jXvL8N)(Pac&WRaO(->7P zA@aBrWcmo`?n}`O5TT|qN#8{_Q{;`f8g~7ji+828ra>YvP3uzW-7p<=^qXIHm9V)G zx1OJp8KZ_bXUSJ8m8V&uowKkCGXX|z#58tU6}u>_sAVgzVo%o{}QHJI|oYp)qvecfNq+t}qQEgE?Y1#CU0ezgq`Q z%a+b*BO#%CjP5^$oi-?0=h;ytx%X`4m|MHg%;+yql(sC@l@L$1atHyVsAN0SLY!_Z zshm4-sz=&5c+^D4qb4Wwvq3^BYG5uaxy6jnuFxHxbb3f){|eGCdWP%vl|k7ZbfWBu zg;l24rl*E6u{a-1#8@Q!8V$Fje8!g0f*Be2p-7l;3f!swoEmYB)7=Vsd zuiUZ545N5%u6WCY!Z$((e2j})zLrqGFV3^6my*XaEzw7(`~gX~aHVA>Jw+4OY}_1; zG)zqHGN$~2NmteYGh#vc<+lQSj3=CnC8fA=E%ct_nswzkaJpWzuk+dx&Q@qS6HJQoZD&lS z)Oj{ZNCevsQN2$Cac7!;iGo9e@hek;kEF z?5k6{=26A3vu4?(lp1HDv@{#)FeJMC!6kOXs@d1pO->3fCKXN`Z;4FSr7e$Bl3=qU zum44jqX}qfsI2eB;GkwKVOAf&877{OVgfe%r9<+ig)D)G)w>e7bG>M>upjDH3 z=Y%Y7q?QY1kx8D#$O~T2DcLhtx+d!{AJgQSM;>Q~^Yukh$KN#gBsA&QR7z^U3hbd8 z-Z*3<=>Y6zkxASrUR75rIJF^Ch{wgbBD5}(0uu$UN!(cM$r@GA$Jn=pC1-KsrXZ{C z%u+5h%!DCOvu&BW)XY>N>{R2KO=nb1i(bXMb3h!EKz7oBejkQ8W0+HVS>w4&br-4C zjSlK>>72^}Fd{^krlb;Ajlf&iFm!4|B+xf7Z8^rt%7T1|_2=KtH0JM80CpR>=6d_? zlKo=b2_E^^`UZE#aPaYAW_jJaZ@K5!M(igC$Dwd?lmCaacZ$*_%C>dWwr$(CZQHh; zE4k9PZQHi(ThZfMuRe!75VbxOPNmtEiS4826W^5Q(C~HJ^MMC4+df0a>%OO0^1L#>z&>{W5+~FhKHn)_F!g~9JVj?;9!^SnN zr0Lw{IunknpsmXIs8ofefm?4XD4X*dYh_d9wSaf12!9@h?)z-ndwChk`N;V{5v7{&aJt2(h z&1i~{44lTr1aXWoFkif92(r64t* z55CcX&&-%xZU}Cr9$Led?y$PuLxZYnMzet{6{^ya?#Od1QoXFA*wl1zaZhJ+490a` z%uCDDQ`12-jl!Hhvj^GTS!wh4pEuyV=*P&er}lOZHnkGg!8B6 z(}$}FA~giv2o>#x6M~1?428R4#gJ0gf>B9mTp=cH1_rz~IV3%DWy=WnSbonLAXAw@ zSf3X!oC<4PfzBhwr2>&ZRq^&{woV76uCcY>AfYdzJUpIn@KrrYiE zcE=0$K;jGH)Ah^gglmtvQh(bG`nKW1@5)__=z73B8$dkoQ=;7;x7`C>jY@C%g2L~s z@&vEmGW1s9?;YP27NC3ORAEmh|X2z2E6BLf;U!N>1<9PJIL#3<`6m%ZYFBR}rIf z+y%mRiJ`Zr{WY~#vE2qlxe`2(03?S+j;ofZi-HFa5^;09B4ye!1DX($4YK$HIot{T zCvX&Di&v_|g-kll3#->C4czIy@P+XZc`yi{3E%JVF_MWblzi|KJoWdSUXJ9ZTiWP#A+*+LX1r`NH6IO$q{VONyG2wQ!dB+U)l#pL~ zD0Sp~y4u!Aiv-~xMc8g3y{T`6F{R4(3fGE5m#H?sC-pOljj>PauFLSCYD1~ks_nX` z>badiB`|Co2S?7TB|hHPYe*d`qbaoqiXb?d9&ye}>n=Su{pg-@Xf^m)WGb!ik-)7M zgpwho_4Ox3K`nVC{1h})WN`2O9p=;jahSnHE2~8W7AA*QZ;D>g_bK^?ed}>`>ws3U zVM!%{dgsw%QiRT?wuchytENm_r@5-G$Ba5N0*E-FnqZj0&ye$R1~*mN_7Q(AJ45)z zj?%6D_?9>HPsxos&OFk>s(-?I`!`{R{g!PlVb12pP?8*4bNs`;y2+qpGHs^eCKz7a zSB1hCwE8Q|pg;f4MoNkT=0o0Up9>f~V`4uIp6LySGt44xE?DR~+!N38Ot6^-t`uXG z-c8R~HZE>*ae~VqXH#D`qoZZ2K_BuS`pvJJY7rBEG9$4yks2ADsQF8_2i9R$7h=&y z$x6DSe0qjM^|j<{r)=E5 zc-}az7E2dI(;Lk%<)Ol({vdsG%N`(62W|?i0o?so2_v0?W=N@lZYRjde#!p8{|M>F zV26rZvvi>mLa67W4l1m^o?_(KsEGOps+LRTl(Lg`VXsKcQzh!%y2WTLsR%_605M4} zS+~}%9b2fTwx^jy-MWUHR{L!rajV{{NCAD}&Ydl|LG6T#iE}cpjMK+GHUE7`2gX-? z*ixEB;QID&`v=dXxjv--w65-7JbQ?Q;`{`i*BWtcF{eF`Wmm=fR?+4C3d4|3RPFU8 z6pVNq5Yo^+=1ohttF|mJ^{=4q3eI5rRHMuv_h`eDDro}*?KRG*S&=4) zw6Wx{FhZ`?P`Fhm6GP@6_cJDKwEE=;kaeaOMwgM4$3QN z?zx~pyL04@@m4UY6f-A1c~0pCei6t~2{r^itVo_H;TRzocYH?P{Uln$OGj($B*r`N zl$f%lz`$*8eYg;9tBe?qxgt7^am``xc(?AObNmpW_Xry{1=EXj!m=*$K6BU$A$C3! zCC~9=_~ze2R8Q{++}kFl^2YE7V7jc{S!$PBA7g#_2mU|?CssT_BaX}`SpC%m*v03U zc}x+|{TvUDkiDb-t6J+HEx^Q+M60$R1g`%>1epJ~2q2;$L5NX}WCVicKQA&-Ms8|0 zGz#phhoh%q9KheWdlaI36|(DL%wE`0=|isQO=WTN-glK=PdzMNKjQ<+7?1pK8K_Cl zVW|5VI22W<%9ZXN$hM6acrk<-=hWW#;t{s5=^A%$J95x$jet|Pa;q9Zpzk;j%&)YO zuJ76|gR)On$DMdc+QvF{P%3myih9TXe;H`XqUqGzc4`gY(($*ItYOoU*J^nPA#XQc zmdK|oI=Z%nG_79X_!k5Ix%uxZ`Y^tuKMX7hT#xVnFtBW%8r-{A(|h<&21@>$0YmR) zS;c$z|6<_rhXIv;GC&!+=R8^N{=R=C()8?i{DRks?I)}K z@@iR`)5A8l1UNs%|6-u!pA0}B@jL&6fw$-GwFM-qAUGGSw6R@2GUl{_AQ(KO48ww> z%|3?Zft=Be^f{PHQOeMM=b6Z@^?5}}D#?E^a5Zwbu`sX?H7G;9vglmUsud|mfNV(Y zM5Io{5_%GV{Z_mWwGm&iUZje>FlV0zQ_>b$z%*^kLVh3OZo!>L5n83U2sgx6n7Q9m z2gG{wABZj6{9nzJe6kJ+z7yC}A$C5K zXkn5&@^${Wh)kS zEiGhtyr%H&w3b_iMQE#{{3ik@eLQT9w=L4nNXcGRUe?om{5<>#j_dw@j{{PBv*X-v zd$WY?Z+QQ2K#qPpP{cn;im~(Iw<&Ws6PD?4$@d!>nSU??dJwI2t$pIiU`#s{NNMeLQV$TX9yc#lx5$`(II zOvd`2lMT!yr$_rk5q~am5jR0jV5Ua|lglI_?yc&0GVZp0pF-#3T(~-H$UsyF4Svb| zk&KsOMwCv*O}6fI2=AC%KbeO4h->%QbWSW(Qbm)3g z3ZAuQ;1^>~4vr!TV>LH~7wr-6L2ywR06R{r1e*9D-poy=$^|js*YWxWcgfYZ!eItb z1l0Dap@S_uF#S^(eu?VS6c{-@aBXh!0YY0d2T{HC2*v#~;{L*=zF~2weOsv6Qz-uL zulHple0{&4a;E-=cQtoXk3rr9%Ur8&1v40uPKA=|ctqA-Dx%@cgZBl!CBnFVs+p;4t77lS`_F0; zPuIw{RUPhU)%N|PnnS5S)s+0H=Ayjrzp7aq9Pek9jkp1F6eS}%fJ-(J=D@A42eWpS z>qn>(cnMxQ9bD%+cQtbujZ)5nTrZNYc)1xOy~Li~>=zFi%Zw)PZJ4m+W%j9JXfiPe8XDBJ&(6qyl)SB?x`NJv=1 zGwCM+mN(h5S%H!9O+IT zBFUZA@>|TH8995Lo=JuzdZ4E@LyUVnKwyqpn6wVbQt5Z&y|QWSk>x?Jz) zl;|DL;_I~@&)ZLV(nS zYQkHFX{`eiIc_2KN|m44v|vx6`5OxpTs1nWrNW9sC&n5HNHe=NRg8{{nM<+E=Ja7Po3Yey9dM>8o1^Wsk+p;3%Dq<;`ilcF-{^s8#|9sxI~Lo%}`&nMyPK(oVF;1JIP!J z=o9i||Ag#O_kYYg)KK{?tRavf^yu?$X;gc>YD;56e$1x85cupdXE#aIME=ocen><+ z4MaNDyaoC98y<0JM&krzrg*2^*@Fc!Og{FQ_U1WRb9?$r0eje|1^8?BlY!4-c|HdS_85?rR33kTBcp-uhZRZKkExIv< zI^yA~iyJV4I*{@lA5w?m)c0U>R|`4bT&PaMW3D5INi{1v2j7BKYhOgk{zvD@bI0w{ zAR^zqu50)o9~|%&UK^^A5Xrf`VuHo$ zEZ;>+Ehh$^)%DmVvS{0DZp?75-UDOslZsmn$3q=jtu|98KV?tbQ!c> zk-aNl!N@jEF-XrAEJ^&<$M*yb1C=MP3;a)o$Fk>%2qV?%UeYL?0R}0M}=R z!Qs$z{4^^h>=sx;!yY&Qgl$~UttiN6TiRb#0`uFw6~iFxA)dGFbYf*YpOYh1{fxAx_E`5nHyz`%?4((6d^O7h4H?9}q z&FO!B30Q0PXUXwC_tBoasJYus$F=E6-|v}Vj;=^MH9t!)u;I4UDtmO(5gkf5E9{|? zk)!Nf?`QKO2Yb@P<{(9;S0!61V}v8Hbr-jbgy4*tz^PsE)z8eUBAZmQW57pMAes?nfj5LX+Xv++2u)Gkym& z`+V(;r8{)%xKMl%GRaK503xix4GIx+ibbAo0U^vNV)PXusx*ri-8_U?EK;;Z0lXtQ zQ3jDQuC-{A{AgTMc0zm{$;6l}taUa}W_SRuJ?;~6R5nsdtI?nm4?B(f?d^Qs*0RX+ zjbzpLI!40p>UaJg{j&S`Z^DaWjJ0*)p4?#7&hUd~<^Y{5#BKt~22mXJ5e(5x?e{tag036HxU&N}p{L?Jrf1X&~JpT~6EFW*NC#I(V zh+HIrfCPzvLM2HM5R+sR5KIW5$p{e?_TzXG0>)*s(h&r{)wEM9s;gIuEwrnb7pT-= z8q!y)RXa7bJ2zG;Y^%53J2jKOv)=!7b)|O!NLt@Bx_g^<9%p%9wLf>q)^^{o9RSoR zTqxX1(Qk#Qd-ergAMAhXQ4CeKyTYQcIyq)X;49pE@dNmcLF;vegY#b$?-Orzq-(c& z?6H1O@OMiMYkMBDmAG_9xH}vhZ>UCjX?_HI)wXMR3c{|@U zzHs`z0mSl-4fE0A?eE|FA++V@@(mny|YeW3CC#s~CHXMDm9`ld!+m%sR;`u9zzs(cU%e}qNi-;Yh|d;~|T zw|Oi>N55dT`b`Yaw!Lto*E-)q)BV}qPnFqK_fv47clVzhzHWQ*W%~*^=WmekH&W#r z-RAecp?^_lOFq0QL0@sY#rl#R?sfizNBuqx;p>`+{2pjk`DjH)S_**F@wiEgu=h6n zW&05mG1^_1nE}-00J>TR)sIqM9%nF64An~2ppx&kXXT@ex?XYPvBzcSr5b+_g56eL zPP7WmE=i8!SuyrJFfBG(8WCu-_dHNtpnxj6Ua$u39wy|+B8ZBt>X<=AR&LZlRYGvs z5<>Lp)tw{Jbal3f*kr&`F=|bmFCrvgRsbTXi@uE)4J(4^1IddAC%Wsg->2FiO0PtwdbZK$PQB*7$pDs!nnl zGt%v8^uUWnQ`4FicCR%TXbsYAsZ_kx)wVV&6p6Rqqlt+XnP|D#{d|6c1TqIB$0`P; z#!S7C#o8mPOLj3fvQcYsNkw0MJB-18>IDL-9caMQxW5XL9DD$Bn)qS3+a-fV=TSBq9jQEJFAVIg{H2$w2{{e>(5CJrcG7h za`L9qCXUYLh9PfZxQhxqyLu}z4Mrrz@~wx3`UI?I917}mTKY08M{!5ZifTWa(?rTf zNwX^sB-SbmTebb6<`R3eo60;@W`6)tPLW5P2BKVWi^v0=)(AGFi6jS>9Gx%)wX;Lu z6e%vuj2*FGGYKpGf`bbArS1GdwM@LQL{%b?eAoc|FkA5v344Kj^K;Fb0xW$K#93Kit*KtJ7UgTEeR)K8cT(5T@mHxssysAZu=IW6z(ZHxO+JC-K^b)_@SBP-HHO=Y%> znvsL3t2#mdwfPL&m1<`VtAoA$bKJc_zeq#ZuFQSAB<8SWxsQ=6O{$NOlyw1~tfcu6 zAgF<=hA5GNlThPiYN2rjW&eg+{`NFrF*f9iOUU>r4Pkz_yABJco=6<>>>(J=t~HjJ z!vxgC;HsbhJOgCpP*1$<5{qXm2M3!jtI;`_pWrU4jvZ7b4QoKW2%Xj=W2znl56RSUL<;{bEIt1+}r;JXF(4C%=TOX%PQR6Sk&~+i`<7e6vO# zEmA?06c?fv(sP!gEH91|j*nfFrDg-n;fD{mJUVAS$K!z51l3!+%IWwv4SipyNS#pCehY_CTV`nymvOfTsp0Gd!LvAG;j2d}ek+3VP zRCyN=jmXO|Bs4}06H!Y0X8P&Xq?`)un)>+LJ(vjPN!$2f6Wh{>4~Z&%cRFQZIQdqC zK9J8%>cEKhKBwJ`H}(N1;(XA#;MAGU04K5$j6z#xZq4=^dVQpxJC}O_QT`;ooX0A? z&^TvG(-tyyb}owd0AbuyC(gW_q;$_)_uwYRv8*2%cKDzk(2{D}W>nkzP|8qn`l<07 znEFfp?|ICPQ88D_#okelG+kzT{k|r2>Pja?z}a4w@vp{c>o4ujUH=mpDgCG-OWt;tgP$G_+F8Z< zrSugSX?Kk8fNe!ufWu{{fR_$iiA*5zS|{Nwhl?;D`UcVg?|Qdr><}15bZHVBMl~xP zUGj<=-#eY9F3S^4OO%7Tv`=~l`+2yA$!J*As06~xt}dHXl6p2h(%JR`YKCEsqxl?K zT(|CysotCkU$L&cm)V#_6sh{iTcPgCYjB$>{b^JhyH*l(JQg2<7di3|^Kb4d!gT zDUEvaUt4))eWRDMMI!sN!A?zpo_p)?$5HObWWyfxdxk_#7|95`BG?MSW0+`qIWaB` zW&A}siyI`pn*QTmKv=xcPplT>WH5LqcYL0r{^vb#h-Bj|iUG?(s7kqQ;+1(dN$sFm zq)8JZO!!AkzMQdHB<99p8rdw;2?-oV?Vwp?YntJ7ES$hWtPh|+b2mUd=!Sv!fPn$C zOS{iRzv>KVsMwrzB!T>(dlyAGyK4HCZ<@gL1L>`Aa9VQ$%vlu!?o}LH$o_^&!R8$A zk?%&5yy@hoiK`~VD?PBZrBu1j|h1fXq$-Bmrp&eji zb-`X+7=>$8gq!XI!ImQV*XyWM7KkA?8-ttHqQaS1U^K% z;R}a4bcX7#Rqmd08fuMCi8 ztkG~GNXes(u{cszc6zMUoIlPzsj|2?R8t9MtPxR&=eQ=QbCU&z`mRMOu!8X>Y+{CD zFTP>>VKcK;rqd&m{NXh2I~c4CIGW@^S9YkUf#sL!M43N?T z(*nT6#0Zt|NGl-pXoUSX-jVqbDHj}^c$z^1FwXUByCt_e6vgFVw&w16gSv4j>k5Kf zv~O^BTZl!u1Rd{Twf;zcobZ$Fd3Da z*3pk<*&Z^?qc7*BXRVkARX)c>Jfkk+D%_s+YL4PCVP=Ul?!-I2D2pfeJ8FtWuVJ~> z3GD6y@!;P((M8kNhjHjUM%cMzZ*N2{37KkUy{J1I_?ISwYBem_EV-xT-+zAbM9W`( zO7gZfdr1rZ$zJ9Pa~6_|`5n!pWT-20%r0$qX2s=R?sKFn#ix)#VA@EX#ajWGjT=xa zb{}At#lr0qgBDtZUI|i{u}(IEH78z`2?T3+P})V^@2T z8C~Ur+aaBsr^8w8tQu68W=gt{FCqQ6T7fy8~C znPSvx%0ByIBQmHY8XzxBqdH-6N)NMcON3Kx1Ev>CW1JfEP&T<^2D+b6AoskYQ?09A znPk_1z;Bc`C&CodM~!e3eHelPz8eaa6K1(m6Z^$&BGf3JuzOh+vJ#GZdjWrbbOh=M zY0LUoNc=_5`})=0pRM$3&cM@X3)xZt_49DDlm$O{Q6A=)3z)|No#Mvv1;htQ?)SI@ z{uVq$E+7D)_a0i$cHR@MPc`AW?@e44KfN`-z7N6&F_K^0&FPZ$BRtX;A&1N*5{J+Y zWrukAcUm>Sh=egAI2q{WFE$7(e=ccu8mef|c5`zw$gm0fPiU zN8o~xDmWnOdHWD;@Iq<^XMzgggzs_`=~R56IpsYC?|B9gU6;!EK{y1UmhvDGy5NMV zjCXKCeDa&1{3T9km=L?&OPvW?>VkM(@ML}OhEO`Nk-JzLaH|7UZE$*IWv`(KH^`Js zt!o|>+BRf_+BO2BEo;Ga9w8eUpsW%KwMv~BLylzttx$NVe+mbzn?i)?K9cZI+rnyM z4SYo=#Z`Pt(*49#{NQSp$EC`Df>SzxV*@+nfo_5u4u3Ws20_)EC>XCb5qV7Uy6?n4`X`+bBrxPgJZaf4#qFkj>( zqM}LUgz%GowbzN!OzmXv_HWA3I)J$Qcb{tWBB!Be|N#;N>Yq7*g?DYCTw zA9^?HGPJo`t`HGgHEIsmxBPJ^qKEjygH_}bLHv@5UJ*D4mT3}?Dq8q7V!tTN5;4Cm zGQ80~#$6&cZ_HnyA=5#NZ`3^&S=zw%ZO7F1>|fAOB)Ppo@?Rj<$U{CmPcPo(2xvU* zr8KNcf?d~xKhf%abzTZpRp3obgiDo$?9&6ph z4FYwW`@r}zCGyz-Rx>kNQbhsQ*bIvmvjT;-`F~-jI7D z%L`O`z4yWtEhgo;%g6ytF(Riv#>(>^A25USms`IEE66@%{YQ_%)Tql0!5p?A^ESOf z|G9JbCde^~LHRcxK>jb|$N_+vHX?QYuL=G+m@koq?~o#Y z{*FC9mA%H30N*s=cSdx)Q6CR1=sj_c{*vR=y|yhejoFa6Z+nfJIek0CnLVlWw1s!a zg--BSX79l!)e_6baa?o!00A+aJ|*l}8x*HVW(mdLl9VS*lG|n-pD;wKdb2&(;}4%d z7GS7@6=^b20Kg)3I8JpyP`5S|`CeRVS~0oS&L5a12zF^?+u2N*8{Qt>Sd}dJL)>xF zGt~u~clOd7G)g7XTwCOjIUIwy0mlTbI-F3bU70%?cpOmX#A9@<#}181`T6@4Q=)6Y zn`Mmfs;h zIx{*`o|Di!V+K4)MsH)`uI*V#IChB~_Kd~`iy1Ie`(Hf(tNN`h2C>$nn)42zPKLQ8 z_NCf@=!2h)5$uLoZxpM3Z`LbPHKgy4qA4(B`izTOlP-5GSZzz7v1!@Xsnlq*XTf97 zM`oye;vV|OPM|u0|BXj4K(&OS(1(J8mXYVdU+OYuP!#>2ijqQEr}y@#i#pivgXu}M3#L$?{ozG?xIe5Al^SPnPlkW> z3ks%RYYPaw4w-J4?nDr`#hCqqDDBxuBg2N;(gYWBv}7z$x+=x4*{qK%izYD%%A(K4 zs@;$~#Q5M;_V;zF$J9z6SH|kXEM-DK|G{@ppqQqk?(&7X{Hx|X%{bU1Lx@kP+Qb7Llm?=Dw zlq-46^t)EzXX|6Is>Rp!N&;VL+u>1 zO(!>CB9w=S8p)PR@Ehy|i)DCz#3y-?s_7kVw6f^%v2(+NZn``+r~5bw4tzP*k`ywK z;DjC!YUUqv(*3U^6IUH0@Y zN!AHLI}EvYLx`T`Av)GlIpQTYPCLvIv^#Z*k0}N|7R5-|b<=Ptr<}w)byybYfN(FBF2i;%eqFl`)%I&KKe;GO8=7EEmxdV0 zO>Qd~&}4TxX>!gepllM&v786<%i4vcX^A9yAg&wb{?;GN^r2^eoncLrWlTdYE_zXk zu(vWO+CE~G$O7~<;*vpTwgs9l9-2b1per_~4B*+#k-9siZ6!LCiiks>fM_3B7$s7! zsSK)3}Bu#RYmeZ!h-LAb1Hm;DFc-1rJy}v*2W&(nkMI7C$*Mkiu!vz z9h}kgUj2K)pWw;&oq7RV?&i#G(mY?>7Ef026 z4r~ldbbUH&987ddiampA8Bwy$7Vy>G=aCq3pVWp(w(Rp*n;Ro5z1(R^uwTH{WQsJgI~_{8`WpLn5&b{tJkh)Hr3#h7O9Rp|sl&bs11N8lldCYBgsKWP={;3jd|GZ=op%+A^ zKuL2ycQJN&I3~hL%qQ9xf!_W`1*>JC)&)Cyxx^39Cl7Bw`Zf- zbNnT_R8zATpP4`z<;-9Auo|=HB(irBQDcga<-QdBSkA`G$9|1T_%uS`0*dV~Zjl~% zU@clp#P|+N4DoMD93~|>H@je{ZkcMzNX<^pZlM;Qjo2~ctTpAN*RyKfDqg@8Y8xie zHqUWhz7rTngfOKLOgH1MuRxPk#oJJ5;o2h{sKedhBK=LbR1IwyB<#l78@sN=I3<(= zkqkAaG}BT)-m+$*ZPQTvUR=-ELcO0axX`(zgHl7Am5$EDf z(?Ls;#4wfROMZCwnAuBx5V2z52>6n8#D?Y<8M0$ysAuzj(RKKoMpldG3>VV1eBw>8 z*=k7N=6ep`zaq|*Rt?tMFfvj#$0d%wVRSEo083@ZEvGBUI*MI$`P=5eEt?;nHMoz` z%w%hfiH17Lzf>EP*$QGK)qMSNRQCU|oY8=$GL__&LV!NdsF)bFr!_K$KmcnepV2B%k zlv_#^SUg`l;4vV|bT>jMhuSRCgO7RDYnU%EHDIt?2`7RfKys|>(i24|3DA6qY_)H! zF~2s0$gI~g%6V$}5OOY{&l$ptNk6k?9l~R3I0Gt?FL!ow$2+ZoWNMr~Nzp(_pr`j? zF`w1v`zDzu+O9C@7=fh)9B$CfQ?oZEGk!`T9+))3?4Sn${IJHLh(SuSKjhGjq)$#n z`4??DgUEb!YO><2&Lk%SW{$jj~hHmV274m7If)t6r5idgeQgLdTt>5;0CVkmD|~;G+@P$aeDfC3t#?bf?PQ*@IX^S#P;x^b zHC8Ik{vq$YmUk_M?O(q9vW6RMPLMwUOydMuZGPu>z^IXrp27;m-uPt43xzKcGppPF zz$Y3k7MsBe%nUpF4c{jox@%^&?F7Kb1=8B`ZqE(VivMdDVQc>Cd(Cjo8Q==1=VVJ_ z!tD4?Fc`>zzIwrrLwX(XK55c0-HDWiYPHYo2yCs!2g8lJwf}KK*P+u1eyes1K9&l; zuW`cpCgG0jLyQw{R0pV02kT7-R%8FHZU?e`TGSczE4OMV=o+*K;-PQbfqo0Hbzs^7 zIeV`rsB%wvA&A9mKfqf@&+95c-rN&%;-z!B%AV~!bLtJOv1g{LmhGI1W+;mWeg8y~-^kt?92X5g7@o-W3me7vY~ zW_(NlyNn9GG}auwmmSSzOubWs7|t!pr#&`vPC4J1t26$n@SK*RNtYvIJnC#ol(e$o zwc~E_7aG)fkg?)9H$yUUubH7I!&%fG>yR68^O) zxK9aG{)q8b7n^;dIJe{4oRqo&;^{d2C-6ZW`vZajEw6l;Dl#{uhw7WvJD4SLkto$yNJ&U9OE#Bap`_*(I zDf#3dzSf>)PrsRCUz!LzVxBExLY3*hxMHatQEfcEY$(YH>x+H|*Iu1OoID(Xfb4@cV$Q`kiq6$Dq;BX_eE@fL#u3h#f5n>TPm97O=Ab`n$Wv*IB+QkQ zc*=*f#25n2H8aT&)5t~UASc{3A2r9YaOi8O#+vZ{c;1=iB5S#`FUi#qQZl8}7;N0M zk08h5YK?J7xm|CPzFH-PyEuJn(KOv!d9GA{ja#Muz`1w?%FNhJMWoBzxRHfz$#Gtz zE-bsgZ&>f<|}_*=xj0%+0j&{3>ZTRDSKf6K_w{- zZqG75$b@ywyZ?`ZyS4iZp_Y*_D*^7&J%$@mIe{+4 zL^7f49;Q-80f>ZvB2ba2BC!im8HF43ksLbzRHzC|$J#0!VmJUEgz`Mr8X_uiP!(7@ z43rJCe-lMl}FXDUEqs|Q6qjr6#BeJ_h4uX>O5c47NM9tr$X^`T;iM5xd3I4 zq*=eNs;yH>E2_@MOgp^N=3~k830h4~!86bW+POE>jkpn2VEPJ$=sVg?AIc{GIB6U? zM4jMoThNn!pqAVd|6eT_f_zLj0P$^zw0Q>!1Kaw)6m_tZn%fww@O;oI^6nR6C+awu zqHJ+SEjA9hrOox&Xs81=q_DEAf%Jp`;QDbS%!IueQ34XwZhyD>Gpy z05_u)n6ywAjHNpE&r3+wuRnN9b4ml;95+Y*-Y)XSGg_5e|2E>&BOMV<8Fv~66Px@J zRYgl_rEciZ9h?BoNoQ4XQO!e8s1(@G%yS4IAddI|qafL=RK!uE@UU1FnBRWZl|=!n zvj;|D58p>q-1-9mN)OMfAVY=HC6d(Q97AE!`V`p?u1XN-%65pHa#N~u>p47fO&>s! zj`pd{dikibSveJ80PF2;aZRVXwKMb+AL+fQdaBh|_${~Ey?iRQch4H)jg5>qI=t}3 zH(h!mS&8yxoGICwf}YtZ0Jv^8Xn%k@FGY z0s0ZLll;h&|Bv2r&d!$RcDAN=F8>mXDon}&{V3Q$LlJ%YPUK-S=8D6ykjGD;@F710 zOzlE3f2TAJ-`LW90Psh6fTM}%63Aw3I^!SbEY>cMlK+y<3Oxx811X65$aeNDF)#8J znt^^e_%pXAlfijPdBW4kr?B1~yi3c|yJ~IUE8W*6Nax%1j)`_zKG$QY050qZU?EmUl<>%| z$1gPVjVyQ&xXL;^&izhL+yfI_?wx<%GiwBfuGEb>0D|9r)f$XhBC`0=9V$ug}JtdDe9o!{q%pntXKrbf8 zk!bGunA)5{+n!T15fE%ScFnOEJFCvj44Pa)y#|h6VHFRv)k%&sZ@mI-ZKT1l=FZ=# zQ_kKYI?fB!>jO#N4JTpPm3U95i*=y%az4!0SkTP4w+x{{TD!iku$GT#(ch$yLJ}{+ zPcKpA6}Lm%bY(S7wWI56K5mBI>(N6I8~nz~BAljS^%pneH1jFAbSn^6DygwV7M7tk zM`M({Iqpl~#vI~>1p8Qc)DnzXpq4C-kq?ptuQsytE%pmdSj zoivO<&fXJ?n-we9;H58dYS^Jq99faPi-02k1`5(r@?PF_4E$9KSdaLe&(o}pggS0+ z1gkCsvtJXwI%mG`n#Ftz_V#lY4T=nW=Z5esqchkR{!WqM@yibwUe+guUdAW9mc=A? zlQpOD;5`!CX=xwbJGC@Fwtw3K_&u2K2(n=GWa*cg%GhavFt z3!-VS2!qZN`0MrrL4djg7Uf$NxOw0IL)kk8_Y!|?zS*&D+qP}n#*S_47u&X-?AT6r zY}>ZEPu?>#XJ+23|EcP(>WjYWu3D?s^IgyL0hwY@1~$i9@QYTEfw=b)&s5Yq6JZm` zKkf|28bV~UzrUs+HM-4grhh#9kbD%7$XY795o+~H@|M(I$5&Q2MhIQXS{M1Sr{2)P zouSzzY6I!s7TuM2xC!HQnyTjBTb=DBW*4< zWV)8SXCFMmqMvk-3PoT6h21yS9a7ORNhiA4cXrVvQTFXcWq^^Y5dN0<#$MPG!+u#^ z|FAJjuq23oD@YVjF{zm#7=R>^6H|5({<*Uv#n&A4#Qq#fq2Zymz%71uY5EPmp7G`R3mvHDZ&lF#i)J1ESU*SV>XZG5I6pRCM+KYhv{YprI4Rnko!REv#J@iy1cj!s5>AY*+|b zx>&WYssGLLypg(!aFK%>(E!!0b>|NER}}9}36E=9>#cgrE40=(+iJvrC&2zE`yFn@ zQREJ`X>;QFcOrp-9gY;g`p*^{50_t%!od?tU+@22-DVZ(q;P(^#edNkng5gOW@%^S z9;6@V_QM|IDxnThGlCd!mlVYGCwHc7B`%e@l)XHDC1P@wrQtM>qVkd?o^mv@lJpl(tZbghL+$qsO(wSju%~>CG}Bm$<&=z{JF`U82aO^mbT31 zWhvf#NUw22hyzR1>2wL{VO*NLd~v8(REQkoLMwR+{k_6HdBr-CDrjEq+t$Qi96pQ8 z`5IRy*osXjBgW#H#NdJi`tF|>fXDiZQbp$R0K(bYo|sKDf8HzHr z{+D{awt<}W687>1{lRLDR4BlC;9T4m@ZUgzbv)Tn`9F>16YBr>r~aRD086P?_A0KZ zqciPS%grv;eTF8yaO&U(6)1>hgtgY4HS^ncom=VhTi5Fs%QcI5Vv{pOH-B`kB>v&iuVQ?Y$432;EzC{@*S+;kku% zAdj$k&kJ_k`)QY2r%D8T5NF_pf(VEp`+~ryVDABbgiyQ)xFE`c*t3DKj6kr!A_2cl z1C0zpIpL#$8GsmoP5NZ|Ci-alI2nJX_mRef-1GnOVI5vXW0yxAZ$E3|B5>QRsAsWX zV|iPvw@od@TY<;8{ByMan~B%HgKPrMJV$F{IybvDy{UmzWh+6q!4P9yB7>KQM?AfY ziGm>st|G|x48}PrHH(cRiun)s(c$EN_!;`{MDUaN+T@h6DKTxV-IA|U;?%1`{ZWosjFkyDufL8_OgsfPMjloH zCLs}7%7-?GXF|lWrBfgI2Dsq1i$7L+#@+=c%ZH@}it2;fb=@Vs1$ThIyrA#f&OCXgZEp)ajZqYrGCc#7#N;R8*p#~|V8 z1I^Q+ml%wrklw&UK!wqABNc}xvm{Z?7`nnbF(bRd$*ffGpRZ79_do5f$0pIr+|+fk z#^M9DhW{G}TiVQJC{776qw) z5nhNaQ)Ez7j*?BmN?AR^fG7=0KA#=PBre9-sK${ocT>iUE`5?1ixO}#li^%Q{T`*U zULhWQPbp9`rAeb9SYFhC`|LJ!v9zBn!5g2?^-%5!&2=fY0!b-=P|mn34pXv*eNmM& zJl$hwI=~wvZ5SJEzdhU#8YJoAa<#@D{HW$OX1sNPgK7tSr@9NS zN6Zs*yV8fpr|RDAmHmik*LK`}U~rsgcO&H{s8!>W$BIILO&MiLk)=6eX%>E6K|z{V zR4GzMb`}T=xsg?fE5h?lic*SpqUy7H$oU?+hwzSAuh5=Uzm&#BZJ(_}s8_N}s`Zp3 zqV){^i~f#&t4M%!+uRsosXk4lOWc^G)hTO;(f$5c1Vnq53GxAN-+W#pZ~uJW1Mkdy z-h@wRi9p`L1MSxvv4Qc~Oxhc?jDODvhvxf(T+i@)p8W$}>bFnYO~d2K2e#$B!Gjf% z+`Dy*b`|+obNzCKl>b%!a*dQfT`io&yupMO3ik94$x2u`x`A?LDTuHfKg5x9u=1#{ za>6&n=|6~gjceZNllV^shzn$XJ*=}r4Knby4CAjI2RYW%66GKwd0S=W~co0Dc|4%3p! zI~ty^UDaC)%}fO%BO>ZgP8@;{Bpu3@XJk&bmF<(4val3cXetiDwau(LB$S(PQcQ~f z*LAaS-CEU5)~@*V+|6;5nZdzgzdc!&_V4!cLDYrn#%V#oqdc>RY)L@+Nx@T9q3(E2 z{JjNYRsiOa!Om++uFm_f$WFr9jr0>=MGwPL?|6^@L{ZHn$lCo4zRg>Kpul+1$LXl@ zmuQDT%DI5)9S-v|%h|VXz5(Q;OvUzN{s&FPw+gu*-I9Irhfd(POzNjR=evF2w?t)Y zvDb#kKbbPH2Ohik?&wb;@$F&A;IohPt!k-Jq7)T$=(Njw2l!4iT8^}b;)AgxpNzRc z0E|=n?35U=mr#m1q$;gigo})$2Yr&&Z~mG6HQ=d)wRYvHB(fvr917gYG^;>a+GMK) z)f4Ym;97?VV+7YZcit{Q+IXcjhsGFxo~mC(Wl3d%RfcM_vL5A<6H(W|6wQ`TLKegC zd60x~Bx`)w;REsPF&qx#z1dxH5_AL!623rma{U?AS=^W*lmba(6AOnShlICLS!Zqz z7l?I`{%gU9)ZEygEqFm0>xtCsZMYiQiH*XB!1dF5r1k=T30qRs9sC$EtHA^SQWOB( z`lIk`Bmk*GZj;q!$huQ$g^3HiH8|?L@dkF(O2-{xG`j7UGRydn6hW*QgmFOuoDwbQ z#xL@+-B95VVc(6IZ@ZG&7(AjClD@kHrO&nI_zEliY1X1CL z1Wa^};+a~09)rOG%pr{pye%WqMbGm@=Cx96ebLbyqB8z}|P-%ku?pl{qYp~^G$C0ha_ZmiO_6HdV$7NbdNtXGwi zqITl2WnM~>M3Q;Bc?*J$iWBzxa-8(ySSH%1*j`-2BL+|+u*@^~vOt9}O^78%^ycWdR zRcOUHavsppnzU^-Te28=C`7AP9oE6fbYV?jx`?&U3LRBbt8}fn3ibo5If`*(+s=ve z?X#I3hQoMN=Bhvkc$;V)R6XRZm>fA7Yd5hm`B_=vu{#S7)Kl)RjHp=UY!F%E1HoV7 z)pm`qqI5lI2q#!8eIal~d5l}a^txd9AfD7tCQ^(L?AtB$7QlMIhamMn5V_GjWo9=w z2V0Z_-K+8F?R^&{ZdB(mVwus?Mnmv>9>kkr2_DDe>*IZ_6)Az*-m2NBocb!^EH!s< zPLLl%*$%wTEfDxs2=c4xVBMlv4cA>;~7IZSb{K+E@7w(o7 zV!5tR@gH1uP*PV4C`%^H_Gi zCms3&H~eWAe&eSQmUWWrhiM^R7#I%1DXTOBGy_6gyU2;XE zRZbleaF++PZmEr`sDIe>%6Pv*IBXsYmOjN+eK?u8BZvLmn-~0KuW=o{y zqtp_Ep2(_iLQm;}xtPc3nHGorf~R0xWiGb_+Qe3u-U*`!9}lLZ_*9cJdQKRiC|p&k zBDADKsWM&Fu>x5+=60GYd>lVdp=0&iN8#iz%`R`*S@s_99VCfHY+wU#fc38A!Nwi3 zMlC|Q?84mY954e?tb5tLKux2}=cCKRKQ**|y*<$#H`61uRc#~Nn)(*?X!{z*W+kIC z54NDJqonRuEo23E)@1^9ta_5!IXL{%2OqW&l+FRiLE)5zwWgkCEY3~2oE+Pl7`%He z3q2E!`ls>S<=A8O6jKrk7UROU?AYAr(&bu9<)#)L)Q7`^)l5MN8rEGlks^8$Cgesy zBFg18v`aPEEG8`Ya!_8%3F65?1Cyy((pA&|s*5!YJ1GvmFm=~L{Xx1gknt$f5)kOy z!250}CcPwa7mDdUN$8yY$c^J$KBFAW8tvo^x5zYM#xbKJ5i?z#O)a;Kl@bdRM+MJ@ z7gP|Bu*hz2lhb|&cMAC0RzOh$82c{MQ<=_2cL(@3)tr1lv zPgiuT-#*VJN2&@-#dA-ng}j+EU#5r)?8MP<`EjEN4h=)3m3V)&Mae7i)E`>MlEU3; zyOLHj6h9;^DAs=n4`PQobdIR+YNtE0{y}eir`@9Ti8tq0Jz#7m#Z!*42?vy15@XGI zsjuQx=FyuU6+nvg`M#>U&XM=eD&J7|kN72@7(Q$gk}p$^`2E;dkZA9(kZ62x0Qe5D zCm*LGcx58jg_1>)DoYZ!IdW|o4JDkX`8?xg33;P{?`S)S`e@gf7l_3|{Mjex6 zx)Z-pH!_XAYqh=MNS*0CPr0cJ=~Th-rh-!eM_woziAPTF&Vtp1Mp`Pf@xwmt^NA`# zg&b+dEt4T~RKLTNh$(3uNv({$I1r)cmx@>iImCxeRR3H;Jz5{-5mL5OvYFFN2ge3W z>7i2pZdstloH$}c;R?6gZ-pnGcTH@8p2P7a9UFFwiaLj-N6&AgrJrt0qg8zoG$IHp zwQp6bkuTGxQoWH@))TVHRg%Onnh%ybIxN5Iv`u%Sll4SS)PcTfZL4K%r`LKeew!(J zgO004b46T?Vuu-hQ2`rxB5T?aFH7VH?~sb6(WIIm#zY_X&E_{^@;|{sZG|K=fJHNl zRhz~s`6E)q`hbW&y)Iz~lkH167H=7204fp}6f#QGMqx>YHx%-IkCmJ*G7TLz7<}hF z;yI7qhhh`IQt~h~L2IN6M9%@^J3)IX<{hQh!#Dh+q`moh8SVWdA1vuHmn10SvbBJj zZTXO}ye3iAtik?S67-KgzM@cI!zN32!s5EYoT_8bGHJ>O(YG$h)GY>Bw3t=qWCygr ze~K(a_caQEoFW}kC=f33i2H}A9$P;` z`{w9}wEDu$z7TE@UEQ&DEWqJhFcUZ9U3Nml-%BeTB-Ly7R_u?}NEyrpE1gJ#4u;EyHIAzRZTi}9D#^HRdD`i3#rW31O>tkri)(O3ZeJ3AG~ zLHl}x{wlONjVcSwCG&38?Q2AT=fk2WtgJ`U3t%>VDnjQ?W^!w}vq*LH_O~ch7+l(( zQgdPXur*&#I%aQL&a>6;+Vqc`6AIAoLFhBhT z^PoL3=q)#i&ngp`$in6j0avmTii}P9^Oy6;=31M)xU?N_xcL8JszFn46!p78FUdNK z-9JqigxxArT-_3tjLnd}RX4x71ol_TM#Hfm>!ORN+Ou*7+4Sh)LpLq?T|QqG4g2MB zqBEWdNG%yEeaifSFmJhxXBH}xv;8%G^mKUOhV!x{bzOC4)CDICLHShA5BqOWS`yxP z=d~Yyq<`}7tPwu%?Bw>x)?Jq(e{<69ds{$FYYo24k@*NL-BDCTyeI_-hy;IXMLq;K zB?&0B-eR1pe-m#F$Ia>c;g-6@|4^tG20u~x`V|(8@Jmd5()9(2Q>5AC$?HC(-jN6R z)n`>CPg&b?P^jr$6`_vdjV2=mABeoL<7Li@((k_o+3eQ;>T0K?rRgZ@R{99%hQjIR zBZak+L=6sh|8?=0F?010(c-``wew@TIG!thVIQg}(ku5lBoTBv1zmc1KUBxaM=e0b zFQHLZ09eNF1cdp7@P5i=h3TfFt_i0kk$n}IZxUQ9p73X9E7$4Iv8h|{l2z_;$ z=kq70yvG-Q!{RlRQwZuI#|u4H(M+{T%o_ zE*h75Y{HH-DwZ;FXNo0-V4Y#~W~e2NCc+;}PxMcUbj{}PhY0JIizkao&I(flVdf`-Z`+}=UdGRZ${nqy*&e+~E0>fI` zpIVPsXC(v{U#OmsL^p+~`95e8FY^7JNmMp1c~ZYs!jxixJvEQ(5_nu(qAi~au`MRE zbj99&j~MibylEkqvvbk31+!R3H+=ebpCFl)B6f73VnD2pHYEhr*Mkk76ei6 zyo~<(i!kVPd?vnAO1U^Yeqriy<<8@guRxs#(FdC>`bK-ft3+x1gSNeN?VAJh&M9i5 zTD`z7sS4`mDq6(345_e#e?A`g2j<<4*?|o=QY#ePZ#~ziUng?VKP-|ImxakIt^==P zeV%k7`$X4(yxam?Sy9AUBmSw(ac$_riJ6wt{Ym$gW7R8Er&;#x-L#`xAKF(P^}ZN` zsHv+)FTS`#@ZSJ8dtD!DHfTUV34%aCH2=x5ijBRA_5YB)rgWfuRF9ek%v-W#?GmLV zAp<2>;gTYLsj4C;m_jnf%MBRUMH_CxLEI5S$Wo~;XCPczuwdm{+-{o^YC)8{nO1prr-Qed{7L&Kl;ml z?ZWyjL9tOD^%G5wg#b^c%$|Yb96O4nG73PzWD^<@wd!$c4&4}2$PP#$r#ciR6C~w4 zQXY(=LQQ0>t3ufs&M%~EgC%iBgq58bkxlRRWO#DIn4k0)afT5%lVzttqaR1 zC%MXKPUYZ3Sx5eIC_p*hCBEiL2~#kMEMrLwB*I?dL<2`mBpw*o+U`gM__G9H~0MpU>V)tv}xzp91 zc;HSSk2s2KOLI#Kyv@cSq<454p_|#qt>V0 z3jq9rfSJ9vs z>ByXF!;#g$0^y`N3qyl0d?%0`*o1pHP@_e^`cJ|A4K*8rp9zRLD>y1)srxMe=vl8E zRSA@HJUkUNAV$WZA-u|>f5nGjt9IfWZI5xMCMiQj}la*4_7%?0vT1Q{^4_Cz6biiLAcsw zYX1zxR^9}U;6}CQDw|a-&zQb#8TDLc`kh2!I*$qI`lAnD&CLb~e;k~W5s`c~eXK!- z8^Yb5`(Wd$T6%=Ee7&)jcn5qcV@xa+ihhXjU7;oiLr;}Z!Hi+9trsRQlZIivD9E!8 zw-lsvVXI)6VgQ1;?RB_Ywpl^el6p(22dAVH`qKc=qqxSjq1{m#gkApC`=({>H+kU( zqb2S=*jiYSu5TcCTcSa@9aj9q)r~s@XW<3*LYi36ubpT`MgGP(Y&1G*#?q68^_!pxKXKkiw@%fLtj_=D* zvmDO$bfuWDLR5L}FRRSja#uW&L#|l3v%z&6~}c08hSIA6yqrTXQi_yx6v~qxYQ{$m$EIhk z65Ag@P34)!oPt<;Nf0CNsYLT2$wc;M(VHda+PtAWM1fUG#X%t*L7nU{v189Yl4&*# zWbRYVaQq?+NAKMGht}~m;oI(hwwc({z z-ihvR|5Fg``@j4ThiqyoagVu{FzCthU+&BzfHMe zK$=dE4-??;UgqHB#5ME1Fm1yQGwlK$WPSv?qs1vxJdU6gA($Q8o93N{3LWNK`t8`; zFG|d;3kxFN+ZLTl1O&n?zK0KJ$HI~c6@nZ`1V)Fg-5b49ZU5~)hPb_O9nSn^1TUfFF<_BVIaAG_ru zv{KnWa~HYf?x4bj4>9}Xb#^8zJ6T-9gchS}eMYcMYf2D?&`%3Ch`;mtB&NA1!?N$Q zHhV?ZIkjq28Ns|G5WI&IZAFz{nXXGO~vh`XHdvklabLWRgm~pq!m9|sxcI$oZ zjpG;Ma1U_Afrg22M5yZl{lV2SWSQo{3WVfIaZ(>(Zi9wp;{hsVl_-J{=TLI?+d_O<%X6;5BEr~%x~-by`WKPU$>!BgvZx1IYNJx@ zrkRxesA=Rhz)_GidUwj>U8ssJBrh2%(o;P$`Ge9U5YPgbgzxt&4==ROt8)T`jk$^K zQc=R=cdd#o9Vn%PP9Pu875{3BL0KZWE|JlqIy9b<;m*__j%iH zSd6o>;)G>%B?gsMEMH5KLYr(E`W^**Vk2)v>dLN)Z@gX@8Uh9M@8PujU5jzlgBkpx zIM@Amn@?|+?;2J--*29fyw{KU@0p3WOz5+xFWY%Pul0A7O2*!_BU)>SBT0B3)LC_bw0_C_q$DZV23{=Z9hZ;3 zuCBRPQlj0&6)NQ~ZfF`5;8u+~Giwtux+z*I3@{%iVr=3NzV7pK*5fh+QmEbuuInhs zs6RV!PzH&2AFs?u{f<|QyQ#AQg)|j8!8D&+L)KYB!5NA`Y=$8Z`6%o{NU}%Lpol-4oG`kQS5pp1 z&0mFEU}U;1I0xN}rSGd2Rf>}(^u8F*zA`t+wc27?&>CK8gJg@bb1*7A##Ry)d^T40 z1&qt-t<`upn72P@O8ky@nx+-muB{w0DrPP`4wP1n}tAt@*U)SSF zBS2e6&tHaX5b7^P^{%mjz66}7D(AG*SizgVyNlSKg+yr^dHR=>u9tvBC+~{ET?rPAz#-rPj9st0QD48;!d1-g!ao?*`%y8rCg^>j2x#T>Iv6m-4vKlA<}mhj2{xS*75gWP+TcBgz2Ubn@Q((5t@S~=}x zV{WbE5jw2VyXV@nq;gfD$5kGoB zmJYT;D&U8Dd4OUYwT`)VhxX{VMfE89H1wd~4Qp!;Ysfm+^`UpZ8b3Nf;>fZeznnjO z{N-Y`PUZ=B@mX0V_I+e9)ieXFZo|FIK4|{}MkfkAbUTU`ezO~l!z=uNBI^F_ER}}8 zr*HBmeW#HEu)U_}a|9}7wj3KV>fNYM5I>VbDKXAL4lBT;tu>Q9tg2Vppz4a>44G

zLWPotROk<14%QGAkZ@6KMGYV8}*%e$^E&0#ooK@s^jFYymy zdvu5qdh)x)4GrS^gs2hj_l%dBNt0?PaY_CRaJW<}4_?U!^OJP@heWh-0k9B22iKH7 zw)lnM19x3i$8EKPu!D%xwcrq&bEt>_EpkPgc3uXTAE4{ps;)(jiMPU6>zEaRXE&isz0R-swkv<#-`rMfDDGJUuyJ#@C&wYc~l}0;sU& z78h`#Xd~!qef*eYj{B=ad?t&|jp0DqRxL05gWDg0IS=`ENpN1cYMHwH?gF&yB^if6 z*^y2AO?*;9PZ&uRG;%^@fh8U>pkXfh3iE)nUCSH|lI}GhFxhr)bV8|6aaOF3zS&No z+=8*tf;^;9zvY172N%|isS!S)GL;OUyjv8SW^^5kV=R)hyI&8om6P&AVHv;#xxpu{ zVB!`;A78*R6B&>-Y~0|g%cqEngMEs6Owi_Po#*04P$7?5j;pzo7jt4NQIMdK{lQ83 z=rYm4&fHY*dat{8~eQVKCtqU4RL=ro@)F8FYgoUr(U2i zSSt)xO6<5mU2{fCV}*R9P8>NufM}ZoD3mh(q%$`Hn2Q{yYZd%uoKwjPtB;#i-Uvse zDF-qR`5VWco%7*O29$Z%6faydodE%1OIA#aR)HFjP34GXC>Ltnf%`qxPhF)n+Mq28 zhUA_pv(3W#zuWq0gqH}A7cDF8$kn*?rK!bF|5m4p7Z_n2M#hbhVvK~8zEU%pQZwyW z0bns$*xFtFP>!sR!@cxwznQuUIt^>^rQAZ3iVt8hva%=b1Q#ws|FxWc^9;)srp5^- zbV5>fOpJ0ZI%Gs_CySQl&}KhV+%i}mI*~(cR5m$%S+5T}-S4M1MfpPuEVv&y4d}Gc zljKf35E7Ie5Y|!bIW&&UA+89+y9J=EWVub4T&9k$(#8E%hP`yobf+eDJ3n#ZFQz_{ zR<}~oF7dMjIwtmYj0Hc~K786Led2oef`k1R^S;fxdBmC%yC7I?nuo=xPz9ec@@DV2jBK%$Wx0j548>kO|sZsxyAW9v5g;)7+DCug*%OfNQO#_*a+-WexTOqR|mmeg3U&92zI- zoCh~hxNE~jxC8c07uX2Sf|oe^+Z}v>^=x|p7TR{>%tJPg79rhjEG3*43|Pz0e!QPf z9A5v_$=1Dngyj9NDAy}b@^f*JxY4k$rC<^9RcWTLH-hcCNmGS41!#F90YjL(xH2o?lRVuF)A z-vVDlQ`kh;F_MFPk^6+$QzyT#we}`Gw!xdg8r8=+b@vJCqT82`dXfvg4zGxGvaZQGqtgnyy!KqRXR|H~P9nKA;y~GAty);SREgbrLL~M1_S5~n|t*m5DY!w`|4soDp{Wo#_BWT_G z=V>w*``=Dac*j<^F>%V|@K1&2Pc^#DwqvY zziTPbOM-Z_y1xLXhJ-F>_v%ZC%DQv(2?DzG_5d}=pXC(;tpcOM83tPgQCF?U`1t-m zL|c&`NDpO>uqEY>JGc}P2#D)H6>VkBJSEBm3uTJ@tz3g|+ z^@a3BmYdtd(g`5TC-rcep1hp&_T0XZ{eHW75di9nBnVm!u0r018wjg9T8(*moo0ht zAuAV>Y|{>8ihdxX5`sYeoo}(K#7J{g-|G34W5dSCtHKL448`24nVV8-qwf zKqs()(t>If+8oFRc!nxZ{8_rQ49$E^=B^s~(_%OD-jtMy11|Qi-%Mr8%H(Yb!3ZhNt&vQqHP6F>T8% z8WrEC=q=bxJJs~$GJd{5=Q=r?2zv>=wpdZ-(izoHVA=i`@l=jtEa1E?cRsX{#Uzt! zJOXRr?y=2ES5+>@zccHXw{k0HtbN2$Qv!ZothF=u*s0t?KduDYU(=tB`-;D*pBGwT zP$m)+Zt0Td(wM6)eAmjc>7BJk-8{!ERrGgpvG*2a9)++KSPe9L>>?JDjT8%GNK2~b ztjZ#O7iYM(_B-(LwUCyUj;v<$8M?(%zd7d<)2u~ZtOkEOLP;7g;kYEv+SJu1)PPU` z8wgh8a)%}|^%z>vrw=N?8)9x_O9yHlh<7lTY~8P8ub(c7gp5-!95b)CO+6eRIu8pC ze85UjSwk;eN+nsbi7ZMffN=e39$i*_SpJFaU!)|T8$lZi&qVy9_trwaj%$vyk$&Pi6#ko#Flr3&N zZe!1X7+G$ULJ$}*8Vk=4DX@bxj z(=;sCzJbTOI*q?W;!kx%{!tM6@vz(*2oHd^_XmZhJ+w!NSd++tHWXD*8giF(8g^J0 zN~0C-un73m?!bDI@6dWu@8EjOET~pPU`JaVaim}6M_$@QT(vfR1;6rNVE}uAL{Ad7 zBsX}2iZ{oF4m<4)ROi-(y65V3m^Z(gm8`!~3NXA^+))jLt2ly-b-q}zy!E?mYj00N zOO^cImQ3%d+z^#SOdXs-WFl%t88O{zl_6!jEe8parHT6S>mN5V6(wCS%FqYc;Ox#b z&zrmTy*3k^u@f^(vb34gQZ}8nU_MTpJHoaH{Q#bPfkpzpZ@0+6ysd$XBr`VaRrxHH z;@D{K=fv>h&XQW-D){~Go><>ukhIX(lrf)LaQl$6KTF7_VfbN=fZro9X{HWZ0`r$; zo^i-};$w=B49VXB?IR6Pd?Dg?GaAa5f~Q{zpny}xsXisIC;}0(^}hfV;bDD==&GPP zeIn|61~sp0iIkQ9(ivHi&of6C^wnMmtD4ZY9mopC%$`bsuF!c8nI7* z!+~6BcxN!7(9v>_c$OYjTsKeHIu|9b0d6VGW)zK8sKVWJpE(~Wdq^mFV}ogW0{Mch zX}#z&NRk7&zfUKUe!Z-3pJ z_cFG`Eq~bwdaUBUv(R^=V24@hS#*#&jfKv_BHv|kUU)5(W_-}Rvz%WSaKCd<;}_)K zm+vXmMa-df?GBQWNr%!$LTk#@ShE_GU1~_}rc;2(92%{HH%)5F}9;iK}>h>1Eck@Dj!nAK6J0M z-l(?LN%|9%1Dn%%EoA(nRC5VUs!=~75z1CmQooRVJ)8H@iEok#fXd(U(k&)^#$F(e z4rB~%%TsL5PO)$gma|t0)t`PEW$%Yw1h_Noi4CB(#RdGsSfo2%!$3-u*-h$f$EXcI zaR2^q#$9ZciBPy7SRxk!5D@Es+J01Aj9kqAw|Tc&W9PpDD!%Oq1qbGp^A{mYYP6xC zkZGU8J}X00v0){l427!b_k`(5JI}7cC#qs^dbD?Z?|+%Ia=MM&tqF|ZZFX*uMR1Z< zFehz!-{d;?-1xm@H%{dFe?9XAY}60V-YQ`l(N4^Xcxpi%ATtwb>!cL1d1@t)UtGOv z%Hubih-{}km3H|MGvtNdVdjm^oy@<(a4j9V$jCQdt^Pi3X(xO9JR7GMsmLk}ux`)*%C~QLS z-hr57uWx}laE({G|K?=3cUS9p1t#H%MM$W?A;O*bhLY+<8Z}Pa=GI#PU2UqT&oz2sXnJ7VT8@5IS06Z1V_;9l<_qU>z<^))0&I% zQ6ul(=x$hQW^q-X1BhEDPz0;N`c!_bP;*9KxNO8B3vtN;pnERBIoyI*slqdQ`!zxM z@1_F~(mh@ilz#$CHO%+Js7O#m@~9&1UKTE14@dBDvn zWo;)YV=JBgyZ&Oiagd26 zmMmH?bV7-v=DwpL%K;;aEMez(d9lakcR&mVh5l~UBR)g$?NU*{On zi%D*&vF9!Jb3bpkgaxcdQ?s|Oc!~v`FArHeWsuR>P#VnT`lxL1J^2?_-H~|QD{IVQ zy3mG^KDNV#L5FU7h6t6}TY7RfS))IAq2MTDa0}^kw?y;Uxqb7DVczqs;@T@)u$bVd z;m09nJm1xlVD8OPeR5+W9xh@lH-3>N>2=@ri3p%aR~T@Or2v$d|@0dwF;% zm*kn0Slo*Abct*X*AbxtD5g~xQ_f!jDcTJ*i(rX1xSdo!xM&gCZK-G&vj#LPo}U$q_^BL?|L#_ffwf&%+UpC4r&jJlkkT@e!c9EV=LJJquCHg#&kZ_ zq7NUtp%w9%iu;DtCeFci$h5|;&REbKe8%^93b-;U zBp_?2^ocko?hUxNp~j6#CeQQv3(^#|M)wq@Czmu6BRHYT7{+s-k0HUmc`}M}IkldL zAC7yodKrvOfE$p0SJz1=<|?y1DKp8b(@}Ggru8$^FE1~na7$I~J5XC+@E>;%c?2NH z|GDK4r(v>0PLvCPk#b%_%p3k5lhAVo!R?d65yVfl;b*H{gqLY{q^|kRka2 z`c?}MxcmR~E%}($C~qZ&&D%-08m$2DIN}@)qzW1@Z72a_RK`}ZbJy4&*^gh5Ym`>% z=k$>B-MhOpAzgCY1}ZN-;Iz2sZ+7SSZlql4&A8i+!X@8~mtMU-N7fyFgBMHypZx~N z4MdY*d3=nM^%DH8>!yjf8n<2AjHf$zG2q2|>wtFa&MBKOgYS+PArSzI&|K;<%$ONu z93>a4xWb1wsgUpnh7lQLcGIe>3Krb`c;$r8X+iPmsiCq7acW1jUYhDzYDRD4NG1t9 z`<8w^%h!PG90Z~t!B9}^b~#OWgI=7-?~;W_X%<|5Sve9~ajiUN)x~o085clE={NGo z<~dt%O_j}OUyQTPS^m`yQCXs!`>fBq@{e{{bAsQX!y219+%Ig)mCC?F&%dO+oys>B z_#PMa0|jN~Cqpx~EGsYJx8Ck@z8@$#Nv@i&Np_X@yYHw&jY?U8?nd7QJ6wlW14Jy7 z-UGLv9-o^_x4gLfT(*2~-{eCq4kw+OqpR)wdBKGjFHJ5hLn}nX_F+Vao@Fz4;Ir=Z zK5^+|{qMah+DgqCV-N!v)M;N0s|$Azi7Xk0sIq!G-iduKD|Gfok6+v2jUZ};eSV)* zPb=5|I_hrV9dT{}$;+Bt?dK=pZ^2YP5`faguya&lTSCCYHr<;3Qy3l}hbBtMvr-Lo z^|8SNBWByG2~FWlz>8;`XlvZbJqR1Q-GgWHTpg~i1UgDK#hW!KhN04L6h*&a8f24C z;Eesrn28AMF_UnEn0>NX%jrJ4=~l;#dEUnZs$ff9CWUg?+I{T3CYKy}@o3h;W~4qM=fuNJO-oXTD=-AhF0fc~&XHMv0Hl~?x<@}Z|9St0ri zSb>a{9^}76ufYeA5Q%=Q^z1+3D=hzMkufs1`9Bjwl;vbVm{7A<>XxjU)rb8Fulm8e zJWgwZAS$#GmAXng<80T{E!;HTH+Ww`K9%+_sDsMHfT1!bO}D!a4<4@Y`mo?hTu3g( zPQ_N^90X4rCL;UXr@K&j=U7^>_6h{uI zE#nfw(*4Z}9-^oEj{GlUst{j8PSkd6I4H5Q?C{lTUccYAAxr*FSV2C0?jFDk1$vkyu0Ul-CB1(x~*cm>Z2lb(iNW&-3~z^qJQ;$3m@ z0-<=ASL#2&{%@CQ%l|{!J4FfBE!( z>1Bja89*9Xm}BxO&h^);#rB+#peLBKHgb1n;`l^O^St^UgB_l*fBo`J8nz{o#K@R~ zn;5vbm~g#zJ)UB1d_CUg{RXjvgjJ-o=L`q;D$i)}(3aV$MCz_8f*Z(2QmHS=aAsB! z-?0IBMrfOkduk`OG7a_Yu{(5K?1Zuk)Nk06;H@*RGvw695xgEoQ*e%5_hjyz!VWQ* zeAGzkoIbR1oK9%jcHuS$N4Jw*i&?bG?D|_=TsR(N-FBCk8n*j%sROwyPyA*0XrI(EhAute56NZOCQ@WWCEs6_eCi z6D6N^*$3kmI?hMEdP<6KSP50%x4GZ+@q(xkuw4SZa=^cC&C|f$IS?N0aCy8=Z%jz>F$1iQ6K@#DR+rect`uaucgD^ zt}5du*>Ti*Ho;})QEc)|uJ4X*RV9q(Sg|J&TJOcUX_xs+2`)qKx9^+p1Wx#dU9i`| zEs3q&wk!7>jy(p{@^G?PrqJKnmG!3Q0fnh%ncF-r3s70^c+_nx+hXf+6x%uV6_-w( zz-?5C@d-QN)CCmA37_?3|C5^vF1VgNKW1|62z-i)WKBZhHPKy_=J`_)3h~uJs2-ME zoCcDT+a8b4ko=n`WuraC1LUfVzL0Rxvhl7TU6zUauai+GZ8J`^fjcB{!NuNBKRb6{f2LS%Y&6C_3{Gz8&`cXxsIa{G&I33R#;uEz6+I7xOR zndKgSU>}hSLPjDG0gPS9q$@~X^p>7d3pE0#-1I_u>aL=RO4N8OZ7O$Q)#;mO%bn;STL-*1C{yH-lCWl%S9Y4*OTn%7WW1G z0krx3d+atlq*lB6iQ8E}E#Utv0s6-rQc&N@>OYN8gkAKl_#I3g|4YAHtYj&R$dA-L z@3Pv0q(LdSO%U+pU*#78fu9fouXrPGW$=+V??R=RvTEWgv4DGEDn*1Q{qg&YY(IOI zT1>J)FL7$3oAJjI($wVmxYm~UmmbW~zFvPc8X8?O1|(d?B;=?Tzjb8i(%pN?IM-i&ANW57EsBK*gaGEa z<0S?9$;IQsfXDABZ95L}eL^jn;@5Xt!0xEo90-yrZF-iM)fA4)=8Gl}IFzf%5kJr+ zB0`72!m*P(mgdx^oxupW3F@_ld$f>7Uu31CmhKSdVNSFWj|01E*lL;X1&TyWnNM`* zQxrj{D!D5Z7b)1-mOoZ>r;rk+j@_m&d8F526xqQ?^NsSag^3%i!+w4BzhLpb(-9rFMgi&gflH4JmSF5KpVIKj~Z+JT@17=D|x zS@&-OZ3MmpmBda;^F!|=bL8DOtr;^>PKDe zum!Lug>#C~tqr!*J&k_g#ChqX3cQYLgZU0Ap)stK*aP-CTIuTfGX&HOpie}-t?y82 z&A@6fkd=y5$br%9vB4XG#Q*Y_WQz%=6WdKt>bqc&i7H5lJ7NrsUqhJm^`B7e_S7x&7{Q{q9*?D*N&L1^O$? z-*@z-09E=W0aRvVY#`NmV`zHa{RINn^~R1Aw)6Ewm3!ufnU=cu>Mx7e03`0y$P3A} z*Lb9cSEi|Fw_4B-@o0Gb=QHjP7X^_F*QfEeRPkWWOBZK0{Mqc8bUrN&%@e>Quxqnk-W z5)hKY^v%NUq@+ohHQo|51XK;rn}if%F^`2+B;q)TltO;+ZN(Vn&K6cuj{P5rVe?1y zgc${e2pAmu;rVi2N{lT~Hqhu64s1De0v+*l2q74dr{Qw%Q?o%SMnjZDY)baMwT_V; z+R)L(4{BwKvLz?crK-3L3kzxji8Be$9qn0?8e!>_bDi~--MI^4E7I}aP4jE>^X)!{ zy3a#)mZqfF>qKmpB>&y~& z)Lac*voCopMd#@iAiTc*pCuf}b1=HfcgBa*OHy4YI`I z#7eG1g$Ftm?C90i(xMh>q_cpGAb~(GC3~98cO1(m2wdz^m>vz1>-;WTc=}5VnqV&H zC1YDj7@Hj;*vOXXyacXlg~cKJf!q}8>V=hJan{3O{OK|jaGpcFj+e22xriSqcIrfT z3*eVVu$!NVyFRCW)_AV2v5=AlH7XcMD_1@r;lUX$Gt`ahdwZBZd0i$<0pLm5DY*gS6 z&c6gR^_oP9lrW=E6$qG1;P0O+L{RO&nVn$<9Y<~smio?$Iwr>Q5{0s7LYP`{xl-z4 zO`eIy2Y385VH;L5Br~#Ui9W68r`9(S<79?HkjW)C)pS5q9xI?KpF}HeM&^E+^jd3I zmnBkU@Ua>yQ7@<@XDMwaGnHMZ>ZISDVYW4UI@XoL=*g03BhKxTk{|1+3a}exiItQ* ztR{e#5@tx1EMHKL6IWO0d>}%M7?ix^FV3;T#*V%`i@ZmleJCMNY{eLxnFugeEGjC68>1P`XH;gHk2FceOZ~$Z%s=%w8s*Zg}!k zJK8{1mR~fRZx|-lQqy=fq(I3}FXJx0a`)Uf3@>YuK!rZg7ecX{5HD-gxDBiC=412` zai4q{8BvRPS9l7ep-$H*wD08e(uf@)(T{-Rm|n>zL3oHSf!2VCYPvp1klsLc+64w4 zuNUBFoJ!cqEk8wWDpckN-Ky0?o>sZHgCUr>69%SDZ-ga#0oPHXi<*`r<#qg4wga>= zYRIK%gG!}rLrcB6Fome;Uxe_9QAPRysFg_LMTDe$A@(8GcZqMBPWaN`M<55@ZwoQ) z&F`vpI7wuM2^;Nd)Ht%g_Q;Rgrg*{jsoi1z@DJs^yCDVk9nwYmNIv}z=~XN@Pb1xT z2DaV7h2V+FMCuCnrD#KE6=!SgT1y8u0Usm4UYGH!`WIvZ1r)z~Zy zv!Uo$7bkR5Le7-&4Lzxurf7v8o6HfqKC?MDgMF5_vy@7(CdM3pvD)pJ^L1*s^mxo& zvTec@jEvKCqcqIu&>pvFYr%px%`h+}ZJJHPD9%c={V4tZhCW}oXM7K!;#1UU2M|(P zDf!|pqn)3aBHx;b&{>+^4-f4e$x<9#NbMD!EC4PKN^XxD7_cZ%U1uplm$w$K{|s<# zBbzrL%DcHWuPs$H>OA@@e9eonh~@htR=epzQf4ZBS_KQr`8+;g(IDv-KL8h7e&TGq zRv^r-WYMs&>qb=~T&;0|$tbcH_-1lTi_f^3t$|lj5}d<5CtSWe;m}jF{;&YUP5yP= z1OT}!gh<9e5SdtyG;Zbt@;wZ1T95(BFj?77GVc?);)|QjiCLZQXx#*JT%|haUNIVA zll-V<&0S6tNt$fQu`TY1%Dp_(n3AJ?5`xX`cMt?u|HjZovq1kNtU3hvs+0uD>t?FwHEcs%FE!NsHlg`QfFF!luAhE=))vDS;g z?-j(<8{yQ0VR^d1?i67oMEf?_8=DG#N=(lg>%&5?De4JBZ)lZeFm%#3ScQ9xnEJ@1 zN;PKEKOX$EkOa1lKM_ndK+aBt&Vvq!V7A94WP9IY-8Bdd6%~1K6A#4Gu`b&B7FHT- zbEp2)7U#3DlQy!;u0ii0?Z4xr1QhVxH@U(@=M@dW4O%!PHWoxAIJ5#WalxJzooKU+ zV1i3%sU5o7mLt^ryL4A%K@k?dUmBpX`A8dmBBL(eBq5EShlSqI>%w5kCV!cT>kqlf z1tblJet$3a(D&0hKxU$Sbcon)B73_*#KQArMXAo~g_qPVts*&>bz@yy9#hn78%-rW@&;&$0;}mn^><7Jv^Ih(*r9;zlVQ z^p{Tn6>%-w&;#U=cwBk~1m^kjkzYwMhnEuqETWG5B?lPHu zAe1BdkvW@%A6M=X^DHu*r6PLD=vC0&3kaqy|1rJ1Tb(Q?xMoj4Pe%H-u)5z@1b?3M zAzTx2HwAr|!hg-F`F$oyl92me5GroT-un7oeCNs0@I7D))dwdTE`l;x!01sL09^kD zSipm8;2GewUt7hqX~8X^yg-s<5HYseuUjJlxO&;p?rf(toD~P4kBz3efiE2JNyb}R zNXleMQi<2gq8K`sk~0FHXM6p1wsbw5MFUcAFN5I-zT_&cr&32B?W#Im$wM5iIL}t? z%gWWo+{=4%UwWJ4Gt9u!w$GDAqu<`R z|Bg`p$lx*)Izj9f+^}ES7Sbb+_Kk998+~_&))M)GHNC?tbLTgY^&cBrLo>C&INgf4RlYloA+RC96U-u`L

Hn3Z1HtWl}Vh#aRL;y$OGU4er~Goll(&#DA>`bJaS;ijGfW!rsJ z2tJ1N_@}cmR z{@-@PXMo?NTiIdhH(T-ilhJ0^(@EbZvXkIV(EG}0`i{TsPMJQzno z-x?lS1yxDES#CEmUB}SBIKU7W<378RR}@fGBv2N)K>5y`mQ3vZG)j_5t%*gH$HoGt zc(UlB`Hpv*Y9r%hw4Y3-OWn*`_& z^-GrUox2_H(3*sjV9Lw{rHKqx$g%IgRt>dz4FcUiWw-qgm*JmyEDQdN$MO%A!4dI? z%CO1RVZM?gr-KsVr(l+lf(HgBj6hvI(?XP4!w3|3}L&_er&;HZ7Xd%yZ8Jco>KDu)Mn7Zb=y886>{RZrl*#Ij9FV|oa zxI4@W$%iY@2cnzkmF~$hO^4r-42Az=hK6!McNRZ;!*D&b37)8zLQBIt6-@Mr(J4otUcfW z8s#JdBL9L2@LSbH+kZ)C9bIAt-8@@1nuv;!(~482ze(#b5KNTcx}c)x<# zZ83jLn^FU5r3ddAfU-ojzj(rk8H#prC#Q{d{;ohAC)`j~$7%f;A*91+SuExOJ-y(3 z&Bt+sVIxcM{2bE28K12}Mrfq@U3<7G1Zb)0A%}G+N@%Sv1R;1bCM#V=yz~<)HHMax zY=*5A3Rx+{r&EfPIXur{E~H3v#JcG(=Ij9-Wc*ToKiB$2F^gkC{jh+xd&Lx1hlY#7 zW0Gse7*qFVQgoW~flj zqbIU?#{0%@7BWQDc4p_EJVUP2B7+kRHN^>tK!pYGamJ}oZTjKL)AZT~V{X8qKm1jz zql|(}Iv4dBNw%#?B|v5DA=NTg@+Jx_2*kIa@oLSk!V8djsVUV^l3RhN6Vow6@FmCE zO-zi5)j~ZuwUd-eY`SypSr|bHVT{VxD~;~fd_CqR#@6!fZ=QvEP+5=F>?KivNo8U= zQQnwyH<;=3wvgD9g(F1k?2uSzuK-zRuHaI4=WGyhi5x};bJtcr$!76mxty7s^qA?} z;Jy%xp)x*$oK2#-*~XJd;W`SUd97U~_d z!hlq&S#=L`^UUE+7$R|rpAhr$ALJ@ zvbYpXUYCwa(T6fIP&ujMt=>y={e};hb3nXHJNi|^aFum1=lC~Z-U%-^WEgF?=9_)p zwO0!hXEL8s&m0NX111U5Gqsiv+{%K_M=NX0h;Tm&1?(4t=s%n{!&d}$9r$ei8Jsf) zdoTjMWT=`a9TMc1rIw&hP6@YxKu2&ad6B;WVgq{Lh{1QaFERA;#9E4k?k7bN|*ujP<0CEmKg;vuhA7;jz=~kO&_>|IGAgFl2}Kus}XT zN$M=R293M-&OnrhQWq$6$=4|%-iW_4loXL1Et=urwI>Mbsw-SvhODN zR@^Otr&&kuSr1;<8zz!HdBnL17a}reSg`?#uZ+qeW%s3k-vk&>-)jK8d3*rN`Eq%A zxWits9zOZ_Kg9Sym_WU`#7*W~#sBa`qDJJ5hlV~;s!R*<=;*s4ahwak`HELVS=nV0V>z9CFWJ(nM}24(K83=tuu*X5Rf8V*6`?p6XN9T1 zcE?ER=p@Y?r8U5+EM^Jz8qA#8;j*2NQH*Tju+_?7nObE)Ep>n(OlhRs$VF*8PcHaH z;^UY04cq@@bY>^EFg8*%XV=Lc>0Em70!&LxiCVcIUf)t_p>W>3h>^pqga;iMTk74Y zCi^+2kCkKDt7viw9SW~*)6=~xo0U6r3tho_P1TvA`-Jv-9ExyBm(RJhY9|5NlIm)< zID?F9Xde36-*wG?)etiKa#7K2Q|yq^nN@*NIwnJ7>A6@9PeyHyw&n`6iTZjG`LgkP z)Nna{!PL}v`$UFoX&yLvKCiz1n9UiJw3iOURsPC62RIp%v9`}w-ux-@-o^aeI<~mI z^kk5sxZ_R@l8PW+&ABv$kNn`@Yq$ijikX$t2Og+Ou|4sIE+izU5(q*W&=JP-%zQ~c?b9GGGWBiaS zpY`xIZNEm7ofK1%npLIBmqjSzI*lz_kz@~zF`iGsYMc3%yUWl3;F!a&u~Af-Y9}Im z*-m(1H0CKX+)|C{*1OAck9eX^a)EBh`J-vhIuN57P(*z)<%%5u}@j?NbkHrt0b=5qf0&`j>&uF7d2rQjpgc2LaA z<9qpq33gico(Zepe((moF5v~KuM{p>;+T!XIsb|Z+S%0k#wW;e*gnK@`i^c>5|+D3 zM-plnBw&3eM8H>g2d5xBe?}09;>5#mCzOr&XvX_ttot2Dsz-%3#tZ6GgsCe!rJ)Y+6Ag^l@0F9|N+4nx2uf#~FbeX6c1Ktsx5yhJ*K|>;Uf@S9}mpTG$>e;-U+c(>orpqkQ?6d4zJI zFlh_C&JO1jVP|#!@Bs&EhEsDuP`HpLEwGZ`Q!{vC%kHu~dIp~M1eEvZ77x zNE%h!!{||z7`Vq1iZ8Nt;MjA5I*E@KZIH5i30dIFQLsJ96H;%E*--i^!|h`5NprYTFY*?85O`9cdshVHpG!2vL&QW1`3@(J zp4d%VkvTFjsQeju%myesYaBbrz(2BwqOk@pIi&`8h781-wf+VQME4GVghauDM z$jer7@&xe0-Nh8YNu}2;%&3ru1|O={=DKFNg<^Ko*-P1|RE&G<_59?dcJA0{n5ogG~oM(dW}%uQhXN?&KIUzBT>C4b_{_v-V$jV#edu`EMV zWG-B6W<1k8Y?`~IIMU?1-h}>4rOu~i%KL5t`3&f<$Rk}9y*6Yke>35=-F(jMEH)si zib|OPQRaMSPWKiv={25SGwA;A3EvF{8Gx+dRYGrQi$-*gAXOgN11}El>+S?n$+#x8 z3#MxU-;aFmtcL$ni2D7k0R5xU=$A~UQvzKQ{ft06N>?1oj2*l@9}P5K zd2wFwk0%%t`W*Qw(S%f8SROzGs?C`KYDCZ59$*Jm1T0Oio89YCC-2~)La$@O)|^2m z&uX&=jptMA^w#@@ZSKe0vn(&*N>nC1)t^Xxsv&vyE}~FF2R52eL2Py06(WCdkr6Bra`Fo--RDUAkBy30ZOHdgwfvvV z|A1=yIDh_dy0;TsK~Cd`(>g&W7)=@u)?bZMFJG0wpI6w2A0%nA*VP~0HLZ`WOWXOj zT+GCaPCG9jUrK7HN)F2S23XeXKQ94V4Ns?>pxvv8&6{7fvf~PW%oYg#$^zVXph>!# z*G)UXrhB_f6le!SNe&t9+UdfBeE6(qifb1g7%SP3oS@36!zel=kl(%mXOL#_XkyXZ zUbKFE2|VSUW(1-{-nU{zvX9HAjyAI_sUUVk2`D0>FYy2(BC;)!Sx1$w zcA%@35v-Nr8frz(jS;mtOCd}J7(UjsB$(60Vo}Nw6Q43k7~g_fWzj@HT^>@Ao+@ri zO2ApOrlm(IA4p@xN}sQwuQ#UG6**P4c5QKZA5oysQ<(WnH*$#O`LeWG&(AlFV>E~h zrv%wTzf$Zwu8uI2K4t!eAhn&MU~gpi9D0R%4aV7S&d%yV=w!+x3~%H>P%AVfRylP; zi#>5eiak|}PK#+aTfWJNadgujaK}cy2?=3!?h20NpxI@>EOd|=Sm)UKgUsGlxyy|W zdW#ikWvoI)D(SJCvcE+Qm+S7TkBVmK znSoJK@uFA7NKIQ{eqmS$27 zC-?&|N`x%ssW7lfR~xzEHz)iKz9stM<*r%ep!X|k)k4`@&UEn)fJkF7nao7-69>u_ zCgf8G+5BaRVZarinp2|t;y$}314cIwL~toL8#4u_oCO=|^@gy^!|-yGxPd9LVqjB) z=AkEcL%r83?Zc$lf%^l^OP~}z9X$|A@#648KDKn1L&BNwt!P}tR*dLb=L%t`&HQqa zv}0OV3t&_02e^%|b@>@=ztTs>5d9cIHa| z99EV8o|a3eaa|yPi6D7xkW&Ri9)p2g?1#jg6edFXzTxv@U#O2{8faAv7;WEC+ zs(t1*!0{Nqw9@%a+hgeJfwjJ_??`G5s#0j%mpJ&LbeK+Rog{ zSlPzhNzB&D$k^e(R1@A&^bq}gNJ7WMX=TA`?jY7$K!5rb;2{49aP?()Ek&Pi*NMQI z?*8UVN^|=Chde|-g!Ov*9#}8jCxS^hmX1lQ`m?zyCf0zm-+*EBRWFHFJ>sK4XnTq$ ztUyW$`9 zWH>N*40)*&JR5|LW>K-8f30t=J1;#5enj{JKO+2pf}8q}wOVr<=l>;-EmqZ3Bos#a z;y@)q2jGnC9Em{fItD;jL5YVJLVYrz8%(aNZyF0FX&Q|)B4JM7d874u)ZKwpGEY8i zq;M}cShgz#<2PJqL4;rmkE0aZoC~!Le-w#RLJF5k^su=X znflW2Emp$_ww$yv&znQOUo`2jbt$@Ptv6Z8wz1e|q*x;SjKMn#gihE&y3zX(j{yW1 z95MPyB6t!0%4Fkj9gr5N?@=GV@hXwO2<|Rs&zEixV><{jO(i7WqLp(aF{I}_GgKoa zm%;GK3oYtp>U&xkjs2=s5neZZ(f{cistn&@EhiySaI>dkm>=f)A89gL$sC61+rLPB zY+UI^zA^c6rq8M)PE+5J!OC+zJGk%BsXn58Oo( zTFZol$Rx z=h!e5Ki6yY{V!VRY$vu)>>1!2bGEdMQjuG#PA0O{;Nrl60`z?*?JyyU{ZO87YcCDDWuuy#2K={GWrH#L}tVv30Z2rJo!Ay z2>+1h8y<5>+gmtl%qG2=CD50+sG?L1Ye_c3#2%p+)xkrob}`)<4Waf-)tmLOU7pd- zu|tDt5qVwZ7wV16Tb@r7t=GCe0BS=P5cU&Abdl`u_K0n%{ajbW^>D)le)Dqvq?N$POkBFx z2TQ<%A$;2zwU{CnigAl25&4gZ0Ns>1PZNj%qZ=YLif~~yuf)W3anDU_7E4b`HxgIM z=ICSZ!X7CP)9&kN%*eI)bg9pQ4d=_rTeR418Y~n(dD^m=$Y$!!-F@1so|6vhz?u&^ z$YRQ}8;t!~d=oFA5{xmeP-XJ^B$;DMA9IlCBN+v_6%S(u@{7@ZnWHftp*jPMK06xS z0+6PR*sjfmk*%(fRL;aibPgSA*XO*DDyKucV@|``?vGQ21kRc;H zk1b9P>y(z}8NY8sYbaKvvA(q)f^J)-cTOAnUW|NnaU@zPoL8;UbD6s>#DQ0}*9+r^ za{7uH*`=DN+Ut!?T`wCpTRu(Xl%}~gL;q}h+D~!7qj|0_3{{qAwU{?*$u>VlGj$1B zbsfgAkLWtFs#Y>bQ@gW7^E?dKCk+8omEOX|rkRvi;|%XJHK@U-z2=S+#+?csw__?R zX0RIPM=F{GCmCm{WM6O4mtMJr7~@t^xR=SilYi9a`d;SFGOqpU!F0Bv1)uoZRDJMO z!MxSJJhU8lMM#b3OE8RR1p6^#$0B=1j*Oe=!=f4*@IIjPTu$r6nHyhdj@*6hk59LH{T)DtiuKlAc}l}#gi@ImeYt$x{;q_-cVCS(nfdMn%opw*6ZL23u)D#5xq0X_p80}F#h z&=h?MnhVO`eCOYGlINEKEs%em`ht$2> zrHV&|YpsR-ZVF^tWkd`nktPxn9xMG|Lw#6ws^~9B-A~lFf^gXXSf;pL2b{B*^NXwF z=h~ zS_-9IkO+f6#A$6T0+K^Ee}_43gvS{{A{2d0QHh-C*ugE4(xn7sFtja~fnqBzSS!rG zbY5Wzv=w!1IDhUw1gn*KxyDCxIP3v!DGAjLTq6riMEA@(joabdW+RPg=gu{*{J2F( zpgCIhT%u$G(`eMwb1UQ$I#ZspyfF6wIEGtlV3)x~Lc(qSkpa%NiNuys*^`(#XDzwu z1Ud%lUArmAf7)!FEM}{{GZVvdQA{&EG&cooAf1VNExSjWK%qUiYe(v9;j|~$&lMO31zeE0zynhYDXH#a0PKd z`n5$;hwW_wpjzst(NIwJmt_RJWNL|TB=NvXb5y#STHvymKx&Qbp0leH8&11}6)5IF z<&Luj^w=RwF@q2#G+rpeGouvD1z#dPR>Yl^+4u_n>Q>+ub1-#gXg33<`51lx_#&T) zwXR?10#ei!>)qZj{)lWC2v!Eo#Ru()82d6w78wNvR;U;M&h~)V#F;TlOHYIeOIT(X z4~9N18Ri}rycqbb0DtWQ3K~m?T}fkskS!t|u9=-`h#aNI;0RH{CDw<=tAqw4z|f}BCydm9;pv&!2kCLhZKkq0vWINtuJUXnl#5U*JJgd6j)$3$o%94S5Z&2u(IcRY z-b@Uv*BCq?1!91NYHgB_9UAaPz8d@d%`I7-C#Wax_JnA< z_T)@dw+7LlgBz9PKF56%9rea<=4D+45smf2@o*ig5645`l&!?VCKs~No4|R-I6#8v zTV}<3@`WeJu9PLJR9kv+H4ksTG?x5rdIHy88fA-)^J;_O&d@L~Pe^$q>2Bf^$}z_R z=AF8?HCQvfvyfi>mI#ped?fPy zHEo0KRtbCTCS^FL&WFu+0xW-Pb`A1YtGFL~dV#pP?gJjgnZBcVk|V{HOd=F$7nF|r zWK^qmcinLg+S<>oe;lm>FdMzZQO$M)0HcPu-q?JQxw+AXI4?2K*gbf!wg2>LI3%z- z#BAUj7gjoE9w3WiN|Dl6T2Tg2N%ntev9LTuMVae{O_Ie;8s6c-$0O}5-dfx-``9w{ zGM6yRo~bT`Lz+1g=_Q)|5I<|5hvcdETKSLkm%w2LS>FV<;4xa!jvqmVxqFR5<=8DT zq@5j2F9_+m-_(mF!7Cj2$}VHsf0?~-98Kg!f@i~(mcg`+c!MpVS|Vr>)NB5K%bJuA z)b@cth8uT3`wIUAP$}$Y=V0vUXl`rs-`P!N-1k4ay*7-aOs7dD!2rT?ax+|JgmUoY zDsN)=&9vrg2Hr{L4ALPbjP@oH<$j#4SDlW;n5bg~Wstr#S}&}xh`pYO0@U96cjM`v z(?4MMSkI~SkH=}-UO+e8?Lj+${RAm72K2vlB?c#bqzM}l_MFtJ?&2l}Xn(4Z(k+Lq z-Ji^%)F&kG)ERkFhjmY08qgs*u9|khCeTpi<30+2>WI|mXQoD&~=v|h(Oid_C6vw1Mb1swK zfC!eYhJTcmVo^s9qw2q{N2oObtPNWXrK@VFG<3GKs?-#=sb_k`Y1?WUoAa#R0gi{Y zYLo0MQyg2{vh1aLFFa(1TmZ%$CnScR7*^kvr|*i-n=oKDjK_Bqb3A_oj?VfMd+ep6 zpR^$-ZSEzVrcbG*5x43;I;<-{9U=~8UwP7z1qE1zen3h%Gjc|J|)XQ7ODWMeNpG5)S7qw|8x zK8r>ruTcuvSjT0fqAYwuc3$Vaap->utF$!X>^oTX(k!&_~yrLm;;gqtjKDszxqohrmBFcSGeL`Rpg?&e;p+F)tj5Lc!L{Gq3e2@ zglO1_Wlxvne!EEEQph5V)mM=I0FrWVVqU5XIN3Ry-s~yxe<9$Yu!evg8p)Br4a5 zTsFZ_-faD58g^Yc_*H57w7}d7rsmJrc?A-(LoKfkI~d7Qc8FL zbovq~J$O`N}ti(@b(0HEbJwYp}lE=_9_0I1sEH&tpbF!@xZ`{a5B^ zqf{y7hh|IvxV>Fw;2TWE!OI5-#f;CJcc$+yj}iZv4g>~1=wsYr1@4btIY9U}xuX3t2tcqY<(2{m<7Pm#R22F=ee;s7U7&T0|Y z!XV3AA#Fhry%QVGzM*xyq&=aW;FHt`aXU&D^#Nzxcp4d7bebcAy~|nj>D<0r)jn+X z_4z>W^L%0OCz7y#*f#-ar7Pca0e~2zmm;Q%PAB5&9~MWVr^I!nxEjS+s z3?~runzdLdIcdwvciMRj+HHg0G4}yQYm}wMw>FC?$qBv zEpLD}F*S)3m0Za{W?U-c{{}sNlst7r;hH+Jh++-`RVreho`jVRSWJ+CQs3hNgu$-@ zziVZe304h{Un?%0VT`thyI=X_lJ&raDqZTc>@+DYqeE`e&gxI7B3QD%ykp>T4Gd-E z*sW?;DeC^vpV^FI3mGE1DwrQjJ&h1ksd_?Dq1~Rwi9}pY9*2>KZi7V$CwR}+7T%vv z7HF&P)XfAIU<=+$p^ICIHXQzV_E;Dbmpx3E!VRQEC=|Hff#)cjiq}6J)JUc^ZbJuU z+NnLya2c|tymh5k&EXnJ+3?~XtX=Vp@R+$P(DHz3 zn9ieVQ4q0*bF@*QA-NGD-+9L${VBAdr_m(=eXX^oB{8Xclc^hGGH^Q5L!kb1VDw^3 z46^b($t9j*Lw1`PFNig6$TDy2wG{C|GMn#M@$1!DNNg%j8Ld=&f96@iXItUbabWt- z9r~eXP!F*%n(4`^Xm2005IqR=GQCtO7eAO^BiperiXz)JXwqC}+YX_Ej~pst zAd=P}O$8W4BIzM>g%Us$+r(93Et1nF+=SOUwJ**fAHEmNZ4rftw>R)yiDhT1yt&6j zV(l0)m;1^%5+ZA#fV&%Zg5HawrNsekv`G2qAsG#5k!UyCZZ3+3(M8x*1aB&gV4IfE?Gnem<;wry96>;EqL4 zv{GdHaAGO_bgBxT>GK67_c6PP)UZVvbEu8JP9GGIPtYfx`q;uKSTTr1Qn>+g-+dxQ z`431_Wuijg1pg`xE};=7{kfo=o64Krf77|6L#=E&{RAlOpYg{(aTxud0QGNf6+cl* zab4vnO5Fk@@q+;a_{(K%@s*Ut6^R>4WZH>KLrJ$y2Fc(Cc|$<79K^?xd!P88Sbp>6 zo=O2SUP_frf68ZH;7|etDC+9P#i*D*zdx;S27Y~B*Q)-y(qHF;?Kuqy5A||4nlbjb zjs!`mv&ut^ReVXARBx%OI~0dtZ>cJ&Bbm{R_diE!HI}pYwx#SO!&3q3K=rCEz>>A* z{97TF_e2N;<*o!{6wC@Kb@-t-{WL!3DbH5NUD|N{^QQEW0Lf8o+U(Pba4`j{K{gTw zUhJv>scCH`yBGfb{w;!7hLk&xxctvU(GhPaw+MG2+xe<*vWAWfnu zUAw!ws>`;!Y}>YN+qP}n{K~d%+qP|G8; zX1QADjsMMmQvkc^Cx2$0J7 zmH*|XLIFMq0Gg}C=9S`&*QnX-ehTqO(LKf*N`CX?k_|$&N~zBWhyN0z5~j!Lsk}`m z_GT(u#$Yd)$orkGveGP12HVA^6;C!Lh0cD1LupsgI|-?temVn90WjSU58@<38NI}D zN+&q&PKZzMK+C1s09`aOK9KJ;)824?qz){-l{-%jYod$4z zX~MT>nIfA>5t(?9(P3Qxv>ZzNne5t@h|uTQ?bmr^p&k|Kb?;dq_vQJuss)R(TG zCy1S;OuB)XpHv`UVJ^PU)Ez2M!=0BjLlGQf_ELqGW>Mo$qN0LWA}@QyncO5D`vJW? zpZrLoe~MW6#di$eCasI5G2B29s5(ie6!9V z4XC|f{@b6sBIvKa>Q=gL?mienX;eGAH&uWjT8?n)+C!bg@cI;}Q`YG(C*Pm%E|6w}P|2bRv-%Pj@w$%GCLq?X|xO6`)99Bx9Sw98m_pj2{9ZXTGJ%7bg ze9pq7fyqMabY07XLZ$AgKe#<2c+6(MOF`ip8`Ld~olTa#zg@ll*hU+`hTue_sh3i| zix-0Q3zJ~cx=Q%=ovCzmL7m!p5 z(a|a2ODY7RgqtLb*k=>*YoYfY8QH*n)$c7u8ph=1A=Kd*m!cdlhzHj*RqUs-yA3Xv zRqnkbZ`|VR=nQOLu_I~=x2A$u1xmke8c-+1SA689tqDFc2>p3)Tr?vxV0|6eXH%(J zYaHc`@jGjQ`NYBUt&1Su)ef4|W?BfA>4Pw5sR_&VuN9i@VV>pao^LOAyb(~KwDwr3 zzV9XZpMTmql@@vI$8PjU^y?SZ|H5!2{Qun_oK(|tMOsAnmT_jOZ$}?N5TgY{pe@y_ zCj=xSUSDu&%So)Bl-3^_WGr9BZkK3NfU{}iI~4s z|KT33vwge_>$(>LA#Hc;=KUF}qwwMG=hB5H%%m-Uvrij*lR#^JqX1Iub|i_`xZ#$@ zQGIi6qPK8AGTnMlZo`T>K-2e=pGF?CaLv-_K5E^r$z<_o9me0uqf)3k&r(7&H5eayG;wN@o&ij zAhKSOXwA*axn*pgMn_@QgkDrxEJd|yT)5sAQ#j`y9OMk9D6QH*G$Q90o&WBv9^A@O z340E{W(7D71;=QRuCHw1b&%uxBSI2fL3jx<2nzG`@Q53Ejc09_(_b)#eQ|ZsmJmX2 zy!`lh6!RG+(HfL zGE@Swvgnb{r*2XAoJ>?7d77wpW#LxiCSk7=Z#`K-Cn0NzLz?9K-jK z8)}%qPPPJnNncKI2_`0HaAx_F+bXviJ?@{|*vdM`)~Y#b*qq(*mXd?HI4+tN9+xM* zG12rUTq0{b{ANf)=&`BpFK>#R@C*@e3XZyyM5vUuQ5mE42|4n2gMK6=bjrF}vb_nnXydQn_ zRQx=;e9MQ5x_-G%{>qB`d(}P}%W(OhXp;Fp8)@I4B}4YSXEaUzk}BjTdt$_vJfFFS zC~z)o`zxaQ6Go}=WDFvf75N7=D`z(iPksG4cOJYS+}#SfAtef;5P zr$Xxy;Rdd%gUh!yyb5A%O1EG*O1C5*Y2lz{@5moQgF3f<{;*0XFQFW`<*b2rSMu(U zENI37;fzDAR#A+ywR3!b&~Z#D5L?4ZRBymJiDxv+qU4AaveO_f)dk5&brWxIJwdps zA-iHEZZ<1owBBtN27OHEQ;^NLk`@pS4(}gtnk{0Djw)y_z3(o_@CK+h1aGB|k-1i{ z4^>Zn5_i};0~MWwwu8;(mtoFv+SRnFikAUyMLP#waX6wGS%cAH^FX+F&AjpCX} zXyw8Ykzd|L@iB{9EbH&bG3yGvR0%Gv=$`rpn|*!W-&3$KOg3W#;6+#(oATn93qV~)c*sReqo;S3GgB2umZUN6$5DtRfDI$2s|T#j zm;ag~(i{hM1dD8~XtTlkG;llO_?586jHxwH z5Vd*K0zdDvkJUcDKrHS2!m#TW^zpUM40J!q;Czt6c_W7PN(kv16`H16iow@Vgocx} zuC3yaUtiPJw#Ga?EiggkLe`{j;oK(3o2aK-tIONcDPI7k;SC!i?EZD{fF5!;gXJF% zoUDgNLHb=a1ZLe=QpmP9@?grmM{N$is{evqH+bV!H^}%|Hz){HmqfHN0*EHu7TxB?z5lgBngJT&LnPebr5EyKDZcD!a!)&-xf0#Cr_5p#z{ zWRSVYxsp(84)Bwwa^Est{v^duwis$0gWa!K6aHz5IYQUsl?{>EmPy{(pC2p`j5U)P z$e$0D)FVW((gVijhc~HB%&0Y^%;1}3tlCeTxsVJsPM*$n#&F_9Ow9s!WuR~+mfSoy zk>!a>ns3?FOpJRcx^uewtMI@b_+yhxFX3#1X6_0Xo5Mj?V8vHsV*SUZj7Uvkjs4G| zDi`}7%4ruIY#-lo`z{_C?)~)ys87NS(P4yg{le_V`me< zIIEqs=QCqUKQ)z@>5`+>t%d@KW2JcUWfpQ##xT%x+{yc(6aLgV+^ z%&p-OmFR+G^J68~#0C$4pbvusiETnE9_iX$aQlP=!^rI-i&q%4cd!dc5ln@rJfg*5r>=Hy zhhF2S_SW9LwWjoX-)b*j)gGK!+wOANmn+Nm&z9UIzTxTXYLat^94FzG2DJ)cFWS>I zuBC+b>ABf?JGbIfzn$1X75h*j#u15i+ejK(No-ATrelDr55XW>{DFO_TX&BLgD|;U zwA!A^2@f(cUG)KiSv#$6z*G0%z42Z;6SLCo#aYhNGV7bF@4g!}eXYjba^^>|E}TNMHL3Ac^z|0*&rf^2s9<62~beNQp0UbYbW+dsA5FX0CqJQtqnWwK*s?IPzEW zIf)j>A{eBh57&dS(tEZz4yBnk)ZZf()Ua|ZTBg)V4iks=Zsf`{nqOi^cQo`VKB!En z(q*rcgQK}3w~d^@f9}D*NND)pZl(ojWn*)?LZNba)67tVQ%K_?m zF%~Mx%Pk5XmfPPZzAC%Kd{FN|fPLpa9D2Atet{Ni^tlya8H7{huq- zJjvch`2hlO|CbuV|6199st6^_tPTGQz0vv)4I!66^QM(nuCKo*fwGlcY3PfRpt+Vg zprJ^EHmZyA5+cHA$+DUA9r1%q9f1x58kYODAkrl`%Fk4?aCD2oLM-iO-mw4$t>a z3_{l=>JXMm)jO&b1)%!HB4rRj!Iv?p48y>^-oO$eo9*(G3$@!4NVlK1k~L2V6bv2x zk=P#1f9ZS&0xVSr!{lBPkc&yi9YhkktHxW4x#Pz_h$E;FohB6OBpC9>*WVU&IM_W> z+{%VfOG=&YM5;l$%vdeqV?U%bH3o3k7dg@DF6}Y?%zOZSzj@a!F4g%LVvS}O8lP_R9JYfY;k7jcC5Z)w0KWwgid(L#I%6I8jMVLs#t{*W;_ZTw z6q4bRw&_10H7hPCUSKQDTqihJ119rJwsF1I5h zSgtJIiM=s)_jZ_D`@U?h)zH@tJ!Gylus8=t-k{vRu=dw@pg4Upw)bM7Si>QguBkaW zb^8R)o(s`2t9Ap?1!#ACxRIYqwuH<|Fj44du~94SfOy^^a>CsLIprs7hG7hpXG4-BtY;TC+1SQoAdP z&xlrWA9fm;iW~XR{BA&4N3$yf=a-*s^@aV_R>MOtyS&s)g(f;vQ6cL?8_!$9M)xRN zG1ZUQNrCF6ttJuWm|)X5QYFd2To>oo0$0VLltuVi8GB9Q-9C@Y=@K1L1?GL zg0?gX4E3QkQtA?sTQ${>yYW}=Yp#eAxDksad z2#{%<$S2qkv6eHklnrv@JN@?nul_$2On8bh)y=)@N0(++=XYuiw6PKflUGH_!7wfi zuw;Zd(68(qoKjhwx_~67*Nwo{+OqP+%S)tGXJ8H)3!B?eHEDkpmKXQUa}0H~DtrQl z3#WJX&B-C#PVNf}a|@Se=B5Uw%EMi*!Vu$U@femM=u^0GrlT$5{Dq}Fy*Y{?Z6#VV zf=IJ+(;e=8bu)hj%BsrF`T4WenVFToP-l#4{ej@8r{wIdsiv)>;A9l_u=dgU8UFdy zUy}}VoS?hSE)A7urlx>6@*>j0+g1(br#E+X%yn0#N5*V|Ar2J}&xUg@Ldlrsyv6y2 z^@XEL$LedsO^?kh7oi?NpBQ7Dg_5t@o-TMZYpuv(9+s2E;b}- zy;Az9kOfsX*S7DT6$e92M>r8lTA+7<4c0F0dYch&;z7n@Ip-Mco}GnYdNdd|RwrhT z&*tK2$6||xv{o%yo-^xas;*Z;eH0k&%5l(~5B^*4p^B|;6@be1%{Wg5UGMoFf?Ela zDdhdZBfH61!) z)AOfSTB~z4;;c`0_EoB}{+59;@@wnk{77rlA{W{E4#il2Y^H1`3;HOfG~i`$P}MWP zID|2oJ<+>Dw+d-V__?Tn?AP;?8B6?V&nL9FmicW|lwwCl-mrw6ITHUzJ_-?sKn8ks z*kelYOWM&b{=ZQ+QaH_?f8kr}ZYxF}*f~rrXlt3q*yQd^hnBx$-1v=KdwtG3zID!H zTxS=CUs$)*8RBw8^b;lPUncISBNrG@NI4Pdvm!q@6qb*us;%kBVy_y$yBV`rgEMt; zq}jLaAdTMt`0c=L22|y&F;n$s;e}^A=1Bp&6y+siJN}jwG;r$ZaZ>;8cva9pwbKL# zE$RS~Oa$*Oau8Crmj}92Iw8DCmhuvjhEK9Va&vr#u`GTm&*7K(R*AHwuPd@@2I`%0 zjP5HdMJ0Mp1ZUX~AQ3t}NPd61+MM#!%^$^udFKzy^?t2aoGyMh#i<1!9u{^m&6lCs zTTqh8I|Mh%q?c9~__95j({+$7=aFmnIotejCJ-4?L#LYmQTL#aKZXNIi9LF%g%#m3 z+yWTw12{tZ@_0#S7iabRbxfJfUiXnv`1tpv0rG4zPiR~jwU3bu;1F3PA z4s%Ed{v`~p9mmXjdf=Bq<4t6SuCZc4JcAk>TkVWw8YEhSaS-AV5m!IVgz7-5ig(0Z z?IpF+ctcnfGO+ai*jiK)(^eF!K-!898j&G+IB4X?J<) z7ff^C(&dD|Sk_YxDD4m;T#DT^Rd%3-dm8rt6!Q~{+z_Q*!9AvhH(W-G2Hnrvn_~z# zVu6AVa&ZzCfhTNWE9pJBwn6s~^u!AP61sJK)Z>@?}~#Lu2#_E^&I`JD6KUQC8&IYL)66zN3a zEsh}dL>G4X>OfPx2Y+QH0-yCE$J=I+DNXeGg&ABp0|}AJFqA*Z>5E|)Cm{peBpM=< z^wUZ?4iS4_RINM-b+^=M4S(38+KEj;1YrgxS}SDx;1-g6i+Pmr_Y*Kt)AhN`cqAo> zx(H;%zYJWD+-`}qxl}(8fLCFRuQo>F@Bc@BopY1X22Q_groS~ z;8%o;L&N3HCxlWw#$l1rKF8}uX=zyu$vK@E`Q5P7IFbPd}Y$xyjyHs6T z0bX+KyyX0LTl__De!qm2)Btfmb0RE1&nT zfhcS_+_Hj+F9_6KTSLKr_Jq~M9fRM25Qpwl<5^tAig|?77gA4h!dbgzp0vycjQ9KD zZB~fyL&gSA*E~Op$39#kvNE>u4ANJPEv1UTq?nUG)T3uuf-=cGp(k81HX1jFN*(_B z+D?tq&*hsAuM7|D*@+n5Iq0tr$Xy2HW8j8(-LA7SF&U#%aNjn$-O1A%x4#-%0GTP{ z{oM*{A997tO7K$qSK-CJXP9$kr85#6eVUuC9^vVe_SS$=e8DJwQ&ic`p5DP=?sA@l z7p)m&9Bxwc=bz%4&(Ut!MeIl3@M_v;-tf+jt-pk>r_J%ZqD3stx?nMLLL+|5JUM1; zZxiUKO2hLgyOaYhPH0f@$h4v>h)?C7jjub617b)Z1WPZdVhgZynJvhL^6sW>5`@RD zIdYZXbLovYn&K|I^;7R7`Nl{>bs!hH=HHZZ+dly;{Z!;o{V>SZ0kCDZ95J2#-Hp4~ ziXU8Gd_gR+2xaqgZM3DT5s-=>1Ye`UKE@N>C907`fsL;9bI&8cW@AlUrjk{uuPZ1!_-Fc1s3FRr2@b zmm4dDQY%~Sd{>>Wb#QrTU0?2W)Y--iTp^9 zL)#>&v?6lk1N1RRfc&7Dsx+I0J2F=-hzdq{M&3l>pEpASbBuN|8gQyHR^cK)hTVLr zFzGX{6#2=pM%K0%qoxD9z{YYmeyw0JR;{zL8hT%6ah4`VewZVViE5K_xHmZf!BUR-qe^X z7j($iR{8!D94i>`u(v{@)qe{e3>dHI4{LX1Qxy-x3WtJGzweba#Hc;#SxM3yIRsA? zY3|kb^CV8?*o(G`NCEVttkE4GnkuXEF7ZWLZ-oMY(`YvA);@B5eJ%1)_Opcudguq>Grz#k4JK zA|XswU@#3{zuzkm$Ni3A{r{o_U#3;@AdoZBWxtK5a!%C-Myp-rQKasj<#;l(Q^M)S z5(X3EEp9vFzd3WV6HOvW0BelTT>eiU;6)YdzSTsj2)14EiUSlAXU+1x<{&Tu}^>^|5Y5?8cF(9h>m2{HR3wI_DKWPZ4E-I6KR zG(y#cn-*bLF{Rhkg+Z)l;L-?NV)`6nx+Gy|gm_tm*$Kc72xSSenS{X!z_th$By>&) z93*u1XTL8<9ttGduTY-n6NI*o%h-VCFjNTVH|1vvA&QUTu3N_h6Ww_dyl&gU2 zjOfkRiyYAVk9EnXk7xH}E8=^SYP`t)?ryG+nVs+b5}M|GB6m-&EuX3P%&fQk*ZAwo zpVA%$nI-_g1FL8sSvII$4@Voku?2K$z@>lH6`69lb0$!wU)>W{)Au^ay5leN{`-hr zRlsEn?4B!^W-v>S_Qx;H5Eh7#Rz5WAoL$=Y9g_utSpyMS!5C;02Hf$y`h=E7T?^t0A`uFHhvp)0-c5Wc2Hlzyn&p&Av8~30jampDv+(kEm~|d*Nowl zyCjwL!5|mW8zPtcq$94su8uxLe2~hUX*U-~-yd9AsGle@k*-i)vs- zG1(wAjv%!l%q?0s+TR2v{pHi|?>CrFe)4N);r+Uy+EHCp-^HhHwWIQ-n()Rr>Jq)32Yu{6@ znCAp+kmyQJ!>7I@A(pqsG~F4<%6j0!^Gpy-&*BvOnYe9{F2aWQj*HDTI38LpeDZ+!Cuf}{^!s=lse_S4j*sncVd%cpgTxh#vIxvwH#ef> zEsXo!R<&~@>9~b(-Vt7&hPZ@ii^ufqioLtDciqFSxI4^e)%AnNiCeCvLT|wXXPxQV z1dRLEw&&nB+U068B&P>k+3OYMv1yCPbCnIJz;qWzG$-U!zx4QB09-X0y8!Qc1u%$3 z_!)2mSw!wtdVUbloiXA;DVaK9t~(~*@gh^Z#VH<>nc~~V~FWKZvLeAh?>)g zJL!4sW$e87$SJ2G`h5%o>mEf=3HhLEZlZpzeDt9>h_niMoJ2}};;JfOqE{hN!8V)T zX5vLPD!k?7M-$IG+O3O-4t78AeRuDGN`?~Q9~z#I8C8_mDQzMewa#W*OF=12l3=vW1aa&<5BXj%t62a|uP6{Q>&7%?jy>g-DKBtC?V1VWCEmlez=nH@QnRON z5#)q)mJ@`I0U3-jbYD^fp)aN>A|*#~&*}P6i*f9k-Lo3vyywOyrjQ&Uz=jKvh%B4G z(9Z~fH>Q}Rm9gswG9sc>;;hkBgS-@nv{Y2b%XQtg2Vg&uqxNtE{#eVg?*@#qwQ;fNyP%tQKq^fjHnl?D*Y-Guo8Pnjt=CE9Uz#)6Y0%rH zQ?(30{`Rf&@y!QiQX~^Sf`mj8%j!!zL&5||%S95kDY!Znlj?pnM!XmrlheRVPf4wN5ArmgonjdWkLkZV%TE{t2haJ5L!N^u9W=6T+}U)9x`rQ zkQr0>JWwyqQ563y^t9(`HA3J?qYKNKfTNe6V2qX*;t_`|C*4iYB0mZt5+U2OMDNlH z{5w;4dCawoan@ zjFV#?F;EAg$o$9i1a}eVUI(3t|E#w)0Do^h8NV zAi-3X8Esl{fS{MjugPyjNYhCnzi?bl&#GhU#?x|Ono6+*_c%*_uT*UX0v?J5LF!?iC-L`?u;P^K2u>}-*b@ny*(bG$bf-pDuSh6J zfy!;C?m&Q}*FX=jzEZs@ta^<WBlec2&ZbQ`Utf7Lln#-n~(BQ_}_eiqoR-GhH#lAB56C|M2+8+SQt*n#z8P|-L_#E%Pu^wB7_8wC9Dp4sO$*~@#mXK4O zqWM!erzo5n>F7uj3<0Coi41e2P?W##KD_Tfs1%IBmK=Y_klm!Oo9Hf07 zK~%(k^qr{IYm)^&-0w)ZSQKYge1P zV&tKmnN8hR*kwaHBvi9nNxM`Ng+>0dYH`Lwrd3I+fWK9e?sSs=yJ2soc6_Xw1)#Tb zzQ^VKMSAR;YA-V$M|O#bn0@LNRI!USx6L+7`VMOIz9NR#B)nO5nklXpW6!ZTsGUE& zeXEDbCZj^}fN9QmIRMkq#3!CJzFXQ+q2@dv>IPm_#JuE@z7mAQeg*{nFHi5#8Z^k# zqpbC%A4It_ovqmgfDF$EOYX$I8G}V$hC<`OkEnVpI3wCGK)pVri;{WWI4oe55C$QE zeI=;wpi;x%yOfdmNxZ(|Doice$>vRMNr*meNy2cABx#gJ;5sgMn^@>NALsg{WAXC)OYge>M9FlUg8t+1KHdy-%u*h;^H+m|W2 zBRChYb-+hkG^$#jc1!}4qJxaZe~_I|jWJk>DI7wE-0amNdWphC>uJW*G)~_bhjOv~$Z1 z^Qz7*zY2W2kg|H(3GG;3wH8kunRU^x=D?}&lTm6ojq?Zg*s^R3a+T@8 z8y9>zL6|6!7mB3#sbblfYW4xHXx8K-o(HQwZ(b3ZbO~izsSM9H35Rj-XzZ+=U?yX%^lK7hBkykIXWP5sl-L z)($;q_lEt^oYUi@!7gCpI0U;BV(W#jRBe!cw-Y>Rk~VJ&2b z(M<4@+$Jru6``8f+lbo3F~(07Viq6w5<0?n^=&2X;^&Th1q;4y4CYUK0)ZC#*^bmq z&_LdGFEZen*f!Ch;3%nnccPmPrq(cK4wnCFuSiyWG37+h9 z8g7K1Pws1MHA=0bBQY#BORFPnMM4jbJ4~jUl#-+4sy7cqUv~}_@a77((5k%kvLo28 z!B$Db%A~hakY)f*D1{IpJzoInx9$CjhBT@nTc;(E1Pk_-%B5!d6SX;@$Db3oE@AK| z=P;k@)2FfH2uDq@1X_tAJVOBrX>Zh}7>WA#f8fg|QpeL4;W#LzwPQ)t9Ak}iEHiAY z;BzlFeO3or=)mrT@JDYb=_r>)@=&V;PI(ToS}KP5K)uklyeC6H7LKZYNd790N204c zDZX^YIMGqd70o71{qHpu`FFo12kgE%GO^_bOsM08An2|^=!kXb0x!08`pPz2hVnAJ zKiWxeUzPMo<_Ra#@3l(7sT8hWYOc|ZLM+$v0$1nM$}ECXYhd4|2?2l!pl_%wMF3pIs~VK`Tl# zaG7kemB65RPEB>A^red&GY8+HQe++= z$Dx{~>96aHK2{$DO)f~@O(n?VWD!93j9b<^hhe(_WM_$iL@&v#hGvHY$?jHf^){z^ zk6%n;ozjQg>`-Y7be%p0#TzDBlf+KV??(xO*ZsU??JbGtt?nX}vDYQfj`e4%LhcSQ zF>0V}{igIDSRebCi&j48k@Ayrm&}|g6xB(|5N_(@42PL-%68-WGxtqWGn7|u{`~Fp zCZYyo7=B|Puugr33fwm!pIod`+wZ@o@cdkO!S70+Y4eZLPvG-G&3KNle>1fRW|5q+ z^E(1HI1;{K_@#91g)|78fkGj4`^TIVHV@kRLCxtvdPd5y6BC=Nbj-l`(>y(ZezseD z&p}_FY7e0KcUAo}J0zjY-V!=t(>KR`fzEq?)G^qev)Z0ZYjPZpXZH%94*h=3Ms-Rk zR2UP%4Beg&6-A`$2}K-kiMc`0(k4FmjXTJ&7SK>El0(3rjkXcgMBf!~LyhkkDRY6} z7O+C?n2#V3UVex4FiQkRL@RZ!H9#xiZQ@mkx`y%`TSS(k;u&a0zM^!OdS_LW8bgin z&pVUVZckEvEBfGRVRz%`i=bJL{3K&e`@sQZ;H&4RU^%qAuQbK`?O93_KMu2NHKXKR z$mC(!0i{v);QwK^ViMSVPN0ANvL*fB$%MoVjjSF2o3&_Eh5U&!AalFuBxwDu11E%o z0|Aj@9?`#zY78_ih`<}@pnMSxnT(CJbl#r;?p*_0TvI<^FO$`{l&}I&<@&}bSE^f8 zdRbg5yf3Xads$R|PETAI*KxFCzH}YE+aGgJzkWMRbsled9d*NU@e7iK5fHM&w7xddatY`h z@2!G)1$2!LHoNYua~^WQw)>gEs4QzGi*^GQVE) z!_s{KqF!tuTGM!ACzg5E%k7Hddu*g{YZ5E?9r>4jER?*YX`kPy9zlj8EzdQk)FLep z8I7SiTK}RYx`G>NI=U;?g%oTaFZgFexL= z8B7kOr$r$Bk<;V%pDjYCbbw3NfHf;NL!ac-nz`VcvIo$Y{HRa1z;pgQbWIM{oWZ=xtxIQsqEw7g{Bb-YY49{kv=)lUc0_gE2U*g@X z8uKI(L}6evl_5?Dxgcy(R~`3*Z+Otnz29Ra(^8UTI+d*X+ZdHz26>iIx1Iqr1~M0G z{-n;=-BH^oK3$WuWk6mkWsxXH0>dv(4v5CTnXZO%tv5C`R*qpv^ls+{d0^X_GUp?~ zipR%7OxQ>B#DeJ$!M|x3%iGSgp3Q}aHz?DXKra3x&?164MtYPA)3}TqKL{gqofxHQ zWPIsGXzFb+d!)`(1OQd?8g!4=%Bkg(6&Pj9We;ycwm|4K2GuThLYU^?amyTBOq~Jz zDYUk3BoR;7Yn6=ct_2TKMPqQ!RG?EPbKgAHUrAL}O_HN&Vg{azG?r9W4R9Ov?=mRP z9qH^fXDaZD#GV+ChdE@Iu(k7vGA6X6CWx{~DG4ws!$PUAXcgMTn>1BG#yVCa2=5xL zn%e17RG)Q7umnnY$6zdA;-D|XJ7|P=qHhHYm@Jf}&=3>8GHuD_H+0X00yQZ-jpnj1 zD^PA{u#k3S+uaeb1oJ0+i|YGwHK?n5YAh&X&POjw_J3m^(N18R2$Rlm>73sNUo6e7dW>{0;d;DP_98 zdGvo(>4Kyb-p&}X^(s%z$1cz@4>e6ih6}6ACr^}jOEw07zfYwr&alm|?HW7VFDxH7 zx$W{&T^C175ynZ7l{=#kEdBO!t~=!bu5@qk$6ubl<@+UVJW^ z#BQ~n)X6V5zH7p4T}9mM4BW-7mRUb1N3JITo|KU3%Ur=8BVboVnLayN$&$nt?d~NBTm`x4v?q*a!q+RG!1WoM}_V-D8f-PU8y# zo%D~h5NU6%tRY*Y1wIL8*p(@#T#VEuW@9Gtf<7syxDPU6UqtiWr%qv8x@M>&jv!X? zr#uZ>`7B0nH0PcX?nE2!L2%q^FG9R1d~4JZa8_|agk^^AG+NXVHK8MpM1TJoix!o) zI5VLx^ChPeL#j!p3_@X~2sTpY=L@4WZZIH%Y>oh#gt4bh0q$*OGDTCUS|Pppm+RRf zw<#jr=`>Ir1Oq6K!rkgvBdrroiQCi=?v3nNtwfqC3_}N!<+5P8wmK%0h>w{k)y|ap zB*-VHuum(+*||UnuLPLAV@)ubjl$l+ws2fuY)^(MawZXnYOGI(le2irPHsdyQgoP| z>%fTD5wbdG6t(4c`;e3N7CV1E|7M6l{431f1(b)+HEK9-&L<*s`DD1+=Ps{Cth|Li zIE=(wrPYWb^qV;gCOK5NAd zw=I(R^T%rR%I`NHi3}Ag^61XF-)BTnmUuxBVUK#Jsic^IhL!=HtF!E453ffZK`*WB zC2xc6V$Q)NqmZWaP37D~1e1W1vl1y&#R=7o$S>4E<&h(jn$ECT=vrj_fE7_}bK?=L zBeC+=baZi!%QNtjO0&UVAMTRFT86YfNc&=z(ddp?QUyjxJQfCR%l37RT_ep)&D*c|=jsk|4q^EYtXg@UQ1r2!vpl{_&MSdF zA)06$`d`2A1~Gu%M{p6}N#X9V3v?%%WB4}`pH%a=g6?eNWwWL5N{%IU{yaX)a>ItB z@|OC~q7CqqTID7_A|CT&G(=7^A>Vu`M}Y&I5}eI{ros)%B4x>u@@qT8UG=3P59v>W zmXJ3E-eQlxoB=`AP>+CN^82D$bmjEDlXCuk6fNU##etdzfUT$qSYx+L2OI>Y&H~p! zJxgi`;8;fn%ONU9@x4`GtgqTEuFU-|A8LY8XKHdD!Jmigv(FHP{d&LIj`?=DnJ!w! z(ofkR_fJ_LNPkucZ1ofLJ>A7L^nVeToVpVP55}O?6rDFiTmhJIN6qMu{Zc@gO!e;XXSCYXj{p1u-6>e{PPg+dyA8XVBB&v;ZfqEN z!spntuUl5@nEypYxtjhMKsURGmdXus^l~8#?;URbQlDe)9NUm$?s5%Cs~EO9Ep5Kw zgptwu%eCHP&*ACX`eWA)(F*oKAj#o`>{PpX)-VsjauCVz^I3v{xNo+0v213_V4A4u z@PMXtr#fS4xj>m*rxHhvgJ~7krg%;*F`_JNVPtQh=^IHk0!*THrZ%w_dBOe6ubJ25 zIJr@+m-1|r>hLBJ^J^#D*_Nqy1Kwskz8KBOU4m%Sklhj&nSn7c%t9FfY3kt|A#T}6 zfYe0n#MHa4@2M>8(O}H@K#)o}k?&x<9Mgy_E{P$2vq#Puq`U~YAswhwu#0oa#Q3=k z7lL|k?NSo_VZ?v4pwJBAkZO9S9Pe4(bak@sQbz0B{m~~e>CmPy zOt-4iHomlNJ1cG5wr$(CZQHhO+pIKBHqPj=`)c<^{D4>^ zV$FBzd3#u1YPvqW+lCnRQEwijQ(1>6OfLh6n`T{|UqjoHtRF-*^0Yuaigy^Tu6kke zR|WFiT-^7wRQ{Z`-Ojq*&VmqgE!uEvLUJ$KxSbNddhE%AdF#?*YqcII&^-Eay=`XibS;Gdq&}USPV?GuL$SO~*MRp-HSe zEgQn*=;uK?P$Z?HMA%^?JHHfs6MIV(=@kny?&)XT<k8p|2%dyxC<#1Cd`&a9eMU7*wLV=Hj zq51w3xk+NOL-6sULsMkS=UHvA9|$Z_YN_+OclgP+)$<&^N4Oz}tfA_R$g|qO^8vB3 z%&N>I?|;^^Ncl9VoPJhepnt4Ess4-YcQNDtlQB-E?ARbHA&>O9_`py>Q6=$1360-4 zx76Yh=+}La2{j{AA&1@D?a#aboVCWfYB|CVzavh`& z(XTi#G#_YW8yqt>r<}E^-B#wKUll6%QO&Par>l=VSX*l%3rlkTR1n*y6#-IvvQk$( ztRq#ao3pap=hdb2+LyIgtKKR7Tg{d>PeQvFg4GUc?|9G4SFjq874McK9GvX*hHhwq zDtUUaK6HS)5zFA}s^WmlQML6W?St2FJrc$W_MT z%ca#vG~c)b=y_C=IX5iKLCP5ZSOoz|*caAXTTg9LOfF8X%0uHwuSC~5Efk9gue4ad zWoc!BqL&^4J(U`~CE#%Tv=!!34>@hwX&-k0b(?S&KxVAJRc9y|lh|++q}?ChiE;8; zx@D;pN%Ifk2OXDNsPckrzayG;vxF}hM5=#@ZB?&tm?KR&vK6ouI8{4$`X9^rz`+-L zZ#fLf;MraKUvGPy?`AIa42h~a)v{btO`+j!|3y>U)(P@x>YApEE_x1sixYUw?B7Ns zK2R3PX;_-(uvhrZ^tknbRIIV!{LE&yJW_>i*BAoZ6r(lCYAh0{A_KFJ=`@GLoRQ(^ z=GHU9HiLC^nzYm0c{~em@~ACld|11ebpF}LM%$bQcGhR`n=+Nsg=!|Ll?uLE#fK|K zbVUI}e1T_0`uK3y!dLRKBDF=ep}Z`;Sd!Xhs)xvo-$UiuFSCwmYzDMu^^Uv{`4zB{ z%$GoNUR3b(`4IbgXBw=O74RVaiO6Hs5DXbVjaWGN@!i!u`ng}FvFjh5pU8zNGQ*UUVuCqg4K3mw9- zJb=;`Y9dbZj#ModXs*?`!6V>|<)&WkCvTs+H<6`Pe&Dtg#NC@drFKlRQfi*{|4d9w z24*cJ?t`%qvn5oIhZkXXHAob9@N0RH5wqDNn+@m;pQA3zL7?etjUf?_+o=<#aqkc1 z5+wQ{L(GNu4A&kqu)|<2w~gh zApKZF#aS~<9}ad8rr$|<)##XxSnuZV3po%P!>s9T2t|#Jyyy7Gq-4;90;uw^V_?C@ z)kvuGStCv!pm`z}E4bA|OiH0$PYMDid@2z{X);| zWx3uPq&qBWyDBe3^@4j5HSh>unv@+sj61Bwf6&-}P?$J)0Y&HN@W_Oi7Go7|3;}5U2|{uJI-rz^^08)?9doLK@OeRTI!}X|Z?k;LRSa}P(-i`AGZ!7ij^-OJ)afH}19$q_p~lR) z@$LoRMFe>pW!R&7V1~wPx$ir319yWKwZq*(8%@wM6fQEEeFgc zly5CB)#g>Ai$Pc?%Yi^-0*U4zJIP-quYt(y()#)%@FjlLM^T-1U6jr$Wq$JA;u~fY ziFpG8+pgygX=X_Pj)lE6kuRL!J(ud`aHNUkL%=y;i~IQ?^Wi)D-m@*sPRqIpI_1?NE$i()1LZJQ6UyoH_$!(7$q0}LNH5N6{3V9>y-ex8MvD!J-xbp5^$mt0 zLF?qq>1ME3c@7jPt9x!*W31^SHWzaX%MkN`MTGDHj1blgHcMHNE4M9D?Uuw_QK-JP zav$F}M!2La)8g6eu*P8cq&HirFC$ultrqX-7ftGr>2#z(={TJGs#!NtLhcKtk9@DW zxb+Pk6AlWsirJs(ar-(g+oEaJoD<{xT6 z*kcFsGE8TH3rytbs90xitQEHdOlP>iAGz7~zzs}i3~%ABpC8 zF4Z(SbW`UR=CaJ2Z#mGYJFk+1#=3r$d=b&)mgA{ zlPRe7Y^(2>3MWsv5XOYbWRxJVmU~mqnF##t^>-Dki|VeSeoGBhb?)tydH0PI3%Dv1Z1qEWnMZ07UTz&Fq) zZlEH4U)dEzz>IUu+N+qVFr>x{~by@ z`j*3VcSKV;?_fjDAu=tdpcYhQ--eW5DifCNm$~#drSSk>m)4kQmN$@8l zOh28;F==F?Ao#62_LR}B1MufuV7w_-?40tiU2i<2sdD{688;<$E$k)Ve z%&cYYjSP(pe$eVR|J%_lDTZ4bh#o#DCnc76PXt+!ozHYXUSEO;vN2Rt^bzUC^dPE^ za6^zCnlx|s7ZxP_20n0Q>M-N7hx?7k|K!63H6W4_OQRTvIbcJ#iG(LBN7n@G*h#Xu z2Bo-R>sJgUd#zYDmNdf7SzIuR$VhLLT5N0BMNmHQs0_U2G?Y*9omka+1KRWk$b(eL zGk5H?LyX1ZvQ-1c@@$3m%)99(*Li4w z$#LFSb?Z1;-mCKU&dSisv-OHz3$&4d4BsdEdgAI4bR?b|v3&vm=h?Y|EDKux0O^z9 ze*NP8e?PnbLks=iGaM>f4oF7G-rJs5h%8KsX25yyyaI?Axw=Ei_j2lHQopUpasAs1 zX~*UrTN*m5=r}mOfNaw4)U7*98RfCE>^kQG-~Qi&P#5eS8%_)Og%EA7ywlkanGc;O znJ%n8pYIbkzl3!QUJ# z4~Jceg8kTbEj_~enbW7&!6!I_f5?S`cZ6<1#IbsE1P zufwl`AU%9O*_G(qfkw~b;9jve-RbJUX0g0PxF`7h59z^8hYMdk3ldM>xb+^D2v03ZNP9$mWO2kpC zznM4zMDZz%6>I4k6yw4|<{RHwuN9oKZl{A$OHbwOMUd#{ z@^>NlG3XIpGZhe09{5-th!efGpUDhiX;f&w0uP@<(N=tf&Pl8i8M1ENd#ze3oPUc+ z&Z@}-yB__~X4mIFydMv86~dp_z{i5&&+Qna4M$>Xkc!W;#_Pg4WZD}y;U|uR7T<5R z7@W3bYBa-T$rE?)RCwRh(&OcOq_yeHiI7W^yPwU$#|{$;|JCFrB6nL7jGwDJV;u zc#`X*Zx?H%%5FPce5Glk~ zGfYcQZZJ#XI~MMnX*%}NU&7tr_Y~!{WFMaubIPrTUol%8jO7+FC%#1nu_dME+eZxA z_dDO4&+7fBCL;7h1zhz+GB5S)C5|EQys;lWKrH?Y&c8HyZ{{!$|4Y->mTq_OzWaX_ zEd~CGHW&U3;649vp8uNxJaH#0+y9I3Jf$r4LwH8!o=LX492fKj^7H(4mO_n`$RxHf z7|0Y$GQ=DR%&*R2CbCK^tVbRwZ>R4=N+gkv8{vlnn14t zpU*i}mtw7054XTHZt9Jt&^SiIjoLFGtFK5`IyqK0(qMElS@>DlrPH#`aF=4VtTId* z>yXA``RRW!5$C={>S0_{4TN3t+vP1NSx51uw@?Tks*cgU&R>IMK z=4LD`h!aI^n)K<3Se#CxkJO>(EUmNns_k1ACACO0IH}ycs*hp-(k7g+lUEo`kS$Ob z{d$9Iy6D{Z&`RQ2$JghqPWB_AeCex6EolGTV_|~Og95#j#njc8tjqzA*Y`;;i~-2_ z#F}4|)Q9aDlWUqgsM{%}mZAEE%EqT~S?+IiT<6!Xc(Q0CMjQ%9#m9XS{mudKJ?;zm z%^n;(l-)T=Mit%iBo*;>2JllO-+Uyn7{F)f4rE{hcX((m=#|U*+G|-hG(M_uP5J1EK~!U&2B*{Z3n9R*J4$w9nNVx zES)ZWzq5*mqKFTGaYjdXTO;)%L(kG9p+<>sWrt5*_?a#kczT5ZDR+61YMZh20T<9G z?&44a)f=zL4eBv@^F{>P(fd5v*V%E#KbEj@gw@vlix+|Nd9s{4zk{Zqg&)Rwld^c- zHPQ=ZVwg_*g^&%{K!b*X9+eRK@IDo2Ou5MB`Mh|E#>dF6qUI)rq3zBY&&R?J<|XKy zn4bP1Bz66LRDs(oG@+F9!0}2P^%mcPr`boY()54v9@}M_cGaN=L>l;eM$8e_g}N3r zG?KUx_7MfQm~r9F$qjKjCVKVf_{NPQ<-Y%GO z{;5j66ysOmcS;Z*M+Jf+#PBijPe4+G*KH5HFG9{JEJhR{oVlHnT~|&1jP3Ve`Z5~E zK~XjJG87|jbg>-hN7zv@DaXM*?1FJ+t6+vQ!~2lQehvQBPBX>BC()M!#))wVX>> z*KFT6a|Ymo9GXDL6mk@mzkMUtwP6ZTrQScD2_4OxeM zwSqF_3XH@B6-$x$_3DJz!0#_y;+>~?Ri*$__{6XE-t~D?y4XYN&D&^N9w?pI3(@%o zFf&hIUwMqzQH!5F7EYKR(O$NZCwEGgY~=36<&+R^6RVp9tJB1b@mJxWQUJGU%8+SJ zCH-^$eu=DK0&ZmOCYjoySn}WlCSXTj4VqHVT`TD}E0Qc*+?AvNa|bF7 zcQAKMHGl?x0-n--WOkDH8X0E8J-&OU*BH=VY*}$?p4?HGM!XIB9Yddaz7pOzM7+1} zRZv=$mMOamDezvZ2e@V%NOLt7*qB#TXs|!`zd%=}VXhsVUvuc=wK-3?w45a0 zrVlhtxy`WFYkv*fmYAt>KufK-^%=RU?-WuOn6@?-K%yjnxS8i()8I=9%GYM>2`dOf zcZtaOl-R=mzL@$IO>*H8WrH~=`epyxoPj}#-Hxg$wUfC3`P<30Z&WQ^!`}2uK>^27 zI3_r2yFy{K)ib*Xc~7*xgKLJG<0|T{y_*#o6Y`TGMKl-7W#Yy=KPwt;QM`#xjK&-` z3VH&j)OC^mA9Dtil~;nRx@6Ygx!s+Y1kQR8NS3ovWGA0_b+&JM#I&@nZ(O|qL%uk3 zVsS#gWT?Oc-FfX9QNls;d0&M4F6gyN%-F!~2QZhNqrl(7EI~s>zyf`FMjtauc*Hp+ z?JBXz31hJ6yF*j?dn~wV@I#D5vc;j>I^vpiC{nVK!ec~4?4GigRAYE+`t*rJ^=vrP zxed_?u`S33-DPx0A{vy+_8ezTG zFpE(Abf_?O6De%rgwN?;O&%wb)mC@wtuyIER*!!aVJS*{uAs4NtYrgdzs!pin%0+t zYqB5m^WXn@4Lg_DjimQiZ=z`h@>sR~oU9h~P>kAl0m>VB0*C84etoCtYb-TsW$3*8 zTtx(Bt8fpZd)a{vA1+U?+N|5VOcHRW^ft+V;|oCqT7!}e8sQ6MROm!AfF+`NpjuTK>O!7!5Kv{z zSnDJ#s&KRh1A2tE+oT>A@fGxq#?jNt5U-WeDRG)q+k_h~nBe?`U@NimX$P{&37NKg zr+U}z^pR5UF&(K$ zfEK%T5Zr=H?`<5r`5fJF>w4mC=DI?%wPwb}U#RjzyqQ!=-^LZaQhN=L4&7ndNZ})- zV!srApDg({aK~*9l$kTW!MU|?_wvZ{koaEjdQ;~vi~!}AW)4f*t`cU$&u%T$g)r9O zHGTOn&)S4`dV?vTuLPqW;W*7Ej_p6FB-(ZNQr-SvbJ^%CHz2J)7x!Pp|E@yEhbzm&2-tq+55S2zWGLYWXE$ zJe2GkuZrvE!GQ3Urb~v5|EN}}1J5+GAY4#Ssst^8Ua&0znI2hiI5W=0?#I-~4K=Rf z62crqFYZ*Qp%Hrr?KpVB-13O9^GW)ISR zjfec&izjl}n*_J{RJ#l_VoS|AHjP&Pu&Am*ZNuvsl-V0*O%D;sRr9CB1z1+s)J0WWw;yRkOWu>W{7|)h$ zTM>=Xs311Q`5gA7W=0JWeO)=Ylzh%6r6ueJWW)Wnk3y>Q5y zp@xsU7FQY#>I4m;v>EzPeXt(ZtSx*96V@|wKuu`QNo8L1?w4)*=hOwQT z!0#b=Nf=b|kC#O8_5)2_U?lui46Ig+`CQ5+Ll+GZ?$snAv-z{sOMA*w-6IJ&*4i>_ z#^Ph;tQ}I9*y+Z9@x%=O3cg)gSy8&tAJGcCX)vh_JL+f6xUV*f>mH^G1<&3545mm0 z%i93UaDf4LYbRaNPX3X;YxRn=4vLf+E+V@#K6xOP@ip71F1ZVQ6_(2Vn2+@JV2*>m zGD4kb0Zcn0IT$>bBfnCZr@&h4mqdr83WOO7T!Rd?i*$7Gec_`9!hi}qK4tI_1^h;C ziG%%P<`Upz_P)i|JfgA#c4g8UPKS_Rxka=~em18<=gyh@EkQ(X+uv3J!*YXTNHfEt z3nj)7>JJqF1E^9!0hE5`u?J8gGVvW5gz%YCqPDPD(z+km^Y5rimQ-{f|G)bFc{m1z z0$-@ICa3HXbTkD15`r^EApaDX_+ifRqUR(_0R#7CfPr&4#^R}(U)N6wrr4``Z)lrj zXJfnDg>h4lB;LX@Abq$Nw*KZ`(rqi{4A~y-WD7J;WS=6yky_D`L=@m&5uZq5b}Y^=C zEp`8ZgTL@}JYLG(b=BM;Mm~gl_NwaNUByJ0qPRO~yED36S^dm%!(F#zewbu*ah0;@a`T7Yo&QwtOMFSbbw8^8^XK_*=B)l_5ODZkdfh9!4%VLzJ@A{rzgOQA zyddKH)R$gbnh2JQ&yTlhwXql(ce)SkHZKz7z;Iv6W$dWKqx%}}*D61%0HVM+9>O7{ zcHB8SmGVXY5o>f;0wmO0+a{VgIg?8lF=$9me>I(QZd<%{29)`BR@4$%6JbSL|2h+q z=s|1j4L1$7l#K<4_>tS1i5nvwfa*KHaOh^{^@Y!l`c|nG6ve08|4Oaho6;WlIvmKcW!eweRGgs3PT;U6 zg&z_0XL51eT%s>BNY8Vda<<2u3DmJG8fC0ifsWGHysq=yV<@aA?jD*7w^=*>Bi4G; zssL?ggy72XXoB6M%VC5s@d~40Uvt_z-$M zne8Cjd_}oqCZYZX9e~AF^GLZmAC|gHv1NB_fM4dcpiXSC^JD^nNv&TSW(Bv_t-+b%C@;*!pWL{|h?hY_l@&liDbE_ROHXp^;+HXkE?s1tN~tRCkrnf4UC)bnZFS z99OgqBot;mHt7#j!7*~a7F45p21|v%oW;Oy4LPaNO4Xlkf%@P)#t*>*YZCE)v4R_0 zBBn}1jAE#;@|Q5}sUE+o-hZWoCKKq13Q#=&q&h+}SK!!2`l?8C2zyDzWLu!gS8rkbP{{Q{KA|(k0tpDM>%zK)a7dL|E_X|Rp!7mLW zAP|_CtA8PP06s`iV80;EEN^UUsP*@U#KHALNGl^E^m0_T! zbGU2;g-8G##d_-gy$t@?R;mvZavM4iR^j8i(;oz61!Yq=7KX8)hOrg^I<~l?75r6b zL`zzkXKlGzsYPEHSI*Zbf25S+Tvzs6=NtA z)L`6#+SN99@Y`Xzvx#Kp{ha(tWXoA)tH{W#H`6=#-lXt6q-T zFg2L7pKi@b^$4b7AfkBJ}) z%dFaH!ETtzX``Vm!SN-wmqVh;?UnN3L(h8V{lyOY1|`p0*+r#tl6h)**)bkrH@VtG zCGy1N^Tmuue8z62;T!0p^eGW#O3cjw&)JEVVR5oU{cXB3`C-AMtm57D0N(^^f!M{# zV*ugBm!xJ_jG^nvoGi@H+Du92LD?9zsiCSy&BX~0kfeCG1~!VLeUN>g2H;Q+22L6GvXQj4HlPa zpzSlaCPSJhrNL5VX7b3j@vvl2kMRvagKJQ3nhk55TCSC*ADlhlrU##i)s#5kk+`>C zRnTOttLD-*m*=1SZ+Y85%fnabH()yT;1(>WSbx2F*;PZiJ1au*h3+#LJv#*2Zzb!? z{7((@owU0nC59^{@=VETLG`cmbJq=cnY&K9;$j^j$$|MnJMw<_A`2l}8M%^$!X`y1>xqd<5_L4SAG>YWc zQG?H6ht92%5Ro^4uw(4-D?NUoCYm~8`ceNlykrTY0N%qUzawjJq0)nN=4d{G8~Sf> zHeqQiMG>qALng?x;p>213&FYM|8xpd{o&R6VacU=L3lOV-$FGqdNa3&?Xt=izQ+!N z4aRasDru5^?Cr@xp?PuDF~B~1`SDPg|3k5k3l8+OgTq~z#?Riz!6|sd=Vx`1{S3uT z@K6i0Le^r5Ljd=YU;5&&x@E-((rSeGPE@>2gv*Pg@SMBIR86DOPHc+5^iQ!>z6kLN z>-HmP9U zdi+|rS><^H_dk^WD~pVPO57OEur35bPw&e^ANo5UCc?a~SI_^BK^vcq;VOU$j;p8u zMGS{p%pRyv%fBfC2@)eG_Xy(b8bpK^DfnYu8ZqPo6emAgUi-Ur)m>X{=qob!PSfr`I>`? zsD1-W@iUEHa@w-e7h=Er*r-rpk+Hv;DnoyIC%(Po*ca3y{%bzd&>=7OlEyK9K#2X&QQn$|L?Jst zb3S#ZA)y0UFyXLC)*bEWfDUD%XizMn$W5F^D#@cPp}37MVq8-(xl+rJX z=}h*lkfLqIF*V} z!i$G_R_>2iTcoLkudbaWn|EGfpDnRa!!`F>#wvCW=N~SgMkfx#L#Ee5~N8k>6OI zJVnXgjM`5C*l8{uG5I=EQ=ODhBePMvn0-a;1Yr|jMS41PTgGQ>;t)dlQGtHkNR_-3-2rZHTov5q-Yna3d4s)k{5iiopFFeD2#`TqIy zFtT>-z#{kvc)35cnds_I+Qb5UDZbm+H>{5o=5Qo$`F_$qlRc!P3boP1RJY}}jV3N4 zbm*Zj|8C_2*B2(Lhp80iuk|k+Cn)u_lMle#IGu7;1BsFN%Y&zM$P%Ch-3Pxa^8J7U zr%G}ruySYOn|iOe6YT=u?y3YvP1IC^2kwVCQj}L3ukLRiSfPpd_dprVTQ#&*Oic!J z&_nMqtO}gX)4|oLx&9$1>kj;K*+7cYsI5y;$-xPP*1G%zy5s4u4g*;0(_~>nXfGEz zgmPhYE2QIegS}0`H(PCv1}L*wSz2boYcFwi3s3Dq53O9D$zaJU_D|}^gluCf(q8&C ze~~H3jhy+7$~y_HwXHc%?)`fYdO)o$UV{;rtLoj^!R4u?U$@Bxq>*Ws;1(*I?kj1? zgVa8Q1z64wskQE&O3w|{e90Q_Q}u&74f}uU`M_;FRzpnPeWL?Uo3iCT#3UH}^%>tV zqhkxG_4JR-OUR53u*BJq>G|6+!o`aJ;~A)Oi7YW2%3_*ZVG9Qg0OTPxAYdF z;4F)h9iDH39i+Nj|JdrQg{`5*++pgd2vt-^3=g^``Y~lTSsvbTLfCn*Hk#P%NVApa|e(vj@yH?{@3{%*1aw z;`J*yKnpu2Yob8Cxm*kle^|CoURsB4&|M=;9YFiDmtivC;JT3>G~h)u*@28_!qbxf zn=3S&JcHS`6ixELNUupnCr(8QrK(yYG%)!Pp16?4*jmEG4h2U3hjc!iM0nqSE;Qum zr#s*qLIA{P*N_YWk^U!{yArP&dnR?ZXPJ4P3jeT{2k|({Ro5z%Gcb)VS!$94v`QM% zNOI-5M#;qB(zj>|dU@DY2Q$j(e2ELcYx1>iT;eoFlP1bQ5y#}u0)2|iWR`rU)6t;+ zOgf(|#Xk3<_veAIPb5B}W~PvHd}O>- z_!E)8U-blkST@gqf$K$F=((-EmO1KJ8Jx7}!axlIE)ZHV1z8aiEwY@mI*6!Ewe2FF|;(U7M^~*m7FcX_G zrz<{o)QAo{dgohj`wx&5Of>xj%#6CBsH@gcTVv(lljTIrYGybmRbfN<^sLSPQfJsQ zh@^^RdnqE*GcWPCf`r*PTB0!=0fr!E0T~>39PdZvrc4MjgCC!7W2&8WA=5KOP@9WF zm}$w|zoSMO(Aw=7t?lMXSgnew)qdqWk68noQtC;wbhxov4@z0D3vW1_5yZXevv&JZ z8s1@BbBU(3p2=&DJjOY7w%cJoIfxZl9zYC9K_hwb$;4z2aO-JZNow8T*B5?e725JdQ{HEx$PG0&lEJPVuqPrDxvFVCH zYgCt+_PqIm;C(sl;JO{9VtCj|HMSXbv+rRIx#z@smA_>Txqw3|w23_A&H8U)TQfXt z$<0E|L%kN+=L}z-gTrO?u6;xb0>gHmhgFMYg2c2Z0k-w5%dst^I+t?m1x4FszNhWX zi*5KzN~P`0ynR?|Wxh|deOl^f1kC;nh+qjU?Tc$_iaI+2;@^P-6swmgv|{3z_hcwx zgPb~VLgK|RUrS)w5@DD!Y)e)qc%ihz+`mV?;V9uE5izui5s#NfDkIc?oDuPCDB(bNg0^ra{6192yuBXjK?qP&UV_r0( zZ18C*v!5co#d@sZfU%j%4~(e-)R?!Wi3Q9kV$O&Y!BwEBaKk}e6`~`TX0yE3B)veE zd?1Jd%M_sC=0g&C(pHyV+~}A-g^fp~+{P&pQEwv(G0rY4gGR%a#O?k%w0hUZaZMI1 z=)xPLNcpOVElv<77n^^FZId>_HgtHQM#IEz?O=%W5m5r6CinvD&bpmET;{3AFYj)_ zzL{Ev0;j_C(qxn1%3dQoDC-L|^W+E9rPf{pT zI|;mM+m4imfQj6aPLFIg>w*gM`6Z~MQoCc*Iv=S>uKxZ}NAt9&TPU)Vb;O8#S?jm^ z1=9u4w&HKsezFDHvLnoqsnX_sqlw_+C>+aqDvE0z#V}f)J0XJsKFCw0)4k$pNBo&A z*c8K!V>lMB9!dz{kMo+_;xnkszFV;;2_J@KJ2Q4hs(y8bdqm<%Yj39)-7&0u5c#vG z>lwZ6uqNs^j6ue06Fu+szLBw!`r}>Q8bO%#9MVF!VnmFI)W7jOK;^jq=a;i-{rdr_ z6pQcqvE5P0R@=-XPcWB9ZqYrg;$1S-6>Y&Wi_)k~1`Uj)Hw{g3BOS|N15gBTvpg$#q zqoZ=mUm+W`RN9AuYEPYR2dn<##XN1Gx}I3tw0R*pDM&k@htvn1cVtHTmPnuley0M{ zk!T)Wt}vQcpJ0ou%i3~iwpkb2foyj|_k^DQ)j#XlQ7z#xgsn>5t{6%KC6w~h5WZ5e zDxLk8S#|M>r5Pe?XsRArA*to#EPHUVU|^;hhEInMRVm3ygfo>^+{8_n7V|1CRU^`DfnG31BJ-@@ZDdxBn zk(&4Zk1NmjOzNCxYksT9v4%IF=`HG1KHdlG+JwqTd~eqxFe$8_;j?u2WG$33k6U~Z zkX))Qp;)6wgRlgW5y}x{#3Zy?CKySm7wTGpzdv6X%1Dg>wUqEV4>Ch=QX2n&Mqsi9 z4=JsSNEhCEV_;XKZ&!1i>w# zrPJEfEXWz1U!aVBTq=Vr4bkOkkOfpe-qG;ay7qTwE{5<#g58--Gp-DR{4_E?p;HsWoCi)zi;`f5-O znW4Yn?zR4KGtQ;0<;6A<#i`rei2!9;CQ3+BH=giQ ze*2;CAj2)(btiLT4LX5tSG>>&KnaFz>@~vG*Nad+&*A$pfZYEv@KPK2P3;pDdE(VU zuMnU|PLrG!#H&g_zC_>H`Q z&&bz<2eI`TdS^WHdbOKqg0Xny8NkXoq)~;-1 zUn-pNgV7$jwG$wlUJD1~NH}w^N~%Vqw3w_~?AeNftP4@`GSi~@_ zk>66-E=a~un#|7CKU9Dyo;PfqMXt;%QysCwRx}2wPhjjw>*J($;z%2Ngx((W+%Yk{ z^;`|ILOV%IxBK%AY~frnf8AV1#%e-zG=E7?{+zJsY+6~SSs;0Y?NGkab_MG>Zd;5A zZSo9|uWLxb9eHeSCuO5GVBDcFJmUY%dO&M{A@1^u|0@_FD;gmuE+NmgZ9=6!n!6L$ zAac$cS%UbvFxk~^>O#@;XPIn5rO=g=bNuS;weJq#Tv}j^CZfxS zsp6Xyvb%3u#UvzcXQUpM)(H7fio)Co8ewV`erf?dhgJu2Q-|e>c**!6otFs+6skI0 z_F!i|O}qo?jyQ1#beB8V{9b3OflwF8ia8kfaI}4KQp%lVV(8``qNLS@X{QTg$&aKF zB(~CrXwhY{YU-?yeBYV@iKGsYeM}$kQ}X|nb;#RwWIZ6G^os-i)SkL&dOk~EwynHO zIyPjSU?@JE`$|TCd0E|N%WhISztl9^U$gD0xYoXoSg8(Q3D!fWUD17F3tb?5#y(m} zTUs6&{K8UZnGCftilk9JBxWpc-7?3liFJMAs{Cz=vPD9Qf$bxqZ;!SJturz1=}g@? zI6%^NB`e{NYsCvM3{Q8%k}E_!1MYHfMD$zR5K< z2RI^5$%f}am;=>)0%3q(p^#wO1S+ea>8W0gG|vpce0NgStEHdx`_jHR$Ed#7svz+< z5ibCcpgGnxbYSt=Yo4yBSjG4lCE)i(b&E|@rY;gVrv<&bl`0%UACYr1$E}Q>W>ik4 zl_2j|h*}aD!sd|9$(~W~jMT9SxXM=ynV~aJnpu>IJjd?GVsVph%d&b?NCJ_z_4AeE zKgY)hkt_~b%Z$lQ#}UHD7T|_aE3VL2#W6fsRjX!qO^qTfwy2j}8C=>bph!&KrN@$> z&)WXBzV4Bz%e<0m%|zF~aFZtSdPgv7I&j^*JsawbPh?f0piYRkI%{7_x$fJSB5M&%UPP+WKo$9X-B#+hx#O=<)eLb(SJ4tuu`E2DvRjtBcYV>g*1I zHm_2b)g6uf2orc_BzT4bq6I}cXYJY*;HO&Bxjj9uozJyd>^T8^Ck{3E3BWu5j6ugs zCOnZsKEuoAZwmBvMg$?;_0&&PcLjgY#_B>XUXfOT%Beku!YEo&qloZS9}*J(|44hs z;9R3ET{Ou`R&1=;wr$(CZQHhO+qP}nwy~m<-Q9KW*}MBzoqccB_v`!l)_CR^bB+h| zY=QNONDr9huu*k~GYZL^6kcQQO@|4ty`s2~7-HQo(Xh z1vNBzQX02yG>Wa3A?v3oM{=o$4WV%}Rp>uXHa@DA^k*lD4H3imLwGjX8_hH>mA*a|32dL3|pZ(L_d-CRC}}S%G0b5Ot8_Z z6K5@cBxZGUm zAy2kKrtINZo#?y~o~XRhlc`BCt01NHq3w#za|&$xTi0zc1mi-xm!B;8Cr?~wS8zd3 zb#$UXY=-<7^0*e|A|2}o$&MVgN{Fqqys*=FDOG@$g};QwFrgJ&cq1~af=1YzIzem-FI`9*qIM1i1P4h`C?)L zw5}D{$_JAS{j2_Zr2RQ2SLpBTrr()4sv&ett>H|q=nG92ssR>i0T-$PFB<5QCJZ03 zS_fLoyb0eS2Bx>Ec7v_5E4lNyFcb^Q>1a*$11y0=m6P)V?eshtd6QG53)Xr<+{0`8 z2d1;uLqJ!|5@~DY`HAMuv0aifsrx>~^pHy6iQ-K27bIolv`N_6Y59`|U@LHH*EbKRhEo`|W}KCwpkVerjrVg=!(?4En>U+543AvW9V*CB=Y`(E z<EoLYp>zN$EQ-B1XxwNGj$Dku?$GVFG-JQjB^`)WYo|oKW9l0X?aI-|4&t#LZdg`XCNTH_p<& zINmSbUk-vuuMK|2wcz&d$i2Zvk7$03{l#*~t0$7x{5hK+rW#&E_C&t|%W!&@(?KFL zAW=EKD!IQwzN^?p&6px6`0hyb!lWJTHYk}GPJg$DagQhM(;k|Rkad8+xnzG=2ZeGFGQSCMp~V0O0$F?ZEwSFJu2zW7wqhA2HKi3)U`c>*7LX5chbv zj{<5#s8K=)0qKc~`u>koA$gIGxs%Kd8vft>-(lQ(p0}W{1(7Dj$ccfA%ynbanOzPy zY%bT+SzoVjR~+zM#UW<)W^+&y#-xR1qC_#{*jrDh*aez|jfYk3vvE*M&QBl&&3&XD z*yW_hz29cXeWrm%@r)qg9=Uup#kWD=vaHQBw0#!g|`gzDUAo8G0c9a;M zAmyrVm>W@|6ts%L7_{rj5i&tCyJ9V4ul1jk;(RL15($g+VXbb72|u`j3vT zQnp~7dLuPLn@}!0wo|YmKztV{%;ic6isUe`f&GzP7pO(0`gdH;m`r+sDR%a51$gVK zUMoGoBJ}Zgr2$fw&D%ps`a7ftQGs3Bj0GYB;B@2PS`)BWJ5QD{0*UG9(rmF&s|Op2@}LX*Bkqj@6dL z%a!Sz=1fxroLR2JK>l6%(6mtgoucMy?a`mP6vvQSMo#qT9w8FPo8g82I9{7`>hqk+ zAVIbnZpy@o$jHxq%xK*Xtc4HikWBq62B=C;TGv_P2vJ|BQU=CgUHzn&K}N;*UW*33 zpyGMj@eCcVz-||5Kw^6S*rRa+jB%wxCp`__MVizu+YMfgO6@p)YLv(5`K*0vB^Sdn zAs$5vfK8byBTxWFf(^B!Jb`P8ToWn{bO}*BdxaA3qJAl}sdBrl*rohTt*B7x9#MI$ zl-6+&X;QezS-HOE(DS#rI{XR(@SjABbmLlX$jmzLVf*(=--X=NhPUYhwc1k{uG%(+ zk2W{QoDZFUrW^!Ay}D`tRCzV%{};-^KNtsudqyr?A*W!a{BJ%rMQJSlhAAR`TJ+oM#ti5 zxaw7^K3bio!cu4$jDEKv46gdZa_5AX^Ub=C&sEfC_T&jTk9o`RACn`fVF$7xg)_Sq zWXJnfa}KM8YsfXLE;=F?j%ETW3HScEP80QLHRKT#Ko!e+EDxsfZfAdGVrR`WD4Yer z*l|RIXv&p*$>JL=oU^0JLDC?pmXva34!%^n(}PW@Aznr_{x0Hyy9=xu^af1!|q#l=L;g2*F0Pq^O{$`rQ}Tv*1^LOpNm-B#}e!E zK|e0r^!>6U6_+>41O5~oJM^}6+b)yli?dZ)H~~$OKHo4YtQROty^UTVXDB4QoV<@e z0-{bY4C!3d8^;mCDvW06EykdRt1z1-r6HHkMQBv_--%~`s`tdcXL z?9W|7W!uG!Cp1_*Br_#{r-YyW zwazn$k4^S?2?qFQ6sSmR5Wy%iR&$tWzd_WJIzek-iO15j#-dP*gbO==mk=$!HILdh zLDPoJ3Zx}TbBJ>c7(`HvC+5}WpNrSQzxtfuM?c^9Q^2hM_7VO6FtfYlr^J6`^k4N= zThyeDF}NceVJUd~oaJNS!4cc~V=%L>bB5f;yuCuFrXcYf7-$=h{$#o(ySk z$1k4;N65X%(fosWF#MPhj5Z62DBJ9Q(_M6j?P=o<;~?neAMmT0&y^XX9k>h_JQ4-> zWlqB82V-bSVpoPS%j`;OEA`e_302P&Sr*Y`pw5yqI;?CP=Sr$1Kz2DFi_thq3J&`^ z(@dlCezzG)xaj9$a^7d6yH`5(J=`_uN5?lHEy2!b^6d*~D-ItSCA4j+Wl0F8u%s_u z=`rKUn#vO;yL-zhlJpA3_&CB0;%__Yx~))t zIm!-LhdtNM*koZ@tm-Y2$LUHvg8%b}7yn~oqx$pFgMWG>*T4PY|3$(1U#*c#pvP39 zFkdmKTuIKXLMwo7Fp-gtiI}K~j_>dBfUGdPtnt`&;%?8ZPreK)&IjNZ>A_l_e`@SH zG4onhgUdD7$uvOJ*I2 zP$$2%U>kmlsH@R0ULxCjM4&MytHvrsN|u$3qG;aEJsS$YwPfaUuGJm+K8?1e5~J!P zmodt*nVUR1YY%oL*X{cNtQj6zV;7GZ2Kvi4zq`gda@zWvvtElbf+^c$y`{LV+WB%X zBTFgV1s)~+dMUrarx?gmklKPP`hYIDbKKEslP%jI_;gRnK3v5o_5v^o+hk7M88ww2*?>?qgx}{ zT;z)FKvcvq^W5B}*b+;qqAdm)=-3ZglM3ig6l-9WE-(pR4BPA@%X)}ZQs)#%(s=IJ zlO*?l$JsuD_r+vV*v1J7HC8dK*MxczhqJ90PS`&IjRRTRf1%hdzX>I8nBap1BWne_ zyly(3VwY2E^zozTebCdjk z`9sXw%+XBG(#+lHzf3urRNfU7SFpY}8(dzlsZ}(26O~L6@lc%Ss=|kH<&l+#C}V); z$*abay8J38wA^3v_0cg3q|zJds;m@?Y`a7wZv zZVsmKu@KlS2UsUS58b%{SjI^z2DptZYmtaj8nt+vnbVcGrJb!Va9^-pM&ijdxo_pV z*b){M4Lr{(xhRSdN~5JwF+(@EIkniDsi;*oUzIr{KL{9IeVnye1Hm1^tb!*Qdwwcl zOn^>d0;-UT!8ldW%)BgBZ^-zrDxIR#9J@P%2sr9ezW?rg1EHbq__;cvNK!9(c;iSR zx8#v;tFc2QICN|Md#XC-`tibel6YS-QU?#_E6;Iw`jpu%=&4y$Hkfit4TS{8Jfulg zoZ;Qz3KZtti#aYt-N^$t>vERTN|vNMvzmsBmC^FzZmoP%XR8CZ`BjdZLV&L|me0YQ zdV@cHQVKVZ8OIKxGo)#ec1t@eB^@C=8dZ$Tpa=FMpIYOBW3e1Mj8XO$8AL{Yy4Cn> zblt@&n;HJv7E5x?Y?kNhRjWI(7O-I~i=s7zFdh}2;O>mWXsL^eilw727-6fzPF&v% zqW;)zRMn;-ke`HDT{5IA@aYZj%nDvL9FuwKOpkt{%E4T@mSkd5ZpmmllczzGuG^fC z*!nr?u(evZ$A7`F!R$q*Bq1xu-i-t}ZwzF3XEw!N!o8M)%kt@r14_F>paEhG-8)8f zgS6g7uC=^WI4b;VwNf7vUFniP0s{Picn%FCpdElPBVtuuFDC{yK*E3)FWmqe+ue}pvK#I!}~ zrLEPdh)R0#lUqAFOVKmO;1e)qJwT@H6)(pN-9zbdp8+A?Em!$AJ{%G?VfIt6ksiE` zlR$v~FkmDrupAcQW6_OXfNbEJ5aT`=+n(QMm03QJeBjqj)W&5Uj&t*7CZz-2Eof{> zQ52A|0{2G?#xp!YL5J4(O1GCwXPE12<}@B&o3Ea|sRzZC+}X<3S7Y~I_ z^t_hAOz{Y7G_OSo6Y2<+iol{nu7UZ4wQzo|9qj4dgU5F2k{tl~(nuAYbY$N9DYKGq?kg!%p-e$FrK<=ZGa*~OcJPL?zx!Yvv^IBy6u3a z$L3!>P6{wnMF1*9@Bler(zqh&oq>=q;Wvk*^V7M)t1js-^*zN z5w7^0i7kAqfS(b8H$vzMO`D=N`CLmundel70n&ry_8Y_b-C{hCm3HN71BAmiU9c#S zE}`ZQ3`d+O^X{+$zXT*TBdq?Q6mXxjJBK@-Ae`eX@IVuDYH&s&$CWv#qLrzt(1aYZ z$iXF($T0@%Hhv%_kgvi04g6Qs~ZD z8IS)MYK#2FAhsKQy%%l$#2VJ8)fG)oz&}P8R+OO19DFZ9iZL*63i8vl%REs)RN%Hf z&?a>L0DfE8X(DBnr#i`VupnvY$+{-Zegw`+%A#rtb4|6>ugp$W8w%iwqD|l3cHY?i z@||H01GtT6;SkzR*bpLh1zsQGMDoW+(nkV~>Z>~)&2R>C}ZMRzk&XF5Q;dxEYyVo00{q4FY*8X9EARpP`jk| z6F9Wu^mQ|RJ#Ef}5D^I6?~6pRF>xI(R3B9)hy;n|4-DoHr(0?jyv2v!kiIn;39w-m zPb|I8Xs3T~8i`0!wy|m_AukyjTX(SAnOSW0*LvaQ{nMLB>O44`fg#;X)K?kf2BhLC&&&W@V3(TS0_|dm=?=cY>{( zb0X!z^9^%>Yoq{@zFto9Kp%<0l{?&9Gko8`m2WtL^6!JE8<{vzPp>@9!>JLrR~(H_ z{GCzu8-&DP@)+&7yC&lL%aj7sks3z6U6r)k6+@_p&lRJO;K&`ryq={%0Phfy9*iND zI}f=|oSvETZ{L%zSe$RsVVsqNs#nD9w*^w}aGr?WnYXV$lCLS8pDOfU?BP9qJL9h$ zXg)M>7_?GPep&5RY3A01IZMEacmuPOeVOKJ=|l%Oz{x2H&}i*d6>Oh;Q-sbE3}<>h z#_I0ds2J@MyikygP{+^{%FrG?=^69ugUC9f?Oat35ync*MHdibA^AEXY%buPbcW4FPz;;&b zBR;D=FG^Ahduh^3atv?UgtUYFC^frlESD=UVeWHntn+Wc_0hfxeMpIsWwflteoMQJ?T{OO=*~%A*A65jw_R>p@aD`+* z??(CXUZ%JL1$9o%QXu*`?^H)l`j{z*#%}fnhn>IsZw{N{G^ZX8ZC}7_{Ivs=Rxi15 zU}BVk7B)32YgD5cYI4+wxu)G$E_tp|?FCQa`?gE(mZ>n*bupV&oegY)gB+ueh~A^` zKPEa0o*-RsZ+ zGb;QQzMod)+gPHblrD8Q#iEZ(CL^1rPKJq<_0Qd-dwQX_$(%-6>1NoTqR*$x5)u>| z+zANMjciJ?Mf$o}i4S&=KR~|}hJDChQM#uRzNvnO*6s)^i01pFsMTs+umK2X-4cJf zO9k5D6puR|2>Mm(10|5}h?}w2{Mj=ZQd&V?_;@e~%W9Al2Te%8{^+E`@x zJvpqHeEwqV*B=`eX&vJt5mF=)MiD>!^PqfPo;F-`TUlKp_`6>XCXZFcV;D}B65KW# z&1EQ?4rNV*l;{NdVfgy`WJ0`*LV^rDlnR0orb>hMb^bjrn?j&p0}dMkAh3J7f{k<| z;xN0dl-9DhwW?L2O!-+NSP;?&SK+Q%t#Y_^wzK5=Ri}pAu%!FMb_`U@WCRs+ahenN zgH#aQfv^h3v42FBP--2upB7fHBFw4E=BR1zfG#Q` z0I(EL>WCHq56O{La~<%KG;=S~4MCO&V@qD{R4{6iVK28hY+HE$t}7MA$CZz@gD}YE z{Fpr@4Y*o(wu%c~1e1~9?YVUm-%7_;-xG>vy{?W%$8CWM12-!^eHPky|I!!AWyNiL zmDOuYZ6o8B!Adz=we$fZTy;QKm*hZnV!MspxjQy4CC~}SavHUCI6i6pf!%iOi+lQunZ3g z3$c7j?%tvAqC^vD{I1Ylvq05Q@!i*2X7X#9w{}Z|;e$K^ws7?emxxaD;8m}#PQsF*fumc6q^ zt1P;6Ef0glbYarn0c&#m$F{S+Dfq@5xt5dLYpJGt8t~)*n#Nbu&b56VqM8`Kslnh(G;}|>Sgij;RTTEyP$#lv*+1gy~o0e7_w2Vvwh3tA`VKEq4&2&gMWgpy$ zVvm_g0MCaPWy|n!+VksGk#Dz7zJinZMYK+fsh(}>+>z}47~O|S*o13_sK>oSj$B`M z>PVQ(#jE}_kJyN>8N9NM8LNFRQVJ~hk5{NJ(e)re=?IXiHJp@ZcA*_24p|mQ7I_v2 z7G;=zia-mfO}ofb!Y3JGI$Sbx+-wCt3Hs1~GF0R&1aS75Ehy&;hJ3WjW0vzcs zZ4~kgl{f0kESD%~&HHH>8!YedZ*uJ%Q=Ki8#KH);nZNk?jv~7np})JYLdD=H;@tIJ z9S7}VMaZN1?S;_{!=wxB0tA8S+>r~$QVU*EOJ30C$rrjf=De}}&_oMA@%_Udz_fT- z@u+!^B?4)j!cfv9iPii6$m%p+3q#Nmf=9{2QOSc*6w$5|p=HSfgO|Zz6qXt|@dWuJ zy(zoA|Hscm`62Wdw!JXseeHpmMCoLL++Uo6ZvY%UF#wN8q4oxm3&gQSA<{7F=K`ux z?#7_itY+GdcuyEP=iuw7Y!3M_4hi_=bXgiEgR(}bi6WHBWOciQMog`Fh>@(TYLuO< zr_|gyTj5K=UNpLSLX$yMNOl5Ytx+_(?fzLW!`n&hnWbi0aL27p@o$00Z{fc?eBac( zHzdh&IPPY!?s>p$%6{BsUBF&=Gy3Ee5A-W4^gHx44|a!~U{?337-4OD^yntFhQI3^ zkmcJY3K`wyHF=25rg^s$n0fd1XPF2Q!_X+R_!f*9RUj^00-M?cy?>+2_GX-MiO>+9 z;t9hNQ_hTG&rFNzqfZ#m%9A;2gayINm?rZ4bxq#7xT_)jT4soyF?eG`ueJ@gI+->& zV#5)v%G5POt#1tAZA6rsr*0JQkYKEQ44+L@00VlOQ)_5pH_qu5o4JI}UbkI8f! zdsGb@t5n1;KG7{WVXiD!C_l%V6cL6Xlywxo z9(Ia{Qx4*;pN|y571JamH|7a*KXJ-=F}-;>v+23qwPW9mPs1iyBsL!ArzD?!zL+7A zSR$UNpq-vPe;KnY!gYlM2^1@%A@_L+#TrD-C|$V0=sW~mWL2DW*;=6 zx~K6m#B^!YlP0*-1*FC)b#8iEMOy|8aYG5eu8FTm>}Zv6r)<;K6$R!EW%E*gRA7j_ zQo;x+#U+Hit4V(cl#V6h%H$x+W_Ag&v><55_Zw>~YLc2OJk&8g|1aN2FpNrJ5ADKs z|6yFm5Q>EMI+>f_nqkL8F72Aj);Ya&g-SsXU&WV8MDS z)~F|$g=e;?$2LJaQR>U=TwE{Asd*C9hz~-m2pRU^ZQ+~rI`kX77D95!Dp#5)f9bw- zrtT3~-I#-53|^RqiMP<1x6qNX>NdWsAjcqHl=wNy126h*8ChLMFm!Fb6$%~T%)R29 z*vWWJq=5(0&zxaUPG9arz|a)~0W@FvqFFuHq&yLFz8M81)c&SNNpN@h?gz4+x5SpZ z>5!zzh2NRaep3`m_9)WyL|)h!*&;3giOpt@ia^He+6A#^jG3 zcW=0Ho?tjXN=kK6N+u-M_(-hm;VMx7Pi$}Z0IR+f=HSApy<}U7Q?bWp5RHDWF8ZaU zqA}HQvxGXB?xwMtS?5*94MuAoUOn@Kgo@%ixpN;@%Ixm@B*RN=Vsd6$3^Rg2$L#0q znLKHC7`rndd4xGxg*6JU7#7hKTEebSu6c>opMVDO6uX?qde~vsrxe6jvXU;g{g4AN zZvPR?lbBtoCj-DmxrM*1QG_HoT3}juP~$M^+~ih?fjCL8vqtY7O;gZ>`82C7urIE4BZNo5q@3B(A% z%+D9*R(d-}OFrWU`u>DkC3P}-Bk){p<_ylb2J%R}$gnwN+`8LhVB1flWy}drSut$$ ze#Oj&8L)8qAa>~^p9`j};w~>d&>(!$LoBU`P1CTg*s!kbeEo}OXtNWqisX+A1?|tq zTJYbzzw+DISQ_bB{~u5J|9OKgQj$~95JLJ&-aKU9)BlyMY(=oFELD1CRrO;_0R)19 z4yGW_KCpq%tcSOj(dIG`>C^Ax<440ydSE&NI%49J__RS-po+RK`B3ZOeDl`*`FzPn z;`90Wjssv^n>^&vdKJvU=IP(T_WH^gBDXn#A2Q=P*_t)e@Cu~!H}#2*BvwYT(#S$$ zC@gyNVB8DvXP4BBlr|`C&|+9oF@O4Vvon^)usShAQlZ!=q5`xbTUP=)saP+4vi1-e za%x-HpB7{?k5!A$x#%l4d2#HwiI7{ApI2;BsCialCq@5tI>fE#27SHX}uW0cugd zkZ^K9A#5nM)@AAL_hoLjp!D6^>Q`LLvn_$6Kl9dXOs zML!U`DiAa-WGjt!-yPSawixG~4c7lK>u)V9T^!B$?ss&;}u zX**d)3k8+a_nHKKMTZ0&SG}!6)dhd~MQK3P%i0Yt+xyG!4)Oe#qV3d(xo=1*#AJ{5 zGVP5vOFt0HOyTKhs?wijE$e5TuVFZm`rz{zY#PWwlaP-g1x>cf#EJ~TWNoz3%@`#C z;^m{i=dqy4tD+y;bxYaw7ZDpp3i2|$2pnT|uuWNBOSKKvO_G_L#wn|31TBA+Fk#eu z5*t-3lu{-xfLwy9wKsz;N-cw2K>>$Zd-|5ZrUwpKqrszX>`yof_tuT|+|B3TvDXMf zabnr}M;Qts61Ev!d+w+}8?MM`}5 zj%8kZ_BUg?JlO2eUc@RN13|o6{Am3yUUhlxEO_^@@ZJ|7S3^c>w|iD$b-+99Aef_K z+_i~*k594O-t~SP$4p%Xxkfa2#9&@Ybp9nh!LR=Y_~+Lq!GMe#fgiLk{-9Ol-$d(= zc-zQc(Z<5)e*)}=J)#I47r}&jquRlcCZ+&prfzS&Ul1>@AWRsQH9dc^r9t_w871^R zx|wnlhkaa+I4MLaXW@M&jySeF*>q)~(3;<>G0yS)hQnV+8JC%y@2_VLUkle*5#I=E z>OG7<)YK<-AUnr#BTAqe4GususE<*ktb{O>kkbN(QsJ*E&ss+R9%~{dB1kF~E{_jvy8&-7 z<|-U3@QM`|C8$@Ek`u={fsYr^@w(^X3kM1)zA(8a>ylYUE1(a`U}gqVc{w3B@X<+o zD*fUet}iN;V#y4qr{HM`b9XDhwcA85V@ia9T1x1}#yRl#y$p_(AXW67s;qC8h2-q1?T#kziZksgKe%DE^ufa~#sEV!=GdfUJ3Twk4 z8t#k1_U_LNaDye-gjO0Z3#OtyzUiPJ~Bx?2)(Gta5r{ zdkrC+qi)*?K=O9U)@ozK&hvN71Dp#Q<{*FwP6!)z-XZvJy`0d!vjiI4ZCnQKWUnYc z9fhL;Os#1+pS|2yjFHwH;pK7zB!jaKddXiX+tt#$ko_A=tlfp%V$~tPOLreqWb$N& zz&(fN*1LPxuqV5DpHrv;*eDRc3y0f#*RtvQ{5h5mx(3(M?@fwz^lxDgwSVlof5kMz z&)iat8_HLPRsZb4aS~Xw_TO0p8Wx)!AJD?9dQDV|R zk6yiT#*Y_jePCXw{u!sKf|yD4KR8|ZS&{!=Sv zgqD_)?t?IakLQ<`g3E#9>b5HS)s2(m1k)1#ny@x^;Xl)PJ=e*FHGkU$eU(e*$`A!q z4-WK7J-fPckd~QwyqwY5@c~Hf)kJEcLk|e5Lv6Ps3GP5$8R#OtKJW_zM}$R-@4h8$ zk?4OYy6DWzG1i5I;sRN@Lv~RN+i-#xKG1`T%XBOpX9!e)d^kf8DBZON3*OFm9F+I+ z(E53w*dmtEnWI9aBXM`PVHYST_X^i8-*A>A-?Q@|>m51y8Jl7Lf)r6eWhR&Loc9laooAE-l-M%F|-k1Q|AOL__nwb-8v{z7`wT!S$>Qoq~oq z#KIjB5?xE`-Ny4wln?IK~c6g=)ZxActtP%>l^~^YJky zxsD-fRb2+;9$E?Z@qgic3C9Ja6ee4dyGdx2ym|e)1@BbE) zvYcaUI9+I~O^Rzh9(KRVVNK6=39?PMb<^LC4j!=9kEW$Y62y?$5#BBiHn0}Uh8mzV z5F0xUIs{+RwMXuq*{Mv>&i@7(xcU}eX%$IHYbxgGsgsI3Dho>MM-O=Vj$(@}3@?d+ z!bOuwY^!-B(=b&am2$aKYZv?8cJ^wCcA1rRt&@CZ8qkRLkd>Awv%2%k^qr;VO)sK%Gd$&1hMh<1q)$@h$` zmE*sfz&LCgOc#bt5X0@>6G@E~rEm)S6$hRPz1rjib8>b8uqGF8FQYKN#keE*$nwvCTvskaME&uCVgv^O5coG6r?iumzLEX^ z07(s%e@wZ-gqR4@xPj}C$ant8rB`(*MG!@r8@~m3!4~(OM2Bf(C;J?TO`Y13X}TYA zR(Wxp@ALCAm-UignBbqLmJzF5O@exl#ij$JkJ<2TzNxz8P`ne z)NHCJ_uu<6y5|s!g}PEJ6O_ti0f0KI)WQAA%+GbwLS3?;H#{vhGbWnbKgtxcWe^Y3 zt+#BA9dA1L73bG7)BXa79fZ8_6)cN1c!DI@DhzKV+G=!fyowbbY|l;cn68s~0$2m) zkB=^9#9{-M)$*2g&4CXs6Zde8PR*ve*jY!3{7Qxg&CcEeAZ`v-{V}nSia0WLGykizF=_kQQ!*q zMjw0v#=7klphe5kU`U9En~)ontiu)Sk7g#{AFQpeDJV9|u--lqYVKhY8C2;vrTf!; z1<&Js%4`$Xt`@fL4eJHIUC`mD+z1xiuszRTZ_d41mHEB4Pig8wdY5VyleT-Rp;Fkg zgWB7ShF6U23JVEoYj>0ZE}nF->GJUJ-z|VoYPb-bi^Unn=#Gm#4yW508d`rqtZup< z_2`D?b$jF~jP2cmDWVwv1}^H1%U)^|wiT^~odx}-JB6ElkS{f8yb033(PoGPlsGdtt%*ivFE?{P_62LV1)A}DG+PQ(AS@vUlrNb|BTIPeH(|-cB5(9P} zE0-Y4-8b69HlGI{{SEy3^A~TDJF3(*Dp#KCgf;A5IMz;IhtrTAx=1BukT&`Pqr}1o z^0*pFlnucb%|9PJJS{gO4L{t@f*+@2;eYesk+nCo`eDO48~yjQN8vy84_ph@JX=ZO zo&@-z>nKJ_l8pT5-TsM4gl0#6EkEi9U5p4|uF(d|2UG|6(*Z86(NeE{M88rK!F~Frc=tM%Li=u|M z*6Fv!nv!>~Qteb$vgq#PPXxz<{8;WFZK@893d}2A3s&{>RZ9nGKK3MXGg_pPiwy0f zVj)6C)p7N$x#tR=jLlG$0=YD}84g=iE}mC#HyYuRB|?jr1x-8s+jqrvXj6C zDQWSZW3#?HPrVR4qEA@@De1ov(<0vcHTEMV4i)IPfA@a#gcD|%oRsWYJT$Fr5v5ei zuc(2qm2cdi>noizw!!p99+Ny}^usP|^3dw_)6)g+A!VnzMynyd(Dv2ZZduby>iF&Z znAQ{ltsy6H^F+&uA^=beH&fra#!WVhRhk_oYdJKx84fu*xt@%wWD(5aVMYsu8 z1?uP-iW7A!&?;bJ(Ad0>lt~~OoyoQd-4un0)|At&z6PNtl}+s}a7nVSv)&)SJ_C_Q z`hbKOhR$|^27QrZ$&idcoN5JlQUC>>wU@uEf;AaNeJnU=DH!DyDv~!<0JlzP9r9qbM|381 z{4eI;aE#jy4?nQ*_@O-iyZupY!ss#r( zR(MV&X&(pGmkJ$?pq>vG#=HCHQN>LiHowBSKxkUAMyw~86C`OGgOzM`>Y7f95gm^Y zG3`;^H36xmJ6yBN+2Ip<&7dx4%$t~&1CNSPacZYR;t$e`gVSj3cGG~-r#W;%k5GX( zCG9vPy|8G>u*t`T?1PB7PXpd`m%EY$t6y0|3JG=Q;(thyGYJf1Dddk;m!q4{Am*`b zeQ>_G%E^+77if1R83k7hCe28yq1^XNRHaJ?elP_;6VO4$A)L9N8HK&Bk)K7_3)pEr zi`rzp7|NNq5qz1B--or|wxAj;1xd?N&k40$Rz;|pIS+CB;xO9=zw)b@sUe{?5qODt zuv8^hv=!IoRY!g?P<6w)&=2XWWqgus|7F{|7^$wI+HA1p2}T;Gj2xl$8$(5>Gty%Z zMk_Q34H#bF&g@e29HsRxKhsTRx6(oTEe+?dcdF_b=HbYgMZ{2-1X!k_sXjW%rEeR3 z^Q0$pZg<~<6*`P~vAF_85OxV37Qa^Rv3Itfth&!H!IMhJGg0d%8O(oIxGsA=l6w&&XSQWW#|^Ip6XFk*YnP_$`RPUK42sF zKn}pNz##skv!AY~F~#e`(fcJrvF<-n5C6yi_P8o*)ZD*?l^uLV=mwezxYlfpxIn5m z;&l4}g%FyPTleJ}zmtCU`*#zLTnnUy(M)5GNW-3yj$WOOsou_9pEvN^qay#uG>LSw z1RuBE<37Dx1|>co{f1#}*?Kqbsgfq7A&^+S)8V{~J#H=Zz9%nQJ~hlf=H!YIoC@3< z$UnE5M%La#)*nor{LH!kZmUuH2>>?!MiKNLvcR3O!GKrdBo0 z#N4xt#fidTMN)6BI=VqcBiL&i>90Iz0bX;jyEwZzMwuPx3eb2&+XK@h(;v>p*2dl+ zAJ1657@g9(ATbH9{w3g*`)UYwoGrKg{Ly{SLCKp|;8zkdOcS6`mHoA{kBK&|^itWN=OWv7D#X9?tWHqt8HvR}%Fa-x5mTICW=Ll*Ep0z7 zu#N+ZFD5BYVDCe$s8$4x@M2rlWiSs4!c>QE1M}8lnpOv~+wJ6Sn18!p9pb{(OphQ) ziWQ|<-Zkh7Nq$w&^}Poq3trzIYsJJ@cQw_VYd@WR)eM-Y?M_ed`R?FTylhq1w)oGx zj!e1Z*E-B&Bns9wMsSv<(QwE;qwdCvd7!(yaJVyQ?*Q^34ln{qzcR(ewsDvFt5 z1>X!SwF=isD;FG=!f1;r*PtoMtmgqYYd9o;?ySn}%6YuW3+)Mn@gHSHTTz>je}007;;HwOLZPGL#|+!JXD z?Yn2nnt07JUP{#07dVz;jj0kdTaYl;myeea4^#P+7%jnX$~rR(cXdIzV%dGkJZQP1 zs;CJYQ(Ts_wt3y$Lc_ztrlP9(sg$eI%BttZ*xA|Hm}O0EFz3sAa=O#)=KHhf!0RUC zWir8r=a$wtVHJ~Sc)v_O@D7Vdt9t?`=h=X}`()(h?$`EnBo5DD%eZBo*MX!*lQ3@&tT2#(F)OP6i(034eRs+g11iapWFPv0fetz5v=U{gBaH^9qr^> z5+35cnao}t!cP1>C7$AaCh(efI%uDy+h81q+r+@0H#$h4lv`LH^g9`F8+-Hb{gjs1 zT_1^8SfAG#9Qe*Dn(qe&T4Fj_le_f6a8!S)a`de_{y9S?bd99bBs8F_yVgqTFrfr7 zwlUc@b5fLvQ`rvwZR(1wuqu1bg06A_g(O!UVU4ME*BZ}kgcJ{kj%sxD)8wVd=JTO2 zLCI)!I-+Bu(4}(vT+?x8wgL}YvI=p=Wugi(B6yvRZSi(06H=Y{P?9E7&5Q8iU_+e8 zZ`1%A3>#cJie|wUA%Z&7P8xgV7nC*;jb7RHyvLz}^;mJNaCVNeBK;>dtSl_6tPGXr zO11_o=z-L;P26(f{`e|!`gN=X53xhx;C*AFYEwwGVJ1o&mwXmyI@c7i=_HC#g!c0+ zOy35v$5;e8`Jn75;hN=|*rN<(!o_Y;ec5Q6vn}*6k%UihCTzKV!$4O?6uHIFAzZ<= z61x;QocfmdOA`AlC<6wNOH@hlw2R+eX_ob|g1{={>OT1{PKr@5qmRj|$+7c~$rf&& z0TjcYqH2@p3y7M4r-iKJ`Y!$r@1=y)I354~|M6oQdX%f|`Ff5ijsMI+A`^0*+}+A+C)`6k^$&rs7PD^PFTiv-w;aD#D;qV2OVXqd*A< zI)mkAJ-`}dqD2_bvm*5)zToQyNK~Z=xiC9=0ww zd&wpxa9YiMlu7?+MiWujXvadR>=~J3%)c1RQ_G9Q>8l#z8QC0ipk&VrTgjrIA4dZO zzc%Lz^fKZV8w1hzwy)-I9>Bl}#EA?Tr*8d$=^p>kSF>F*kGAbqlZ~PVjgD`46RG1L z^@u@7gXwS<4!RpT^$BPDaE@6Aw`Y2h0{lK(OLg(2MkSkBnI&EB@>M0~Konlg-qwYk z9(LFi#HT`=KGk+DtmD!(IYQ{Gf-!1jY>cSS50PXFQRZxMYoEGGH8x@1ORvG=Vtr~_ zBI?=iD-}-*(=+a-E5n2)W^2s;NkaZP`HiYU)rv6M6hd#t6_i$xSTK=wz3#N))gsR; zQZHIdo%v}iIqXl3Ph6$jX=DO0+w?WIvBcVG@;7w}qUA&O)9GBW3N~-*L>$3V>~F@-G&B4DSAi$0l)a|4kOel6$aGk0lpe{4t;<0> z&D;*g_zHuZOhuPuI!LxbnBXyv^51PDnfVcGd$o?_N5L2@A>vgLc+rOpOZgyq1+6$V zv=D|3&py4K_?S1o6=6g*Xd8=s;so3jRV*dJ3n-=ebvQ=mhA@nmC&ApNyG4pEGh1uH zv-H(jHpR_urYlZ3L6fLX`7O;ro^a+`7U;kri%0Mh?nwvZ7DF2euB@57^o}-{xsv&u z(~H!`YZq{6T_gZ>^>MX=ji3wJ86X}UG!sS^F3U|tOUKJ^@e~}h!v){ z!=VNY;nJ|R6jqdDLQw=sJ!r%(R({fk3mB-a;CW}w4fx?x;I--Oe7zDJfCA^WtmMup}bp|9quxPj8PMB!y|046n7#bH6)c20;%0? zR5JD$!$#OLVIt#p2&tm9(1^Zj<1crbSlE8S*n3fzZ;LGt_psWZW-K*t@|xu&lbe+0 z5C^SfmQx$FE-Q*BeCi$4C4?H1r{-br#nXZH&(_9is=a3b(opFyqhcgBv5IA0a0%1R zj`!RyQA~38TR%9d2VwqrR50^)l#rQOrYP#4$-Eh*qm`(^H&KpZAt8?SYFv_71CL(F zvn8PnWp5nY3I~&&YL}R<9Iy|HnZDC(6ho&|0sVZ99go^Ju0wb*88%3&@ukC z3Bm3sFO$sHI#RP>~a=!^QrDH+XXVIoQMFZBB>IG=rvjXx1 z)q~+2{CSX%k>U5-sS-{_m!}nTtBbiFL4@i0@hJFF$Uhna_0bgy3}mJji{=k}5b3+c z4%{PsKl<}6$L>YPJwPt8@DRyHB`@)wWgT&!x%laZx5WSj(<2_(qbYmC4ue+^n)K=z z{3;#t3RN0xse}p%H-N=S8|mIY8gKz_+qFwCd}HH4&cuG(F=d1ttCx7)w0-$|Ey;AT z*C@p9Hmw~#`RxAa{>d+bZN3wTON3;`By7{JDg4+Y{rVjhEs`QQfn=?CE|#!1!8 z2;ynC<;lFGz{19Z<@gJ5HO@*leFggpr^nz@6M+W>0_eJ&`iZTNJ_0sWMt9hBhOXL2X>VMjBVjd|6|3B&6X zjYjt1UGpa!(@S#@T9Qe+-#OtBwoqSR0nF0z?)Jx4LmhDz2lv= z{RCBANtkR&$LM%9^!SXJaoTQ6$BLTe!LyPQS}}tCcdC7>6#AmAE`{%!XBmD4otUWH|@twxuNy6yz}))QJn`h?b5kNPIA7yxD{6yf{jNSh#00 zZ{{XN0T}s+w$BAJyaPJi8h9d9**=`YnAGG~l4j6MDTw7WQdF0;Ry`B<$U$)S5K>qb zVx#!1e^0B##uGuo)t0Zw27Rz93agJvvB?~Dc2)~dg5QDAWK|-3Mo~PkC{lCT@VP5>K9~h+kvEgXpt4Mogi;`f&bit83o3rl_5;l?Uf&i!H95NO+z258e6!*Be0KKViT>h zh3!Cgym)o3)qX`H{!!XF=lCToQk{$D5uh-i17FqFAt%h2+;JKhRUVYx2z`5eDJLjR1xtk79V@evq(?CDB;3u9VBJ~mkf5)uOxXZmc(W5+sOj3Zl zQd_RX+L?-;vd@c6pVoviAxtlHFSdD=QiC`Vu*bnb)K|PmaZ80-C-rE_VCCa<&RfME z1pacykIfgw(L7XoY96UcCDo#mX0wD=N;m?@5>NmKiTQ%X%!(OPy~iviYD)G{Ikdu3 zTw@6gazUmWofkLo0SA5;3VpXxdP-5ebKZzz(bd$gKiLH3s}7z^*Nj*$Evb~5Qp!r* zcY|9v)Jt`Zo6nUStkE>Y+0+ev!7cWK%V^F^ZN|Hw<%i%6I9(?ll+X5%<`*h@KT%*5 zQFFjh0IN+63V>8Wv3qjGC}7J+ir4R@Q6{q*yXGkk`c6889l%L~;tmkOo$LPzi2V|fIhYBJB~bT{^*1FMO(PDaOR3Y;2ZsBSJIMnOH>Jr^bPl4XB_l0$1@?nqjcn7 z%hi7rm0CokS(zbcq_F293Tl)lJTcoDMgef_LZ2;6xDDFaG@ zCD8%hZBXWX@C^G%~Ofx7sa}!VsaD zo<@_EDGO1BU0SmdV;GoYONN<;wq|%6h@crF*lbJ|-}M$}=UO_F>BG`fl;_3#?EZ@F z|29fUxP45vXQrNgUCiQSZ;UA=BL0vxMqh-a=xAguRATUw5PS!vcAPexJmm+S2ARxc zu2C#0ps_>-$#72B$pPg+9gnl0K8FI3l&z1CgqX5wK zS54i*RJJ_6@IB`j>_!rqny|fN5|lfgpwlFIwiiG5a2BuM{^4O?u-f-;Z0s(+!Q2A` z+{fzNVxvIkPpre&OQZC-CiZ{)ulssp1@m^AYMW@7_4UQlGXd`@k$9- zKmqaMmK;`T?Z1NZWP34C@{mLY~P$}q?Yd&3?O2ff#)jwo)HXuozy ziddfkEw(l-T2*awBvhx~ke3s)NKk$~W?m#|6Peh|C_((4F-rFhW4nnl*H4X`$Y5d` zfUP+Qh~-!BH*Lt0{b5%d($KT+US8~oShHgHti+{TbtY94c;OW#^N=$+E!7p&Yk?A? z;ZKmt5R=3}ss(r@0nz8VIxNmDa7r7{jq=(FeNyGOK()KB-Re_!5$!$`_qXh3L~HA@ zV;12EEr(Ryiw;;AqTGMnzW%*uq(Ic-bTmpZnilG`oH* zIo`ZPT{sJGT28)2PIs7hM^2OC4P0y^pYZYW?r@=)4Z?C)iXJ__5uKKPO>7&@k6_wRMq?nL?`p8DMs4)n$)Ma zg_#$>7G?g}b=CjoO!(XXS%oU73GHOi^0IUp3nG+l-BYI2MJnw6)Azp?cywfz^10sv zfBB2^ru|O_g|@bK|2ul9N@Yh8Nfr5(-7lw7`s64NAX2(p(v7=$)qtlvkxZFTeWj0I1)ELIzc#z)6WqS}t;D5^< zj;=KPNYO@CG!Z#ud6eb2R#>M1_jI=>vCh<)w!dmL{^)O-^5VRpjv&!5Svn(t(bskB z4*9p$M9s!?ruG~~g(|G#mU9qOgQf);L>_IFHB^Gxeu3GRY45XP#!a*=d^Tx_21h4eIzr3dXMt9CgCEN~>Rxaj?$IFR5BaMtN%Hoy46}UHt^hZ z_L>!*xz1*e)cVa}3-7$D0-l&5X~x3r{LDC=l-gFNwE`9H7IgaE#O0`69UPLlTjTN5 zzHFwE7Q)x+1B7Qwam4TmC3<0)v`dAOF4NMa{Z#YqG3n^-zPGZ;D3jOlpMK+U@2yT} z^sLt6y^*&0?AdF5`&!o%z3Qg**2bbo^NAMoX(4Y~L)1KW7E36x`rStMBXoD7KtS885hVZPPyjI~PgR~Qnzse9@jikFJF z`b{_21t4qI7WI--lObhE)`pZR?A#RX;2E8RDIvj!MC@qpNihr1ZBc&MSe8qw8ryFN zhFjPe^wWL@ppm;A{{lDF9H*mQj91`5PA?!l!`Z(GqE3|PBP5CPl~ppzscAP6I!e zu=vSf_pHAMuXm{j)iW#c$F1v~7E^}`yDQ0_U>cHSZZE~BeMF*sd)F5Cs-JK1X0Mw< zB+MuYvJ?`*$*{tTISH|56|dew4a#CpLHBzoUAM5g5RTZRfLi~i5;6NAP(U%FP(1>+z_&g@1DOPQs9drTBKPufQ&EwNx-cytK_$m!G9DWTmiPVAIQ zm=asNM9s9xm!x3bg0vMAPY_H@Ail3n0KElp7l*wJPw`lQ+H)G4ACo^d+$< zYe@i1gY3on@Nb2o;|rWMExp@`HJ*{0O{UULSR0z}Rg#nsL*M}(DQhV#6(#TQlI*pa?YM)END2ucSyJ=s zQ#kjU0+xnyHdk`zB%R9HDcQ+yiy+)aP;o8v8Ru00tl3uPATc}bS`zR3r>KItegOXK z%fTvvSjq2qyNUU`N%~I`>ZEOroUM$Bf73&49h`)1oE+T$SHyy{q%GDj$gz6I<#JH` z0iT)XpR|Ml8B07!5!prSsd~1lcAJZ00(=KR?|Jf; zogh?a!Io5zoarpVeqY>-i76W>V3jd6^wEnDYUc8e`Sp1hsz(#@8J^W;>g?RDQ~*!i z8pSWm@*B$#YaEDQ87e{5QiMv4U|D1{|=fSy`rifJX-B4Qtu;^jY!QMaj z^tpSXbITDyCyuU;?5Zi|Usf&$kNk-mDMZg4PkI*P3PYB?$8^g{BErx$!qd#;eB5(G z(lkf(s>xillcB6$nh}nfY@?Jg(=+3=ra>&qRETnddiIP*ZovWR2tw;$gXO7qoM8=Y zqS4{_zrNT&RtZ+1YtVZN*aNM^b{C&t2b~4>Qu+zw8Vu4MHkuk!>5=b$!EHrraYbQo zW>1>mLp#9gg}mGCSp}AIuF$uIb?`U(c-t1brBeqtjaER$=+@bY`I@RnN$%o%)s(rC zDvIUjFX2uH<3$toSD6QUzG`TNV+|38LD{?&^~%4b*>ciC=V9#6r!7b}8e{t1g!mxb z7r3n!fx9!ta{q8(wO3;tMO4ai#6yv}|G>67D<$IYalLI5%_&D^8YovNMYPkDmFM?9 zrZ+-peVwp(3h+(XzbnJey_J;V+lGM&R*AdfIH_j9H^El00N~+y)EhoN(p2ed*cQeR zD4dacm9>$6mc5YzmfihD>RS}64VSYZUKDzz5PrYKUb7s8qsI3W7OE4Hnn?P>9k$1# z+oh2*4EEC78jJb0YsleNpo|-K@gCI~^xUgC{&lb9EahIt=~uoV5d@Y+q%ZphJrW*`LN*dW!Y%Sy z?&DnM^f#=6!M)IneqT)CjABY7nTW#B;9wrL>U!f-eLIlx%hZ*#?CSm2xlPU4Z6sr# zh5BAc;y!_ZeGcfk1D-i324jEJhI{ptwHrplz6Qbb4A3o}m;~4(s(t=)YX5&!HF4|zR@H`-w`{O}1NRM*_k?;a9saM7ebPF z9$!v#oP12DI}Y=GdB2GMS&)aiA!xukJ0U$rcQ8T5E1ov`DL-Fn>5F z;;=eBmwf^GR~}e*iE7{UYnLJCU(?oZ?5h=lvt2p+}kms_(j5230dIjwJ`{aiJ5Mov2LA@ z5rU$8zV1>xglSNJr_&l0tGG+^vAf*8GNM`Wz%u20mLT>ep#GPjK-fd8g)!oQDXUKs z&CF;HO%<^aKg%_+?A=U<$9@clGlWNgBi|Ee3eDfN9;-n zQ(LvUkmKY0rzZ*!mNiCMxx+c+Y&*wz2Ni6S?Jhx`$x?wm)V@>j#Zovd&?R#3Lg0vi z>_%%@x?_9yy3w@lmHxVi!v?3>&F`&he0L02au`mY5i7c!gv0oWc3@~^f0+L5*S{#m zXDRqtfm#Gx+)YYTqM=!%?pp-9VkuKY5u`MefL+i+XC6JrX}k%*WWoiG;fNCy{RT@o ztX3FU%7A)>|BSO16i3D(9@aY(wu_t4Dr?pw*Qsj{+p`DgGorq(eP^(59tEVKxS6($ z+!42o+*7-ZJP@{wJVH(?h8HO?bxhBr?y;CFK~iQiMfL+AFK?5M2mcecDx1|4IM||H z%n>*|e$x}HeO5{I^=5@#dSyD;{ZRoY{NessSvzVutBaO!V z?_b)o1EvzcaX;oc05nmHsdpNUgk11w1+Zx}5oa0^XDX2p86e|V1swBEm3O!1hZmk1 zV`0Sw0B`7tIAt$k)i|>R`al2I$C~{T9w0v6Xp=lgw^D4#v|gD5vfqG{CHa z9ccDgFiteUY{S6m1^;QLMcJGOL}ksZBc(oE>{)G0M(AVu1vH8i(Fi>i%_)7I8YnlB zn~>8@G_(9^`GVv-)~Xff1^P~j8O#6fA9S{c=3x0PH9F9L{`>}3{jqg0rL(m&wy`j> zq_fd?G_!TEp)>#G%RAWUThW<2(EZN_>VGM4R5lc_eocab*Z5I@!^Z|dF?B>nRzopJ zWP2tAz2v9?_!u4INBOACA;HF~YdCHFxra;%<#J{Bg1q=}Z-q+jt{`F^0fhcwd}hrZ zCy(jZIcFGOPctKY0NewaGQ_vqfc9Q91HZf^?#WyF4avMW^m_*C!CLZJW%g3_W)0?K zZ}K?mwBp8xkYo1waQ*OOG+&zxJRLQy@h(agR0*7W}9m%2&WU81^J9JR^|fQ12_H(?1oxR>}QEAXHPmE`j+#YJiLay%>Xj(ls31h!nN+J9@D7chouSLeuz?*ZY8 zIwyYvZ!v!LALqqUBG2Yo3b90=8WE z^tcN~6b9AJtQ(G4yMG$y-ov|Ai&tptf_gSU+5MkkT(gVE`*I?wCaC@p#2lD)fpATz zY;C7Va^fA)+U|))5mgB->BetL$RN7j3L>&G#0}EJwYZ}n=|cl4ZMaV;#MJ*XwqRsr zk?9V|N$Ua-ujYmrNplw9Ra>6PW&O#1yaj2m7w^M}8I|93DzGSt;4Q-d0~+N#3<{7c z)R0Qua|~_2Wc#Ahur1q$*5Et6wbeSL55s$#IEc0(Ho0e?NxD=vGCZm(whmk)pIbbh zRLyVW25*UM4TEtT7C5#(^{ysB2P3V;H&jx*(Se?98_m=|o-%sr&J23TxCkdQ)Qjq) zJx9xS+f6y{w>Ch(n@{<;XvUby?=$q1oJlAuRsdgUku!To-`Kf}ed!_IfsjVmen z{0g1(QXjiXzc|mnq<;hSbv#Co<0W8-+w7e+K5(Z^hGIRsiIQ+mcw zi*D48;W$U&VrJvhxdq4{Zo&5^^K|G_ku>GnutGp-;lgNrKl>3h_Bv-86}NM2)#o0!W7O zBs^NmMZdYdlGJ;AY+SB73j-WW+Q2UWlV_lXwi+n=loXmsfOuq1xWaYe1=D-w4$}C2 zy3L#PHp{2Y_rIOr8rh7Uaep0H{%C*xQ2qzy=YJ3Ox>U8DlvGi^rzf-962}fhoqA$E~!{nJA&|SC*kF@lg4Ymxpq3p!p z*Nlx^B%E~oCPChejl4K#B_`pWzUre(-V%v>5k)kHS_$zb%1;2JCem8OsbpBwq2CHPw;!;kMa*f{yg3<2_yrXOD$93+4TQP<~x?G3mN>p2f^ zCXt@PpvBFr&u(}YPEA2W!6g{Dz?^x5D^Der>F&=$v5_vZ&N0Br=}v84^svxuNFrUK z$F9Pe$((&i-(+*#92)ft4Q=DPDvZU_Dzb{}g>2(u<)aywkW|OWEeV^)V}796n1*;Y z+yVn8H1W~owUtpHfr-p9XQ=d=V|dm$4$qI5kVRKte_q^tR~)8D^wHmh94_lWzJBxQ zPn`niu9bV*6y{81aA1C&-E7WgelY2CA$Vzfc*837oS!U~_n^?ua|PDvpjxB}54LHv z{SFC%d^YQ2sG{&+R&zZ!dpF`tm)2AiPnC!_@==>Mv4|i1H^dDDk&1iyZgX?7*_ev(UB_@Q zyHnv|daSJ85e)ihy?m$W|I!>~ouSKj+F=S)Ufi@K?_36W6%#*E%w)yXa)IzV5ipYm za!=K=89lj)(;5P=I592Lj?wvjjXZ0fc^u;cGv_|oJ)Rr|`_rym9J-~`F_AQ;ONLUN zTo;8Oo-167O6B~?9RT$z49ug3!(ZdY4f=fftp47mg&Q9`LWDk)1O6QF_4EDaWadv| z%(|@zEvoQ=GvZRn-?M1X5I`rUNjo-T{7D(WOiDN^o~3)hHkdBkB~**U1&o^7;F5Vz zC_Ay3%-$2dYmL2lCj;CwaI+>??)ejC=FVmFV5Y3JAQMyWS?xYd!nUo0Q(iP)vD2B! zcw?O?!rRc{G;CJK?{2ls-Sj;5htgs^MK!1vS=RkTL9BZcn=%lJFZGifi}?BP)Pq`N znP&+f4+ll9i+=hT<2`3cuO^iAg{S7-I;GE*Q-3He^$FR6LS?Unk3;2tXeivvKcE*w z=QD4Z9X`!E+yl=}10H`Bb?vN{alnIBLdjS|F!uyEKW@1DDL*{Sk>^+Eu-Ax++ac9X zs2^psyXp$?Pksu`5mT%~*Ev>3TTvVrJp`A8<=9X$?O*E7V$9QO9&gqcHugKp3GFdOEX<5Z`lEs^eYLh=E#}n| zBv&AAMiTDrCH4zo<16Xo-32(zkY|dvp!N()&_p>V$^;hp9`}i@ija}LIZz}+7#TQ{ z0FMGXMWl;`gPLgJ^QxWxu0?EUxF_23#COA1a|Cg?~vuyp%jPC7}! z$HE(JX4Q{_T$FF{nhH?iGi+(Qu3#3Z48gotr_-4iT{rtH-&O#4Ddr zXAkgtX$c=;Q?EJ|$Edp3q=?EP>FkFaEJVYutSE+7(CE&ml(4Fb{wpjH!>KlkPhcGG zD(xAv_6C$55x0k$aB8eG+2HL%+R)celeslyg)qGiQUM zF}+E<1OM}9&n;lfd+j@?UQ!tj;UX_Q^mHBu<*SPyM^dhViTg1q1Lt6}g!QbrTx%kA z`Mxl^Vi5I-i47^o^w9kHJj#D2{zuZ{`RE7ezs{46T`_Rve}gAM24hZR1_9GOy2+aU*}P4`;pAqT0TO^3mGt>$nq#g z%0-;>WuuRZESfe`CN2|Bi*BC%d{&cFiy0Q+M~c-A#>7U$nO9$}%uJTJW;;`AOI4$J zTZr}9TBCVM?CNII*2BRYMuD&2enytSvQzB&a^;?xn=n47x(0k$v|5TjoU!u=1Gs6S ziMK>rg{*1xb-h+m>w(#v!&DZ7F(+vY7!B3UIW3M54_Ep4(!}9eygHRF7V_FUjs3dC zli!)xFP-#B+&_e;*ti`C(3+<}GWzr;8xb64jsVbdL}~(^?zkA{Bh3iS3}eF7&4p7o z&e50Qa5_C`O%>&M@X?&;fYKByXzLSWfG|xF_+S!YE$r_{)%^^b&AC~Q)6-=7fCv8+ zOXs*5yH%^>TRKsD)d@5uLD1gO^CC9z{C9YchL!qxUoto5RO%^+jTibI3)W-2a9Lvk z|BO*o0M)IVGuAX?%2sD~3VU&|wa(kA1Y_;;o&Cz*W3V2V$3C5@i@O1@BVz1{98rSuyyjepCRSl#66_0Wn<6#<0 zWuv468BO{qp8Z2&wY5eAV5Uc>)ifi?!uJ06Z$rm$`JKqf!_uP$X{AHceoL|7G74{3 ztR7-Rl~dH*+r}s~(4t0ZzGPR%+%j8lJmmgUKh!SaP=s6YY^UadwW3n2^HU3HN%yx2 zKuho^sm8qB?0cH1+d8-u;r$ZvH1r%iZR15Lo+rI>=M>me2_a_^?JCNCHVJ2FoiWK; zp`=&a2aeSsTl*i>hO#DF$^;5Z={I`wK@&Qwp(M2UTPq%1?Hom>crVl2CjLdaAPd$6 z1iM>>H)dt!gSJL!g7SF1@?S0s}#@Bgji<`-D3vLPHYU zRf|$|<{Pp5v6E?5->9sTWY_Y`Bdsirf7L`s`8Qnuyvj0b(u@hkAQi%0e)7ajTFK%_ zMe6CxbLIXb+o}jbg{hjGjWnTukMHA_EsuLUpzM>Frhhnq&U5)YAEd19u$|htiNsQj z?z_#Le){F$7WghMG5r3kVJjLS9$4#gG^)BB$Bq1h&a}EF$Nlj}+;NBh#|yw+bp4^j z_TZb#dv6q7WXn(9m%GKOKeK^P?wZw{h=iDhW=AZP-tVtfeA|-eFq!J-4bJ zZH&EloMA6VLV~7ZW~&oL-k&UB6cjQT_VdHul3;I!Yme3*8BAq7Ia1o9hV`-ECS($+h_JP!gQe4@eIX>! zVAw11ohan|A?36omHuWM!RIUfvxJT#(($*;YD8l1FA-{uwjIN24{3t3=(&ud2<}b9 zxkmtBGm5t;!B&t2T45}E4Hh1Zl^odau* z!fAk6kWnxdojXXqKWl&?t#HzTtVC+9@4yZHf?+J1zU_HmzPQB6uJ~{ldWyGNYI>L9 z+T+?v@RV=0KZ*U$mYBKm3rYGX|EfE%M^KWaKPYLz6=d&Ffbxx&Q8tc>pE?aWo6uT(LG+(=}b!Gi^)8h{0CBcl87?&`%Xy*xJDUUwgOJ9{C85oNp345>l5;7if*sNcqMn( zDWb`V*4Mf$@Bj|7BXoM6FtC$v6pRpdWvJ?}XyKWHaDkBK_3Q6o1QMH@=XL7U=Xl4( z{O=^t^y_XB)L+aT*RNIiKWb0xoPJ|49G%P!|K}V!S&m0`fF9nP)*P@X?pEGMaNG|J z6to#uEV&dDf)v#Vk;>WZYLr#%lLgf04W^Zn{7|m?wOhwxIe1s!tCL8 zBZBe}~(V(u!L&x_bG!)@n!@?vwAOmkdJmx*oA#}mC zZ=YoeZEzUq9%eG=v$P0*kUzj7V!^MNE(rVlnJ0i1YLkO^~iCOTH0(sW;IAVf&v znzeG{OdQ==-t}3toMuyTNvrwkUv~v{1oCT7#@lfOhj0RY*Np)EYc~<)6(MbMI$$i@ zY-;($*rJ4Iih&Nri)?jchd>Q#g)9Qa1c^!)4HSgRhX~nt|AcA>-#{?N&mAaID?!YV zM{ME<#xVes)T_PQ+=!sf!ddJROb1fhtGdM;L1xnx%S4~f-~>aJq| zTL?nMv!o?sEHPzM^hw#2=2zAiuj+FhHi<$fb=QFM-!`v-NEBi!NQ$zBGD>@Hn6vew zUwB&>(wlak{YGzI7(j4w*s-dr4#3v+gCU)`SRu{D9*zlm=!c#<96PFBWerhzqV}~W z7-RqKR*y`^)e=3cy|$up#M~{7gw?YjJw&6EqSPRHL)sjxkuilzGPE|Guk+5d$34`N zJq_x`DTlx{F401hO&D(8+B99Y3f2XOJ3qCs(79p3#O=pYkw@XBQ>C^1t^crnw5aFN z2bkgff&SC_Y^LGeI!z~>@a3z%F`!sO51mih7P?^MfL=tGG_+fs!Q-@;@N>@iN?1z^ z-#1>xc64OS6jHYrQqeOErT7K@2OWUP2rLetOK`b>u|X(9Fon=i8}pq39c4y{ky;~I zfI+L+SvRU*9ny%x8AQ-XaEU}BboRTSY>=0TpSC;Ju{SigeGHr)&%90BPL|5Q0^uC9 zTGR(UHJ{gT5Q8+g-kaBZLk!FQe?0X6 zv&=N9SS$W&gnVoe)xe<&J2%jzEK9}ngw`!I5fP-I@(GlX^7NW$$E@q)E{C_G1DIlb z`{9~>d+EO9Osyyw4^CBm6A#?3uUR1gf*73{9UWgbJ#!wiPO>~*f8I}8b^td2&c*Ui ziocG+!5yzN__;H9ZTic`CDh^*T!~_#a^de)O!ZXXv%i@v1{A9)2QpbP*N|aCQ}jy~Iu*b#P-$(9+72@h zQJ;sPDzZdkWhg|HiCER4atyUBW+_OVH?|uGp9}3PJ{p=ymu6T{_FaW(vPNyem0@_ba1+6Sac26|SK# zjFtTg8fyop%XKoO8mhVN45CA?R^TOjuoUafovRRunJLfTqs3l=ImoX<=bYbcL~U(N zm%`xp8k<$MZN(UnYrc|H9Cp`=vqwE4=8j{Cs6lUkb|n?OXK5_PM@assr!62Of{Xl4 z(9c+XpdC_oKsc$=l;tPBF}1jn6p=@9KniKlN@56!7v}HK=B{xLeNeM(3;i!qX|cQc z?D^8h!&BZ{XB&bnWN zUvWrs@7VJ+E>b+dg$c##BP?v9Rcr?^@R4DUKlT(e<0%XkaG=)OJCPR&ed(QWiZ0$5 zLMH>@YYwOO6qmb#>y|f=&f^+G!>3MryPn^RS!TfvYAcN&k5?#+8YqTI#15m@l#qO2 zgV|R95#A4dfH}g9;oa1L;+;w(XK_~3`k*tKoC`}!Ca3!FQ!SJa8M1R?` zr%{KzBHh7043`_@fZ?M^Zh<43wgK&%Psq&2poxko9djOB$LWeyIL}E52tJOu13Z9_ zT_(XHxtGhMtP@{8JhBZOzE@e>pQ_C7s@H2NeL7&+|NI0Ru!pI;# zn|V3{)iYrWu54!~ZD&B~gPQ;Q+D*d_{Ve&n3uyiQ{3qcXat^`N#p;emz0*4 zgazP7t{h403kep457*IH-GC={3}EG~8gZkMO`Ke!#_s(mG$T+dT%=GobMfT{m{yQV zFfmI4T?xT;heq^o`?P@|YB_$U5qiQ^my%|b??}d$Ga$cE%=~G3eIEO{Qe=$;d z?(p7Dp|VDlt`ZxJsFLbP=u3IO_eQ^iP8vZ0@Lw=!68yRtX0Xg5>imB*hLJNw{Ql(6 zABd1GGg1|j7ZMYu z_)BiY^=cuCGUN*3SplUcZtB3-rwR2`CK>O8mxbvmr}z@jSHsi6@`Yoj1bwPKa|5Xg zd93wfL7LAvSUcJeJMeVuXJxaO=fzMxMyq1au;KU)YgMYt1N%x4bn4MiVP{pgItEKD zInbt}LdKj;scMzzadjVY9vk$zK+WiuwWpA=jv(ht8VD1xn!X{+vo9+Ds7+D4FY>BW zs5intU}o5FWIjvp(9X|5Wua0XSxmJ^Aahu8HTw$f{qj)4r(R$9lC=+Bjtl9uMyJVz zhR3L^avO0jjHZ8)uLV>npY&bOSG=rza5BjB-cf$f$k06*3sL8?5A$Lwd%ortg1jHx-vqu_47%&Lx-c#YF`6x}{w1>)VV_ zQKe0*>90DPnMUxR>kuQM$R^8WLj2hormza7)^G`Aj8c)ry0I7CK7#H zit?36kW?L6<$DCYC zu6U)~D$Z-1W7jdSu0xx&QS0*1+@>CPPctHQtF5cvEL`z4k=d^P2|2bSz(P!E^G-jd z?_Py_O0#B--JglAZ@WF*iOHy1*vxU7NCG{!Xz&rgDXn_9AHTY+W!M!0u93}f$_V2U zb|*s0x;U?Dq0mVsO|a;CSW^efe=-Muy{bTh1#w47L8=pQ94fMt8zHNr+;@ot?aA!X z%sy_sszZw2HVRa~td`heE>L6*FzdceB`HIa*;58L2>gzg3U4JpDTC2&EeYuAxpFWW zZe<{E_IU_;&icG8F^Qpflvj5oH||L+Z3Tiyn(Z2HvkzN>9$lI}zd*Z$*RH7Bi!a{4 zPFsX*ET(`SPDyoDDAvA(C$#F9*VET3vvdA0%Dyp1v~Jn9yLa2RZQHiZ-L`Gpwr$(C zZQINC6f2X@~(;*ilFnim)Ts z0ae(s3$d!6R}q{~kQ3fP(JWDjVH_?-U*uXa1gA%qZ1+$g**W_ir7zGOxEEIJdF&l` zPay&LGkXrdx+4%#0S6%Bau$EMxojVL1`@mdG(;Y-Zwvt3h&iK-p?QYHB7a7OIb|_L zPKjOyZR00q8Q%#xDP#Kw%#b4($YWUu_Iz9+rh~K)DcP zF^wF~LV{e_D>eetJ01r~Mq|M6>8G zylie1O2MOLN>5sIYp>ckn>G5Zxk&G?oQiDoYNI?^!QZe#-tcwP(bribs%(T?;=97W zI`3#Y+hMG1H`RwdmmK%33kgvg3xr4t2G<%-ABdHNJ(n?Tm3@d80f4omPv;S4X$Z7ol^UH7UT+1=U0^FfKSL>wMaq@6l>#GqC1u$&EG-lye>zVlKDr52;J+w{`A2!?A za&gNKzZ^LMwRdO$bsMplC-GRMjvxpDDc8a&Z3$zI&lYPjS5wuICZGPoWc6iU1y)iQ zwWg)CDx7#yhNDAp+ma6K8{2D>_3b;ylMM`ZfD?H>!@^$h6AL0sIR2m=2_bw)E~fH5x6MhmP4+1Xt=$OyjC3&DzL-zDW=C(Q=)AR)s-a@?k9?%jj=wxrHxF^I zzi&0{eDxl_))cj?vuV4rlYYvN?rgbmmf%WVOe;d*%|b7ZVBiF=HRk{XOF#nJa8OmW z@H=Edz3>CNY<~D^OCI3wEI~{-jqui?clx8$S=VX7W}(z)M_#{zPz1}62FS4Z$6Tfa zRfW>d(}L}|f--RkvMzsiSV3NJ8ZTf3UJ~HnTL1(8F&gZ71V?ucVBqy|oA@7HS=rQpGStG^V& z1}Y*$Ru43n1b);C*%if8*8T*g8Gg_JS_(pYn5WsRW+nYAocKs|I^LOt+O9^A%Xqs+;?mw+r#0#=<66DL zynUfQD#C{IO<9YR48Gk!v&2(Ik2||JkuU6KEP>x_fmXubsK4n*y0C*X8^rKOV@-fq_*WQ(P-2QSER!J2&q+8Rl}V1adp`v_#9rkfVS{0Jd`bv3Ql{JdtpSYwc( zB0=@+(B}&Fv0TgKw^>%qoByY+M>`idxj+XCypX4*O z%im8x+v~1vyNHLBa*Nb0UQ8u@DCij$>eZ@=u42exAl+8#O%`pBgX`?Gi^(+Cbl{!$RL3AhosjtITI~+DL@+@6kQnR%XVx>x@Y`Tkq($ z4_HQicqo87`G)MUtQoDI(Ox^03-%8ob{So(?Mw)wLEqz3dw2D7j$u>dcedakR}|g- zf4~E@=?Fulipc1vwn55lQ;D;?kG=be?Lke=RTXR(&HD;>2KT%pwFS za7*3d3g~ZTp{8}ds9br{uN;AyPF(~Z2xyMu`cUA;>*#hw7XlF!2MUu`{Z3hM@i(7G zETgH_j<2!5lg;p-lr|-X4!y20RzDU~?0x<=v5bzZnAT6xfwLg&l>2rH@vW@jZL9D# zM1Eb>yk)sTtDXB!4pk7FHl~vn%-XR&QUUoM3bPi5Ss~T;!rKzg3(uAyp07_?DRLjR zv)`!L#)qV;XL?ePF)lv#Ii9c5ZqAsZ-tK#M;Mxu;o;2avYwdG=$qtFaHWkO>psd2Z?$;&dn}X4+&>XSCYh+`l3`lS1sE!pF znWBKEp)?sGt#ls=t2baZcZ|(LoXb^?xvbZ%Ac$8affm1;lS6P=jEgk(TcE3+F%|K> zSN?SOSx$pLMMu#>5kD^V^z}5Li8p$$$HyE0%=N+`53RYT+l(LWa&)w@UtSO(f+A)wJd9XD zX;B9ZYu|jc*&kvP(%jOi@l>SCRzM!5ti9J{oPv@PNnJ^8TUS-zHU5A)xmiZ3e)`)A zMb429ZQrhVPy`l@BkT5O^9rk$&_?ad4m4^W^Cz2Q#q~tJmv6KK?R=Yxg*GoGfM3*o z*M`+Nv4q4YJ))6BM{~FySJO-V`*}tER`c+Ss%7^ihfGv-b%F;aH{Zh2dAVNkuW0Vc8W2&0%I_Oyai zLn_GHr*hJ^*<1eO-5%>%GU>Nzwq`}OE$IFZV4Ue~P_#k)3(S!eusAeDd;rZd0JLWq=*CynH z)_cJAuzL^oMf@#A92}4&9w{L38A*^IuK$2wFDV@_7a5R7l#7<4p8vtiErvxL34Ff# z0-8H-HlMHmW_`E@*&GrK7WP`(u;qx6fPU`86Je-_N#vdHXbOEg4o1!?9iyG`1u$xN zzSZyhA3{#M#i#CCKmA^7=l}p5|DT4hXl(Cn?CA6_^B-0-chVY0`O2Y@nr*N$;v#4{ zOd|>4gbWw|A?_Gntu-7f$43)$#W$jh_|)qjh>Io&yB^dyY(>*?aCpVWc^dEWrhBW8eG2waANf0A_XjePP;E=R zdWiSLO!P63#T@FB9hc9G1#wdz1;y2ZyGxH93g*B+6vdSWabNeTWwZa%O`Ks8?78V< z%X%U0DJCLk<)xaky#xqwsJsP+7$Vt11uvjnUK^JzW8`hx9c}E3Pe4U+* ziA`|$06~;Jzjq-)9Hl&}PMyuVfJqBZtVGmv)E2+S)U}!#RxFlS9yBX$6s1hadcvgC zGbc=Fc*7Jhu0Lofp;0Sv`(%4|d-FH6jNojypdUB<@2h7ga~9%*>#NY0Ua0RF9^@Yf z!S?Tkd^_T$MTB|zgWjZ~O*-KY9s!t{96k1h8IXo9Vthlum}IceS7U+RG+f*ZIG49h zj{u*yBNMhYEDED#mv+8(FwG4 zO@6s-3L>WXhLl>5!()4!g|PTLV|I?~M&P7jMUxXr7|qy_2{&p4T($!ftEsMthbDoX zXwy8Nxm!5PHoHx3xxy8q(e#P5ap8_d=~0(xj6v>%&-!`@j-%%3$rGyem40407Cq9a zy`df*QA)xE^Jc`IPAwrhM?EEM^rUojjb5vq3k#reH!CYYE>cZul@?*E zTmr*dg+cr^DEb;}7M*m>ntd zb{iY3l`)7#(pX-Nv6#Roq1));6)vhY$|>X6U?<%|s%*e+M%#%&XWGfvU~O&Mf^mz% zV(3}YJ|h*xAtq*NsoI2T(V-sxs99Y#5$;m3mg-Kx8VL#(M=7=|^ zIbD|2OiRjZszPoXMC{V^-aI|ELbbhcU_4xT>x_{EeG8;fH3F0p#^Gedi1TpZx4UL^ z`<>-Sh&?Y{Fm2JD7MRcJz%9d7AD*@wLZ=S9UuCd1(UBzD0lG_noIgTThn_*~FJGs9 zIk)j(zC%#75W8|Hq_*{7nukAVT}OSo#@jNq!a+>rMwxMd1YE&a@C`cR%44bo?{#8A zIzD8Zr8a`Tl*!WJ9AG+a(k3H)@EW{esW>m0P`;ziw7|pJdEgKb5K!l{>m&ZYVYymI zmX&d3aRLOJK~`uDLSwQ7LV{luN5&d0Qc)Ed7~oz^L!$}dt4F@KH9Vr)0ha*-lGQs7 zmyz53OvMh{5gf}zCyV^HaJecy94msN-%Fd9(JV6&bt;&eJo{8r(q2i;(XRJlqna1> zDIO&$9#cuB;&u9jiFq~iAyUjM^*D^$kGeOW~Y8B;vfAW~S%hOD8@;d$R*5V;$T6-n(f9n*jn zN$Ue=L)p+~@b>@?Rp;aPmK6=J9>{A3wd_4^py#o-MJxuMJOYtn^vd%BbwzCci8yLA z&~E~xR7z)-3v8P}u-qXBFUZRKbf9HR>g8J{lO&HTmy(V5dqAq?FKZ2Z@<`U$~_S?3m>%(6{=eUZq%bcu#F`ft43#iU*_-vSGs_P z+fBfr3!7DOXerH9_N^nx2I}6*xr|A3Ii>YtiSky3vR6@A#70&`$g0x)hqkg;Q(44C zR>W{^>A`S&*^9aY3JjUT{lm`-?A$u#E&XzE>EqGv_K!6^k&+pgo2r(KYe5tt{>s&e zMpCL9%vfY$jYP_%S_2x9>P5-9iuF2y#zLvZdI?9=G;DHujZp7uQUvD3v(1udpCLvM zyOeov5YbkCO3RtOW-Du}Y%46s8CA3*86km=qlMBw5V~4av(klN&L%S$vqjALau)qU z7fzs!^s`0oFf&ub$`$Rs89je46mngzWF#h@4)3?3+Gg&HtmApkNutu@VTsIZI{_o* zahif`xlFqKmR7X2RSqIS$=}0M;PVnWs4=ZD{Qj`Z_tAMQxYc#~MxGsk8ACOB5%=Ha+p0rRjen+`~Hw zBU?V+NFp|GT#V}pX04UCU|o$>XDlK*)$KoTDyTaiwk(FPU2+ZFOOj|*rlM_HN%$Ff zBGwOUW(<^2ZD$#N-DHf8oq*CoiCJr8A;qK^*E3=t=TJk6wS!d-D7~=d2=}S@QVr&=AaJivnHiO6T=218cS=@n>=aYa)Ql2~P6q3sO@&U^_8o&lr5w znpo%zV8ZLoiC`2#lx+SnGv;sI2e%w$g04fG3sy@bbusZEQZi-<&}3UQ<`UdEPD<{i zl+upk&itd@9{60bSL)shnN!Tz5u(fL^Zbxa?GDlUrTg!o9}oKR`_K=o91rWC6%qeW zs`{@;wOifN4rvwTtA@lqxC>Pfp1(djE{M3nNDD$j5^4!h+U#lrw%^YX7sArBeszq> z)b*Hu7kR<;@r;VVn(`va(7bQ`YVPp=aENa9DGn?>jzK32gwGhdFP9d*#*3%ndJ z_JD^MaZv1m;m1o`fZW{!#P2U7@SmbbkO}v+W$wXwY2b@Ac<%7&%e}jgS3aQb5EBf0 zl^u=eof_FDmn?_%)h9N89v#i;oCgmmixpFPRE;SbYi1hTh zW(WUiz^c|n0gsc_#DevTfg>4?YVR;TOsP^H+0LR4(sD4c)-%E@?FK8Lmi7Dm$PZINCko4AVWw}8EUJ~6u*!kM;@2t? z;M}@9J|ge7QLulMGKw>k+%_r}hhM)h8`SxGJO=eG!}{VMh@UeuoV)&N4Umsh@&hX# zb6B%=eR}!4`jI2TgRgfSF89t?Fp3CNQ!05J`V~fFUoVzy6;(%_!=BptzaJRSVCEx@ zI58d1xo`~&YHv5iBX~!=;8D){Zq&h$85XBa^nA1debL$vC)X71(18-u|Bl>Bd0<$q z#A7NWR4hC=8^zXm%&i>di~Uu%jNtMluUOrL)px8dZE9zSbb(snE)Nc0_+K zV?b=HTu8jU()V9W5L4u05piUj81k{8ia}4gCV%nZ{b(6yrIy@RTL`nJ(WvV9RJ`H{ z0y`u*ub-G?YQGDzVA4r;%dam0uCL$v)NYu!roYljVK8SOVRr9Bvn9a`0%g>&6y7x- zo2eGB31mSwb4cxwfmN17*_38FG=rrOEZI^|30GaqxyZ?y;>@y1lj5_#SZvpCR@sWQ zEfX)05#i!=dy@g#RlK+Eno5NESfhw5+Zu!Xt#D5{Q)iSKal6+E`CG|8MXe{4Ng1eu zo&ay5{NR19cn^&7_b3NS>_I4{sKBjH5FS(zm?*X3gnck4@>CgnZ0_A*=|@}MUO@!5 ziHi0>P{d>#VO>u`q{wrHb#{aRcgbEwMDBh@gjm`*7Uj+maI@kv@L+e+t|d5bM@q2Y zZAo>;R3G348i#_iJOSF1Z2W-zCbblojU!0?D<#Ah2A_PAFfK;UoY+f53UTmCL?nAM zSBC%yBoGdYc7eN$*4Sr8d*Yv(JSmEm){#q1)-5O*;O&zq_Vqc_`$mNlsJ9J^4wUW? zOkvsu_tBdJTZU)lXPS`5Nj>Xpe)AAodtc&e%f&ZtAJ57oHFIOFMq!!5ylldIg^u9j zW~Z2^f@Zs&uUCeiv#%4k)o%}+99vw<#}HR3wOOhh$JSJ-gEo_ArFZ;2w&T$XH%kgC z|4hgQoph%R)W>zc1Qs%;!V!emU;Ma#|B6^xhMy{`w4_!=XPX9Y!i&QD!Lc3EZo&Ao zPhZSMkfXt4inWK#c}Ujm1|Bn)UyR>@YjG)hc;dSQ7=A3xS?97K_0as zjr5UQ#V7+bR^*q<9>I7)Ka))0&|m2KLh#f>bJOH64uLNY31HcH-<;nAe2&Bwce62w z-(LI+o@*?Vxa12Z3XGB8m;pjPMzE8hCJ35l*`{@Z+*IM!lTvO&1wjim{e`p)pt`E3 z{!Dsu2>494vc!q0bMR?{%3vWMPxEc)Pf%uvbdqPw?iz3%>GZtfr%ZbM;{5)I;3&;v>^5j}X;}m}Idbf+E#|}50oE%rRIUXJP!M05pj7aR+F`02;SSndV6-K!i z!~%}0Dg9;XfY>KAgj|AyQv@Wmh%h$yp-g&y7c>!t0V+?ZilGx_jJp9Nyzb(h1(u^- zGc?`9!0|H~jw)Q~6BR9{w5TCRb*{5pqkz-=(&0%sTZULg)rhdtn@8~~* z{rj!%RpPKueolD61y+r^i z50_!)4EVldK0Z7l=lG}*@?IxY?;bVNVI+lG;$UOuHW{Ki^D`a(K+^jMOMaG%m;0f$ zf|6?QD?N>;=MP2(?-XG{OiQrA2Y6d^R88);EF%9po#}ZL`{tOvu3R@im^DN2?cYAA zHAC?g%X|qfwSdxxQ-Cxo=F(~RmAc^j36aD!gbJ1IHoI~o5gC@NOjbifk% zQC^q&Ev2Ea-$y#+4*;r)_!l&EE6(s>403ENW{{E*9jG~_^hidv)1cXb#J8VHw+{*~ zj<6sCjMpz#@_0%zzFwzCxJ;61n}?^gbN27l$NSZ-FM#$i942iseT-b98%U~BTR^2n zTgs5LSIU9BxIQ4;QeE26Xu}? z1kPTgp9;p!%jTtv9vjIsynPjcXAcmOs49uY>SY58ZmKRd3=?5+WogJ3~+MU%fM@SL96K0Kzv;j2EoxY+FI_ISNf@sQZtxQ~w!mS5F)w|AU$U3K$?=&~__YbNt9i80e~Zf!<)%Cxp`dxi?F|EuY} zL5|y+#nqCNcNi?Il%cwIC2iW$`JRoOy|tHk9kIH)`AlVHU5dU7=0%{D{4L~pJ1>U z$}P1p!qh>vCVVRw{nZ-Yl$r5&?A@OnAw1nZp03$2i^d|?s7GPmRgn+0ECt5{wIRn| zvEXY08d32?o?C$uq_lo8?Qc7H5e?n~Bl#8h$*rZbvL~c2%ybXd33@R@T)bzB_wYso z{9NCFdDW0R7V95_XlVD z*f|XoqX)dhRe8V1mlVf{8mXh3Ot%M)e@~RIeZtkbeq=2Z5CH(_{sFZA-&ex_k|Vm+ z{)Zf4yI8cyifT4FtqPJs8tms+Mk9h*2J8u_7*U-kn`p>&ZA3aFE!hu+rf?TT2;*bz9;9(x_@ObG4pot2>*{_x*M43Lth@h-dAlrfo80yb^ou zHVcMF8!<^@@2)$?D}R@ti_6S+5_d-gLHoj|NKZEg6;QxtI3G)b?o*`qrcbaP6$_GC z-G?w+gQqhF?oHfQwZ|ybx8G8OvAE^pG|7#})!!$})Feu?XwR~ky%Gg&D||DGjwwwu zjKMjA+H6c?V0Pj~e6!KP!8bOSP^W0Vy>}|JZS3Ye)o@CrnvoefUL01=0hMAc>a=*= zQjQTHlzjBK6(tPCSX7`tBl%nZ%(JD+Mobi%s$LjKMu<}amMN{Dj6kKl*d_0C5tv1i z4t-e8vfgl8$ z!KsGshU&Z-NOvu4Ab;Xh9x+#cU@!9WvE;2D9yQ~zv3(>f4#|^=#Uwp_TNHZ!px(Po@mDI|mbGkn zj^``~gYl@m%=Wg!`ic%ON9!0;Q0*vGvyF&D(RftmOdB$KP4SUmWh>)Q>!F4f^j9KP z!Ls?S6LJ;3cE#1--U~h7t%)@V-ipW^%gN4GIX|N{A6ml>kBCue{h+spHgnAy(tQT( zax-D7Ix&NC_Js(=^m;Yl}2hg3B8P}As`*jhucA1)H;x?j#0wAu+UeUJ5UX=%2TPk4fG%3Gp1n3|X-BwnpA3IXV{(5($s8 zrP2w_M9i9sOgy90I=8hMM8&|L3G^?IuSE|YU+2*rM#f~Ux+wU;ei&x1KQw6A_XDry zx-_L7J{gzRpsOkF@gZ(9m=)QqI&$mgLkMIt2NEIO#xvV;H_X8}l=5!PtPvvDqA=6> z2+ii(v?Srgi@ya6kkG=1{wQzkwQpNXaUd_ch4)0a1ctC<*1>Sy6IOek36;QhpTQ$V zt>75=jB~*-b+p3E!G8`R6_lbdCs5B}VU7jN_r`qvsWbx*#w%c8zJr&^?}p3&AZ!jf z-+;JpXAT1hsRI!kzoB2}4go9(apG+o<=3oudTkH zuG!2dQN4Cq@Zg*re|gjkxiehwV3I0oO}s*y$iLJ;&q6?wLI_D10gO^CF7@D&a=8Ds z;Fd?=1voO`!t$We`mU)#$u7>)pjWhpq~24}>Y(i*p}hBF&>GI#@Zl*ntR@T^y{^^> zZGc?d#89(~m-7_5PR+9M`y?U&=zz9U!M4Rv90dih3NtWBU~n7A^EoL7MY)XAKiE=E zcF7ZcL(pYY5mY6EnT&&X`_B6OGtG7@Z8uDywcOSu+}&tO~b7;%x>hwS7g7Y~;;1Q&~n9`WzBv17aAMiSr) zX9_>sQbn)k6u7atPwgUnbXX*U|Z;{C;b<0mc9bIZvfT6h~ggrKF9}C9uOgv4tKm)S&k=Jw_B%!qpi1q+Jii3XxwgXXZwh> zo4d)>H6BimJfYM+uJ(h4jTZ$Y@?k+K+-49sS-;kzl_Vg7-(! z8~!ApY#dB1=!7^%%e#Ko(Jh&rme;y9Ay^zs83(Cy-xdGO{N&G2(FfI3!HB7XO^WB4 zPl(rs8!AxD-w){$g-#tcxTm}D>8=!{0cTW7l+17sQLbMy5T^^q>Aw>aqO4~;tA^?x z8AlqFCbCjW$WkQXMJSmLAzsaiS_!(5ctASjCQ#q8iR{sTX=G=up_v#7+R!CfttQZ+ ziEeJ#K<>>{XVOj|Lm0eTP3O7wov|@Q?9()^QW4@#Jc>UV3^Og^lw-Iu$K3Jp$bCjy z4SpZ63d#=Ee$2s_qI9UC=!{szpZgOT9>FPL)_Ehe5+qYte8GrK#mf4M`uwY=<4fMn z6F)F!ZkfzwB+D#Y)LjfmvRSUHK08mPKtE$8z*SWQ8B~jyL!LT%jaPja+1l_|S#zLK zQnv~FoVC5&NHMlXK8NL~od+Aou{{8r&eW+r6qGyl0c-QQs8Rou9i9&Lu|1gUBe@=) z&@q?GdH|jgc{EGDHwsU_Sv=$Lo5Oa>?E-LhDI<6#10vAi>%9{5+s9fnEpUKsc&$9H zH`#;@*u@;|qyBCRZ@TN*>&JJViWJuj3hi9t^*q+qo|rTd+=c1X^)SMwz=3Jl>|8q^ zFU}_Mfk}901Vw?L)%tkxPMBju(y${mg>-ZPpXt z=1RJ{gZ^k2L{xjZ|nmtV~f71EwXx%B_e~iPU@5w3<`svb5`{B}Z{nJR%>A#eWi2r4* z`ZlKjlSgJN>HMfPBl8T>T17S$BUk>6Vo^Y8*=QCW3>+zA21Tw!m`tSmC9P+dPSB|( z^C81)W?*;_$k&Hw+{MJaz&sRtL+W_D)qc9=c{B6%@_NnX2Wev*-`}iJWWP;K(wM~a zg3HRYb!V4IfEOwLP)AarZqcmvW1VssCl#8*5NZ;Uq3?vz_k-xN&QEs4LiMFa19qMH zg}?K}1YtUCe+?h@#3)kN0=ljU1TtM6C{(XZ^e@~FbrB{~x zo>i+&)f!SRCJG4-Q8sY(T|=F>+<~P~G&_SGKO|208!EtdKAA?ai5<-7KCBXJ(B7d) zSP{Y<1Cp)swG|!(1SJc-KN1vrbetPmRNG1CJ`@_T5I%JoUSv|tSKN+x{ga+iAnB{>b-O&$&FI=zRJIOfKP@7JJOyd26 z(Arsp)D3w*yc#wTqGY2)UHXY?8ud%X8N`Sr#|#$&jZ~}oZ^#o{T>Rr&+`ju&*gc;X z>B%PA!4DKTibn($cj+mWgSCE)h&My-nS-uR+lUt0#+>r3hMe+iz2py$P{fD7EY%EE z`#{wt>3&s!C7*P5d|x#=_5G$Tnl@Q>>e2e%lxrNYHnJePL&X$(OQnW;Y)XBww3}5_ zYjuAqr}13A%pcV19-#i+d$_smaG(F2$)ul7{eQyo{_g|%-)HlGoJS0g>>xkWq4@cBwx*^786+0&2mmT@gS~L{q5cA`S*sZ7TE?zD` zO}(^Tl3tFUfOxB?Lh1OjOY7&%>798jhYkyLmrtzca(O@Esh#T=HnfpP6h;y+F zOTUTJ3Soyya)~r>y#9wte=Mh>CHE9b1mrp+DpDLe5*@6aQE)Yh^?mOd-rWcmQt}X#uOHvUOHQLE znh+4}I>+t%UvAy&*VoH;IDptX14Hl|dTM3krB32hPt*%kFQ`dqqVTX^JKHGs zh{F4mBTS$xM E`Uwy8+sE5V`UT&H&@OD?BJWMDWCon;dgN6g`f^%Wv9iK$PIgzJ+~>mfXJGo22XU^S z_?(YQ}|1keqditvobiiqte|7z!^Hh_H;q1MaE_c+Ifjzr70u8g}88T|@j+5QTp zZCi(``Tb!~4Bo9|7<01=j~Xi||uEFq(^YHkt-QQ_>Ym+V=9>&ut?;RSUQ0eGTXTE#_Vduwlo*$c$e55A9?O=7Yimz$ zH(#LbH?DO79jx^BcZo?q6>bp#WJ^vD=OtuQ8lkC3OJKraSy+cQF;7P1yK*j)m(>R3tv&{x zJ~^$o-=&$KCG}xPbxF z7uFA7Q8b~srAq=?=!!g?z|Yc3BPL6}h8^92OukfB@Q|3a1bJm)WJmoshIw+~@fie- z>1_xx*l#~<{PySO04Nv}O>G!>5W)GWYty%-N1Ov?LwVMkw)9riD zv&qiX!;~)`ALyRJhfu#5)qOnxFfVQ&q*no?^V;OF_wrhhw{~Z4Bv{)Ev8%g$zYt*A z+AI!k%?Qj3GFq!?`St~?H&~OS0o@MENC6NuHv2X*D=-BsXRdF z{W3txfh8pFF|SzLEc)umYwIW_147>+`-FGTOtbCXo*^K&{~=_Kk^0Y%NYQo=4k&1Q zra`7(as2f(UBpjulF#+pk^<>o+q_r~4t(Orox`EcegZTM(c`_Cjw>0n< z5~X286AL@qiFmPDnxEtujYVM=7nDj;!Z6FvG)rAO`9-9qF3lU(8-+H@MVM0J!~UY1 zNBUG;uz<=;PZT-%tGM&kQd$&8o@~cOU0&lbtI*fjt};SIVs%cp^<<(uBNaxOZ>8m< zO5rUjEv?Y)H8%m1aJ|PKvd8GgC zLYRe%Smshr2~UgD!*5MH!c&;Ye)^=TY30_UR`EY4+jPI59uXm?KW3*PP8X+B$N}+oK7PM}Waw#$LqZX&W@1tTCl$)54aPv+uWcGDBv89~lCY8PzJH zjQ&NZdRbGzts*-5`Lu_{P~J|HAuG#b&-tnWgZC0*Tc;JS$onnxLP+w|!g@w^s_gH2 zF(_>}$q?WWS>vsW6bIuyVdsygFjvYoNjO%jb<1Mt#0E2OB^L%#W=HxA>#HgB@U|R_ z+f(N~2{`Z|lPe)HKLw9gb{_C4oTX}(n;;}}%9!zF2TF4rR>dF!ritlppyrhq-{r4%V)HU2cM-XF%I+B+0D4d*b)=|C3~Jga@nfa zUvkoC_!5)e)tA2;s)Du&PfZxfvbp+kYQI;m8q`R+`06*O(30&Sno4RY%{`Z_Yxz>g z4`tlBPcY7D+Fj{8yfocqR#T%}dZp1%; zXvZpij0sH?^kYzfH>JXpFy$_zO;^Ddo3&Wt>AO!mmpECgvnJJ@7!ZHw*@znZHYl4G z9lNnEn9fzx8LlhosA+#{WF!9S9+##p(KWv#aoSO~qdx4G(Yr5+GRGi7PP7W1t7e@p zD%Kzdyzgl%L$R*#Z1OZBHe?WD#G=)~1!VC=>h$p=QLG1@wY&F5N!?RKp_;Eb0CQ6n z0i#v32T`p&6Gn4&m)(mh=kai+EWX~4&hUN9kHFg>rSu5^MBy8r15@RKQqMQp{KE`>r+y% z|LL)#tG0&xtDVG^(56&8^^;D+QUN024@X@-p&+MLsC;NliWx4vHM$))=-%L5v z@gjFArZ!qg(<4xM*mQ!=RQ9jaVxCV{xbt7YRnEkF9-wIwtJdsrBdfF#Ib^hc4xr>e z7)8tgjY>+9qo#JYeAXuv-pu*KX0ubVjfg$*$=Q;QZ>c4!{tP5VNgO^NURt6%e z02P8I-@u%L(CBpbxFGARN0?x5*}eAVhY^&6guaOGLWL$4<+;J~LOuwL11i`9ChDHS zD+Uh0fkMqe@oEYlsL7RUhP^VnV>B=BNe4D%&{q*k*cP$Q_IYuZI%L0W^I$D{v*hio zL)+Yqta+MWjiO&w#xR+lCO1H>iZp6UIxJ$fB_7}JQ7d%8XCboSLROSJ#aFE1@~nOhW_MR{`KY-*ja^U;=K zDfy(VD28|>U$Q<-YvlbP|3FInktc8ldIY`^!cqwU8fEY;rgK1kYz_7z*hRN!V-JjE z8m-p@YQ}xSVZXPMdLh4P2{zA&07prFqAn_p$mwGjjMbmFr{+BZ)~Igki6*`fBg)r| zY)vp*mDnFxDn&3=niQo7I~O-ojBQ1Tm9!fDL!32=K)mUI(QNlJ6@1E97N2iFXo)OM ztq8JBC=T6UDoUvl^tqJXteZ{JE3Ah{YjBqnS~rDIR62}Gyu?T`_BMuo@ti;4dDj=7 z>J8Qn_6S52!h}!?Qky8(k|-Afo-S~Mlk#CPe&wJn;e!oaY3&o`1?%-aq5u{#F!7Ez*gmBXeHxv=FYhM#D8iUz zLtL#$ES!PdGAI2<^(vt)#iZ1ADQ3gMQHBf`SM9}qGOxM?tw2Rm`#2u)Qg#YPL6?(v zlYxz&XMJEGC4((0zdp9Vl^SF4m8SoMf%y+kieXIx7S8)8>B56a#wDtzW@`sLtrI*g zu?Q04tYB1JOpe6Q*cB^bk4@o)zpS;MH=1-jU3d}feoX50KDqpjp*)^r_eHucJu(~9 z4mGZ?68>V_cQrYW()l)ZBms zHc9Q$QXn@gNdUDP&FSFHlO<+X7ETu)@}Tsq1M5}<)GzU?k|idi>sEs6vJ+7$#LC6{ zak&`Wvs;mjXrEil+2UudD0RBCuet(P1X;rhUBXoA$Q}+a71X>`)`Wp|9j{-ylIQ9z zH;S;feWImn0x@(ho!kGSxBrE3xK6CWw{i@!{8^2U+A;4}Yn{S1Gg|jEKtn*ULpc=U zS2}L4fCm2NfSI#2j70UP;cHOBSHlHE>^NwoD-_|~qY{iZ1}b2WVcp7cYo7N#KtmNN zS9}olBxHZqsD!Un2>;F&4AsPKnh_XyW$nS-yI^T;jjX5xC=m07Xg=t?(V-=Xf=Ltx z7AfQeIJ8i4E&{7Pww9Pr#1JL$`5rnMo8RHUz8$+TDw;*2i(QU~P_cGNkP8GL8j-s$ z%<$n%G%Vz81$RP0$E2{nTDoYCD{e%Vd3r{qpG?G^)O7db8T z1;R}L;thme6W{@U{RfP^Ez}vyfC2ze^MfJ(6Ui7QcWVP%tABw2#hg%j%FD^8UD6Mv ziK#}6#KZ)Epd#Rj;zv}7L0$yE5`ohJ9*{!Di7?WqhF~tl3=YSa7`Yd_0+^7doJgd%xkh-FfUi&3PQS%*FGC z0H7Vw$Fdsf@~X?93QP|5!nI<>gv16=pJL*wrHY>?I7G*gvd+^>k218D4{~Q^_d90& z8P*OudM`gCgaWtR<;guus%~F^lC7F`1Tgei&7Q8Sqc=?Pk!3NANhRA@5Sgz@wQ~O` zNF{q}+KobYa0*Q|)6}n@HB2~TLh*pkGaBKTK0FLP)p$Unnl-D)qHgbxLT7v>h>NOG zAD=4&8+YfG)4aC#yfz_(1sCbAK0vpT9>@i@H-!OqWEPpaJ{O3??ewr&1Ed|3dS5pX z^83og4@7O|6}Z(f-{TSwcT_&wr$(C?c|MZ+qP}n>Dac_d1G50qvNFitlE3+f3J1)RgHNt z&u2Ze#u)c~UAVu7K@c_W8`8YMYS$P|q{bRzf3oO@875RG1u}NX#EJdf|1IM|3(6o) zWeXiq%xpl_Hia2m(^>c_9O~QdafqU*;gS-8(PSMvLwZzQjt(acr5>ElS!?;nkSk)( z?0Rk`p5lh(Dy_ZD#tXNVX=?8T6z>`t@ART%3Lb{}}Gu=oz|}SMcUO zxW0u6jE_dk#_ipfr=JFcF`6+oNDZct*hjI07xT*ru|~vpqwjPOCPIs(7sJwV;Q&0h z07{)@J0#7Ze-gK{?(}QLxjy)Fh^#3ppOt8mX-aAv`~7GVj-$jouSGd5Dc?nAySCkO(iTirQW_8J}{n9mZF3l>pku0fFtROm$ptGOGQ&=6-sV^-;(3rVW!4lxOq zF!D-?SLMTe34|H=@i(=mEEIrAUiAj*GH899`4uLUnm<)1tDp=bznqJ8c_%a~QoS)A zYbbH~iso9)g}!#m0>|r@v#DcuF>bDRy7QV6+$tOoj3;-0-A&*MrWj=1C3Q>x2&{?vfC+hT)mvpXICTKnra!1e6F2f<>AVg95o#42Di5}{bRVbvrc;MMk!F%wKqR`2FrIk)RAN^h~8HXZ@ zn^jj~^N?0uAje9a#mt_{%Z8NTC0>V?QwoBAT1r@N9`skcFr=$D9qR+d%TXY$LM1Q7h9`L_nzKoU zQ<=f#DH>NOsC^B@OF^*08{B^+aTNP##NNsNaFb+y(*8nYuX|&Psz4jue?qB`wWtTw zft6%+vMdSIfyK}b!ICo?bk`hJ0Lll=Q=IPb;#mdb-W*)tBKAmkDC8S_`(jqj#CDgv z`SU2jQ_keiQgZ8o-GzHsuYz(t?JY!4<$#S`iA+EeRYP7ms7_G9K#clFG$^Gd|IW~G zE#KDI@Ri)5-b)zhGiWP`2bCiZLB^c|_{~r^dJp6cV6VopAl3kIE3k9R-eWKZiRO#+ ztmzaP*QGHQf%d_3@d~mfxoin^+xAPkb&ppDCmi#GxsIqX^A#KXiIzHXJH1ePRz2ne5wrSx! zVM+tnyTsQQ>|a}Y5G&ykx70M++UhOrZtdLPS5eZvre6IHw0hRW{fuqd#ViXEDtFA} zs>$q(JW ztr2;HY22n()f&P^a%C6w0kk%)EAn!v%GQUa1GTIUd0(T~9?^=`X4=d2WIv$i#p#F4 zN*Es{b*ZE{EWsNyZD!-tF*_y9ORr9mXymN&#sBWC2qJAtw-qiCJb;TV(A`P47GC7~ zlBkV?zb(J1ySema)FkG&Bq}ORp+SsR5*EGARdJ>HHL!)8N`_2I_1UW><6h4wlOoqH zT1H8dSW~TxIeHa~$k>QUdoRpH@8~gJ(lo0Zq{jFx(f4^WwGF8)p;wp@hc>Fcfma?f zGx(Aa#w^CHmjKCs0X+WdZKQNK>yTUue{O;#krmA2rp2*pvn^%>&9K`NyAk6XUHhNA zM@4^mOUi+mS{f)O#O|KN+&Rjyqe0n~pz~nU=P*3In(R*~d`q0*PN5^(#~{y=Jt9P` z#WLl{HxRSSfL4~R+TDyxsA?rCt#POErKxenVcb5iT`rJq3dN(CaNg8XIHr8Tj)Rq^ zB{2WCE({^o3|Wr?3B85FoE|-3A3;S31NuWj)P~fda2y2(iBa(=JO{uG#2v}t2+>p2Ejk$h{R3CLb}PEI#0Ky>MRi>k>AQ6o#r^FxOgKu;J8sYVe zo&=g!J*ad?lfL&x$kJTlr8Pv86v=9fk^hE2{%RhAyc_nvBwnH56lH#QLHR%rXs@w1 zl1W44!+(A}DZfo-ST^RR-%0AzkXo5GatD%m^GIEOWDGvVo`O-?SSN_=QwwWSEWo%j z$AG=_hhmd3Kf@%A0k3`E-r>2ZR(M_aH52&;+_T&_JI22|#kfZ>G>eKt3)v+~Tgsx0rPy>WoGB@6(xeFj; zdLOZ3@xz@Z!4cEgkkpf<@Wwkr3Yi@7swcoX?6$3kj!xhOiavLY`2S@KTY@Ud3#y)3 zi074LV-keQPQV>f3;n@EWfOxZrho}m6-`~`rm$ii%Bz@N^E|!zp_6CQw8sgD31u0L zAdjvThaiud7*oX49S5&FeS#mpVagP)`NK2(mwP7N2cGmBpVliBU1&)Iw0_|&AA-hE zOe&EA@JB#Y{5&;}ZvF2fuQY^d8?i&S*HifD2+qlTheZWz)T#n+^qr!UD~nr`7w%6# zO#h$#!GZ(z^7%fyP?yh9|C)zfZnF3|AyZkEDj=VX?um%tEiBWZ$rpr-Zl>BdA0A-8 zsc+52;#8#8_u@5Yn}C#{ZErs=30ocDwD2} zqGrWKY$YVM1rrGT2kmjWE2&Lu{AN$}oa7e_QRpaL5Oq2*NQ^{D$1)?|L*BBzz>{H+&dhCmJ5YBUgPwQ{djG?Su*3yFQkh7R}0q*xPnMH z<$(7|X8lmDx?V~W2YLAgi6gMk>iee&~PP(9V$@V1@olwz=5M>q+Q5zzL^vaZO zri+WgOM%uy{)m0`uqA(nd6TSV)s{cWVKq-0_-T0gl5E6^D_4nQe*+S zqk;{*w6iIsucw4?*|PFh?gvO~EhHSQ>#I$rS~7Ct+`-q1O%ofL{OLP5(kmXZy1&a# zLDSv(lDu&i^^^zB zO9NSzsEVY4#-c#ug|&S8s+I9R8z!Ba)itwV?Cq(rp>9AoYeTB-(qHretg7E5O*4jC z5dCb;cqxXjvV->;5YvBi-_Nz`8n;Ry)zVyt4Jpb^qO7=6rPCddu*J|Z;f_hW6iQ^n$^%D#ljwogl)g3yTL;iD7iLjov7<46m>V(!w+A*jp>l*tW>JOs62-+hh{EK$OIG0q zJO*$wWFdSK{MPYJ3eTp`i5GIwj!sugETdN<$%@gE{PRjfd_>4(*@#^cqqy1Nad4Pc7VQ0@VP%LeI)2wE-2sco1q2c*!g5CR68HK-PYs0~MNf7&2+} z;8{y;jOtwTCcPivE@n}8wUqn+(7SgIB{S`8oA&s=Aikt998vJcWTfD3cKpa^7TYe5 zp@@kvlD1QUxa$?}LUk_d7D~DO&|n{3jX zk6(co{0oSfG;ZV{vzW#MhTg1rok@cn z+v51(Pe(W&1kqC~$AwJOZ7X9t!5?}@zmCWEnv68>Y_qw;sWP9EX$gYEfWs6nbS%1! zPy)z=v46+R!jm0GD2Fiq{S2T8)~+hu-8S`hxVL#~miTRjNBASgHSqG43cq5!X=^ti zs2^oqF^rn*1=lxy?-jFQGv_5>>P%fa{F%wMLeGwgQq(g5X5*g&0})Hbo=e^go!+Cb z#?`bTZg^V6oG^_6W!ejqQF{pb92o*@SJXFPy&F%=5>UJxfba*e<_=4@Z^jMByqk~G z5T4uY%H_8g=t`{<_X=ScJigno<0t`^=vT@57^YzgSPf$S>mCsrw{OpCNK4-n+YFm~ z`@{7{%l4I3x7o%@zUzkw3zLm$L=DwSi4r(77NS!o%;p}-nu2m`49r-K5j&hpm^Xgw za5uCM{}v3V3egrF9diyG59-t_=A&uF-BCn>cp@BDhx1x|@=N7)8OxIiX{fHDDo#~k z4&CsNJBRHGrYF^}zPWy{7dRTW*hh`zmEzAYFqhvX`JEHmYmtMCFL(k!)1=zzxAQvH z4v8_M?)aEFVw|e5G`{fvXV#*VKcB+vI~$1bows2B5AuO(W_FIY-+3ZAt8at1(f@k} z`wiRF`1TllDKN;PK;R?+RqU3%r@^scM^}O;$}mMmwQ*(WwcVr;CWDM9Y|Mv>zo0Ve zKf(KoYl>g??$`hA7dUn<`OSsu08yk?rgD3qFPon|p6uod{(U|12HH4wF$YP*6H+1# zrJ4#vp|aDjXK^KG#+e{+qIQIB-aAQF`)dyfBAT?N)aZTus+!<&-NwhnXFPohmYo#1tFn_-`M> zsmj+=KE&%V=i;=lPF;lCc>~5<;f>z6TIHva>YQROofU&KGrz49DMJ`vXu~yC=8EU! z;S;&46r2|KLnmQxV=x|=v*CLpuM)E?e)*VWtn@?7 zj!Qd>8~L+GpwBf5uYKp)xVbHpUvjKVs`=Ukay5jzVkET=yO+{yZ0D5N z94(r=?D<==<(1d++t8a1i6A(7@JBJ+0+QbplG-Mm!=94ySk-x#IWukBX{S2D&JyUC z5A+5ARmBBP*$Y_n+N7QvUKj<+K1ICw%o{X6jWzV}m^hv$xo%^yq4lnYsjWOs^-)Hj zM58md#VnS3Ez^JwWjcQxL&n5NQugk_ev@qoPaKI&p+;pXn9>a2kl`q z3IVFP@luC40*iMb;^8xsb{Co>qWH3&rH3wjD^5SAc_17WhB0~iMo{4m&R=kL&zKsE zOG29v;sN;RYV1(>Hkzx3=WBtphPlrEN<9b%$tfhuOudpJqe$gUthw^XzzCp=gIQdL zsi%oE8P`QFNT{k6&K$C$?=*T!Nw`6o@Hv3mY(z};fm@4s{HGm0o@sf<3z0_0qPtX| z<5{fRZ@E-+26k-`_f|g#j)e0^7Om!v&PknL{MQ2;Q9A0E6|Q?yu$N`p@sri(=$2@e zuE;ZywV7W_u&RF7IYZqL>c$ugJ9ZDn>1PmkPY~zwHrb}* zKa2&Q+91=!%4so4YVlCMfhP{=U1AeCV%#D?wWPm@txj(SLwBmJS|`kmI8Ghb&hUGj zDnLqSNXnd*H&Rekw=U{xrVS_<=nv1VOX+=p^mrujNTwo17{ox1UED+i_RHf2uIiH& zZei1Ch(Lv_uLPhE1ZJK7c3+rP!rBR&yM;TAIx}Kv;UrlfoIvm<{Jh;YfNZ=XEgWF` zkyDg7lN`eh&LVm*``d8uXIautL{2#g7Jz9HYZ%~6Z{$w;h=_tS_K%_&#h$M;+AQ`9 zj!*>(w6Y}5ahz<)z^R=3$OAEqZ{-#n@0Dj#KxHZJe^V{hXYZIpzvor%_rvfX%`Im; zEBpT*SmN@`2t!Or4{1OU6qI=?SkC*O>JXW0BHU@Aj>1;^PH=pIlElCt`-4rFyzif0 zeIUP>*_j|vhi2S-q%|rykR<-zC)VoqR7h|zKpB0>YU~Km6~$K_d!qiWfUUWjW2WZ4 zB0t7;kNRg9vmX+&oZc?UC~tra1rDKwb2tND&GAFfD#VoZ=I zhGjX-zdq^upWf#={GI>jd*=Z%eya=$Ap=dObUh@j*`%9drv7w-(%{4|?5@8ig9l48 zUqPA!(6VmoAqlQi%7=391Tk0>PkIj3mf7RpN^GCb>!6-NJi^Z+oI0r)syuE5(;vrm z>Y>&a1SUSxt#Z(+reCZBPJt6CI4#QX-Sg`B|D_n1`flTVD+aP5w%hp(hJR}==h3U# zq`UO;892&0)cxTB-@Kz=Wu^t+q~~kZb)i>`#b9@RyDlc@#fhe8m#k@X8ElUPp60To3xq4F*3|(UYByK?kiaT z(l$nxOAq$#sCsv4t03M5o|h_J&pP2)L>K$J?Ko?$Kcow4&>Jx6xeOLF$$!!=rq}*U zD80VF5iwWIEK3HMMD(rktdMUue^%=t%~l06Aha#PEaG|m!cF-@cZpKHunW=Y;*rBf zz_W0>7Jq9g(wVMpveG1 z^{|r7k)Jd#j`r|7>x(*VLCB5Vr0t#FA9@Z~PC3F$6oIL$WN#y4ZVU8Ur?%Y2lWs5( zW&G@qdb+N@K7%+;M?!Z*`A)gj4sU8Fj)E&F!Co3YOic|9XVXRuI4{)x=0pkYaHdR_ zL@~KR#9H?+UU&|;o{;@aj?k(QeKChPP0t3{@Z%o2YCjh{toOisEceK@{G&YJV-NU$ z-W+N`FdTaQ)G&Po>F2m5%Rf-Y_B0lH*@f~7K6-Xs7yzR+cRTaWnde(1 z9dM(P84yQfFEtF`i?;)Nm$L4(t2f;Mt8p~4p5NGHec@zpN zFz~M!G=E$_7w&;~3(5^Eft4C31!YNKja&W?+&NWCKuVHS$;XJH>o84`#cTcpKUL5Z zP3?GRK+J+$!YTO}k4k9Yb0veWBK|wcka(DEy;Gy#8w^UPX?Z*(W6j9p>Rqd&ixP)R z)eg^-!4Bp&(JHC4-@ZB3YWSfeH%lHrLJ{qpIpUvp zm1pg!k-LEXF#5;hA+N$pRTK+D*Ar}W$%E@)n6Kd$wpuux^D7#C=@wcS5NZ(|$_Dg} z0r$}^d&by}tIYq*6!%H@e7b+bJIq%=b7+br_0=fMb5dajgbR#($!gwlA&lMSVvK{w z34Si59Q4yDXh2?3T4`-0XwYd`3C1Yic~MbKiqf9JPTl=QcZDn?LSOv{#ec6w%1OOU zBD)rb%SSwohbK!@UdWFS#teS7aCd9vGziP@>Xlp+rAg!HN_6YI=K}YOBi>j}x~FZ< zl7_8%QbXRd3Mn|@-d2o*w!c+@dqe32Qwr7Wo0s@5pH|NXh__e&s;pw^j~((dRjCt= zlN*}zlOe4M82-k==Mo`@c--Dvldd9GM&0`moG5aoeHGeP{QPt)Q88C?BqIHdVl&QM zQawB0NRj{7EgwCRp6_2&zF^6zpu@hF`A%8 zRE<#4k19b?5$V>YuRni9=tnXI#H8(Ftgv+&zNOkUp$5Yiy`Zk>|wEVhi5 z=kVnyBp~(B!vRQ6ge7Is<>UOE``~xleVVWL^>x}V2$DNyjTXVQ8Aae*&EWFKS+90G z@_=T7t* z4OHm7PTR3ObgrDaMKh$4hlQ*1K&BInN}E=FxqjQZ8Aj>`AFmg84Iem_d#vZZA+)M z24I%zv|$k=$;DUuF+F%`-GN=VoA;Rm=;xhSqd$zd^YQGkmlIimkr-Ud7>Po=Pqlu! zUah^O?kM>V+wd;V+ax5dk4Igc+~Np&-19T_`kb`zXLGr&#yR@4WuL$3!8i9C9Ts@` zC{H`ndr6p5bA3$fva*t`>P`Q|EV9?ACE9<|aMLpGWn{W4j9sH2*8;+`WS3uj7&2JDbhE(lX0x1=O=XCEM}lpEIW) z-nq)^x=iz@&}Z1>4;923VHu1$g8gJBV?rAyimz_1Ct)(tln@AaG6hq4cPPdxcO(u6 z)fCt4&y*nX#2SW)X6_r97c;DSCkvNK4|BwkKcgb)63^4-vvBwMMKRRF-(;v@i`F!uf5GYXB_-RF`K3jjo2i+ri&Kk>k z&N^hEh%6HNJh-%wq<=^*W=-@68l>j%0v=3#2Ri1GcY{^$2X&sXjF2nLIfrtEhfHyY zp;3XPu64_?vp|>{;)!{KEUk63$XZxSwhCuQZBJ}Rt!@+xyoP6{Qh4UVKXi?HFv>0B zBZu$0=&5PwQ+p#9sbUs)lVZkaaxd}@T8*K5K{3nz-ehL~5#tY0Y(=!v7SV}T7y5pb zbYuiXQTa@NyTz;#{d-Ww9`^zqjd^NVfsQc>BsJ)FmIs;qt$;Tyl0YI>GmQg*ap%h0 z5POk)tWd2oK$dbKxu@jeSm=y8ldujU2%p=it(vXrhnX-Hwz#RWxShrsgMNw_h!AMf z=9abzc1I#9vVpnt8{|>2GAF#Fw$Kv2q!-OyS+9X=M z-kf*aG4a*_aMCq4UngJiKOP2UPi$$cy5Lux{_Uh#mp^42CwsSwd_nD65ei74SjR3D z%}q$%JH2xfy_FNaO)PF{6n!vGIUzQ1LAaA}qxeiUhSr;(mutG?^-fo@`a-DA)+g!P z*HF{=u%WqbD(ayaMfFFA-WKTVThMqTSZG1@FJN0lxVBAm@F??3;@7R{mFYJOX=82^ z*8vpW$6blM^8)=vHWYJ49Lo7;Q;1v2y!jh?L97L@IN!~5*UO+xDxP+4RK!3u{{F=g zuzE2I3iw}jH!7T(2}O7ikSHn;5c>aQr}zKg74>StdZK*`*KPz@a?GG|gc8A$GNzik z6N!k@#F#=$Dd4f-5sUsZug97iHsiW;O4-(;*R}y{YF99-Tia;t#KGZzjL`U5wbm}T zJr%Vg*7)hvEVmU(&3E$hzRPTFV@*GsJ)=j%*?dz!@efZEHq4qz=V( zzc$4%KHnbeb6yYI6I|_2fA(De0RL&-);U_?i!!&~=B^H$P9HaSJB<7KE#ME2{rM+UJ+d{E=zKKiaNH z`vNWaUWF_0F%xHT>q77;GRTO1Q2(;t43qC-g5W=ko3C+;%W3_3qK?s)$oz@XuX^hP z&Isjc?7j5+n~`NX6B(+}PKSw93u=N(y057VJHBdJB(NPdB1pV<7VG*KcE&nJjB`oM z`|s^oojkh{{R)0mxJ|61ZbEpgQXF!v7DhA}=Q4CGwDp}~drwSKS6kc$Mr}ExeW>+D ztY$7osyi0$kaiDaHEeGB-;O>Zc3I5@afsQlw+!h(0#ZDdA#aW5TsBvrUbO@KpI#>x z)qxxhN67$13oLuusA{Gn51@WA=^&m!&AMZ ztG}S+e#K`Zk{Zl7LD6uJC#9r2 zc8Txir+@~Ujs9e?=6R8H)20UT0qg`@;A8+f8X4&dJrlF4YJh`Ejzt*3O}uNXjdHCd z<$CFBSCUWJEf~#^pTo&~ektwFre-`BcFc-JQ&6qO0rY05=AHh#(NYPgHUh4=p;dnE z?_p+|kecDK6MowW*({{TzeF0!cGG%JLb8*I*g-ITp^5z?ZK!+Y}~FRbr;7$*D0pvYctaJ<#A z&nFuzsIe3$agXK6fv+bmN>UFV9WKVysjDJYu%S_k6|z8QT4a|b5j1oC zn!1@4=}l3Q;!~)Wb{8(n~%pAnl z5|UG1!gQ&~50Nh9f%Mand8I;O#6N~ERoz+Klq;5=WvTX&y&<~^~MU>Y9} z*H)JnXX&8!@bBS_Xk1$3GP%q5jP)V+_kqrmU+-us!{^c1J+NV5mv-uvM5LU%$;v{x za2cLXdnFHgv&|G&%Db$hD1ci$g4tquE5)lTnj^eiiA1(f_@n2olrU<_J9^Gf_tXU% zy>NSTa(dkUq1T#K_hvZZJ6;i<#e?M`S;~mBlmqX73Yd*%%OIs?dd=-~+&Wv(8Tyq{ z$Pqe>JcaS)NYp@8uR!10Q(jh4!8muaQe`I-(fs58=P)kgp2L_jYN%LjF~A#gdNfqp{-@E7umh79Gg1gwIA|5Disl)69v*M9=S= z2T4=sbCUHVu#1-@QIYnkqL+0fDwL&aStJ`)BSt&c7{dwbsNtE^R+B2JlKnjYExm79 zWR~sAO*2g=`S_NhJXvXde~LTzI<*us29<{?%vvJ*H-E=7z}UsFk(>%3p%pgW;3z+1 z36kR3seHX+Olw71rlk2LCsmhf#8u-$N?hXhAgP2LIn7hgltqk0^??hWaf%9CMG0yn zsb1%-1I%rQVlTDPafSU_7aw43!64wAvB3=>KP$V^19b_v?F%*-4i zdEWDL0a;cspS)}$`N8otI&-KjOvcE&_{6o(#%QC=R=Xyw(ss7!PIB)b!+c2e#>DTJ zWE*TH?CVoh|C-J#|NcAchrm1Eiit5nK)P4;!g3ZMo4-F6@iFaqnNH z=oTVI_*}48_W}B#T)3x52n244X&l_z;w}i>#vu$U0St2tw>048*juQlv@% z5m0`zDNzx=mTgf){+3abE))9Cg(`cTPqb}j$NKqx#IimRTt3N-3qzmvp9<$c#J!2e z_a30OUeT&ynGi6GQE(_qP#2L$=y*2Q_gql!=|g&tV%?!PY2%tFk#Q|DNIB++CGqGg z@Yu71=_%!urKHR6W*=9j0;cIDe^DY4jzn0F#B0W>P&>W}cdWkp z{yd8K;XDk&Z4{`>B!qw!XhS%5nVz%$lN<;ms+}doy4~c=<8|WeJ!hG_6qc+$=!d4K zwK@AZU5MFOEL^$CyviBb2&p77{|U*YV8~l-xrdyZaZC%^Vh)-6q{o}kd$wk_YbL%A zmZe}6EfmVP6VtiWw4@TlPUuuTj{&<$$D+vE+dCn)E*)U9?+J^F21ZF36z5F|lWku< zHhtvl{%-?BxMnTh-F?hNX+$CQE|DpY(w24|%%x`y`S!I2FC=rXfHhJkAkeRa5EarX z7mwDN&k709?RO~|(K0A`CM;hFn}Mp=rp#j?{Mesd`Ohi(9U%6*hPtA@5XnzwqdCw} zzN`W>iQiqD9~XBfSIg!#p!Pg~z)dyw8!fq?6~{LG)NM$(TV42}hFztS6RXyqMw(+O z3dL_8-vVTmoIS3uBp5kr_4%ddC_ApNIQSA9%0=He7kpTZBwp%_30@VZp7Up@v|57# zvNhY>c}ZxZEZ42=()GP3wSZ~FY6^2pH3y4v7^|vVJ^YvfrD^jfg&QA>qUibXMbz=8 zO{H4yx<4<`KkTxLbT(gm`g`@ZQbh3{`|m3}oAk2%N&qqi#r=tfQymtT(;Bx#*aHjl z%iKusC^|AgW=aWgCd&Gt{d9RU+4vz>vC;al2K)7ynKYFb#Uk!eg!im;y(=p@;#cb4 z4(`p+DVpLXVlDr24EAR1B}C>z!rYuDkq7+IR+9Ovu)xwOw3JL`AH^A>*DSN#vB>;k zIue%^`Ynnh*jvTD0Rk8dpl~A6LS{S9k)_|wHHO$uE1Ze7RYoj}k~sl@u1Q+O?-a_d z>?C3Y_(*)1iS(F!;9K1Vq_*%$PzdI8e(82Co&#uQ?|)-Aju@{Tx47db@o*mNt(Ob( zn@$->FB5;ia8p=V_!+LH2BgdR$IJN()KB`tz}xLPW3SYNA2pF~He`geU0VANv0d6c zPM`N?I(@ssa*81v506F>>bjagLT>FPsS_-2cDydJlUN$H2@2~CVS1zHM{2+>*?EZc zE5!Uoi*J=5EmR^NrD>WFwArCoGh4K5I}MrZM%^f+)z)@{bFdD0jn!FY{@6@b7?k1Lxda@Z=yBWj7*aTPOwmKS8^0g;X=8I`>i3VF$fgmuZ5kK;69t zca#{UV>G$Vs0Kkuv#bSIz+zCk1_-}!<6g#HEi?;ETC_R6KDIDDf>(8day8tqL6#KgsvQ(@#n zX{H2))!Gy=$0c;ZQS~j)anviXfbLj3S!a+;3y6lrQ4WA&=CRu!A4+2`e_DKuBFU~E zwsw0@^L>1-uM!G&pMJ~`WsGRSQ6!Fag0;@EB19!(F*Lw6$2P<^nQAgL#Ww01V_Re) z5yy0p7^+wEvHSJ5{aPj;-%ic#GI9X4lj}TK$J@x3U*Xo(YN_bC@DHb{S!^~eZ+8(F z+Vv6<^%d%r+r7nrl9Xftek}Y_EgQTpw!$qNOMxE-w8F=Ddq>CDY1L%guyFG7()N`* zEFQ=!sON^);CiJLT?Ey3c9TzWHkMskrm8i%P2b7+xUApWC;&#tt_n>(zx2i*#C7HO zamrQB1{cuEnNx=7PQV@?Y}bL8@|{O&INtOQIO+f=y>a<{o`~6Ix`r)7Ez{0P=a^%T zX%txuR?x9Z1?T4fTK5G!ljXGkw%f$O70_)r9h=inyuvAo@UP2y8qLo70%rnFQJMS= zFK<|rb>^trO0?khJ)GH^_4$^Lym;brll7=ImVEb%Kwf_@ijUW3vtj7MchN#Cco;x8 zk;9fTxdj(-BNgcblbs`4e*rm&Uob}he4G*O3SOdoh1sypDD@KthwwYe{lk!#Ns?5wNU+f+D(;1GJ1LKIMIQ2{k;eg2=R(40 zpTfneT=3;TfULsvAPgy#R-J6zz`Oq?r&wMdDy~ELR;iKyXD7!0-`D>C^b}jQpgr*{ zy!-?3d4blh8_D!K0!gS=W?p_|=_cIqZeupC$kOfyiCiXDbaC`o(c4K0Do|)rRHca! zD2b?26GrX!mgwY3d8P10l0X%2kT4ag>4l##ElS>^pgp@YmL4toI@*H5^Q`|q9td}z zzjiYB78$knzhXbf* z4JPOM(@$W9eUS}NZ~{v9r|<93Ute_Ap8@az8p5~#h+zAPZTqz*PIQ32I%8jrApg)t ze!f_&AMCwhTJ@2__R*sL(+*A8_W^(L|KVS_ziT?*FZ&>Bf&+Hqk1jUBl#^3iG$|o!`D05O4OK4~Z!#SALpu`*5+e$< zDHQW9ma(UtyHNBP1c?pwC4sPmHcd7oTxFkrW)7>9;ZIc<;fEu!Ex8HfusFTO7bz|d zBZ&kALRiVT5lcD(cpc@P( z#=|F(*znT_VxEDuN;Wp<+)Lg4hvjp1J3slPR+SGskn>w=8yz#!8aj&`c5$L`o=kJ` zw1MRwdijT;$Q6b|KO0TVm-gFp9;+R)V>ebhI(KC>BgSXaE!oU*Ip!s}^_vT+Vwdh6 zP04U#ris&+q6rS>EW6W~q}(%;xhs*@%*5$^+J{cM{MI8o{RPW5B!Z|@-L;gPEu23Z zunCc-V9@m%iD@Je%lSQ9e)BlCIYp+>TPw@s%ran6G}WG-NgSP|kFiApVR;w5_S> z2W6y+hE6+c@U^BfD9U|Q?bw)k( zltn$>$-5<8>NdGfM7Cx3qRM&7WvnEMkY~A#liAP4b*fa|kG+GX1&ADmZo=j|k`P3o zxQMnh8dhN!>$yW>d4?RONk08%{OKW6dW57OTYYjxMaVjah=|+j@&)M5!f3UzoT(wQ zL}h!;;$+}=wxdyDRFa;p==L$1dQiY{eGsZ3cWNQ4+L1p{fT8UTrbFS+FCw#yewfB>txh=0I2;{ai+4G@E7t zP(`x{=%iTzfD&MvFYs#AeLroD9Uh3%~p znFI@kWpVVsUg){0ISTMMjXg@z%v*)92j=}yVIY%e z#0VlX2KM13M!F*Ut+>((q1(+-gkeNW`&D=$Z3PFLYiUu|rmD)O8?QO}RS;?+JHoS* zna)V>9P&E5!vyNt!r-HKVcB@h;0H|Aljmiqs&PhnB5(hFqmTtz^TCND%QI`}MoR8P z8N~XN>+ca5@or8xt~WZgWAD%0HRLc4^TcsRhf;yh3O@O(Wq_F5`3wD4ZG%ATa5O>1 z+pwO(=XCZUmoePJx#B|ln5sdxaSN>LsPQ-jC7QsEV65Z-J?9h7!ag&r@%#J5np_thV}JYC!8(VD?XTS?`RH;Dqv7JYl7_eYiw1Cc%>=y5;T+@vn8!j)7(bBl zonV+M+4cHF;F4D}&#_O~l9;YjP4jhaN8Am_^EN^%eFjbHF72z`9@A;W3p*UVmv22{ ztyQl0TsG`PZy=x7-DH1=$#)?JXTD^QI^u+BlW2E9|CcBjg^x`VEt(@3>72vj1@oW6 zw0!a0POd3`BZP=)E~OI?u9F|0y!X(^l?b(3fGiJ!u%bU77RYeNd@;p|-WcA|4(La# zH$TvBm&o~^QS!aH=jpNPoKC7!u>NZ?kF|CxIOO*7RZ!kfdpM?@a(9fq#6Fy#rjVwU zN8FI@igeK|ITDN(#Rb>0C=RfjgFLIZwJQs?r%Vy6sq9fZ0;C-Qk&P9(`5=={-o-<@ z6`W@Sj{~E8N{}ybJVS?JA+>?H$Lhq*P7vfpTB&ozwu{{U^-2GbZj+kp?ghORPXs1C z>V9^P7+Dm>N5Cgl3oYw;6D)OnBe3Gh`B9|njHoOC0$S;US*bz;*nHQdDK|;VDtX;G zX=BUIJKaDcLbsI|rysa4ctl4q!TC7%;bF< zoQ}^M)O0rAkZj?^y_Me3zoA?)obnOn_$jjghq8AJ&Mj!ScGpT)oUGWkZQJG(+qP}n z&Wdf@wr$%^PIjGL=iA@@^`5HP|7UmIv%2T#dyFgX*k*onIuA7GX;&+2pgkSKowfBt zJ-$A&Bc!v1kVBc#CDG-U%v#lx)?_<{>Bz>N#!)Aod+W4S4n8MiKHGTPq4?TM>ng$i zpW1?qUg^39Lj(-dESLHGPsUVLdfypHiXFpYiM3@KKW8>SX*S^%s#y^2>w#C!G@69m zjbcMNvImm+t8I3(&NRuJn0SFYf;l+z0#{7ApV~rU$WzKGRP~F1hq%HPu`93l=B}#1 z9~aw3)U|EdTkoFQlr;C$3}%@eRy5% z%a$B7C+_ZSE+`qzU=;VV1{O|Rl#bR@)^Q+i>%b;v!i#QG<`kBo8ml}qkVY0JwU>NG zd?iVX;y(KhEy2nKoC?sHNYu@{l@3 z1zrBqFBHzZ7N)(+r6GVc-LGu?SIo$_ZGFub7Qqu2)5Gn;b9>CMxwe?QDbjC)0v9kZ8OKg2 z;p%+#eo^L)oOaR9U>(gF4pyTJMB&|(gY;A$pXrl4W(lv4Dp$I&djo#)E)VQXciY$E)<^+ znce)Qfh$+QKc^fojw5%7_YHAjR>j$69f4V}c&^WI<5`uZzRZunVYhl-(rfW&%I=vX z`#Vh%8bpB@`ba~(g?NxW;JY%P-FvmP$Ii z#YW8*O)D>|6d%XZai6cp+xbISyjVNscb6;8#!iE>GbuPvwGY5c;4L{}+xf{QmK}vm^#j#Rly-5L72m z%2215CaEsMeitXrIXkt0v?hgGQ_mTRLW9wP*%57+HGY^Rtx=)1Xp)OwQ_s0O!gtyE z9Z8{KYM9B*(FIdBc0FR5sLa2l|4wTSDd3}tEh&>ihx%)k*ckMQFR~9p7x@cS35+dF z$x9oqFNDSN*J)WJMT%Mf5sVqDjY83G<=Wa|D4|EM7dZ~y5qXM8iBh)dH zorV!qrVm~qD4F57;g2)Ut`6w#ag3hWU9{gWYS9R{ROg@`GdDo@tBi^!WU`wOps$Jg zC1YwkLa02b*zMWU^;b(|_K!1AtUmu95)*{YP8(*`EWcfz+?)!|LXbklZ1%>JEe{X& zVn50;o3DPpVxL>BjnJ^@pUPBWNJq}sxw9AOu`f?`ZCVt*7B-eTLLJ!D9Gw! zxGC7G8AMRy>H7-A(cAdVZo{7~(!^GfW1ZjOqt=#~XKJmFeS>A&|xS4iCg~w+2 z`k$nnG3ooI5*2+}UTx{S(t{KeRt$+q|7wZ{iGiJUeKh{6NA?2*j1yFqUI}96dc5%y z18eRSibHt%l=!ejF<#C|N{z=r#ys0;rjp30*>!T!h(cu;NzC6LI@h*1^Zb14Gjr|3 z*XTfvbf!n;E*mTmJTvr14x!_(EZWvsOB;>0Uj*0dUfeH9{nt%<0mG%+HpBoq`~4{_Pq)x+J9mzM(SjK+RGFaicm`Wj>nXiwM> zm2lUYWl3=P!T2OM`8)zh^UP4!Zd*tKwbZ!QW^fv29b~H(o z)1DB==QHGb~qG862k+cb@Do~h>J$?fS^ zFSmQ_hHAT<%K}f{ZK4zT<9Z6E?xG>UaW#~yNp5G}($yZ?0U}Yhgya5iiFUZy#g!oCq za+;4v*Vys=`3cgp8Wt5I5LnGu%dt9#x zk<}?)a=wAE2lB-FKfa?tu64*IvnFyXs{Uje2Z73V#G9(UoLS`P{YB$|kF9CDC`j62yp#y`vCEH-AOb z1TqGc>9f&pV3<(zLbs)zJ3}g9-Wln31%}7wOp~ARq-O)-QSYUekJA43Sh%}Dbr@6~ zN=G01)@iCHrrcA3Z2aTW6k692dRLIY6y0FKv{z6b$D+;==&9(EFbfj1sD3laFH_do zS1vkYt{XmAjW%mq%YYI(4IovWY8Ip!tcl5NWS(Z2D@GilgrEQR>jeJ`2Uy(&u3Pa7BJg`Xq`UM|0Hze@5S$`^!#;^=Q9$@Pkv zWRI841Ju=8JD1_9IRtnl`yQR}xj8mkj8(dPP128<+#Q{wI5TB+YhU*AcK zADEo4xNaYFJD{_mm_r8*0_I@B;_z#2g1&zpQr>T=9@68YRKVK@=OZ{o&W2z zqaVIXpow~92eC(@0qck8&`p^lmnGjz^i|XlZ79C5L5B zsAXtrt|?tQTBUHYJe1*|FR0ui@Sq5IY_3bNkf|0VICc$4JpO^nn|zA;{%;>1XIb?% z46#TZAvi=q zvM(S3vE{j5b*k&|%_Mqh-Z>(x`G|&2Q_@K(8u!I5TYeLTc{3A+4X5;oFTbx8nH+}( z?QxMRBCzg2?=h!LQc%y#i7atU?m8!8@}z*{UY;z~6IJRSKW<7fNrpx$J$ zPX!ifh3Kd5$p*49Iq-1DAZw!7|K&2yp_fEP7kv~3Z&2GpH)%3WB4X7>6w7R|9LI&P zVc0(&u9jxI@|+@h+W%4X)c9)@!4z*;%jEN8+AX=+c zlt#(*@)rBGCTqY!AUmn#f_GspE$Km>aMr@%xRFmwcOGA?e4K*zj&1oFNaVlMEC)|f zy!2C(!A99PWA9ymwZC(mdPnzl6H*^7Mi(&8v})OhoXU`li+1;cQGDt z?T3r;G}gkqqeIxb0X`z3c~*1j&C)q(1UEMpw>K)bEVxLrZ;EYppS!mSs~s*(Lx6S9 zD7Vv`rKnooc~$#HW(srVQdnHx#+@6pFcH$(>(=nrbv@Zpj00XGky4h^BYvPpro|91PNq zfuccwqOEs%21y7OqgNYkPy{PIjTBO@NTvd-9RXYN@kj9${A{ZTea4d)s|T@4+XQ1s z$DKPf`Nh*G6^s zTwQd1X4P8fbHaQ5io1;bMvm(?9lgHm1eWiL$;T$Gfuh~RGpX*EvmeRT`L5PytwYPT zX2vKu48_6TQntR#zVm7u^Y&jxOK+QhBa}{1?k)2GAduTNBg?jJR z`yw}U1g6kJc7;Cn{7Ntywr=<+YgAThFV2MhCnO<#zo`!4dGkradFp{H3M*5{r?PFH zOBLvre7>Ti$e(4_F zm}{fJf&<%f7w{8RMa38`#Y&_En#D&f#Ke?kyaYm+@wcSUbQ#>gT7D-0t=;?3QzOo6 z>AN@MlPLE;(wELDC)ttrICA$aXcSw50pc93AHF=8!QBP_bmIGKZ*u-{3d=wL(N^rN zorqjn&3wymp4@ez=4R&`8F^gui7rDvO;9WVb5mv(zyIK~+n3-u9ckZFC!d!8++eD# z+gQ|G!_;0IbxNCgc&MdJSLx*xtTYVqAnNBp*0?CQ3voIB*eT0`FI||vJlb9pFch?y zFUkB&H58unS7-Wkb@354PtvE&8-&W#=ZaGe0YJ-G5Sj*tNrKs1Xsy7^dm_K|nI+7M zL^-6bj#q?mW`}~C%7v{2)z>jC4Idi{H5Q!NDx(LQ8)lq3utPV0{qN}H-#I!whCeyXY6i;2H8N zeGg0IiP~cQY(CO#r8EA07iD}J+l(WoN zif7vfkOGqDE$eC3`i->G{bJ!>?N3D`Fy+eF_#@(ovEt?cr?MSsSuO%X>Vjj*5w zc5i{A(GYR$Xj|-MrOj56?8t7}yoNs1kw&yyDs2V1ZzIki??QJ~#liRMXJlTlA2Z%c zreCoESgzkf$564A(>g}#aRH_e#}CsWb(C5c#mj?K#Pf9m9w^k1+t zt5DrJIWkSM)mW^ynBd*E$9ZrrA3vD0N-!@jXk?%xY$T$j6@ZG}Tfz@B!&9UTCOxfr z=OetDhaY?X)=O_KBd4PxUtLN(bl*#H#b>`1FAul#i|AIVk)wk&jV+CVf4uI6jH-NI z@dU4}I8M1^5_Sw;pMHK&{oRB6s|(j**8*vql*ld3PQIQl8+EPVnsps*_ek9LK?Mf- z&|m5|!wCMV`$7)%);ps3$47x?OKu4ND_a!z8*?QfVr5u_dn$`wsDNH(NBU8j$2UQY z(Kj&|oG_Ot%LAWSoJ@~coDSZw0INve{|thHq$GZ4{Ezf%56>7u&K$qMG?$>*v_xR> z+Apladr3pK;LetcUuQ7wBt3s*)l1Z(fus3<6wl&PZGxQQa&+f&Rk2QSK7K{B4Yhr#SYIv9a4*r#e+LWI}XI{ z)%m5FJ6QAn?lX;3BszkcKs;KF)gDc;Uu6P_QPqMvdHEv#Y!j&xr=>6gCf zQ1QI@TRXBsuAGN^V#uBU9em(lMezb%{o(J0DOOePDbekb_(j1ezvow{aZ{ZBqBsdz zxBzdyBxq47x{syR|HvW)v>sN`NZdEx029}PLZBvYB}yRsUxD(kZG>?mfCj?8A~DUl z93YcXNj7A#(#pY z{J)mU%F5|~kOGAxIR7i}MaZOon9r}}H#V^bzBvXF1*A+3go>^0(uf@!76{mmTxbD4 z;pB-~!Sid^)~}~jOevVi-pHDkKo*pzlMZ4kS*J~s(auekJET6Ef%yJ@bEnRF2?r?o zRkJ$z!TJcQGJJ7+e$21&K;`OsRO;7EK$BSDqb&>q?pF&wTwN961Ab)tB;EhS`TbW# z6e|lIF8xeV@TVgF|4viE$k@o<$lBolD+*~@Abw~s5IeXa+rvQ8=&3N*n*)ACMBWjg zt&qmP3WMQ^srZ*=t{dPF`60ExJ-q`Y)mCfE%TMpe7Qj0Q+(C*#j6pPcjE~AAuS*;x z4-DRiJ@NO#dP~7{BNSmzSvgi_sL|EJo=mguqU zqTh0)TdN<8FeR2>Z0#N`_>o?!=HI;v9UW%$hht!~Dg#?GiEaDH$bp8sg3ItLi76rLcOoMDZr1uY;;7ce8h_rrVlBh$Jzjh(4WNX$efk^Wj8Kvi+N+Z zqxCI{yIs`VoWjH#^}xXcl*`CpZT$g!Gv@-M_n1Y?NLSvMJ&tZQ$FEgp?Rhs^SE)@l zOG1r{bjxGyHeMNJtF1-^wH^J<{=|4WVtRl$ZCg8;&jObcAPzAMXytIt$o(K}X5d znHbb2PQkM@_YyC);tjwls-ymG19JPP)|k-WHot0}q|n;LS`}1@J#E+zmU>xiMCh=E zvHPJ%t>CZR8d*1vL;MTotD480`w#WTNvl>R%ZSwgyNCcZr>eWQHd)iYNu5Apk!np@ zYGE{^>O&3v@nnJbslS#YRe2h9r|QwDE7tjy62s7f)6w94DcW!(x%s5O5$idQ5K@N) zfIh_-CN^K(V@CnZve=qrjH#?d#gwyfyTLVBR}pI}$0U)5ZSFkwlSoiOu z-LV_(o*?@VgJ!X^nO}xbCoANPRz+_MIN(BKz1gJiQ z4Knn+PLV554>Grg@?o@Pi)Uf81M5^mYiOc5|sDy)@c* zjSBG%BN3_xAYwpU7RA;V1C9Gc+8D*9=S*gp{?gwbCJnG+SI>{Yn=UC1)9lYw-`s$k zhY(U6Fx$e5THSn3)F{`50Rt4Q6K8{r!P|ZFo4~LHl#LS>yRm?G+qD7Mg+18KmpFCa z{UJ5x?}D&ms)~X6@J&V(mOVA7=Y94?uIh9j$9<4Tuh}Tv@@w8#@-HLAYSq-hoHu;e z#er^BvOx=PH1oq_l%jtc5FY(eI;k&(U`8)VIRdRO$w81Emm!F>?{NVDO?$W!v@ zLzx(tXei0scI?|iXzVYQm~`1iV6Y|GyG_(}m*$$)M7OCo(8y>~9ugQ$b|p|j4=cP7 zqC%>SB%-j`mzKzFw^md3dR%lW|D!-XqJa}bteB@NF>X%>lnJ!_PmC&Fogh_>g`tVD zx|m>m6%lDcNZ;AY!y-Sc1ZozD$witAD_k2_LTG`F@l>i&$TX$3|IVfkb3KDH#j2!S zR)}pJ@nvbHG}(2&O^#r}+#%d@qx2 zz9D3X(Fm1$T33q(LqO1qnL)WMXmQ8!748J#;-sp$MMcgwc1Qwz9OF;B8O;z5Dg zM4+@qAOtqC8!aG=3@vLWHYbgjBJKNa&*WMZX}Ih9HKlL+NPUb|Hy ztp_j&#g!%zxHk8!Ue`m6>moTr31tLtlfTPUrsA4}QlpyOa)@ZYuh84D%c3H@ziXwf zMxt!3HZ^jMD^+XdoNyO}3Y#m~qh4dl=IrGBkGZc4P^E$eh~#_pE>bOh(%es0)-F{+ z(WE}KZcRS@xwuDecLk|m2_NoIAheJwRSwQRPi`FhdoI;FMR{%%pmEl5Ctj)_$V#?c z?q6tVHwwL2eS83&44#38#5xjHWNGLq0c^tRc1W_gL8YwynrMjww*agVxwtM0k#%;Y z%EccO6lgS>$dkP|Te682P9w?GOxgaRZRa1KC)74}K}=74kwoGo{&atE=c;sC!p??C zlhgle^IxBRFjG0OjlE<9gQ8&%NmdxfVMpMfU@@hmn`8|BUjqVRo2PP?%0m=rfog=d zCdvgdqLD?SjOtQhl#-1QLs}guC`#S~3h|4_q8s#1H1kfjMgl@u;Qhd+?qhSOgp{$W zZ_u4Q?z;B6i}MCsU7TU!>PvJD_UUOx(Zb>-QEPP8Ca|5zD1WK*-XV=dOTya2$LqlL zrB1APII1~0Ers}^pXw=OUhWv~iw|1tasC8g5L-z$L0w+! zSAa+co^E5T8Q+f%U{v$Oo1eOI-Ts;vH57T=+fQ@P)S6O^X|Z=6K`@ug5S|iqGh7@F zUv*Zr7i6n~RKIVKh#gW*Dk#}NK_Rz+}wD7_Nn3npC*0k@1#t& zgw&XzIG?zR6NI{2a_>&&)IfVVSFC>~g&Ak7=ugTO0y3@m^{PTMs zWoxKQ`Ee%S|HaloF!b7zZ4)s1vrxUf)DxzD zpijNVj*(R~^!MVAqpUdmg+8w++l^gM3WCO9SN7gQ@a?AUhAYxk`W+;^kIZX79hR1@ zvSTk7gV zsXZE>Hr4^f57NgQ74nP{NapkQeWBQ43EkBM#6CKIhVS!&@n-VryVLjc%=#J$5~aUJI?9mbMf}Cy%t2K^2bdMkn8( zZ_#cy$~U>2;^f?yR27YkE7*b)xJ$9-$w0tukctM?F^^=26QX?OaAU&sx~DQ+chcK) ztv0ttV3L7^(i294T(}~!RN35aK7C)6@}!UB+sW&!@OW8K(5EM7wnpKxAy}oRMAp`B zcT0fX4~8$dWaWnxuH#y@FQ7&WZO}QZyAgRd;lkV-Xhy1$wudLy8j6Mk6qBh zgFVptklPwYeRPUXGgHKOO?y2H!e;^tC{BGyUBI4C!^4afLQVirzNaL0+Tr z_Ekr@+*I|4ZL>*zEa}}YK+b8CmT*huzF}P+?1oX?xOx75O+3t9v<``x(|kdo28?_n znk|!sla*q-%p3(R?0!LSLS`0Obq521IB_rk@-S0X;RZ<#zWPKy#cF$Yl5jZN$~wr* z_jn&!4Wig5|6uOo2d`$4nU~xqh^%64J1wWZnF~V)JE=vhPD31WGP}IZrvL4t7z`Mw zxnV6BpP$i;k1Nbw9kJ!)vo?G40N;F$Kf{TneSAwE1EKOC7RrafIO9R2l-{tp>%=2r zsTa?ef(@pV|x&?@+iI(fR!@5z)GGju;|$%8SG(z^Mb4C?j^`!r+t zg~6mF)29&AY2ZM1N###gJ@C|i%a(87zqee*9WIaE&O5~xJ>HM{A0%uPMHs6&sng5)cPx;wt#WtEFcW4H16^-W^5ewL)@9PUtvjP7Pl&tn0&l)^+ zu=U`q{uLbWRU(0BfUj*#WUn6jcpxlY-tewC&;VnfP?ET7_5-arxpp!+M`6

sH0FIe87{kWj`sM!pY;BzHnelx-+8DwEyXqy@YhdKtEa=WO z5K70HBEbf%1z&H3dy_##mTw2J_P>?inUS?G~fx>MKt1H!7GAE7K%73B_KAAJk&*L4iaC znyvI?KYapgw*HEb%9xqtb`@Y-?B<9+B5r<3HCLZMNpE)Lulp=s+Bm-BwtaS*7w-Je zz_tHU*eE8O-!Xm^wtV3KOrrjOl?Dk#S(*POu_aj9An?Nte{10Uy`fO0tb8n8oA+PK zj(>?Ym4XgJI_w2ElYTR2XR)=tito%Hs@})cDG#F`dH{aOi>yZq=%UAO%}U$8n3^zl zzB;bHCjEueJE@C}Ixl8GD7{1+kV36VZ@U<3A!4cCVyzdkTRZQ4EV`B3I~+adB^nik z^&}E~u*+w5`oe&qe%FKUfDL)+_yf3{dau=0s$aeRm{X9VztdQf<4NXywVX1f0 z2|qfp0-XH420c(KrHlas(|iv$nL2bH=!8eku`8-1SwmFb=6e!uU*uZg+0=CziD7r6 zn1fO}8TS?x0$!}&nL$n`h~(qE(ZZ_{_YYhCFwjJ4#A z;6%AWWo<~y!E!iM3kh9f0mNnLzn*7VR85_B?%Aw|&DX`ucW7s6iM;XOu!8Um2S<sbGkTT}os)i>Q#^0$J6}jn6eGc$D?ZEo}i_(4XQ#972|zG!NEkC-c?F%zg% zUMxBnlzX>Jq60cI^x~P5^SPR2$jg{ZUuj-Ic8q~a#Q#7isdc81l{5-6lHN(#Z1Tm=}h*e+yA$+_0rka zj{g(x*Jb$Y7t4RF=>DI2`)>}g2AHRk@Yna`1j&1y8lELMTmi)IzW5o7lTH#mS>IIha#4EQ}rakm`H1h$QU^MGM#74#rV)ZzMpv=J( zk)X`cHhFO7S;G8*qo5S)0kq)M!)%0oGQP?EHJ0GiTm zDQrC0{eG-~T_o26CG7nyMW`4Kjo^psc#OK>(rvoT2E;w^p9;nUcfkdR>Mb}-)gAa` zUu!ES!-ZIw;7B{#shA-kyFe~~T!W>xk5kfaOwrGZfS*d0E z54I*bEXtVu;alz2s1yMDzi!!wI8wAbA;D}z@E*=pQrWos*V-)HVvIYc94@Wo`+aD^ z?9&93(bEsrlOz+&J59mt<9vsM*WivGxRjRxcQk@qmkG}|d{Ez879;PTKfx6Bl3UYn>roo8Xm>F@uXwWW3HU>= z0}d-LTsU`QAzlw97Y9i(H61w22RSgWsra}ii@v7WJ8H6T13HI+KqY&M!Jg@ya?vJ` zf$`II-d}^U)980@I3XzAx+ADCxAC%FL>8TQMuJ`XBY@WDbbOuXFrTq*j5`>?uF=V$ zj}q1c4W3t4Gz=WHxEclUQ!Bx){=T<%*)PGbdD3svyRnAa-&ZN`K5k2B!x=R8?& zBHp9P?-+buJ%f-=@TCt~m{;^?-s4ssv^&WwPb0BW+U}=On;Q5#jaQxOk15J;(h*)> z6ev<-5y4SIHgd5_D|ONm$JyYSqL~^~vL^49x*riwgEyZXp%RLe1o8|iY*FMV-zj3R zF;9sI2a#wAoPx&S7X*&jR5+2KMgA@azgtwE5CfH|*Xuu5{m&X#1Gc6->-`#4GBsS; zfnZ_%QdrMH#KQ?k(T1p8&F=DgchLJ+$1B6jM`vUOr>m*@V~m_BKT8yV<)gr{zI!k>P??uU@R1~vKWvi)Nw1uU%iI9HY zz*450v@ENe*139i7GT+&A6xn(%+@sjaMGU7W_oD_YN-encJ(LUMLDl=m#ji-o{kYI zDqW7EK0#M@2??!nYEJixa^5vz*Q>`urs;M~(xKO)(l7-F0>p{cj#C?lo(|c%CpY(&U9R@| z$Hr>cLhWndjmYI3Rn-E>d`h61$+ve*O*1TOlz*9ww$S*E*;QS0QNu=RUCJlyY+uo8 zY#2E}(37zZR~=EY9xkG2fUQe-t!^SXfirCB+oU}`I)dsja~>HOH+siHvUeW$=sgSFI#+k02iS6P}%``PIdFnF)M8^eoar$cfmIPi6E6D04`Rd~4X~34;I9C^*BD_nz zE2QR*Z4(j95HwBSod4nLR{H zJuZ(J)FV%hVyX)|?7D3PWwHvGj+2*CQ(ZB3Z>Fym!IGr7reQ<-S8yOe+~YaU<*?&@ zNs}>V18IysqH%fEUQ-!2EZ8AApK*QRY4Ya{jgqy`mB*^)0m2H8?8~Q5h7O3VoRam+ zr&AIaFGMt;MR>2&I*hKwseG!8uu>pSm~*ud<`03YC-(_O zTEhH|;vQ?^{7qGJkc>4l+Cy}Sc%&1B>+?8CY8!8ZgW+O_)BlyoJ1!XuWtE3y#z+9{ z&r(VzF>o*HpNwvcgerM-#MSMgE$pLB!yRDQYbvQt6{AQNMW{FL)98+cTAI!hd5Cex z#%rvYcMZ!pCIBdK&UDWX?%j7KT-9UGl+_GBT}>(NfehHu#-s?hLQZ5T>eJ}j$DKnT zdtSGV0OXe>Su}6uk`FX_Y`DtjM-g9quN_pL#o20(QiARmIZqaM4V>X+O4mqFUD-6dW41s52q`3Le)9Z&MEn~0o&Z4e;oMH$ee zusI;gbw`d1J%ovop|Hzw<#U9Zq)(P^9Qhjy>C@c%n@ud7ZSAKt0md)NU z;)tz^xfMc%C`0W&?;z{q@n2i_0+BbwIcppBQht0Y=b<8#W$K_g1b8W<(!>#*pUy+as(!iIB36sNE&f^zNKzs-^&C1?2L3dM zo4#8dG8nyqmrgwf9GcT5DvO?^&B`I_T;DS5l=N2nQ+42X{Hyn(I3Xwn>VLw|l61v> z98I&(CI%YkIhgU5$IE@%oF8J{>-Ff>X5e+J3Vo&|G8%9(jkv zRM;3&O9Q~%m+$JA=tRi{eBK!|BUqCj2f$Th90Ky)9?`rb7c-bivr}_Kb^!Yeu3g+f zMkA0nbC2e+7}o&A8l{3i3q|3JMJqg@>Bv=40(V7}&@1^@o5^t;-RUJHuZxBvyu!LU z#dikj=~v2(6B)g7z@Wx=SC~#m%5rnG-ny8taaIEfGa(zHY4n@{ck0K|-nGJ$cjc?* z(K?exf9@AN`Hfmz47=5HQ3O!N&k4m@2YI}_5!F!j07`5=S(5P)A7vOCx#p_CPy+HW zbRgVX7XV4s8sRrQ0?lJ}2j5)rce$Pv>En?EJ9qhpXE{>tgQB%x>f@2gb$Fdip55w2 zM;?oJmONEi^-%nqYptQr<;dp5nz&rZ>g6A2d{1g_K=QiG>I&j9I+c1>8-hkrg1y$FRTzZKo0)>>_v9#a(Y?{d?`9n8q?f0)lw5 z?y|*6Lo|%JQL0+4x$61ajR!jaw|AOADRi%yh`5ilYv>3Ys#5j^-^(fi$N|WcK*yMn7dP5^cdIO~kE>t&}@W zn}H9CDvZxFaAqhR-4Qr0ie04$l@nlF)&Utx27@TT?dM}xra*wk`vF@&=Ii+!_k}Vp zGd51y;{@A^v^jPb@^X?tCshN^NzZGC1uT3+=f#UrH^twn@7pqxldiP`<&0Olua z6Vi4pp@R?@eFkD6?T2|e$6R^zU3oq`6uSuac(Dct^L_cgsv(Y{r73Q86aCIKyYW!I zG>mOGsjgU7iLu+FxVBTBPFCN5>c2d4a8h&X`o-kW=a;bve1Zva68G)E=DxhjW*z9< zm(_%pTC3bMV3kaUSs=s=L*U zBp;|GUmC`;JT}|^LU+R;QY=`KqE`?Lb_P|rAXD&7OQIH*|MLUn(6|N?Zoe~Y@5t0M z+#`EGX5GkDpVfZS)-+slZFe=n>`Hd&_Qf$x1mZpBys-YFMT?I5$=0{+*l_k_bhQzd zu-I(8SMzg>Tn~4}P3t3GF+KO2~Ya1o6aexyJD`sA}NxTxd8sQS; zkw5;8jg4CP5x75Dc$1{dxu$T6??<8yy_jH?4xy4ci*igs(nM{z^1dpAdW-E z1iZP(N02RzM&`Wwa=2Ba;M0X9#0YDgRugMd8GJEO<4B1*Ls3{_!ZiNI5~|5k%dwZz zZX}5_Ygr>?aw5D4 zkCjkHM6qQMd<}DMXd9Z?(y`Md@;qPa@Z8kYVQ{n=lyLk7#WEH@h~t}DFUl*@Kb3k` za+J`+KFinm-?R#shcy!ql>SX(^=dNIqI17ZGp{}Y9ImAkyA`$a*%Oa6*{x4MtzOF? z#xmfn=I^`-Olu#pq%^5)OsBx4V=BH~zb5*gDbFeb|Ms@^oSi-y|PlyU)8A{a&P>E}`ymeId z2np&CV^+V7i^dXL$3WZ>&eyPI7Z?|C-YXr)!ST%&+c^IX-V3WAH55&S4Cn763^(02 zef{oZb>;ng649odV|O@aW=)rU*t^0pDfabKU{^GG zTI_siE?+m1@p9&i5nn5J=4|RtaCok{PD=7N&_r!ImKwO_J3Aa%UdOjP>zB#fELJ4A8}YXr3lb8dp3N z?(cRq##1j#3|wz|zZnj$j3VVfOiOdR#W|ufWL!o=%EQR~sJwWJJg<(MuqmQH6QYq6 zXxbr8C6nRUB8xid%f}ZNTerP;jaOP<-+*`tA5BQL4;Ge6wD85cO@FIF#evw0koJ6>7Ui# z*LQW?gkygqgRj6`Y&+$FP_L`0}zaJzdRA$_CVuPPpZoO{cLC#a494L{P1WS@WDJ!tr>$oa<|(9 zUsF1}$^sun>vhwz362Q?s8B8dH%^d|sb9jTiui!w*Lim(@uU6%x`G?0+VHhexW@QF z3PynR1%>nl2IEp*)AtiCZS5Pp#uNc#mg|>#pVH>YdEAP0uLWw?osZ`f3GIpx?uySS z6Ct~YS8Okfpqyk}-&TjWklJ#C(GURj>tJzGDPl_R^7rg0JWMH^3Wt;OjoxL!F85 zxn@b6VOElu_Rl#oW8i_ma33{(Aja(z4J$Rkf}qdUF=!KPX?p!6XzwZVEGzO1N->}H z_Aiuy+ceCy#x}JXbz7#B{?lzi56g^ubC&Z4JM%%t@D7{yz{c{4JM#fQ^G=`K)M}nZ zYEs;2{T(+Mm0}s|QcXIC$!3l`Pn4;I3nK;Q-87$Jz$G33?cHIboyU+ugfA4}=KE2K z@3#=`cY_>quDBM>3(}e`nB4V`&Kp+ti`MCcE4jp!!)O}eZorH7RHF7r)FA1p13m5; z$BKJecA!D*;B1sW|i`_UA?tqrwgDN)fA zx!n_U%Nr;mxPWNxJb2PNzJ06KzC0+=vzSoHO^9_oLcQtVrog=X@Oe_WVo9OKlApx~ zYOO1~3k0?4WPXb2vlf%bNcMj`n;7Uj?eCrj(GYm<5P3dkOrx?$^Ab1^nB4>TB?^z% zqiAVR#~o?{3)3zS3<8B+8&Q99tvk^(oLnwb!4Y*FhWim%#zuN80fcdu7NcD6JA7(mV1DsYK^y`%ZG!X z0WSKHKF$4vFHN_`N&=L%OjO!b!OrT#%2UA0_eK-UDoXufV+kGG`4wPCEjZ|eMQb95 z8Ug5}RK)ct4ig;I2hf%x1)FL}%Px_o#|p9%6V@&n?2D{J|D8MFDk1k#Q4(|qE75LH znCl;K!fTIK~~CVW;C@*eyyAJnS98ZhYNh(W3xUeBfBC7{!F ziTEYR`ZHr*6#jG?I1A=*OQ8>B;B{cOi2lOpQBV-@V(a}J zi%$7Ws;mNmtqRYRnBa+=)JKpM7qdvtN+v@r@VT+h%|ZR9_MVyD2yAyrW?Xcbe;aNA zevKEL${p2}jF~p1TdGYrpEFdl406Gcg?KA&rKYys8)+EJS?B+P5EnRfaire(BcFoF zU=V@@n+2{FEeS6)(!f{gkj{>Oj5Mh%C<+Nt(6PxW2$`3gd7ZRj$MD#3NOnAus%{KpFQUs zYnv^CHE>seHhtzaB08nNA{X?1xG3sk^x0sA9Ko|#v0qHj8nIuPe%>EuU&(u_TQtd+ ztAnVq#n)R(v*v3f*tt2#?i*Ctp4qrG*zgpaKCotF30wFlMx|OYup1jDf=?eCD`}m! z#$#3G3w%au%kpk@o80bM<4lT3hDZJ3(@#V57)6uFT{L=REA;m8SY~fmvI|dSukW`n zo++Zk5%27sfwObRq#7TW|Ih|K`r>-d*jD7b0Y4seo(VtOb+q|A-HM23Ozs3E1ITR% z5ZYr04&u&)bLFDD3esQl>(2_T4|q3dpXV_Td^klt6Kj*;6_gKT=U~InM*L=Gfgw3@U^9VB9aI1r7J)r(4^c@9Uv>!i6!SeB zEFh}rKKA3G^W32P*lNeD+BBjmYVY*9YA3Qg>@KU9HDyJ7#4MLsbMIbCdtwTIGv{^5}mg_bxKq^1f5Y2ISq35Is}l4MqM9()GLk&Y6*kDJ@w#653_D-HgA(f zAy;?mwk8cJw<6_OCA6zE7btFVxG6oHaj{l5%h0fn%3g{BcV#rWd{Naq$3A~)Ub(xf z@%-w~@`XS25psAyjw{(@k$IOw9>hzF>S5P}DvJ$E8uLP*FUKiT4UHe9)bqe0I(eLU z*4dj1lUq5|dE+$+T(l$1= zAPQE}eJ&7S3>lfAZC-IM6K@Q!>T?ivSsczh_p*4BcFsJgez7QR3NVW99Z{F5wB+or zXBNmRw~6!qbNES6k3T|_kLU2?At|6lDoL>rrb&@KlXglx-ld7`Ld!nuK$5QcS8rv~ zE1$s8d5%tRPb*yD5v+U4lzHymZfE+*zTVESqsc(BMs)ijw;Q-L)f)zFt-BxE{`FsmEMif?Vq`NAoUucr;w<7aMX3%{lkxM% zKR7d0#xa$sP3J^7!_)Sy0P0h@iU(Q%NxB?tAoWQ*AC(~x(ST9De2h(%;d+Zh|c1~uJwK-+BprMQ+Ko>Y4DC*5j)7ptL? z0ZKPtj3Fr3eU^4;%hFWo6n+M6D;n`~qjwo`GQSvg4p#}|zTZqH>qW+?R^-mf@%SRv z)72nrQkU1Sjq^s`pLu8HH)d7*LF}`fU@%}dX3G9LD^$ZBvGXEJPjMyyY)O~8GH!n4 zy1z9{LfAvnXfQM_sUYjfECIR4$BD2CI$#BwG-6fA3zl*NaoiENMj>7fw9W~QC^61* z70%z$gTb05!T#2N{jCe+nV>+!XO&jP9F5}8Mo~~ILRChj8$@h!`wvt!$fFBjZ_O+c zhG$INX>duSi;)i?8?>{21+L2+jVqoYxnpV5P3M#!q&ih{r@M?f9=o{HZPHE`za4vo!X!8dg))9B(gSEpVgo1VCYctm$L18Y z3euH1JR^6^lYaAyC;KcPG)shz5tSgz=D00CTjMVRCuf-Zmbx|PUxuwIplzn{l$_*A zec>bDL0E1z%=RCYh|CE+>nF%+T#e6W3T1;2v~!MZlAV9!Z2Yvx)}>o9){2chV~ae4D~P?=Ys;6`YX5uo z>0-ATnBZiqu(busI+fU=CK34g7L8CM!EQUV8tqlb+NNoZ4cSS){^*`wdep=xT=})Y|Jev8xHLp~!bf;24G&=o z4q>`Mkbn*j31&fz4iT+X^!w425-q)56Z#x4I3P%d2+#F^1c+AR`x8wB_0=k(Je3Vi1 zRJ6_Rm2q|Ec+60jB|Cn(GAM`fg<~zKQ6t>p*PkY^XH0s4ByJt`*igwr7 z`IbN-d!xmETmZLJ5{DbhxTA?CloH)l1fGf%r{4?PfHEGJ9$3gQ+}SP*^xRVPODRW> ziIr0q)RT^HCT%-19*~JMEikY7=Y@eKi*`K1>$CYnR)W}Hjon1ne4** z3JsC!vP2EvsiG*QpuT8&Y828xC*c6ahv`ZN;BA@%#*LwBz&oK9!o}M*mJfs;!A1;A*qJ*e(asG9TM({G?(Z+jGGpfhIGu*)^Y& zt2@n;iujpno%!|k-Oaoz9>dN?3tPJep>Jw^O8;0H=oP@_vj8b=W&Jrnf)#C*p-CYG zA(v+5lx6c5MX;*#SfmHdqQd5XIsc}Wht#Nya|*G@(`XD`Dxb6?(xfbm@-x7Z6)i{B z!W>}}omd~x1lj_M0c+e{^9GOc#_4>sKcn2c3Jaf_+=ijZ+bivmn)d^a_*O7JM~AMf z8{7?QHcG+2E&wp!+p^mML~QeaRfsX23+k(!Ijfuc&13>AyK&`(pd~$_;>#oJK z^ZMFb^H<^%nlgM1+%HPIUrR@KA-deAyofFQvS)b1-#zhZjUShZT=}|uVwa0z&qS0* zi1&d3wOq6$*(2dm)wk&9DI-=<*56Pb$Om^Qvd0QCMzgrxYaVprke7=IEfm5crNc1U z=XK0VUc9*eHj7dxxxCb{0n+EE(4?thpkjDCXi8CoQ;-sY!aF&wPo`q|(P+7joqE-7 z4m-|$P^eBmRSX+d0;RADd;1F2Jr>-7ZpZ zr=HOI$0t|QK&Eed^F9*0Ui?lqPEW8#SFWpP+B;{n2KG)jVWn$s&J{oZwrlRiMb1T~ ztR`;5CQXB`CK$)h`9?9;wR|gLyZU=XJFoM)et}`QZgcPnMr*$p6g^%`A`3*KOTMLHO9q4zqQ$7^hHTOp>;_>F+$6`by>gc z#otmhXesi)lorSnmrcFDyG9y_k);@WoXn9Yrh(|xgg7AKdtrM@e_zbNGcjPiRt0o( z@0_8%=LNP^z(Kx1BeywEKEAH8+OIzn^mc;7bZGAlp+AheRwp;JCv8GG@54FW;kS)H z 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%0DdS4UwJ$b|Ed7|9)bL?4wV0s0Q_Eo_}2vD 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

02WSRW4#+BEp-;_ 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�`VIM^f@e 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ª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