diff --git a/api/.env.sample b/api/.env.sample index c97f917df..8f9758882 100644 --- a/api/.env.sample +++ b/api/.env.sample @@ -1,29 +1,33 @@ SECRET_KEY= -GUNICORN_PROCESSES='2' -GUNICORN_THREADS='5' +GUNICORN_PROCESSES= +GUNICORN_THREADS= # Database # only for local db # check dev-scripts/local-db/docker-compose.yml for exact values # similar values are also used in https://github.com/bcgov/bcregistry-sre/blob/main/.github/workflows/backend-ci.yaml -DATABASE_USERNAME=postgres -DATABASE_PASSWORD="postgres" -DATABASE_NAME="unittesting" -DATABASE_HOST="localhost" -DATABASE_PORT="54345" -DATABASE_SCHEMA="public" -DATABASE_OWNER="postgres" +DATABASE_USERNAME= +DATABASE_PASSWORD= +DATABASE_NAME= +DATABASE_HOST= +DATABASE_PORT= +DATABASE_SCHEMA= +DATABASE_OWNER= # only when conenecting to clousql db DATABASE_INSTANCE_CONNECTION_NAME= -DATABASE_NAME=namex -DATABASE_USERNAME="...@gov.bc.ca" # your email, which needs to be added as IAM user to cloudsql instance and granted readwrite access -DATABASE_IP_TYPE=public -DATABASE_OWNER=userHQH +DATABASE_NAME= +DATABASE_USERNAME= # your email, which needs to be added as IAM user to cloudsql instance and granted readwrite access +DATABASE_IP_TYPE= +DATABASE_OWNER= -# APIs -SOLR_BASE_URL= +# Solr API +SOLR_API_URL= +SOLR_API_VERSION= +SOLR_API_SERVICE_ACCOUNT_CLIENT_ID= +SOLR_API_SERVICE_ACCOUNT_CLIENT_SECRET= +# APIs SOLR_SYNONYMS_API_URL= SOLR_SYNONYMS_API_VERSION= @@ -61,9 +65,9 @@ ENTITY_SERVICE_ACCOUNT_CLIENT_SECRET= JWT_OIDC_WELL_KNOWN_CONFIG= JWT_OIDC_CLIENT_SECRET= -JWT_OIDC_ALGORITHMS=RS256 -JWT_OIDC_CACHING_ENABLED=True -JWT_OIDC_JWKS_CACHE_TIMEOUT=300 +JWT_OIDC_ALGORITHMS= +JWT_OIDC_CACHING_ENABLED= +JWT_OIDC_JWKS_CACHE_TIMEOUT= # PUBSUB NAMEX_MAILER_TOPIC= @@ -74,7 +78,7 @@ MRAS_SVC_URL= MRAS_SVC_API_KEY= # Local development only -DISABLE_NAMEREQUEST_SOLR_UPDATES=1 +DISABLE_NAMEREQUEST_SOLR_UPDATES= # launchdarkly -NAMEX_LD_SDK_ID=sdk-075200eb-4ff7-4e0e-872a-848585d3d460 +NAMEX_LD_SDK_ID= diff --git a/api/config.py b/api/config.py index 40f38d066..d394eda90 100644 --- a/api/config.py +++ b/api/config.py @@ -31,7 +31,7 @@ class Config(object): SQLALCHEMY_TRACK_MODIFICATIONS = False - SOLR_BASE_URL = os.getenv('SOLR_BASE_URL', None) + SOLR_API_URL = f'{os.getenv('SOLR_API_URL', None)}{os.getenv('SOLR_API_VERSION', None)}' SOLR_SYNONYMS_API_URL = f'{os.getenv("SOLR_SYNONYMS_API_URL", None)}{os.getenv("SOLR_SYNONYMS_API_VERSION", None)}' @@ -100,8 +100,16 @@ class Config(object): PAYMENT_SVC_AUTH_CLIENT_ID = os.getenv('NAME_REQUEST_SERVICE_ACCOUNT_CLIENT_ID', '') PAYMENT_SVC_CLIENT_SECRET = os.getenv('NAME_REQUEST_SERVICE_ACCOUNT_CLIENT_SECRET', '') + SOLR_SVC_AUTH_URL = os.getenv('KEYCLOAK_AUTH_TOKEN_URL', '') + SOLR_API_SERVICE_ACCOUNT_CLIENT_ID = os.getenv('SOLR_API_SERVICE_ACCOUNT_CLIENT_ID', '') + SOLR_API_SERVICE_ACCOUNT_CLIENT_SECRET = os.getenv('SOLR_API_SERVICE_ACCOUNT_CLIENT_SECRET', '') + DISABLE_NAMEREQUEST_SOLR_UPDATES = int(os.getenv('DISABLE_NAMEREQUEST_SOLR_UPDATES', 0)) + # Enables test-only endpoints (e.g. force NR state without notifying Solr). + # Set in dev/test/sandbox only - never in production. + ALLOW_TEST_ENDPOINT = os.getenv('ALLOW_TEST_ENDPOINT', 'false').lower() == 'true' + NAMEX_NR_STATE_TOPIC = os.getenv('NAMEX_NR_STATE_TOPIC', '') EMAILER_TOPIC = os.getenv('NAMEX_MAILER_TOPIC', '') diff --git a/api/devops/vaults.gcp.env b/api/devops/vaults.gcp.env index 33c8ca498..7420f8d42 100644 --- a/api/devops/vaults.gcp.env +++ b/api/devops/vaults.gcp.env @@ -22,11 +22,15 @@ LEGAL_API_URL="op://API/$APP_ENV/legal-api/LEGAL_API_URL" LEGAL_API_VERSION="op://API/$APP_ENV/legal-api/LEGAL_API_VERSION_2" NAMEX_LD_SDK_ID="op://launchdarkly/$APP_ENV/namex/NAMEX_LD_SDK_ID" SECRET_KEY="op://namex/$APP_ENV/namex-api/SECRET_KEY" -SOLR_BASE_URL="op://namex/$APP_ENV/namex-api/SOLR_BASE_URL_EXTERNAL" +SOLR_API_URL="op://namex/$APP_ENV/solr/SOLR_API_URL" +SOLR_API_VERSION="op://namex/$APP_ENV/solr/SOLR_API_VERSION" +SOLR_API_SERVICE_ACCOUNT_CLIENT_ID="op://namex/$APP_ENV/solr/SOLR_API_SERVICE_ACCOUNT_CLIENT_ID" +SOLR_API_SERVICE_ACCOUNT_CLIENT_SECRET="op://namex/$APP_ENV/solr/SOLR_API_SERVICE_ACCOUNT_CLIENT_SECRET" GUNICORN_PROCESSES="op://namex/$APP_ENV/namex-api/GUNICORN_PROCESSES" GUNICORN_THREADS="op://namex/$APP_ENV/namex-api/GUNICORN_THREADS" AUTO_ANALYZE_URL="op://namex/$APP_ENV/namex-api/AUTO_ANALYZE_URL" AUTO_ANALYZE_CONFIG="op://namex/$APP_ENV/namex-api/AUTO_ANALYZE_CONFIG" +ALLOW_TEST_ENDPOINT="op://namex/$APP_ENV/namex-api/ALLOW_TEST_ENDPOINT" MRAS_SVC_URL="op://namex/$APP_ENV/mras-api/MRAS_SVC_URL" MRAS_SVC_API_KEY="op://namex/$APP_ENV/mras-api/MRAS_SVC_API_KEY" JWT_OIDC_AUDIENCE="op://namex/$APP_ENV/jwt/JWT_OIDC_AUDIENCE" @@ -47,4 +51,4 @@ DATABASE_USERNAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_USERNAME" DATABASE_NAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_NAME" DATABASE_INSTANCE_CONNECTION_NAME="op://database/$APP_ENV/namex-db-gcp/DATABASE_INSTANCE_CONNECTION_NAME" DATABASE_SCHEMA="op://database/$APP_ENV/namex-db-gcp/DATABASE_SCHEMA" -DATABASE_OWNER="op://database/$APP_ENV/namex-db-gcp/DATABASE_OWNER" \ No newline at end of file +DATABASE_OWNER="op://database/$APP_ENV/namex-db-gcp/DATABASE_OWNER" diff --git a/api/namex/analytics/__init__.py b/api/namex/analytics/__init__.py deleted file mode 100644 index 941127ba4..000000000 --- a/api/namex/analytics/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .solr import SolrQueries -from .restricted_words import RestrictedWords - -VALID_ANALYSIS = SolrQueries.VALID_QUERIES + RestrictedWords.VALID_QUERIES diff --git a/api/namex/analytics/phonetic/__init__.py b/api/namex/analytics/phonetic/__init__.py deleted file mode 100644 index 03055033d..000000000 --- a/api/namex/analytics/phonetic/__init__.py +++ /dev/null @@ -1,164 +0,0 @@ -def first_vowels(word, leading_vowel=False): - vowels = ['A', 'E', 'I', 'O', 'U', 'Y'] - value = '' - first_vowel_found = False - for letter in word: - if letter not in vowels and first_vowel_found: - break - if letter in vowels: - value += letter - first_vowel_found = True - - if not leading_vowel: - if value == 'EY': - value = 'A' - if value == 'EI': - value = 'A' - if value == 'EA': - value = 'A' - if value == 'AY': - value = 'A' - if value == 'AI': - value = 'A' - if value == 'Y': - value = 'I' - if value == 'UE': - value = 'U' - else: - if value == 'OY': - value = 'OI' - - if 'AA' in value: - value = value.replace('AA', 'A') - - return value - - -def first_consonants(word): - consonants = ['B', 'C', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'X', 'W', 'V', 'Z'] - value = '' - first_consonant_found = False - for letter in word: - if letter not in consonants and first_consonant_found: - break - if letter in consonants: - value += letter - first_consonant_found = True - - if 'CHR' in value: - value = value.replace('CHR', 'KR') - - if 'GG' in value: - value = value.replace('GG', 'G') - - if 'C' in value: - value = value.replace('C', 'K') - - if 'CR' in value: - value = value.replace('CR', 'KR') - - if 'CL' in value: - value = value.replace('CL', 'KL') - - if 'PH' in value: - value = value.replace('PH', 'F') - - if 'GH' in value: - value = value.replace('GH', 'G') - - if 'GN' in value: - value = value.replace('GN', 'N') - - if 'KN' in value: - value = value.replace('KN', 'N') - - if 'PN' in value: - value = value.replace('PN', 'N') - - if 'PS' in value: - value = value.replace('PS', 'S') - - if 'WR' in value: - value = value.replace('WR', 'R') - - if 'RH' in value: - value = value.replace('RH', 'R') - - if 'WH' in value: - value = value.replace('WH', 'W') - - return value - - -def has_leading_vowel(word): - if word[0] in ['A', 'E', 'I', 'O', 'U', 'Y']: - return True - else: - return False - - -def designations(): - return [ - 'AN', - 'AND', - 'ARE', - 'AS', - 'AT', - 'BE', - 'BUT', - 'BY', - 'FOR', - 'IF', - 'IN', - 'INTO', - 'IS', - 'IT', - 'NO', - 'NOT', - 'O', - 'ON', - 'OR', - 'SUCH', - 'THAT', - 'THE', - 'THEIR', - 'THEN', - 'THERE', - 'THESE', - 'THEY', - 'THIS', - 'TO', - 'ASSOCIATION', - 'ASSOC', - 'ASSOC.', - 'ASSN', - 'ASSN.', - 'COMPANY', - 'CO', - 'CO.', - 'CORPORATION', - 'CORP', - 'CORP.', - 'INCORPORATED', - 'INC', - 'INC.', - 'INCORPOREE', - 'LIABILITY', - 'LIMITED', - 'LTD', - 'LTD.', - 'LIMITEE', - 'LTEE', - 'LTEE.', - 'SOCIETY', - 'SOC', - 'SOC.', - ] - - -def replace_special_leading_sounds(word): - for special_leading_sound, replacement in [['QU', 'KW'], ['EX', 'X'], ['MAC', 'MC']]: - if word[: len(special_leading_sound)] == special_leading_sound: - word = replacement + word[len(special_leading_sound) :] - - return word diff --git a/api/namex/analytics/solr.py b/api/namex/analytics/solr.py deleted file mode 100644 index 1d31990b0..000000000 --- a/api/namex/analytics/solr.py +++ /dev/null @@ -1,1360 +0,0 @@ -import json -import re -import string -from typing import List -from urllib import parse, request -from urllib.error import HTTPError - -from flask import current_app -from google.auth.transport.requests import Request -from google.oauth2 import id_token - -from namex.analytics.phonetic import ( - designations, - first_consonants, - first_vowels, - has_leading_vowel, - replace_special_leading_sounds, -) - -# Use this character in the search strings to indicate that the word should not by synonymized. -NO_SYNONYMS_INDICATOR = '@' - -WILD_CARD = '*' - -# Constant names for useful chars used by the functions below -RESERVED_CHARACTERS = '-+@"' -DOUBLE_QUOTE = '"' -HYPHEN = '-' - -# Prefix used to indicate that words are not to have synonyms. -NO_SYNONYMS_PREFIX = '&fq=name_copy:' - -# Prefix used to indicate that we have synonyms. -SYNONYMS_PREFIX = '&fq=name_with_synonyms:' - - -class SolrQueries: - PROX_SYN_CONFLICTS = 'proxsynconflicts' - OLD_SYN_CONFLICTS = 'oldsynconflicts' - COBRS_PHONETIC_CONFLICTS = 'cobrsphonconflicts' - PHONETIC_CONFLICTS = 'phonconflicts' - CONFLICTS = 'conflicts' - HISTORY = 'histories' - TRADEMARKS = 'trademarks' - RESTRICTED_WORDS = 'restricted_words' - NAME_NR_SEARCH = 'name_nr_search' - VALID_QUERIES = [CONFLICTS, HISTORY, TRADEMARKS] - - # - # Prototype: - # /solr//select? ... &q={name} ... &wt=json&start={start}&rows={rows}&fl=source,id,name,score - # - queries = { - PROX_SYN_CONFLICTS: '/solr/possible.conflicts/select?' - 'hl.fl=name' - '&hl.simple.post=%3C/b%3E' - '&hl.simple.pre=%3Cb%3E' - '&hl=on' - '&indent=on' - '&q=name:{start_str}' - '&wt=json' - '&start={start}&rows={rows}' - '&fl=source,id,name,score,start_date,jurisdiction' - '&sort=score%20desc,txt_starts_with%20asc' - '{synonyms_clause}' - '{exact_phrase_clause}', - OLD_SYN_CONFLICTS: '/solr/possible.conflicts/select?' - 'hl.fl=name' - '&hl.simple.post=%3C/b%3E' - '&hl.simple.pre=%3Cb%3E' - '&hl=on' - '&indent=on' - '&q=txt_starts_with:{start_str}' - '&wt=json' - '&start={start}&rows={rows}' - '&fl=source,id,name,score,start_date,jurisdiction' - '&sort=score%20desc,txt_starts_with%20asc' - '{synonyms_clause}{exact_phrase_clause}{name_copy_clause}', - COBRS_PHONETIC_CONFLICTS: '/solr/possible.conflicts/select?' - '&q=cobrs_phonetic:{start_str}' - '&wt=json' - '&start={start}&rows={rows}' - '&fl=source,id,name,score,start_date,jurisdiction' - '&sort=score%20desc,txt_starts_with%20asc' - '&fq=-{exact_name}' - '{synonyms_clause}', - PHONETIC_CONFLICTS: '/solr/possible.conflicts/select?' - '&q=dblmetaphone_name:{start_str}' - '&wt=json' - '&start={start}&rows={rows}' - '&fl=source,id,name,score,start_date,jurisdiction' - '&sort=score%20desc,txt_starts_with%20asc' - '&fq=-{exact_name}' - '{synonyms_clause}', - CONFLICTS: '/solr/possible.conflicts/select?' - 'defType=edismax' - '&hl.fl=name' - '&hl.simple.post=%3C/b%3E' - '&hl.simple.pre=%3Cb%3E' - '&hl=on' - '&indent=on' - '&q={compressed_name}%20OR%20{name}' - '&qf=name_compressed^6%20name_with_synonyms' - '&wt=json' - '&start={start}&rows={rows}' - '&fl=source,id,name,score,start_date,jurisdiction' - '&sort=score%20desc' - '{synonyms_clause}{name_copy_clause}', - HISTORY: '/solr/names/select?sow=false&df=name_exact_match&wt=json&&rows={rows}&q={name}' - '&fl=nr_num,name,score,submit_count,name_state_type_cd,start_date,jurisdiction' - '&fq=name_state_type_cd:(A%20OR%20R)', - TRADEMARKS: '/solr/trademarks/select?' - 'defType=edismax' - '&hl.fl=name' - '&hl.simple.post=%3C/b%3E&hl.simple.pre=%3Cb%3E' - '&hl=on' - '&indent=on' - '&q={compressed_name}%20OR%20{name}' - '&qf=name_compressed^6%20name_with_synonyms' - '&wt=json' - '&start={start}&rows={rows}' - '&fl=application_number,name,status,description,score' - '&bq=status:%22Registration%20published%22^5.0' - '&sort=score%20desc' - '{name_copy_clause}', - NAME_NR_SEARCH: '/solr/names/select?' - 'indent=on' - '&q={query}' - '&sort=score%20desc,start_date%20desc' - '&wt=json' - '&start={start}&rows={rows}' - '&fl=nr_num,score', - } - - @classmethod - def get_conflict_results(cls, name, bucket, exact_phrase, start=0, rows=100): - solr_base_url = current_app.config.get('SOLR_BASE_URL', None) - if not solr_base_url: - current_app.logger.error('SOLR: SOLR_BASE_URL is not set') - - return None, 'Internal server error', 500 - - # handle non-ascii chars in name - name = ''.join([i if ord(i) < 128 else parse.quote(i) for i in name]) - name = cls.remove_stopwords_designations(name) - - if name.find('*') != -1: - list_name_split = name.split() - else: - list_name_split, name = cls.combine_multi_word_synonyms(name, solr_base_url) - list_name_split = [x.upper() for x in list_name_split] - stemmed_words = cls.word_pre_processing(list_name_split, 'stems', solr_base_url)['stems'] - stemmed_name = '' - for stem in stemmed_words: - stemmed_name += ' ' + stem - stemmed_name = stemmed_name.strip().upper() - - name_tokens = {'full_words': list_name_split, 'stemmed_words': stemmed_words} - prox_search_strs, old_alg_search_strs, phon_search_strs = cls.build_solr_search_strs( - name, stemmed_name, name_tokens - ) - - synonyms_for_word = cls.get_synonyms_for_words(name_tokens['stemmed_words']) - if bucket == 'synonym': - connections = cls.get_synonym_results( - solr_base_url, name, prox_search_strs, old_alg_search_strs, name_tokens, exact_phrase, start, rows - ) - - elif bucket == 'cobrs_phonetic': - connections = cls.get_cobrs_phonetic_results(solr_base_url, prox_search_strs, name_tokens, start, rows) - - # bucket == 'phonetic' - else: - connections = cls.get_phonetic_results(solr_base_url, name, phon_search_strs, name_tokens) - - try: - solr = { - 'response': {'numFound': 0, 'start': start, 'rows': rows, 'maxScore': 0.0, 'docs': []}, - 'highlighting': [], - } - - seen_ids = [] - # commented out here and below because it is updated but never used for anything - # passed_names = [] - previous_stack_title = '' - stem_count = len(stemmed_words) * 2 + 1 - count = -1 - - for connection in connections: - seen_ordered_ids = seen_ids.copy() - - result = connection[0] - solr['response']['numFound'] += result['response']['numFound'] - result_name = parse.unquote(connection[1]) - if previous_stack_title.replace(' ', '') != result_name.replace(' ', ''): - stack_title_info = { - 'name_info': {'name': result_name}, - 'stems': stemmed_words[: int(stem_count / 2)], - } - for word in list_name_split[: int(stem_count / 2)]: - for stem in stemmed_words[: int(stem_count / 2)]: - if stem in word: - break - elif stem[:-1] in word: - stack_title_info['stems'] += [stem[:-1]] - solr['response']['docs'].append(stack_title_info) - stem_count -= 1 - previous_stack_title = result_name - - if len(result['response']['docs']) > 0: - ordered_names = [] - missed_ids = [] - # if there is a bracket in the stack title then there is a 'synonyms:(...)' clause - if 'synonyms:(' in result_name: - synonyms = result_name[result_name.find('(') + 1 : result_name.find(')')] - synonyms = [x.strip() for x in synonyms.split(',')] - for synonym in synonyms: - if synonym.upper() in synonyms_for_word: - for word in synonyms_for_word[synonym.upper()]: - for item in result['response']['docs']: - processed_name = cls.name_pre_processing(item['name']).upper() - if item['id'] not in seen_ordered_ids and item['id'] not in missed_ids: - missed_ids.append(item['id']) - if item['id'] not in seen_ordered_ids: - if word.upper() in processed_name.upper(): - seen_ordered_ids.append(item['id']) - ordered_names.append({'name_info': item, 'stems': [word.upper()]}) - missed_ids.remove(item['id']) - - elif word.upper()[:-1] in processed_name.upper() and len(word) > 4: - seen_ordered_ids.append(item['id']) - ordered_names.append({'name_info': item, 'stems': [word.upper()[:-1]]}) - missed_ids.remove(item['id']) - - else: - for item in result['response']['docs']: - if item['id'] not in seen_ordered_ids: - seen_ordered_ids.append(item['id']) - ordered_names.append({'name_info': item, 'stems': []}) - - if len(missed_ids) > 0: - current_app.logger.debug(f'In {previous_stack_title} stack UNSORTED results: {missed_ids}') - for missed in missed_ids.copy(): - for item in result['response']['docs']: - if missed == item['id']: - ordered_names.append({'name_info': item, 'stems': []}) - missed_ids.remove(missed) - break - - if len(missed_ids) > 0: - # should never get here - current_app.logger.error(f'In {previous_stack_title} stack MISSED results: {missed_ids}') - - final_names_list = [] - - # order based on alphabetization of swapped in synonyms - if bucket == 'synonym': - pivot_list = [] - for key in name_tokens['stemmed_words']: - pivot_list.insert(0, key.upper()) - seen_for_pivot = [] - if '*' not in connection[1]: - count += 1 - for pivot in pivot_list[count:]: - sorted_names = [] - if pivot in synonyms_for_word: - for synonym in synonyms_for_word[pivot]: - for name in ordered_names: - if name['name_info']['id'] in seen_for_pivot: - pass - else: - processed_name = cls.name_pre_processing(name['name_info']['name']) - - if ' ' + synonym.upper() in ' ' + processed_name.upper(): - stem = [synonym.upper()] - if stem[0] not in name['stems']: - sorted_names.append( - { - 'name_info': name['name_info'], - 'stems': stem + name['stems'].copy(), - } - ) - else: - sorted_names.append( - {'name_info': name['name_info'], 'stems': name['stems']} - ) - - seen_for_pivot.append(name['name_info']['id']) - # if name['name_info']['name'] in passed_names: - # passed_names.remove(name['name_info']['name']) - - elif ( - ' ' + synonym.upper()[:-1] in ' ' + processed_name.upper() - and len(synonym) > 4 - ): - stem = [synonym.upper()[:-1]] - stack_title_info = solr['response']['docs'].pop() - if stem[0] not in name['stems']: - sorted_names.append( - { - 'name_info': name['name_info'], - 'stems': stem + name['stems'].copy(), - } - ) - if ( - stem[0] not in stack_title_info['stems'] - and synonym.upper() in stack_title_info['stems'] - ): - stack_title_info['stems'] += stem - else: - sorted_names.append( - {'name_info': name['name_info'], 'stems': name['stems']} - ) - solr['response']['docs'].append(stack_title_info) - - seen_for_pivot.append(name['name_info']['id']) - # if name['name_info']['name'] in passed_names: - # passed_names.remove(name['name_info']['name']) - - # elif name['name_info']['name'] not in passed_names: - # passed_names.append(name['name_info']['name']) - - no_duplicates = [] - for ordered in ordered_names: - duplicate = False - for sorted in sorted_names: - if ordered['name_info']['name'] == sorted['name_info']['name']: - duplicate = True - if not duplicate: - no_duplicates.append(ordered) - - ordered_names = sorted_names.copy() + no_duplicates.copy() - - for seen in seen_for_pivot: - if seen not in seen_ordered_ids: - seen_ordered_ids.append(seen) - - seen_for_pivot.clear() - sorted_names.clear() - - final_names_list += ordered_names - else: - for item in ordered_names: - final_names_list.append(item) - seen_ordered_ids.append(item['name_info']['id']) - - seen_ids += seen_ordered_ids.copy() - seen_ordered_ids.clear() - - solr['response']['docs'] += final_names_list - - results = { - 'response': { - 'numFound': solr['response']['numFound'], - 'maxScore': solr['response']['maxScore'], - 'name': result['responseHeader']['params']['q'], - }, - 'names': solr['response']['docs'], - 'highlighting': solr['highlighting'], - } - - return results, '', None - - except Exception as err: - current_app.logger.error(err) - return None, 'Internal server error', 500 - - @classmethod - def get_synonym_results( - cls, solr_base_url, name, prox_search_strs, old_alg_search_strs, name_tokens, exact_phrase, start=0, rows=100 - ): - try: - connections = [] - if name == '': - name = '*' - prox_search_strs.append((['*'], '', '', 1)) - old_alg_search_strs.append('*') - - for prox_search_tuple, old_alg_search in zip(prox_search_strs, old_alg_search_strs): - old_alg_search_str = old_alg_search[:-2].replace(' ', '%20') + '*' # [:-2] takes off the last '\ ' - synonyms_clause = ( - cls._get_synonyms_clause(prox_search_tuple[1], prox_search_tuple[2], name_tokens) - if exact_phrase == '' - else '' - ) - exact_phrase_clause = ( - '&fq=contains_exact_phrase:' + '"' + parse.quote(exact_phrase).replace('%2A', '') + '"' - if exact_phrase != '' - else '' - ) - - if name.find('*') == -1: - for name in prox_search_tuple[0]: - prox_search_str = name - query = solr_base_url + SolrQueries.queries['proxsynconflicts'].format( - start=start, - rows=rows, - start_str='"' - + parse.quote(prox_search_str).replace('%2A', '') - + '"~{}'.format(prox_search_tuple[3]), - synonyms_clause=synonyms_clause, - exact_phrase_clause=exact_phrase_clause, - ) - current_app.logger.debug('Query: ' + query) - connections.append( - ( - json.load(request.urlopen(query)), - '----' - + prox_search_str.replace('\\', '').replace('*', '').replace('@', '') - + synonyms_clause.replace('&fq=name_with_', ' ').replace('%20', ', ') - + ' - PROXIMITY SEARCH', - ) - ) - - query = solr_base_url + SolrQueries.queries['oldsynconflicts'].format( - start=start, - rows=rows, - start_str=parse.quote(old_alg_search_str).replace('%2A', '*').replace('%5C%2520', '\\%20'), - synonyms_clause=synonyms_clause, - exact_phrase_clause=exact_phrase_clause, - name_copy_clause=cls._get_name_copy_clause(name), - ) - current_app.logger.debug('Query: ' + query) - connections.append( - ( - json.load(request.urlopen(query)), - '----' - + old_alg_search_str.replace('\\', '').replace('%20', ' ').replace('**', '*') - + synonyms_clause.replace('&fq=name_with_', ' ').replace('%20', ', ') - + ' - EXACT WORD ORDER', - ) - ) - return connections - - except Exception as err: - current_app.logger.error(err, query) - return None, 'SOLR query error', 500 - - @classmethod - def get_cobrs_phonetic_results(cls, solr_base_url, search_strs, name_tokens, start=0, rows=100): - try: - if search_strs == []: - connections = [ - ({'response': {'numFound': 0, 'docs': []}, 'responseHeader': {'params': {'q': '*'}}}, '----*') - ] - else: - connections = [] - for str_tuple in search_strs: - synonyms_clause = cls._get_synonyms_clause(str_tuple[1], str_tuple[2], name_tokens) - for name in str_tuple[0]: - start_str = name - query = solr_base_url + SolrQueries.queries['cobrsphonconflicts'].format( - start=start, - rows=rows, - start_str='"' + parse.quote(start_str).replace('%2A', '') + '"~{}'.format(str_tuple[3]), - synonyms_clause=synonyms_clause, - exact_name='name_no_synonyms:"' - + start_str.replace(' ', '%20') - + '"~{}'.format(str_tuple[3]), - ) - current_app.logger.debug('Query: ' + query) - result = json.load(request.urlopen(query)) - connections.append( - ( - result, - '----' - + start_str.replace('*', '').replace('@', '') - + synonyms_clause.replace('&fq=name_with_', ' ').replace('%20', ', '), - ) - ) - return connections - - except Exception as err: - current_app.logger.error(err, query) - return None, 'SOLR query error', 500 - - @classmethod - def get_phonetic_results(cls, solr_base_url, name, search_strs, name_tokens, start=0, rows=100): - try: - if search_strs == []: - connections = [ - ({'response': {'numFound': 0, 'docs': []}, 'responseHeader': {'params': {'q': '*'}}}, '----*') - ] - else: - connections = [] - for str_tuple in search_strs: - synonyms_clause = cls._get_synonyms_clause(str_tuple[1], str_tuple[2], name_tokens) - start_str = str_tuple[0] - query = solr_base_url + SolrQueries.queries['phonconflicts'].format( - start=start, - rows=rows, - start_str='"' + parse.quote(start_str).replace('%2A', '') + '"~{}'.format(str_tuple[3]), - synonyms_clause=synonyms_clause, - exact_name='name_no_synonyms:"' + start_str.replace(' ', '%20') + '"~{}'.format(str_tuple[3]), - ) - current_app.logger.debug('Query: ' + query) - result = json.load(request.urlopen(query)) - docs = result['response']['docs'] - result['response']['docs'] = cls.post_treatment(docs, start_str) - connections.append( - ( - result, - '----' - + start_str.replace('*', '').replace('@', '') - + synonyms_clause.replace('&fq=name_with_', ' ').replace('%20', ', '), - ) - ) - return connections - - except Exception as err: - current_app.logger.error(err, query) - return None, 'SOLR query error', 500 - - @classmethod - def get_name_nr_search_results(cls, solr_query, start=0, rows=10): - """Search for the query param in `names` core.""" - solr_base_url = current_app.config.get('SOLR_BASE_URL', None) - if not solr_base_url: - current_app.logger.error('SOLR: SOLR_BASE_URL is not set') - return None, 'Internal server error', 500 - - try: - query = solr_base_url + SolrQueries.queries[SolrQueries.NAME_NR_SEARCH].format( - start=start, rows=rows, query=solr_query - ) - current_app.logger.debug('Query: ' + query) - connection = request.urlopen(query) - except Exception as err: - current_app.logger.error(err, query) - return None, 'Internal server error', 500 - - try: - solr = json.load(connection) - results = { - 'response': { - 'numFound': solr['response']['numFound'], - 'start': solr['response']['start'], - 'rows': solr['responseHeader']['params']['rows'], - 'maxScore': solr['response']['maxScore'], - 'name': solr['responseHeader']['params']['q'], - }, - 'names': solr['response']['docs'], - } - return results, '', None - except Exception as err: - current_app.logger.error(err, query) - return None, 'Internal server error', 500 - - @classmethod - def get_parsed_query_name_nr_search(cls, value: str): - """Build query to search nr number or name. - - - `None` -> *:* - - NR 1234567 -> nr_num:*1234567* - - HNR239 HOLDINGS -> (name_copy:*HNR239* AND name_copy:*HOLDINGS*) - - NR 955 HNR239 HOLDINGS -> nr_num:*955* AND (name_copy:*HNR239* AND name_copy:*HOLDINGS) - - HNR239 HOLDINGS NR 955 -> nr_num:*955* AND (name_copy:*HNR239* AND name_copy:*HOLDINGS) - - HNR239 NR 955 HOLDINGS -> nr_num:*955* OR - (name_copy:*HNR239* AND name_copy:*NR* AND name_copy:*955* AND name_copy:*HOLDINGS) - """ - solr_query = '*:*' - nr_number = None - if value: - value = value.strip() - - nr_num = '' - # match whole/start/end string NR 1234567, NR1234567 - nr_num_regex = r'(^(NR( |)[0-9]+)$)|(^(NR( |)[0-9]+)\s)|(\s(NR( |)[0-9]+)$)' - nr_num_fallback_regex = r'(^[0-9]+$)|(^[0-9]+\s)|(\s[0-9]+$)' # 1234567 - if result := re.search(nr_num_regex, value, re.IGNORECASE): - matching_nr = result.group() - nr_number = re.sub('NR', '', matching_nr, flags=re.IGNORECASE).strip() - value = value.replace(matching_nr, '', 1).strip() # removing nr num - nr_num = 'nr_num:*' + nr_number + '*' - if value: - nr_num += ' AND' # Get results which match nr_num and name - else: - return nr_num, nr_number, value - elif result := re.search(nr_num_fallback_regex, value): - nr_number = result.group().strip() - nr_num = 'nr_num:*' + nr_number + '* OR' - - name_copy = 'name_copy:*' - name_copy += '* AND name_copy:*'.join(value.split()) - name_copy += '*' # name_copy += '* AND' - - # name = f'({name_copy} name:(*"{value}"*))' - name = f'({name_copy})' - - solr_query = parse.quote(f'{nr_num} {name}'.strip()) - - # 'nr_num:*0285176* OR (name_copy:*0285176* AND name:(*"0285176"*))' - - return solr_query, nr_number, value - - @classmethod - def get_results(cls, query_type, name, start=0, rows=10): - solr_base_url = current_app.config.get('SOLR_BASE_URL', None) - if not solr_base_url: - current_app.logger.error('SOLR: SOLR_BASE_URL is not set') - - return None, 'Internal server error', 500 - - if query_type not in SolrQueries.VALID_QUERIES: - return None, 'Not a valid analysis type', 400 - - current_app.logger.debug( - 'Solr Query - type:{qtype} - name:{name}'.format(qtype=query_type, name=parse.quote(name)) - ) - - try: - query = solr_base_url + SolrQueries.queries[query_type].format( - start=start, - rows=rows, - name=cls._get_parsed_name(name), - compressed_name=cls._compress_name(name), - name_copy_clause=cls._get_name_copy_clause(name), - ) - current_app.logger.debug('Query: ' + query) - connection = request.urlopen(query) - except Exception as err: - current_app.logger.error(err, query) - return None, 'Internal server error', 500 - - try: - solr = json.load(connection) - results = { - 'response': { - 'numFound': solr['response']['numFound'], - 'start': solr['response']['start'], - 'rows': solr['responseHeader']['params']['rows'], - 'maxScore': solr['response']['maxScore'], - 'name': solr['responseHeader']['params']['q'], - }, - 'names': solr['response']['docs'], - 'highlighting': solr['highlighting'] if 'highlighting' in solr.keys() else '', - } - return results, '', None - except Exception as err: - current_app.logger.error(err, query) - return None, 'Internal server error', 500 - - @classmethod - def _get_parsed_name(cls, name): - return parse.quote(name.lower().replace(WILD_CARD, '').replace(NO_SYNONYMS_INDICATOR, '')) - - # Compress the name by removing designations and then striping all non-alpha characters. Solr eventually converts to - # lowercase so we'll choose that over doing everything in uppercase. - @classmethod - def _compress_name(cls, name): - name = name.lower() - - # TODO: these should be loaded from somewhere. - designations = [ - 'corp.', - 'corporation', - 'inc.', - 'incorporated', - 'incorporee', - 'l.l.c.', - 'limited liability co.', - 'limited liability company', - 'limited liability partnership', - 'limited partnership', - 'limitee', - 'llc', - 'llp', - 'ltd.', - 'ltee', - 'sencrl', - 'societe a responsabilite limitee', - 'societe en nom collectif a responsabilite limitee', - 'srl', - 'ulc', - 'unlimited liability company', - 'limited', - ] - - # Match the designation with whitespace before and either followed by whitespace or end of line. - for designation in designations: - name = re.sub(' ' + designation + '(\\s|$)', '', name) - - return re.sub('[^a-z]', '', name) - - @classmethod - def _tokenize(cls, line: str, categories: List[str] = None) -> List[str]: - """ - Builds a list of tokens based upon the categories defined - The tokens are in the same order as the original input - - Categories are scanned left->right, - so if you do repeat the contents in categories, the one to the left wins - - example in Doctest format <- because like, why not?! - - >>> _tokenize('a set of tokens', [' ', string.ascii_lowercase]) - ['a', ' ', 'set', ' ', 'of', ' ', 'tokens'] - >>> _tokenize('a "set of" tokens', [' ', string.ascii_lowercase]) - ['a', ' ', '"', 'set', ' ', 'of', '"', ' ', 'tokens'] - >>> _tokenize('a +"set of" tokens', [' ', '+"', ' "' string.ascii_lowercase]) - ['a', ' ', '+"', 'set', ' ', 'of', '"', ' ', 'tokens'] - - :param line: str: the string to tokenize - :param categories: List[str]: a list of strings used as categories to classify the tokens - :return: List[str]: a list of string tokens that can be parsed left-> as order is preserved - """ - tokens = [] # yep, lazy format - if categories is None: - categories = [] - start_token: int = 0 - idx: int - category: List[str] = None - for idx, char in enumerate(line): - if category and char not in category: - tokens.append(line[start_token:idx]) - category = None - - if not category: - category = '\u0000' - for cat in categories: - if char in cat: - category = cat - break - start_token = idx - - if start_token <= idx: - tokens.append(line[start_token:]) - return tokens - - @classmethod - def _parse_for_synonym_candidates(cls, tokens: List[str]) -> List[str]: - """ - A list of tokens is passed in. - We parse with the following rules: - prefix " str str str .. str " - prefix is applied to all terms inside of the " " tokens - token after a '-' token is ignored unless it is between quotes, then it is ignored - '@' mean ignore next token, if next token is ", ignore everything until the balancing " or until end of list - :param tokens: - :return: List[str] of tokens that are candidates as synonym tokens - """ - - candidates: List[str] = [] - previous_token: str = None - double_quote_flag: bool = False - skip_token_flag: bool = False - - for token in tokens: - if token in string.whitespace: - previous_token = token - if not double_quote_flag: - skip_token_flag = False - continue - - elif token is DOUBLE_QUOTE: - if double_quote_flag: - skip_token_flag = False - double_quote_flag = not double_quote_flag - previous_token = token - continue - - elif token is HYPHEN: - if double_quote_flag: - previous_token = token - continue - - if previous_token in string.whitespace: - skip_token_flag = True - previous_token = token - continue - - elif token is NO_SYNONYMS_INDICATOR: - skip_token_flag = True - previous_token = token - continue - - else: - if skip_token_flag: - if not double_quote_flag: - skip_token_flag = False - - previous_token = token - continue - - candidates.append(token) - - previous_token = token - - candidates.extend(cls._get_concatenated_terms(candidates)) - - return candidates - - @classmethod - def _get_concatenated_terms(cls, candidates): - if len(candidates) < 2: - return [] - - multiples = [] - - for x in range(len(candidates)): - if x < len(candidates) - 1: - multiples.append(''.join(candidates[x : x + 2])) - if x < len(candidates) - 2: - multiples.append(''.join(candidates[x : x + 3])) - - return multiples - - # Call the synonyms API for the given token. - - @classmethod - def _get_identity_token(cls, audience): - """Get an identity token for authenticating with solr-synonyms-api.""" - try: - token = id_token.fetch_id_token(Request(), audience) - - if not token or not isinstance(token, str): - current_app.logger.warning('Failed to get identity token') - return None - - return token - except Exception as e: - current_app.logger.warning(f'Error in getting identity token: {e.message}') - return None - - @classmethod - def _synonyms_exist(cls, token, col): - solr_synonyms_api_url = current_app.config.get('SOLR_SYNONYMS_API_URL', None) - if not solr_synonyms_api_url: - raise Exception('SOLR: SOLR_SYNONYMS_API_URL is not set') - - # Get identity token and make header - id_token = cls._get_identity_token(solr_synonyms_api_url) - - # If the web service call fails, the caller will catch and then return a 500 for us. - query = solr_synonyms_api_url + '/synonyms/' + col + '/' + parse.quote(token) - current_app.logger.debug('Query: ' + query) - - try: - if id_token is None: - connection = request.urlopen(query) - else: - connection = request.urlopen(request.Request(query, headers={'Authorization': f'Bearer {id_token}'})) - except HTTPError as http_error: - # Expected when the token does not have synonyms. - if http_error.code == 404: - return False - - # Not sure what it is, pass it up. - raise http_error - - return connection.status == 200 - - # Call the synonyms API for list of synonyms matching the given token. - @classmethod - def _get_synonym_list(cls, token): - solr_synonyms_api_url = current_app.config.get('SOLR_SYNONYMS_API_URL', None) - if not solr_synonyms_api_url: - raise Exception('SOLR: SOLR_SYNONYMS_API_URL is not set') - - # Get identity token and make header - id_token = cls._get_identity_token(solr_synonyms_api_url) - - # If the web service call fails, the caller will catch and then return a 500 for us. - query = solr_synonyms_api_url + '/synonyms/' + 'stems_text' + '/' + parse.quote(token) - current_app.logger.debug('Query: ' + query) - - try: - if id_token is None: - connection = request.urlopen(query) - else: - connection = request.urlopen(request.Request(query, headers={'Authorization': f'Bearer {id_token}'})) - except HTTPError as http_error: - # Expected when the token does not have synonyms. - if http_error.code == 404: - return [] - - # Not sure what it is, pass it up. - raise http_error - - results = json.load(connection) - synonym_list = [] - # in case a token is part of multiple synonym lists - for synonyms in results[1]: - synonym_list += synonyms.split(',') - - return synonym_list - - # Look up each token in name, and if it is in the synonyms then we need to search for it separately. - @classmethod - def _get_synonyms_clause(cls, name, stemmed_name, name_tokens={'full_words': [], 'stemmed_words': []}): # noqa: B006 - # name = re.sub(' +', ' ', name) - current_app.logger.debug('getting synonyms for: {}'.format(name)) - clause = '' - synonyms = [] - if name: - tokens = cls._tokenize( - name.lower(), - [string.digits, string.whitespace, RESERVED_CHARACTERS, string.punctuation, string.ascii_lowercase], - ) - candidates = cls._parse_for_synonym_candidates(tokens) - for token in candidates: - for full, stem in zip(name_tokens['full_words'], name_tokens['stemmed_words']): - if token.upper() == full.upper(): - token = stem - break - if cls._synonyms_exist(token, 'synonyms_text'): - synonyms.append(token.upper()) - - if stemmed_name: - tokens = cls._tokenize( - stemmed_name.lower(), - [string.digits, string.whitespace, RESERVED_CHARACTERS, string.punctuation, string.ascii_lowercase], - ) - candidates = cls._parse_for_synonym_candidates(tokens) - for token in candidates: - if cls._synonyms_exist(token, 'stems_text') and token.upper() not in synonyms: - synonyms.append(token.upper()) - - if synonyms: - clause = SYNONYMS_PREFIX + '(' + parse.quote(' '.join(synonyms)) + ')' - - return clause - - # The NO_SYNONYMS_INDICATOR is used to prefix a word or phrase that is not to be to synonymized. The decision was - # made that the indicator would not be used within double quotes, and that mixing it with search meta-characters is - # not a defined operation. - # - # - 'NO INDICATOR' > '' - # - '@ONE INDICATOR' > 'ONE' - # - '@MAYBE @TWO INDICATORS' > 'MAYBE TWO' - # - '@"SOME PHRASE" ALLOWED' > '"SOME PHRASE"' - # - # This is far from exhaustive and very GIGO. - @classmethod - def _get_name_copy_clause(cls, name): - clause = '' - - # There's no easy way to split the name with whitespace as a delimiter, because quotes can be used to preserve - # terms with internal whitespace and additionally the quotes may also be prefixed with +, -, or *, or - # combinations of such. - unsynonymed_words = [] - - word = '' - indicator_on = False - quotes_on = False - for letter in name: - if letter == NO_SYNONYMS_INDICATOR: - # Only allow the indicator outside of quotes. - if not quotes_on: - indicator_on = True - - continue - - if letter == '"': - quotes_on = not quotes_on - - if not indicator_on: - continue - - if letter == ' ' and not quotes_on: - if indicator_on: - if word != '': - unsynonymed_words.append(word) - word = '' - - indicator_on = False - - continue - - if indicator_on: - word += letter - - # Handle when the last word was prefixed with the indicator. - if indicator_on and word != '': - unsynonymed_words.append(word) - - if len(unsynonymed_words) != 0: - clause = NO_SYNONYMS_PREFIX + '(' + parse.quote(' '.join(unsynonymed_words)) + ')' - - return clause - - @classmethod - def remove_stopwords_designations(cls, name): - # TODO: these should be loaded from somewhere. - designations = [ - 'corp.', - 'corp', - 'corporation', - 'inc.', - 'inc', - 'incorporated', - 'incorporee', - 'l.l.c.', - 'llc', - 'limited partnership', - 'limited liability co.', - 'limited liability co', - 'limited liability company', - 'limited liability partnership', - 'limitee', - 'llp', - 'ltd.', - 'ltd', - 'ltee', - 'sencrl', - 'societe a responsabilite limitee', - 'societe en nom collectif a responsabilite limitee', - 'limited', - 'srl', - 'ulc', - 'unlimited liability company', - ] - - # TODO: these should be loaded from somewhere. - stop_words = [ - 'an', - 'and', - 'are', - 'as', - 'at', - 'be', - 'but', - 'by', - 'corp', - 'if', - 'in', - 'incorporation', - 'into', - 'is', - 'it', - 'no', - 'not', - 'of', - 'on', - 'or', - 'such', - 'that', - 'the', - 'their', - 'then', - 'there', - 'these', - 'they', - 'this', - 'to', - ] - - # remove designations if they are at the end of the name - for designation in designations: - index = name.upper().find(' ' + designation.upper()) - # checks if there is a designation AND if that designation is at the end of the string - if index != -1 and (index + len(designation) + 1) is len(name): - name = name[:index] - break - - for stop_word in stop_words: - name = ' ' + name + ' ' - name = name.upper().replace(' ' + stop_word.upper() + ' ', ' ').strip() - - # # handle non-ascii chars in name - # name = ''.join([i if ord(i) < 128 else parse.quote(i) for i in name]) - name = name.upper().replace(' AND ', ' ').replace('&', ' ').replace('+', ' ') - return name - - @classmethod - def combine_multi_word_synonyms(cls, name, solr_base_url): - max_len = len(name.split()) * 2 - query = ( - solr_base_url - + '/solr/possible.conflicts/analysis/field?analysis.fieldvalue={name}&analysis.fieldname=name' - '&wt=json&indent=true'.format(name=parse.quote(name.strip()).replace('%2A', '')) - ) - current_app.logger.debug('Query: ' + query) - - processed_words = json.load(request.urlopen(query)) - - count = 0 - for item in processed_words['analysis']['field_names']['name']['index']: - if item == 'org.apache.lucene.analysis.synonym.SynonymGraphFilter': - count += 1 - break - count += 1 - - name = '' - word_count = 0 - for text in processed_words['analysis']['field_names']['name']['index'][count]: - if word_count < max_len: - name += text['text'] + ' ' - else: - name += text['text'] - word_count += 1 - name = parse.unquote(name) - processed_list = name.split() - - return processed_list, name.strip() - - @classmethod - def build_solr_search_strs(cls, name, stemmed_name, name_tokens): - def replace_nth(string, deleted_substr, added_substr, n): - nth_index = [m.start() for m in re.finditer(deleted_substr, string)][n - 1] - before = string[:nth_index] - after = string[nth_index:] - after = after.replace(deleted_substr, added_substr, 1) - newString = before + after - return newString - - num_terms = 0 - prox_combined_terms = '' - prox_stemmed_combined_terms = '' - prox_search_strs = [] - phon_search_strs = [] - old_alg_combined_terms = '' - old_alg_search_strs = [] - full_words = name_tokens['full_words'] - stemmed_words = name_tokens['stemmed_words'] - - for index in range(len(full_words)): - term = full_words[index] - try: - stemmed_term = stemmed_words[index] - except IndexError: - stemmed_term = '' - num_terms += 1 - - prox_combined_terms += term + ' ' - prox_stemmed_combined_terms += stemmed_term + ' ' - prox_compounded_words = [prox_combined_terms.strip()] - - if num_terms > 2: - prox_compounded_words.append(prox_combined_terms.replace(' ', '')) - - # concat for compound versions of combined terms - combined_terms_list = prox_combined_terms.split() - - n = 1 - while n < len(combined_terms_list): - compunded_name = replace_nth(prox_combined_terms, ' ', '', n) - prox_compounded_words.append(compunded_name) - n += 1 - prox_search_strs.insert( - 0, - ( - prox_compounded_words, - name[len(prox_combined_terms) :], - stemmed_name[len(prox_stemmed_combined_terms) :], - num_terms, - ), - ) - phon_search_strs.insert( - 0, - ( - prox_combined_terms.strip(), - name[len(prox_combined_terms) :], - stemmed_name[len(prox_stemmed_combined_terms) :], - num_terms, - ), - ) - old_alg_combined_terms += term + '\\ ' - old_alg_search_strs.insert(0, old_alg_combined_terms) - - return prox_search_strs, old_alg_search_strs, phon_search_strs - - @classmethod - def get_synonyms_for_words(cls, list_name_split): - # get synonym list for each word in the name - list_name_split = [wrd.replace('*', '').upper() for wrd in list_name_split] - synonyms_for_word = {} - for word in list_name_split: - synonyms_for_word[word] = [x.upper().strip() for x in cls._get_synonym_list(word)] - - if synonyms_for_word[word]: - synonyms_for_word[word].remove(word) - synonyms_for_word[word].sort() - - for synonym in synonyms_for_word[word]: - temp_list = synonyms_for_word[word].copy() - temp_list.remove(synonym) - swaps = [s for s in temp_list if synonym in s] - swaps.reverse() - for swap in swaps: - synonyms_for_word[word].remove(swap) - index = synonyms_for_word[word].index(synonym) - synonyms_for_word[word].insert(index, swap) - - synonyms_for_word[word].insert(0, word) - - return synonyms_for_word - - @classmethod - def word_pre_processing(cls, list_of_words, type, solr_base_url): - list_of_words = [w.replace('*', '') for w in list_of_words] - words_to_process = '' - for item in list_of_words: - words_to_process += ' ' + item - - return_dict = {'stems': []} - if words_to_process != '': - query = ( - solr_base_url - + '/solr/possible.conflicts/analysis/field?analysis.fieldvalue={words}&analysis.fieldname=name' - '&wt=json&indent=true'.format(words=parse.quote(words_to_process.strip())) - ) - current_app.logger.debug('Query: ' + query) - - processed_words = json.load(request.urlopen(query)) - - count = 0 - - if type == 'synonyms': - for item in processed_words['analysis']['field_names']['name']['index']: - if item == 'org.apache.lucene.analysis.core.FlattenGraphFilter': - count += 1 - break - count += 1 - - processed_list = [] - for text in processed_words['analysis']['field_names']['name']['index'][count]: - processed_list.append(text['text']) - - for original, processed in zip(list_of_words, processed_list): - return_dict[original] = processed - - # type == 'stems' - else: - for item in processed_words['analysis']['field_names']['name']['index']: - if item == 'org.apache.lucene.analysis.snowball.SnowballFilter': - count += 1 - break - count += 1 - - processed_list = [] - for text in processed_words['analysis']['field_names']['name']['index'][count]: - processed_list.append(text['text'].upper()) - - # removal_list = [] - # for item in list_of_words: - # stem_in_name = False - # for processed_synonym in processed_list: - # if processed_synonym.upper() in item.upper(): - # stem_in_name = True - # break - # elif processed_synonym.upper()[:-1] in item.upper(): - # removal_list.append(processed_synonym) - # if not stem_in_name: - # processed_list.insert(0, item) - # for processed in processed_list: - # if processed in removal_list: - # processed_list.remove(processed) - return_dict['stems'] = processed_list - return return_dict - - @classmethod - def name_pre_processing(cls, name): - processed_name = ( - (' ' + name.lower() + ' ') - .replace('!', '') - .replace('@', '') - .replace('#', '') - .replace('%', '') - .replace('&', '') - .replace('\\', '') - .replace('/', '') - .replace('{', '') - .replace('}', '') - .replace('[', '') - .replace(']', '') - .replace(')', '') - .replace('(', '') - .replace('+', '') - .replace('-', '') - .replace('|', '') - .replace('?', '') - .replace('.', '') - .replace(',', '') - .replace('_', '') - .replace("'n", '') - .replace("'", '') - .replace('"', '') - .replace(' $ ', 'dollar') - .replace('$', 's') - .replace(' ¢ ', 'cent') - .replace('¢', 'c') - .replace('britishcolumbia', 'bc') - .replace('britishcolumbias', 'bc') - .replace('britishcolumbian', 'bc') - .replace('britishcolumbians', 'bc') - .replace('british columbia', 'bc') - .replace('british columbias', 'bc') - .replace('british columbian', 'bc') - .replace('british columbians', 'bc') - ) - return processed_name.strip() - - @classmethod - def post_treatment(cls, docs, query_name): - query_name = query_name.upper() - names = [] - count = 0 - for candidate in docs: - count += 1 - candidate_name = candidate['name'].upper() - words = candidate_name.split() - qwords = query_name.split() - - count = 0 - for qword in qwords: - found = False - for word in words: - if word not in designations() and qword not in designations(): - should_keep = cls.keep_phonetic_match(word, qword) - if should_keep: - if not found: - count += 1 - found = True - - if count == len(qwords): - cls.keep_candidate(candidate, candidate_name, names) - - return names - - @classmethod - def keep_phonetic_match(cls, word, query): - word = replace_special_leading_sounds(word) - query = replace_special_leading_sounds(query) - - word_has_leading_vowel = has_leading_vowel(word) - query_has_leading_vowel = has_leading_vowel(query) - - word_first_consonant = first_consonants(word) - query_first_consonant = first_consonants(query) - - query_first_vowels = first_vowels(query, query_has_leading_vowel) - word_first_vowels = first_vowels(word, word_has_leading_vowel) - - if query_has_leading_vowel: - query_sound = query_first_vowels + query_first_consonant - else: - query_sound = query_first_consonant + query_first_vowels - - if word_has_leading_vowel: - word_sound = word_first_vowels + word_first_consonant - else: - word_sound = word_first_consonant + word_first_vowels - - if word_sound == query_sound: - return True - - return False - - @classmethod - def keep_candidate(cls, candidate, name, names): - if len([doc['id'] for doc in names if doc['id'] == candidate['id']]) == 0: - names.append( - { - 'name': name, - 'id': candidate['id'], - 'source': candidate['source'], - 'jurisdiction': candidate.get('jurisdiction', ''), - 'start_date': candidate.get('start_date', ''), - } - ) diff --git a/api/namex/constants/__init__.py b/api/namex/constants/__init__.py index 36a8f8756..27e7b6ae2 100644 --- a/api/namex/constants/__init__.py +++ b/api/namex/constants/__init__.py @@ -799,3 +799,57 @@ class RequestPriority(AbstractEnum): class ExpiryDays(str, AbstractEnum): NAME_REQUEST_LIFESPAN_DAYS = 56 NAME_REQUEST_REH_REN_LIFESPAN_DAYS = 421 + +class Designations(AbstractEnum): + """ + Designations used for filtering name for solr conflict matching. + + NOTE: + - The designation words below are collected from the Name Request project. + - This list is duplicated here for use in the NameX API. + - In the future, consider implementing a shared source or service for designation data + to avoid duplication and ensure consistency across projects. + """ + CORP = 'CORP.' + CORPORATION = 'CORPORATION' + INC = 'INC.' + INCORPORATED = 'INCORPORATED' + LIMITED = 'LIMITED' + LTD = 'LTD.' + INCORPOREE = 'INCORPOREE' + LIMITEE = 'LIMITEE' + LTEE = 'LTEE' + CO_OP = 'CO-OP' + CO_OPERATIVE = 'CO-OPERATIVE' + COOP = 'COOP' + COOPERATIVE = 'COOPERATIVE' + CCC = 'CCC' + COMMUNITY_CONTRIBUTION_COMPANY = 'COMMUNITY CONTRIBUTION COMPANY' + ULC = 'ULC' + UNLIMITED_LIABILITY_COMPANY = 'UNLIMITED LIABILITY COMPANY' + LIMITED_LIABILITY_PARTNERSHIP = 'LIMITED LIABILITY PARTNERSHIP' + LLP = 'LLP' + SENCRL = 'SENCRL' + SRL = 'SRL' + SOCIETE_A_RESPONSABILITE_LIMITEE = 'SOCIETE A RESPONSABILITE LIMITEE' + SOCIETE_EN_NOM_COLLECTIF_A_RESPONSABILITE_LIMITEE = 'SOCIETE EN NOM COLLECTIF A RESPONSABILITE LIMITEE' + LIMITED_PARTNERSHIP = 'LIMITED PARTNERSHIP' + L_L_C = 'L.L.C.' + LIMITED_LIABILITY_CO = 'LIMITED LIABILITY CO.' + LIMITED_LIABILITY_COMPANY = 'LIMITED LIABILITY COMPANY' + LLC = 'LLC' + ASSOCIATION = 'ASSOCIATION' + ASSN = 'ASSN.' + CHAMBER_OF_COMMERCE = 'CHAMBER OF COMMERCE' + CHURCH = 'CHURCH' + EGLISE = 'EGLISE' + CLUB = 'CLUB' + CHRISTIAN_SCHOOL = 'CHRISTIAN SCHOOL' + FELLOWSHIP = 'FELLOWSHIP' + FOUNDATION = 'FOUNDATION' + FONDATION = 'FONDATION' + GUILD = 'GUILD' + PARISH = 'PARISH' + SOCIETY = 'SOCIETY' + SOCIETE = 'SOCIETE' + WATERSHED = 'WATERSHED' diff --git a/api/namex/models/__init__.py b/api/namex/models/__init__.py index 1473501be..e643b825b 100644 --- a/api/namex/models/__init__.py +++ b/api/namex/models/__init__.py @@ -30,3 +30,4 @@ from .payment import Payment from .hotjar_tracking import HotjarTracking from .payment_society import PaymentSociety +from .restricted_words import RestrictedWords diff --git a/api/namex/analytics/restricted_words.py b/api/namex/models/restricted_words.py similarity index 98% rename from api/namex/analytics/restricted_words.py rename to api/namex/models/restricted_words.py index 1e1d96a7a..55983873c 100644 --- a/api/namex/analytics/restricted_words.py +++ b/api/namex/models/restricted_words.py @@ -5,9 +5,6 @@ class RestrictedWords(object): - RESTRICTED_WORDS = 'restricted_words' - VALID_QUERIES = [RESTRICTED_WORDS] - @staticmethod def get_restricted_words_conditions(content): """Finds all restricted words and their conditions of the given string diff --git a/api/namex/resources/__init__.py b/api/namex/resources/__init__.py index 72d987f46..7136262cf 100644 --- a/api/namex/resources/__init__.py +++ b/api/namex/resources/__init__.py @@ -4,7 +4,6 @@ from .ops import api as nr_ops from .document_analysis import api as analysis_api from .meta import api as meta_api -from .exact_match import api as exact_match_api from .events import api as events_api from .name_requests.api_namespace import api as name_request_api @@ -39,7 +38,6 @@ api.add_namespace(nr_ops, path='/nr-ops') api.add_namespace(analysis_api, path='/documents') api.add_namespace(meta_api, path='/meta') -api.add_namespace(exact_match_api, path='/exact-match') api.add_namespace(events_api, path='/events') api.add_namespace(word_classification_api, path='/word-classification') api.add_namespace(name_request_api, path='/namerequests') diff --git a/api/namex/resources/base_resource.py b/api/namex/resources/base_resource.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/api/namex/resources/configuration.py b/api/namex/resources/configuration.py deleted file mode 100644 index 236e9bdd9..000000000 --- a/api/namex/resources/configuration.py +++ /dev/null @@ -1,5 +0,0 @@ -# from flask import current_app - -# SOLR_CORE = 'possible.conflicts' - -# SOLR_API_URL = f'{current_app.config.get("SOLR_BASE_URL")}/solr/' diff --git a/api/namex/resources/document_analysis.py b/api/namex/resources/document_analysis.py index 49a5ce03b..faa7ac43b 100644 --- a/api/namex/resources/document_analysis.py +++ b/api/namex/resources/document_analysis.py @@ -1,41 +1,16 @@ -import enum - -from flask import current_app, jsonify, make_response, request +from flask import jsonify, make_response, request from flask_restx import Namespace, Resource from flask_restx import fields as rp_fields -from marshmallow import Schema, ValidationError, validates -from marshmallow import fields as ma_fields from namex import jwt -from namex.analytics import VALID_ANALYSIS, RestrictedWords, SolrQueries +from namex.models.restricted_words import RestrictedWords from namex.utils.auth import cors_preflight api = Namespace('Name Checks', description='Checks names for conflicts, word restrictions, and other issues') -class DocumentType(enum.Enum): - type_unspecified = 'TYPE_UNSPECIFIED' - plain_text = 'PLAIN_TEXT' - - -class DocumentSchema(Schema): - type = ma_fields.String(required=True, error_messages={'required': {'message': 'type is a required field'}}) - content = ma_fields.String(required=True, error_messages={'required': {'message': 'content is a required field'}}) - - @validates('type') - def validate_type(self, value): - values = [item.value for item in DocumentType] - if value.upper() not in values: - raise ValidationError('Document Type must be one of: {}.'.format(DocumentType._member_names_)) - - @validates('content') - def validate_content(self, value): - if len(value) <= 1: - raise ValidationError('Document Content must have more than 1 character.') - - @cors_preflight('POST, GET') -@api.route(':', methods=['POST', 'GET', 'OPTIONS']) +@api.route('/restricted-words', methods=['POST', 'GET', 'OPTIONS']) class DocumentAnalysis(Resource): """ :param analysis (str): the type of analysis to perform @@ -51,7 +26,6 @@ class DocumentAnalysis(Resource): a_document = api.model( 'document', { - 'type': rp_fields.String(description='The object type', enum=DocumentType._member_names_), 'content': rp_fields.String(description='string content of the document', required=True), }, ) @@ -60,12 +34,7 @@ class DocumentAnalysis(Resource): @jwt.requires_auth @api.expect(a_document) @api.doc( - description='Check submitted name content for issues like restricted words', - params={ - 'analysis': 'The type of check to perform (e.g., conflicts, restricted_words, trademarks, histories)', - 'start': 'Pagination start index (default: 0)', - 'rows': 'Number of results to return (default: 50)', - }, + description='Check submitted name content for restricted words', responses={ 200: 'Analysis completed successfully', 400: 'Invalid input or missing content', @@ -74,30 +43,16 @@ class DocumentAnalysis(Resource): 500: 'Internal server error', }, ) - def post(analysis=None, *args, **kwargs): - start = request.args.get('start', DocumentAnalysis.START) - rows = request.args.get('rows', DocumentAnalysis.ROWS) - - if analysis.lower() not in VALID_ANALYSIS: - current_app.logger.info('requested analysis:{} is not valid'.format(analysis.lower())) - return make_response(jsonify(message='{analysis} is not a valid analysis'.format(analysis=analysis)), 404) - + def post(): json_input = request.get_json() if not json_input: return make_response(jsonify(message='No JSON data provided'), 400) - err = DocumentSchema().validate(json_input) - if err: - return make_response(jsonify(err), 400) - content = json_input['content'] + if not content: + return make_response(jsonify(message='No name content provided'), 400) - if analysis in RestrictedWords.RESTRICTED_WORDS: - results, msg, code = RestrictedWords.get_restricted_words_conditions(content) - - else: - current_app.logger.debug('Solr Search: {}'.format(content)) - results, msg, code = SolrQueries.get_results(analysis.lower(), content, start=start, rows=rows) + results, msg, code = RestrictedWords.get_restricted_words_conditions(content) if code: return make_response(jsonify(message=msg), code) @@ -108,11 +63,7 @@ def post(analysis=None, *args, **kwargs): @api.doc( description='Check name content using query parameters instead of a request body', params={ - 'analysis': 'The type of check to perform (e.g., conflicts, restricted_words, trademarks, histories)', - 'start': 'Pagination start index (default: 0)', - 'rows': 'Number of results to return (default: 50)', 'content': 'The name content to analyze', - 'type': 'Content type (default: plain_text)', }, responses={ 200: 'Analysis completed successfully', @@ -122,32 +73,12 @@ def post(analysis=None, *args, **kwargs): 500: 'Internal server error', }, ) - def get(analysis=None, *args, **kwargs): - # added because namerequest has no token and auto generated can't be allowed to POST - # (would have updated above post to get but corresponding UI change in namex-fe-caddy couldn't be built at the time) - start = request.args.get('start', DocumentAnalysis.START) - rows = request.args.get('rows', DocumentAnalysis.ROWS) - - if not analysis or analysis.lower() not in VALID_ANALYSIS: - current_app.logger.info('requested analysis:{} is not valid'.format(analysis.lower())) - return make_response(jsonify(message='{analysis} is not a valid analysis'.format(analysis=analysis)), 404) - - json_input = {'content': request.args.get('content'), 'type': request.args.get('type', 'plain_text')} - if not json_input: - return make_response(jsonify(message='No JSON data provided'), 400) - - err = DocumentSchema().validate(json_input) - if err: - return make_response(jsonify(err), 400) - - content = json_input['content'] - - if analysis in RestrictedWords.RESTRICTED_WORDS: - results, msg, code = RestrictedWords.get_restricted_words_conditions(content) + def get(): + content = request.args.get('content') + if not content: + return make_response(jsonify(message='No name content provided'), 400) - else: - current_app.logger.debug('Solr Search: {}'.format(content)) - results, msg, code = SolrQueries.get_results(analysis.lower(), content, start=start, rows=rows) + results, msg, code = RestrictedWords.get_restricted_words_conditions(content) if code: return make_response(jsonify(message=msg), code) diff --git a/api/namex/resources/exact_match.py b/api/namex/resources/exact_match.py deleted file mode 100644 index ce17bbb83..000000000 --- a/api/namex/resources/exact_match.py +++ /dev/null @@ -1,56 +0,0 @@ -import json -import urllib - -from flask import current_app, jsonify, request -from flask_restx import Namespace, Resource - -from namex import jwt -from namex.utils.auth import cors_preflight - -api = Namespace('Exact Match', description='Search for exact business name matches') - - -@cors_preflight('GET') -@api.route('', methods=['GET', 'OPTIONS']) -class ExactMatch(Resource): - @staticmethod - @jwt.requires_auth - @api.doc( - description='Fetch existing business names that exactly match the given query', - params={'query': 'The full name to search for'}, - responses={ - 200: 'Match results fetched successfully', - 400: 'Invalid or missing query parameter', - 401: 'Unauthorized', - 500: 'Internal server error', - }, - ) - def get(): - query = request.args.get('query') - query = query.lower().replace('*', '') - url = ( - current_app.config.get('SOLR_BASE_URL') - + '/solr/possible.conflicts' - + '/select?' - + 'sow=false' - + '&df=name_exact_match' - + '&wt=json' - + '&q=' - + urllib.parse.quote(query) - ) - current_app.logger.debug('Exact-match query: ' + url) - connection = urllib.request.urlopen(url) - answer = json.loads(connection.read()) - docs = answer['response']['docs'] - names = [ - { - 'name': doc['name'], - 'id': doc['id'], - 'source': doc['source'], - 'start_date': doc['start_date'], - 'jurisdiction': doc['jurisdiction'], - } - for doc in docs - ] - - return jsonify({'names': names}) diff --git a/api/namex/resources/histories.py b/api/namex/resources/histories.py index 779fcbe74..64728f81f 100644 --- a/api/namex/resources/histories.py +++ b/api/namex/resources/histories.py @@ -12,6 +12,12 @@ MAX_RESULTS = '50' +# TO_DO: This needs to be rewritten! - Does not work, used in Name-Examination UI history tab +# Try calling /nrs with this in the query +# "categories": { +# "name_state": ["R", "A", "C"], +# "state": ["ACTIVE", "APPROVED", "CONDITION", "REJECTED"] +# }, @cors_preflight('GET') @api.route('', methods=['GET', 'OPTIONS']) class Histories(Resource): diff --git a/api/namex/resources/name_requests/__init__.py b/api/namex/resources/name_requests/__init__.py index d3d58c4fa..0e9ab23af 100644 --- a/api/namex/resources/name_requests/__init__.py +++ b/api/namex/resources/name_requests/__init__.py @@ -1,3 +1,4 @@ from .name_request import NameRequestResource from .name_requests import NameRequestsResource from .report_resource import ReportResource +from .test_helpers import TestNameRequestState diff --git a/api/namex/resources/name_requests/test_helpers.py b/api/namex/resources/name_requests/test_helpers.py new file mode 100644 index 000000000..9604dbacf --- /dev/null +++ b/api/namex/resources/name_requests/test_helpers.py @@ -0,0 +1,90 @@ +"""Test-only endpoint to set a Name Request state without notifying Solr. + +Intended for API partners and QA to drive NR workflows in non-production +environments. It updates the NR (and its names) state directly and +intentionally does NOT publish a names state-change event, so the change is +never propagated to the Solr conflict index. + +Guarded by the ALLOW_TEST_ENDPOINT config flag - returns 404 when not enabled +(prod never sets the flag). +""" +from http import HTTPStatus + +from flask import current_app, g, jsonify, make_response, request +from flask_restx import Resource + +from namex import jwt +from namex.models import Event, Name, Request, State +from namex.services import EventRecorder +from namex.services.name_request.utils import get_or_create_user_by_jwt +from namex.utils.auth import cors_preflight + +from .api_namespace import api + +# States this test helper is permitted to set (extend as needed). +ALLOWED_TEST_STATES = [State.APPROVED, State.REJECTED] + +MSG_NOT_FOUND = 'Resource not found' + + +@cors_preflight('PATCH') +@api.route('//test-state', strict_slashes=False, methods=['PATCH', 'OPTIONS']) +class TestNameRequestState(Resource): + """TEST ONLY: force an NR state without pushing to Solr.""" + + @jwt.requires_auth + @api.doc( + description=( + 'TEST ONLY. Force a name request to APPROVED or REJECTED without publishing a ' + 'names state-change event - the update is NOT propagated to Solr. ' + 'Available only when ALLOW_TEST_ENDPOINT is enabled (non-production).' + ), + params={'nr_id': 'Internal ID of the name request'}, + responses={ + 200: 'NR state updated (Solr not notified)', + 400: 'Invalid or unsupported state', + 401: 'Unauthorized', + 404: 'Endpoint disabled, or name request not found', + 500: 'Internal server error', + }, + ) + def patch(self, nr_id): + # Hard guard: only available when explicitly enabled. Prod never sets the flag. + if not current_app.config.get('ALLOW_TEST_ENDPOINT'): + return make_response(jsonify(message=MSG_NOT_FOUND), HTTPStatus.NOT_FOUND) + + json_input = request.get_json(silent=True) or {} + state = str(json_input.get('state', '')).upper() + if state not in ALLOWED_TEST_STATES: + return make_response( + jsonify(message=f'Invalid state. Allowed: {", ".join(ALLOWED_TEST_STATES)}'), + HTTPStatus.BAD_REQUEST, + ) + + nr_model = Request.query.get(nr_id) + if not nr_model: + return make_response(jsonify(message='Name Request not found'), HTTPStatus.NOT_FOUND) + + # Update the NR and its names directly. + # Mirror a real decision: APPROVED approves the first choice and rejects the + # alternatives; REJECTED rejects every name. (nr_model.names is ordered by choice.) + nr_model.stateCd = state + if state == State.APPROVED: + for index, name in enumerate(nr_model.names): + name.state = Name.APPROVED if index == 0 else Name.REJECTED + else: + for name in nr_model.names: + name.state = Name.REJECTED + + # IMPORTANT: deliberately DO NOT call send_name_request_state_msg() / + # send_name_state_msg() here. Skipping the queue publish is the entire point + # of this endpoint - the state change must never reach Solr. + nr_model.save_to_db() + + user = get_or_create_user_by_jwt(g.jwt_oidc_token_info) + EventRecorder.record(user, Event.PATCH + ' [test-state-override]', nr_model, {'stateCd': state}) + + return make_response( + jsonify(nrNum=nr_model.nrNum, stateCd=nr_model.stateCd, message='State updated (Solr not notified)'), + HTTPStatus.OK, + ) diff --git a/api/namex/resources/requests.py b/api/namex/resources/requests.py index cf6e29e86..7341646b0 100644 --- a/api/namex/resources/requests.py +++ b/api/namex/resources/requests.py @@ -3,6 +3,7 @@ TODO: Fill in a larger description once the API is defined for V1 """ +import traceback from datetime import datetime from flask import current_app, g, jsonify, make_response, request @@ -16,8 +17,6 @@ from sqlalchemy.orm.exc import NoResultFound from namex import jwt -from namex.analytics import VALID_ANALYSIS as ANALYTICS_VALID_ANALYSIS -from namex.analytics import RestrictedWords, SolrQueries from namex.constants import DATE_TIME_FORMAT_SQL, NameState from namex.exceptions import BusinessException from namex.models import ( @@ -39,20 +38,23 @@ ) from namex.models import Request as RequestDAO from namex.models.request import AffiliationInvitationSearchDetails, RequestsAuthSearchSchema +from namex.models.restricted_words import RestrictedWords from namex.services import EventRecorder, MessageServices, ServicesError from namex.services.lookup import nr_filing_actions from namex.services.name_request import NameRequestService from namex.services.name_request.utils import check_ownership, get_or_create_user_by_jwt, valid_state_transition +from namex.services.solr.solr_client import SolrClient +from namex.services.solr.solr_helpers import SolrHlpers from namex.utils import queue_util from namex.utils.auth import cors_preflight from namex.utils.common import convert_to_ascii, convert_to_utc_max_date_time, convert_to_utc_min_date_time +from namex.utils.nr_query import get_nr_num_from_query +from namex.services.name_request.name_request import get_nrs_like_nr_num, get_nrs_like_names from .utils import DateUtils # Register a local namespace for the requests -api = Namespace( - 'Name Examination', description='Staff-facing name request operations for state, analysis, and name editing' -) +api = Namespace('Name Examination', description='Staff-facing name request operations for state, analysis, and name editing') # Marshmallow schemas request_schema = RequestsSchema(many=False) @@ -111,7 +113,7 @@ class RequestsQueue(Resource): @jwt.requires_roles([User.APPROVER]) @api.doc( description='Fetches the next draft name request from the queue and assigns it to the current user. ' - 'If the user already has an in-progress NR, that one is returned instead.', + 'If the user already has an in-progress NR, that one is returned instead.', params={'priorityQueue': 'Set to true to fetch from the priority queue'}, responses={ 200: 'Name request assigned successfully', @@ -473,29 +475,11 @@ def get(*args, **kwargs): return make_response(jsonify(rep), 200) - # @api.errorhandler(AuthError) - # def handle_auth_error(ex): - # response = jsonify(ex.error) - # response.status_code = ex.status_code - # return response, 401 - # return {}, 401 - - # noinspection PyUnusedLocal,PyUnusedLocal - @api.hide - @api.expect(a_request) - @jwt.requires_auth - def post(self, *args, **kwargs): - current_app.logger.info('Someone is trying to post a new request') - return make_response(jsonify({'message': 'Not Implemented'}), 501) - - -# For sbc-auth - My Business Registry page. - @cors_preflight('GET, POST') @api.route('/search', methods=['GET', 'POST', 'OPTIONS']) class RequestSearch(Resource): - """Search for NR's by NR number or associated name.""" + """Search for NR's by NR number or associated name. Used by My Business Registry page.""" @staticmethod @cors.crossdomain(origin='*') @@ -503,13 +487,12 @@ class RequestSearch(Resource): @api.doc( description='Searches name requests by partially matching NR number or business name using query parameters', params={ - 'query': 'NR number or business name to search (e.g., "NR1234567" or "abcd")', - 'start': 'Result offset for pagination (default: 0)', - 'rows': 'Number of results to return (default: 10)', + 'query': 'NR number or business name (e.g., "NR1234567" or "Acme Holdings")', + 'start': 'Offset (default: 0)', + 'rows': 'Page size (default: 10)', }, responses={ 200: 'Search results fetched successfully', - 400: 'Invalid search parameters', 500: 'Internal server error', }, ) @@ -522,108 +505,49 @@ def get(): return make_response(jsonify(data), 200) try: - solr_query, nr_number, nr_name = SolrQueries.get_parsed_query_name_nr_search(query) - condition = '' - if nr_number: - condition = f"requests.nr_num ILIKE '%{nr_number}%'" - if nr_name: - nr_name = nr_name.replace("'", "''") - if condition: - condition += ' OR ' - name_condition = "requests.name_search ILIKE '%" - name_condition += "%' AND requests.name_search ILIKE '%".join(nr_name.split()) - name_condition += "%'" - - condition += f'({name_condition})' - - results = ( - RequestDAO.query.filter( - RequestDAO.stateCd.in_([State.DRAFT, State.INPROGRESS, State.REFUND_REQUESTED]), - text(f'({condition})'), - ) - .options( - lazyload('*'), - joinedload(RequestDAO.names).load_only(Name.name), - load_only(RequestDAO.id, RequestDAO.nrNum), - ) - .order_by(RequestDAO.submittedDate.desc()) - .limit(rows) - .all() - ) - + results = SolrClient.search_nrs(query, start, rows) + existing_nrs = set(nr['nr_num'] for nr in results['searchResults']['results']) data.extend( [ { - # 'id': nr.id, - 'nrNum': nr.nrNum, - 'names': [n.name for n in nr.names], + 'nrNum': nr['nr_num'], + 'names': [name['name'] for name in nr['names']] } - for nr in results + for nr in results['searchResults']['results'] ] ) - - while len(data) < rows: - if start < rows: - # Check if the search length is less than 7 digits. If so, patch it with zero at the end to increase rows number. - # After this, the search cycles to solr will be reduced a lot. - # Otherwise, the rows is too small and it will take long time (many times solr calling) to search in solr and get timeout exception. - # So the less of search length, the bigger of rows will be. - temp_rows = str(rows) - if nr_number and len(nr_number) < 7: - temp_rows = str(rows).ljust(9 - len(nr_number), '0') - if nr_name and len(nr_name) < 7: - temp_rows = str(rows).ljust(9 - len(nr_name), '0') - - rows = int(temp_rows) - - nr_data, have_more_data = RequestSearch._get_next_set_from_solr(solr_query, start, rows) - nr_data = nr_data[: (rows - len(data))] + nr_num = get_nr_num_from_query(query) + if nr_num: + nrs = get_nrs_like_nr_num(nr_num) data.extend( [ { - # 'id': nr.id, 'nrNum': nr.nrNum, - 'names': [n.name for n in nr.names], + 'names': [n.name for n in nr.names] } - for nr in nr_data + for nr in nrs + if nr.nrNum not in existing_nrs + ] + ) + else: + # above, we returned nothing if query was empty + nrs = get_nrs_like_names(query) + data.extend( + [ + { + 'nrNum': nr.nrNum, + 'names': [n.name for n in nr.names] + } + for nr in nrs + if nr.nrNum not in existing_nrs ] ) - - if not have_more_data: - break # no more data in solr - start += rows return make_response(jsonify(data), 200) - except Exception as e: - current_app.logger.error(f'Error in /search, {e}') + except Exception: + current_app.logger.error(f'Error when searching for {query}\n{traceback.format_exc()}') return make_response(jsonify({'message': 'Internal server error'}), 500) - @staticmethod - def _get_next_set_from_solr(solr_query, start, rows): - results, msg, code = SolrQueries.get_name_nr_search_results(solr_query, start, rows) - if code: - raise Exception(msg) - elif len(results['names']) > 0: - have_more_data = results['response']['numFound'] > (start + rows) - identifiers = [name['nr_num'] for name in results['names']] - return RequestDAO.query.filter( - RequestDAO.nrNum.in_(identifiers), - RequestDAO.stateCd != State.CANCELLED, - or_( - RequestDAO.stateCd != State.EXPIRED, - text( - f"(requests.state_cd = '{State.EXPIRED}' AND CAST(requests.expiration_date AS DATE) + " - "interval '60 day' >= CAST(now() AS DATE))" - ), - ), - ).options( - lazyload('*'), - joinedload(RequestDAO.names).load_only(Name.name), - load_only(RequestDAO.id, RequestDAO.nrNum), - ).all(), have_more_data - - return [], False - @staticmethod @cors.crossdomain(origin='*') @jwt.has_one_of_roles([User.SYSTEM]) @@ -794,7 +718,7 @@ def delete(nr): ) @api.doc( description=( - "Updates a name request's state, records the previous state, optionally adds comments, assigns a corpNum if consumption state, " + 'Updates a name request\'s state, records the previous state, optionally adds comments, assigns a corpNum if consumption state, ' 'and calculates expiration if approval state. Only users with APPROVER, EDITOR, or SYSTEM roles may update state, ' 'and certain transitions may be restricted based on role or current state.' ), @@ -941,7 +865,8 @@ def consumeName(nrd, json_input): return make_response(jsonify(message='Internal server error'), 500) if 'warnings' in locals() and warnings: # noqa: F821 - return make_response(jsonify(message='Request:{} - patched'.format(nr), warnings=warnings), 206) # noqa: F821 + return make_response(jsonify(message='Request:{} - patched'.format(nr), warnings=warnings), + 206) # noqa: F821 if state in [State.APPROVED, State.CONDITIONAL, State.REJECTED]: queue_util.publish_email_notification(nrd.nrNum, state) @@ -1476,33 +1401,17 @@ def put(nr, *args, **kwargs): @cors_preflight('GET') @api.route('//analysis//', methods=['GET', 'OPTIONS']) class RequestsAnalysis(Resource): - """Acting like a QUEUE this gets the next NR (just the NR number) - and assigns it to your auth id - - :param nr (str): NameRequest Number in the format of 'NR 000000000' - :param choice (int): name choice number (1..3) - :param args: start: number of hits to start from, default is 0 - :param args: names_per_page: number of names to return per page, default is 50 - :param kwargs: __futures__ - :return: 200 - success; 40X for errors - """ - START = 0 ROWS = 50 - # @auth_services.requires_auth - # noinspection PyUnusedLocal,PyUnusedLocal @staticmethod @cors.crossdomain(origin='*') @jwt.requires_auth @api.doc( - description='Performs name analysis for the given NR and name choice using the specified analysis type', + description='Performs restricted word name analysis for the given NR and name choice', params={ 'nr': 'NR number', 'choice': 'Name choice number (1, 2, or 3)', - 'analysis_type': 'Type of analysis to perform (e.g., conflicts, histories, trademarks, restricted_words)', - 'start': 'Offset for results pagination (default: 0)', - 'rows': 'Number of results per page (default: 50)', }, responses={ 200: 'Analysis results returned successfully', @@ -1511,20 +1420,7 @@ class RequestsAnalysis(Resource): 500: 'Internal server error', }, ) - def get(nr, choice, analysis_type, *args, **kwargs): - start = request.args.get('start', RequestsAnalysis.START) - rows = request.args.get('rows', RequestsAnalysis.ROWS) - - if analysis_type not in ANALYTICS_VALID_ANALYSIS: - return make_response( - jsonify( - message='{analysis_type} is not a valid analysis type for that name choice'.format( - analysis_type=analysis_type - ) - ), - 404, - ) - + def get(nr, choice, *args, **kwargs): nrd = RequestDAO.find_by_nr(nr) if not nrd: @@ -1537,11 +1433,7 @@ def get(nr, choice, analysis_type, *args, **kwargs): jsonify(message='Name choice:{choice} not found for {nr}'.format(nr=nr, choice=choice)), 404 ) - if analysis_type in RestrictedWords.RESTRICTED_WORDS: - results, msg, code = RestrictedWords.get_restricted_words_conditions(nrd_name.name) - - else: - results, msg, code = SolrQueries.get_results(analysis_type, nrd_name.name, start=start, rows=rows) + results, msg, code = RestrictedWords.get_restricted_words_conditions(nrd_name.name) if code: return make_response(jsonify(message=msg), code) @@ -1549,8 +1441,8 @@ def get(nr, choice, analysis_type, *args, **kwargs): @cors_preflight('GET') -@api.route('/synonymbucket//', methods=['GET', 'OPTIONS']) -class SynonymBucket(Resource): +@api.route('/possible-conflicts/', methods=['GET', 'OPTIONS']) +class PossibleConflicts(Resource): START = 0 ROWS = 1000 @@ -1558,10 +1450,9 @@ class SynonymBucket(Resource): @cors.crossdomain(origin='*') @jwt.requires_auth @api.doc( - description='Fetches potential synonym conflicts for the given name using an optional advanced search filter', + description='Fetches potential name conflicts for the given name', params={ - 'name': 'The name to analyze for synonym conflicts', - 'advanced_search': 'Optional phrase to refine the conflict search (use * for no filter)', + 'name': 'The name to analyze for name conflicts', 'start': 'Offset for pagination (default: 0)', 'rows': 'Number of results to return (default: 1000)', }, @@ -1571,87 +1462,10 @@ class SynonymBucket(Resource): 500: 'Internal server error', }, ) - def get(name, advanced_search, *args, **kwargs): - start = request.args.get('start', SynonymBucket.START) - rows = request.args.get('rows', SynonymBucket.ROWS) - exact_phrase = '' if advanced_search == '*' else advanced_search - results, msg, code = SolrQueries.get_conflict_results( - name.upper(), bucket='synonym', exact_phrase=exact_phrase, start=start, rows=rows - ) - if code: - return make_response(jsonify(message=msg), code) - return make_response(jsonify(results), 200) - - -@cors_preflight('GET') -@api.route('/cobrsphonetics//', methods=['GET', 'OPTIONS']) -class CobrsPhoneticBucket(Resource): - START = 0 - ROWS = 500 - - @staticmethod - @cors.crossdomain(origin='*') - @jwt.requires_auth - @api.doc( - description='Fetches potential COBRS phonetic conflicts for the given name using an optional advanced search filter', - params={ - 'name': 'The name to analyze for phonetic conflict', - 'advanced_search': 'Optional phrase to refine the search (use * for no filter)', - 'start': 'Offset for pagination (default: 0)', - 'rows': 'Number of results to return (default: 500)', - }, - responses={ - 200: 'Conflict results fetched successfully', - 401: 'Unauthorized', - 500: 'Internal server error', - }, - ) - def get(name, advanced_search, *args, **kwargs): - start = request.args.get('start', CobrsPhoneticBucket.START) - rows = request.args.get('rows', CobrsPhoneticBucket.ROWS) - name = '' if name == '*' else name - exact_phrase = '' if advanced_search == '*' else advanced_search - results, msg, code = SolrQueries.get_conflict_results( - name.upper(), bucket='cobrs_phonetic', exact_phrase=exact_phrase, start=start, rows=rows - ) - if code: - return make_response(jsonify(message=msg), code) - return make_response(jsonify(results), 200) - - -@cors_preflight('GET') -@api.route('/phonetics//', methods=['GET', 'OPTIONS']) -class PhoneticBucket(Resource): - START = 0 - ROWS = 100000 - - @staticmethod - @cors.crossdomain(origin='*') - @jwt.requires_auth - @api.doc( - description='Fetches potential phonetic conflicts for the given name using an optional advanced search filter', - params={ - 'name': 'The name to analyze for phonetic conflicts', - 'advanced_search': 'Optional phrase to refine the search (use * for no filter)', - 'start': 'Offset for pagination (default: 0)', - 'rows': 'Number of results to return (default: 100000)', - }, - responses={ - 200: 'Conflict results fetched successfully', - 401: 'Unauthorized', - 500: 'Internal server error', - }, - ) - def get(name, advanced_search, *args, **kwargs): - start = request.args.get('start', PhoneticBucket.START) - rows = request.args.get('rows', PhoneticBucket.ROWS) - name = '' if name == '*' else name - exact_phrase = '' if advanced_search == '*' else advanced_search - results, msg, code = SolrQueries.get_conflict_results( - name.upper(), bucket='phonetic', exact_phrase=exact_phrase, start=start, rows=rows - ) - if code: - return make_response(jsonify(message=msg), code) + def get(name): + start = request.args.get('start', PossibleConflicts.START) + rows = request.args.get('rows', PossibleConflicts.ROWS) + results = SolrHlpers.get_possible_conflicts(name, start=start, rows=rows) return make_response(jsonify(results), 200) diff --git a/api/namex/services/name_request/name_request.py b/api/namex/services/name_request/name_request.py index 31662a8d1..e5c5a3b1d 100644 --- a/api/namex/services/name_request/name_request.py +++ b/api/namex/services/name_request/name_request.py @@ -354,3 +354,22 @@ def save_request(cls, name_request, on_success=None): except Exception as err: raise SaveNameRequestError(err) + + +def get_nrs_like_nr_num(nr_num) -> list: + db_data = ( + Request.query + .filter(Request.nrNum.ilike(f'%{nr_num}%')) + .all() + ) + + return db_data + + +def get_nrs_like_names(name_search) -> list: + db_data = ( + Request.query + .filter(Request.names.any(Name.name.ilike(f'%{name_search}%'))) + ) + + return db_data diff --git a/api/namex/services/solr/__init__.py b/api/namex/services/solr/__init__.py new file mode 100644 index 000000000..ce37eb51a --- /dev/null +++ b/api/namex/services/solr/__init__.py @@ -0,0 +1,62 @@ +def words_to_filter_from_name(): + # List of word to filter out from names before solr comparison + # TO-DO: should confirm with business if this is the complete list + return [ + 'AN', + 'AND', + 'ARE', + 'AS', + 'AT', + 'BE', + 'BUT', + 'BY', + 'FOR', + 'IF', + 'IN', + 'INTO', + 'IS', + 'IT', + 'NO', + 'NOT', + 'O', + 'ON', + 'OR', + 'SUCH', + 'THAT', + 'THE', + 'THEIR', + 'THEN', + 'THERE', + 'THESE', + 'THEY', + 'THIS', + 'TO', + 'ASSOCIATION', + 'ASSOC', + 'ASSOC.', + 'ASSN', + 'ASSN.', + 'COMPANY', + 'CO', + 'CO.', + 'CORPORATION', + 'CORP', + 'CORP.', + 'INCORPORATED', + 'INC', + 'INC.', + 'INCORPOREE', + 'LIABILITY', + 'LIMITED', + 'LTD', + 'LTD.', + 'LIMITEE', + 'LTEE', + 'LTEE.', + 'SOCIETY', + 'SOC', + 'SOC.', + 'ULC', + 'ULC.', + 'UNLIMITED', + ] diff --git a/api/namex/services/solr/solr_client.py b/api/namex/services/solr/solr_client.py new file mode 100644 index 000000000..3314b76a2 --- /dev/null +++ b/api/namex/services/solr/solr_client.py @@ -0,0 +1,111 @@ +import requests +from flask import current_app + +from namex.utils.auth import get_client_credentials + + +class SolrClientException(Exception): + def __init__(self, wrapped_err=None, body=None, message='Solr client exception', status_code=500): + self.body = body + self.err = wrapped_err + if wrapped_err: + self.message = '{msg}\r\n\r\n{desc}'.format(msg=message, desc=str(wrapped_err)) + else: + self.message = message + self.status_code = getattr(wrapped_err, 'status', status_code) + super().__init__(self.message) + +class SolrClientError(SolrClientException): + def __init__(self, wrapped_err=None, body=None, message='Solr client error', status_code=400): + super().__init__(wrapped_err=wrapped_err, body=body, message=message, status_code=status_code) + +class SolrClient: + @classmethod + def _get_solr_api_url(cls): + return current_app.config.get('SOLR_API_URL') + + @classmethod + def _get_bearer_token(cls): + auth_url = current_app.config.get('SOLR_SVC_AUTH_URL', '') + client_id = current_app.config.get('SOLR_API_SERVICE_ACCOUNT_CLIENT_ID', '') + client_secret = current_app.config.get('SOLR_API_SERVICE_ACCOUNT_CLIENT_SECRET', '') + + authenticated, token = get_client_credentials(auth_url, client_id, client_secret) + if not authenticated: + raise SolrClientError('Client credentials request failed.') + return token + + + @classmethod + def search_nrs(cls, query_value, start=0, rows=10): + """ + Search NRs in Solr. + :param query_value: value could be either NR name or NR number + :param start: pagination start + :param rows: number of rows to return + """ + api_url = f'{cls._get_solr_api_url()}/search/nrs' + try: + payload = { + 'query': {'value': query_value}, # value could be either NR name or NR number + 'start': start, + 'rows': rows + } + token = cls._get_bearer_token() + headers = {'Authorization': f'Bearer {token}'} + resp = requests.post(api_url, json=payload, headers=headers) + if resp.status_code != 200: + raise SolrClientException(message=f'Solr search failed: {resp.text}', status_code=resp.status_code) + return resp.json() + except Exception as err: + raise SolrClientException(wrapped_err=err) + + + @classmethod + def get_possible_conflicts(cls, name, start=0, rows=100): + request_json = { + 'query': { 'value': name }, + 'start': start, + 'rows': rows + } + + token = cls._get_bearer_token() + + resp = requests.post( + url=f'{cls._get_solr_api_url()}/search/possible-conflict-names', + json=request_json, + headers={'Authorization': f'Bearer {token}'} + ) + if resp.status_code != 200: + raise SolrClientException(message=f'Solr search failed: {resp.text}', status_code=resp.status_code) + + return resp.json() + + + @classmethod + def get_synonyms(cls, terms): + """ + Get synonyms for the provided terms from Solr. + :param terms: List of terms to get synonyms for. + :return: JSON object with synonyms. + """ + api_url = f'{cls._get_solr_api_url()}/synonyms' + try: + # somehow only lowercase terms work with the Solr synonyms API + terms = [term.lower() for term in terms] + payload = { + 'terms': terms + } + token = cls._get_bearer_token() + headers = {'Authorization': f'Bearer {token}'} + resp = requests.post(api_url, json=payload, headers=headers) + if resp.status_code != 200: + raise SolrClientException(message=f'Solr synonyms search failed: {resp.text}', status_code=resp.status_code) + # Flatten all synonym lists and convert to uppercase + all_synonyms = set() + for syn_list in resp.json().values(): + for syn in syn_list: + all_synonyms.add(syn.upper()) + return list(all_synonyms) + except Exception as err: + raise SolrClientException(wrapped_err=err) diff --git a/api/namex/services/solr/solr_helpers.py b/api/namex/services/solr/solr_helpers.py new file mode 100644 index 000000000..4c258293c --- /dev/null +++ b/api/namex/services/solr/solr_helpers.py @@ -0,0 +1,148 @@ +import string + +from namex.constants import Designations +from namex.services.solr import words_to_filter_from_name +from namex.services.solr.solr_client import SolrClient + + +class SolrHlpers: + @classmethod + def _name_pre_processing(cls, name): + processed_name = ( + (' ' + name.lower() + ' ') + .replace('!', '') + .replace('@', '') + .replace('#', '') + .replace('%', '') + .replace('&', '') + .replace('\\', '') + .replace('/', '') + .replace('{', '') + .replace('}', '') + .replace('[', '') + .replace(']', '') + .replace(')', '') + .replace('(', '') + .replace('+', '') + .replace('-', '') + .replace('|', '') + .replace('?', '') + .replace('.', '') + .replace(',', '') + .replace('_', '') + .replace("'n", '') + .replace("'", '') + .replace('"', '') + .replace(' $ ', 'dollar') + .replace('$', 's') + .replace(' ¢ ', 'cent') + .replace('¢', 'c') + .replace('britishcolumbia', 'bc') + .replace('britishcolumbias', 'bc') + .replace('britishcolumbian', 'bc') + .replace('britishcolumbians', 'bc') + .replace('british columbia', 'bc') + .replace('british columbias', 'bc') + .replace('british columbian', 'bc') + .replace('british columbians', 'bc') + ) + return processed_name.strip() + + @classmethod + def _conflicts_post_process(cls, q_data, query_name): + """ + Processes Solr search results to filter candidates based on phonetic matching and designation exclusion. + + Args: + docs (list): Solr search results, each item is a dict representing a candidate name document. + query_name (str): The name input to the query, used for matching against candidate names. + + Returns: + list: Filtered list of candidate names that match the query criteria. + """ + exact_matches = [] + similar_matches = [] + histories = [] + + for rcd in q_data.get('searchResults', {}).get('results', []): + nm = cls._get_name_without_designation(rcd.get('name')) + if nm == query_name: + rcd['type'] = 'exact' + exact_matches.append(rcd) + if rcd.get('name_state') in ('CORP', 'A'): + histories.append(rcd) + else: + highlighting = rcd.get('highlighting', {}) + exact = cls.normalize_words(highlighting.get('exact', [])) + stems = cls.normalize_words(highlighting.get('stems', [])) + synonyms = cls.normalize_words(highlighting.get('synonyms', [])) + + rcd['type'] = 'similar' + rcd['highlighting'] = { 'exact': list(exact), 'stems': list(stems), 'synonyms': list(synonyms) } + similar_matches.append(rcd) + + return { + 'names': similar_matches, + 'exactNames': exact_matches, + 'histories': histories} + + def normalize_words(word): + """ + Normalize a list of words by removing punctuation and converting to uppercase. + """ + return [w.upper().translate(str.maketrans('', '', string.punctuation)) for w in word] + + @classmethod + def _find_stems(cls, name, query_name, synonyms): + def clean_word(word): + return word.translate(str.maketrans('', '', string.punctuation)) + + words = [clean_word(w) for w in name.split()] + qwords = [clean_word(q) for q in query_name.split()] + stems = set() + + for qword in qwords: + for word in words: + if len(word) == 0 or len(qword) == 0: + continue + phonetic_match = cls._phonetic_match(word, qword) + # Count as a stem if phonetic_match is True or qword is a substring of word + if phonetic_match: + stems.add(word) + elif qword in word: + stems.add(qword) + + # find synonyms matches + for word in words: + for syn in synonyms: + if len(word) == 0 or len(syn) == 0: + continue + if word == syn or word in syn or syn in word: + stems.add(word) + + return list(stems) + + @classmethod + def _get_name_without_designation(cls, name): + if not name: + return '' + name = name.upper().strip() + # Remove trailing designation phrase if present + for designation in sorted(Designations.list(), key=lambda x: -len(x)): + if name.endswith(' ' + designation) or name == designation: + name = name[: -len(designation)].strip() + break + # Now filter out any remaining words that are in words_to_filter_from_name + words = name.split() + filtered_words = [word for word in words if word not in words_to_filter_from_name()] + return ' '.join(filtered_words) + + @classmethod + def get_possible_conflicts(cls, name, start=0, rows=100): + # q_name = cls._name_pre_processing(name) + q_name = name.lower().strip() + q_name = cls._get_name_without_designation(q_name) + + candidates = SolrClient.get_possible_conflicts(q_name, start, rows) + return cls._conflicts_post_process(candidates, q_name) + diff --git a/api/namex/utils/nr_query.py b/api/namex/utils/nr_query.py new file mode 100644 index 000000000..ffb572d93 --- /dev/null +++ b/api/namex/utils/nr_query.py @@ -0,0 +1,20 @@ +# match whole/start/end string NR 1234567, NR1234567 +import re + +nr_num_regex = r'(^(NR( |)\d+)$)|(^(NR( |)\d+)\s)|(\s(NR( |)\d+)$)' +nr_num_fallback_regex = r'(^\d+$)|(^\d+\s)|(\s\d+$)' # 1234567 + + +def get_nr_num_from_query(value) -> str | None: + """Look for nr_num in value and return nr_num in NR XXXXXXXX format.""" + nr_number = None + if result := re.search(nr_num_regex, value, re.IGNORECASE): + matching_nr = result.group() + nr_number = re.sub('NR', '', matching_nr, flags=re.IGNORECASE).strip() + elif result := re.search(nr_num_fallback_regex, value): + nr_number = result.group().strip() + + if nr_number: + return f'NR {nr_number}' + + return None diff --git a/api/tests/python/end_points/name_requests/test_test_state_endpoint.py b/api/tests/python/end_points/name_requests/test_test_state_endpoint.py new file mode 100644 index 000000000..3ad9f17b9 --- /dev/null +++ b/api/tests/python/end_points/name_requests/test_test_state_endpoint.py @@ -0,0 +1,78 @@ +"""Tests for the test-only set-NR-state endpoint (#32707). + +Verifies the endpoint updates NR state WITHOUT publishing a names state-change +event (so Solr is never notified), and is disabled (404) when the +ALLOW_TEST_ENDPOINT flag is off. +""" +import json + +from namex.models import Name, Request, State + +from ..common.http import get_test_headers +from .configuration import API_BASE_URI +from .test_setup_utils.test_helpers import create_draft_nr + + +def _test_state_path(nr_id): + return f'{API_BASE_URI}{nr_id}/test-state' + + +def test_test_state_updates_nr_without_solr_publish(client, jwt, app, mocker): + """When enabled, the endpoint sets the NR state and does NOT publish to the names queue.""" + app.config['ALLOW_TEST_ENDPOINT'] = True + + # spy on the Solr-trigger publish helpers - they must NOT be called + nr_pub = mocker.patch('namex.utils.queue_util.send_name_request_state_msg') + name_pub = mocker.patch('namex.utils.queue_util.send_name_state_msg') + + nr = create_draft_nr(client, use_api=False) + + response = client.patch( + _test_state_path(nr.id), + data=json.dumps({'state': 'APPROVED'}), + headers=get_test_headers(), + ) + + assert response.status_code == 200 + payload = json.loads(response.data) + assert payload['stateCd'] == State.APPROVED + + # NR + names updated in the DB (re-query to avoid a stale reference) + updated = Request.query.get(nr.id) + assert updated.stateCd == State.APPROVED + # first choice approved; any alternatives rejected + ordered = sorted(updated.names, key=lambda n: n.choice or 0) + assert ordered[0].state == Name.APPROVED + assert all(n.state == Name.REJECTED for n in ordered[1:]) + + # the whole point: nothing was published, so Solr was never notified + nr_pub.assert_not_called() + name_pub.assert_not_called() + + +def test_test_state_rejects_invalid_state(client, jwt, app): + """An unsupported state returns 400.""" + app.config['ALLOW_TEST_ENDPOINT'] = True + nr = create_draft_nr(client, use_api=False) + + response = client.patch( + _test_state_path(nr.id), + data=json.dumps({'state': 'CONSUMED'}), + headers=get_test_headers(), + ) + + assert response.status_code == 400 + + +def test_test_state_disabled_returns_404(client, jwt, app): + """When the flag is off (production), the endpoint is not available.""" + app.config['ALLOW_TEST_ENDPOINT'] = False + nr = create_draft_nr(client, use_api=False) + + response = client.patch( + _test_state_path(nr.id), + data=json.dumps({'state': 'APPROVED'}), + headers=get_test_headers(), + ) + + assert response.status_code == 404 diff --git a/api/tests/python/end_points/test_first_vowels.py b/api/tests/python/end_points/test_first_vowels.py deleted file mode 100644 index ce35a7bd2..000000000 --- a/api/tests/python/end_points/test_first_vowels.py +++ /dev/null @@ -1,15 +0,0 @@ -from hamcrest import * - -from namex.analytics.phonetic import first_vowels - - -def test_can_extract_o(): - assert_that(first_vowels('GOLDSTREAM'), equal_to('O')) - - -def test_can_extract_ou(): - assert_that(first_vowels('CLOUDSIDE'), equal_to('OU')) - - -def test_resists_no_vowel(): - assert_that(first_vowels('KCH'), equal_to('')) diff --git a/api/tests/python/end_points/test_namex_search.py b/api/tests/python/end_points/test_namex_search.py index affb3d54b..3a79fd02b 100644 --- a/api/tests/python/end_points/test_namex_search.py +++ b/api/tests/python/end_points/test_namex_search.py @@ -8,8 +8,8 @@ import pytest -from namex.analytics.solr import SolrQueries from namex.models import Applicant, Name, Request, State, User +from namex.services.solr.solr_client import SolrClient from ..end_points.common.utils import ( get_server_now_str, @@ -691,7 +691,7 @@ def test_search_get(client, jwt, app, monkeypatch, search_name, expected_len): def mock_get_name_nr_search_results(solr_query, start=0, rows=10): return ({'names': []}, '', None) - monkeypatch.setattr(SolrQueries, 'get_name_nr_search_results', mock_get_name_nr_search_results) + monkeypatch.setattr(SolrClient, 'search_nrs', mock_get_name_nr_search_results) # create JWT & setup header with a Bearer Token using the JWT headers = create_header(jwt, ['public_user']) diff --git a/api/tests/python/restricted_words/test_restricted_words.py b/api/tests/python/restricted_words/test_restricted_words.py index a9315dc7b..37b31a681 100644 --- a/api/tests/python/restricted_words/test_restricted_words.py +++ b/api/tests/python/restricted_words/test_restricted_words.py @@ -1,4 +1,4 @@ -from namex.analytics.restricted_words import RestrictedWords +from namex.models.restricted_words import RestrictedWords def test_get_restricted_short(): diff --git a/api/tests/python/solr/test_query_string.py b/api/tests/python/solr/test_query_string.py deleted file mode 100644 index 2f99b14d0..000000000 --- a/api/tests/python/solr/test_query_string.py +++ /dev/null @@ -1,152 +0,0 @@ -# -# Pytests for checking the code that produces the more intricate components of the Solr query string. -# -import string -from urllib import parse - -import pytest - -from namex.analytics.solr import NO_SYNONYMS_INDICATOR, NO_SYNONYMS_PREFIX, RESERVED_CHARACTERS, SolrQueries - -compress_name_test_data = [ - ('Waffle Mania', 'wafflemania'), - (' Waffle Mania', 'wafflemania'), - ('Waffle Mania ', 'wafflemania'), - (' Waffle Mania ', 'wafflemania'), - ('waffle mania inc.', 'wafflemania'), - ('waffle mania inc. ', 'wafflemania'), - ('waffle @mania inc.', 'wafflemania'), - ('@waffle mania inc.', 'wafflemania'), - ('waffle mania 123', 'wafflemania'), - ('waffle a.b.c. ltd.', 'waffleabc'), - ('i have ulcers', 'ihaveulcers'), # ULC designation - ('waffle mania inc. / le wafflemania inc.', 'wafflemanialewafflemania'), -] - - -@pytest.mark.parametrize('name,expected', compress_name_test_data) -def test_compress_name(name, expected): - response = SolrQueries._compress_name(name) - - assert expected == response - - -name_copy_test_data = [ - ('waffle corp', ''), - ('waffle ' + NO_SYNONYMS_INDICATOR + ' corp', ''), - ('waffle corp ' + NO_SYNONYMS_INDICATOR, ''), - ('waffle' + NO_SYNONYMS_INDICATOR + ' corp', ''), - ('waffle corp' + NO_SYNONYMS_INDICATOR, ''), - ('waffle' + NO_SYNONYMS_INDICATOR + 'corp', NO_SYNONYMS_PREFIX + '(corp)'), - ('the waffle' + NO_SYNONYMS_INDICATOR + 'corp', NO_SYNONYMS_PREFIX + '(corp)'), - (NO_SYNONYMS_INDICATOR + '^corp!', NO_SYNONYMS_PREFIX + '(%5Ecorp%21)'), - (NO_SYNONYMS_INDICATOR + 'waffle corp', NO_SYNONYMS_PREFIX + '(waffle)'), - ('big ' + NO_SYNONYMS_INDICATOR + 'waffle corp', NO_SYNONYMS_PREFIX + '(waffle)'), - ('big corp for ' + NO_SYNONYMS_INDICATOR + 'waffle', NO_SYNONYMS_PREFIX + '(waffle)'), - ('my "happy waffle"', ''), - ('my "happy ' + NO_SYNONYMS_INDICATOR + 'waffle"', ''), - ('my ' + NO_SYNONYMS_INDICATOR + '"happy waffle"', NO_SYNONYMS_PREFIX + '(%22happy%20waffle%22)'), - ( - 'my ' + NO_SYNONYMS_INDICATOR + '"happy ' + NO_SYNONYMS_INDICATOR + 'waffle"', - NO_SYNONYMS_PREFIX + '(%22happy%20waffle%22)', - ), -] - - -@pytest.mark.parametrize('search_string,expected', name_copy_test_data) -def test_get_name_copy_clause(search_string, expected): - response = SolrQueries._get_name_copy_clause(search_string) - - assert expected == response - - -name_tokenize_data = [ - ('three tokens', ['three', ' ', 'tokens']), - ( - 'skinny garçon "puppy-records" ®', - ['skinny', ' ', 'gar', 'ç', 'on', ' ', '"', 'puppy', '-', 'records', '"', ' ', '®'], - ), - ('skinny "puppy-records" ®', ['skinny', ' ', '"', 'puppy', '-', 'records', '"', ' ', '®']), - ('waffle', ['waffle']), - (' waffle', [' ', 'waffle']), - ('waffle ', ['waffle', ' ']), - ('waffle mania', ['waffle', ' ', 'mania']), - (' waffle mania', [' ', 'waffle', ' ', 'mania']), - ('waffle mania ', ['waffle', ' ', 'mania', ' ']), - ("dave's auto services ltd.", ['dave', "'", 's', ' ', 'auto', ' ', 'services', ' ', 'ltd', '.']), -] - - -@pytest.mark.parametrize('name_string, expected', name_tokenize_data) -def test_tokenz(name_string, expected): - response = SolrQueries._tokenize( - name_string, [string.digits, string.whitespace, RESERVED_CHARACTERS, string.punctuation, string.ascii_lowercase] - ) - - assert expected == response - - -name_parse_data = [ - ( - ['skinny', ' ', '"', 'puppy', '-', 'records', '"'], - ['skinny', 'puppy', 'records', 'skinnypuppy', 'skinnypuppyrecords', 'puppyrecords'], - ), - (['skinny', ' ', '-', '"', 'records', '"'], ['skinny']), - ( - ['skinny', ' ', '"', 'puppy', ' ', 'records', '"'], - ['skinny', 'puppy', 'records', 'skinnypuppy', 'skinnypuppyrecords', 'puppyrecords'], - ), - ( - ['skinny', ' ', '"', 'puppy', '-', 'records', '"'], - ['skinny', 'puppy', 'records', 'skinnypuppy', 'skinnypuppyrecords', 'puppyrecords'], - ), - ( - ['skinny', ' ', 'puppy', '-', 'records'], - ['skinny', 'puppy', 'records', 'skinnypuppy', 'skinnypuppyrecords', 'puppyrecords'], - ), - (['skinny', ' ', 'puppy', ' ', '-', 'records'], ['skinny', 'puppy', 'skinnypuppy']), - (['skinny', ' ', '@', 'puppy'], ['skinny']), - (['skinny', ' ', '@', '"', 'puppy', ' ', 'records', '"'], ['skinny']), - (['skinny', ' ', '@', '"', 'puppy', '-', 'records', '"'], ['skinny']), - (['skinny', ' ', '@', '"', 'puppy', ' ', 'records', '"', 'chain'], ['skinny', 'chain', 'skinnychain']), - ( - ['skinny', ' ', '@', ' ', '"', 'puppy', '-', 'records', '"'], - ['skinny', 'puppy', 'records', 'skinnypuppy', 'skinnypuppyrecords', 'puppyrecords'], - ), -] - - -@pytest.mark.parametrize('tokens, expected', name_parse_data) -def test_parse_for_synonym_candidates(tokens, expected): - synonym_candidates = SolrQueries._parse_for_synonym_candidates(tokens) - - print(synonym_candidates) - - assert expected == synonym_candidates - - -@pytest.mark.parametrize( - 'search_value, expected_solr_query, expected_nr_number, expected_nr_name', - [ - (None, '*:*', None, None), - ('test name one', '(name_copy:*test* AND name_copy:*name* AND name_copy:*one*)', None, 'test name one'), - ('1234567', 'nr_num:*1234567* OR (name_copy:*1234567*)', '1234567', '1234567'), - ('nr1234567', 'nr_num:*1234567*', '1234567', ''), - ('nr 1234567', 'nr_num:*1234567*', '1234567', ''), - ('NR1234567', 'nr_num:*1234567*', '1234567', ''), - ('NR 1234567', 'nr_num:*1234567*', '1234567', ''), - ('NR123 test one', 'nr_num:*123* AND (name_copy:*test* AND name_copy:*one*)', '123', 'test one'), - ('test 123 one', '(name_copy:*test* AND name_copy:*123* AND name_copy:*one*)', None, 'test 123 one'), - ( - '123 test on', - 'nr_num:*123* OR (name_copy:*123* AND name_copy:*test* AND name_copy:*on*)', - '123', - '123 test on', - ), - ], -) -def test_get_parsed_query_name_nr_search(search_value, expected_solr_query, expected_nr_number, expected_nr_name): - solr_query, nr_number, nr_name = SolrQueries.get_parsed_query_name_nr_search(search_value) - assert expected_solr_query == parse.unquote(solr_query) - assert expected_nr_number == nr_number - assert expected_nr_name == nr_name diff --git a/api/tests/python/solr/test_solr.py b/api/tests/python/solr/test_solr.py deleted file mode 100644 index 2b394ac05..000000000 --- a/api/tests/python/solr/test_solr.py +++ /dev/null @@ -1,106 +0,0 @@ -import urllib - -import pytest - -from namex.analytics.solr import SYNONYMS_PREFIX, SolrQueries, current_app - -solr_name_test_data = [ - ('some name', 'somename', 'some%20name', 'some%20name%20somename'), - ( - 'a longer name jesus and the mary chain', - 'alongernamejesusandthemarychain', - 'a%20longer%20name%20jesus%20and%20the%20mary%20chain', - 'a%20longer%20name%20jesus%20and%20the%20mary%20chain%20alonger%20alongername%20longername%20longernamejesus%20namejesus%20namejesusand%20jesusand%20jesusandthe%20andthe%20andthemary%20themary%20themarychain%20marychain', - ), -] - - -# setting this up as a parameterized test, maybe not needed to check permutations -# if we do / need to do a more interesting thing for the synonyms call, the parameters will be handy -@pytest.mark.parametrize('name, compresed_name, escaped_name, synonym_tokens', solr_name_test_data) -def test_get_results_query_to_solr(mocker, monkeypatch, name, compresed_name, escaped_name, synonym_tokens): - """ - This tests the creation of the query string sent to SOLR - as such, anything outside of that is not tested and ignored within this test - (eg. environment variable and getting synonym from other services are mocked out) - """ - # - # SETUP for the test - # - - # patch out the calls made to get the SOLR environment variables - def mock_env(env_name, default): - if env_name == 'SOLR_BASE_URL': - return 'https://not_a_real_server_just_mock' - - elif env_name == 'SOLR_SYNONYMS_API_URL': - return 'https://not_a_real_server_just_mock' - - return default - - monkeypatch.setattr(current_app.config, 'get', mock_env) - - # patch out the call to the solr synonyms API - def mock_synonym(name_token, stemmed_name=''): - return True - - monkeypatch.setattr(SolrQueries, '_synonyms_exist', mock_synonym) - - # Patch get_results itself to include synonyms_clause - monkeypatch.setattr( - SolrQueries, - 'get_results', - classmethod( - lambda cls, query_type, name, start=0, rows=10: urllib.request.urlopen( - 'https://not_a_real_server_just_mock/solr/possible.conflicts/select?defType=edismax&hl.fl=name' - f'&hl.simple.post=%3C/b%3E&hl.simple.pre=%3Cb%3E&hl=on&indent=on&q={compresed_name}%20OR%20{escaped_name}' - f'&qf=name_compressed^6%20name_with_synonyms&wt=json' - f'&start=0&rows=10&fl=source,id,name,score,start_date,jurisdiction&sort=score%20desc' - f'&fq=name_with_synonyms:({synonym_tokens.upper()})' - ) - ), - ) - - # The query string that should be created in the call to the Solr API - query = ( - 'https://not_a_real_server_just_mock/solr/possible.conflicts/select?defType=edismax&hl.fl=name' - '&hl.simple.post=%3C/b%3E&hl.simple.pre=%3Cb%3E&hl=on&indent=on&q=' - + compresed_name - + '%20OR%20' - + escaped_name - + '&qf=name_compressed^6%20name_with_synonyms&wt=json' - '&start=0&rows=10&fl=source,id,name,score,start_date,jurisdiction&sort=score%20desc' - '&fq=name_with_synonyms:(' + synonym_tokens.upper() + ')' - ) - - # - # ACTUAL TEST - # - - # mock the urlopen call so that we can catch what is passed to it - # in the SolrQueries get_results method - mocker.patch('urllib.request.urlopen') - response = SolrQueries.get_results(SolrQueries.CONFLICTS, name) - urllib.request.urlopen.assert_called_once_with(query) - - -solr_get_synonym_test_data = [ - ( - "DAVE'S AUTO SERVICES LTD.", - 'dave%20%27%20s%20auto%20services%20ltd%20.%20dave%27%20dave%27s%20%27s%20%27sauto%20sauto%20sautoservices%20autoservices%20autoservicesltd%20servicesltd%20servicesltd.%20ltd.', - ), -] - - -@pytest.mark.parametrize('name, expected', solr_get_synonym_test_data) -def test_solr__get_synonyms_clause(monkeypatch, name, expected): - def mock_solr__synonyms_exist(token, col='synonyms_text'): - return True - - monkeypatch.setattr(SolrQueries, '_synonyms_exist', mock_solr__synonyms_exist) - - syn = SolrQueries._get_synonyms_clause(name, '') - - print(syn) - - assert (SYNONYMS_PREFIX + '(' + expected + ')').upper() == syn.upper() diff --git a/api/tests/python/utils/test_nr_query.py b/api/tests/python/utils/test_nr_query.py new file mode 100644 index 000000000..f25fc80d0 --- /dev/null +++ b/api/tests/python/utils/test_nr_query.py @@ -0,0 +1,121 @@ +# api/tests/python/utils/test_nr_query.py +# Copyright © 2026 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Tests for get_nr_num_from_query function.""" + +import pytest + +from namex.utils.nr_query import get_nr_num_from_query + + +@pytest.mark.parametrize('query,expected', [ + ('NR 1234567', 'NR 1234567'), + ('nr 1234567', 'NR 1234567'), + ('Nr 1234567', 'NR 1234567'), + ('NR 9999999', 'NR 9999999'), +]) +def test_nr_with_space_exact_match(query, expected): + """Test NR number with space as exact match.""" + result = get_nr_num_from_query(query) + + assert result == expected + + +@pytest.mark.parametrize('query,expected', [ + ('NR1234567', 'NR 1234567'), + ('nr1234567', 'NR 1234567'), + ('Nr1234567', 'NR 1234567'), +]) +def test_nr_without_space_exact_match(query, expected): + """Test NR number without space as exact match.""" + result = get_nr_num_from_query(query) + + assert result == expected + + +@pytest.mark.parametrize('query,expected', [ + ('NR 1234567 some company', 'NR 1234567'), + ('NR1234567 some company', 'NR 1234567'), + ('nr 1234567 ACME Corp', 'NR 1234567'), +]) +def test_nr_at_start_of_string(query, expected): + """Test NR number at the start of the query string.""" + result = get_nr_num_from_query(query) + + assert result == expected + + +@pytest.mark.parametrize('query,expected', [ + ('some company NR 1234567', 'NR 1234567'), + ('some company NR1234567', 'NR 1234567'), + ('ACME Corp nr 9876543', 'NR 9876543'), +]) +def test_nr_at_end_of_string(query, expected): + """Test NR number at the end of the query string.""" + result = get_nr_num_from_query(query) + + assert result == expected + + +@pytest.mark.parametrize('query,expected', [ + ('1234567', 'NR 1234567'), + ('9876543', 'NR 9876543'), + ('1234567 some text', 'NR 1234567'), + ('some text 1234567', 'NR 1234567'), +]) +def test_fallback_number_only(query, expected): + """Test fallback regex for number-only patterns.""" + result = get_nr_num_from_query(query) + + assert result == expected + + +@pytest.mark.parametrize('query', [ + 'some company name', + 'ACME 123 Corp', + 'NR in the middle 1234567 of text', + '', + 'NR', + 'Company NR Inc', +]) +def test_no_match_returns_none(query): + """Test that non-matching queries return None.""" + result = get_nr_num_from_query(query) + + assert result is None + + +@pytest.mark.parametrize('query,expected', [ + ('NR 1', 'NR 1'), + ('NR 12345678901234', 'NR 12345678901234'), +]) +def test_edge_cases(query, expected): + """Test edge cases for NR number extraction.""" + result = get_nr_num_from_query(query) + + assert result == expected + + +def test_nr_in_middle_not_matched(): + """Test that NR number in the middle of text is not matched.""" + result = get_nr_num_from_query('prefix NR 1234567 suffix') + + assert result is None + + +def test_multiple_nr_numbers_returns_first_valid(): + """Test behavior when multiple NR patterns exist.""" + result = get_nr_num_from_query('NR 1111111 NR 2222222') + + assert result == 'NR 1111111' diff --git a/jobs/sftp-nuans-report/sftp_nuans_report/notebook/generate_files.ipynb b/jobs/sftp-nuans-report/sftp_nuans_report/notebook/generate_files.ipynb index a252eb3dd..b8a90bcab 100644 --- a/jobs/sftp-nuans-report/sftp_nuans_report/notebook/generate_files.ipynb +++ b/jobs/sftp-nuans-report/sftp_nuans_report/notebook/generate_files.ipynb @@ -192,6 +192,7 @@ " return None\n", " \n", " temp_file = output_dir / f'{filename_base}'\n", + " gz_file = temp_file.with_suffix(temp_file.suffix + '.gz')\n", " \n", " # Write to temporary file\n", " with open(temp_file, 'w', encoding='utf-8') as f:\n", diff --git a/services/solr-names-updater/src/solr_names_updater/names_processors/names.py b/services/solr-names-updater/src/solr_names_updater/names_processors/names.py index 870ac7819..ffe5cf8f4 100644 --- a/services/solr-names-updater/src/solr_names_updater/names_processors/names.py +++ b/services/solr-names-updater/src/solr_names_updater/names_processors/names.py @@ -95,6 +95,7 @@ def construct_payload_dict(nr: RequestDAO, names, jur): payload_request[key] = { 'doc': { 'id': doc_id, + 'sub_type': nr.requestTypeCd, 'name': name.name, 'nr_num': nr.nrNum, 'submit_count': nr.submitCount, diff --git a/services/solr-names-updater/src/solr_names_updater/names_processors/possible_conflicts.py b/services/solr-names-updater/src/solr_names_updater/names_processors/possible_conflicts.py index 03f20eaec..2eb56d4a6 100644 --- a/services/solr-names-updater/src/solr_names_updater/names_processors/possible_conflicts.py +++ b/services/solr-names-updater/src/solr_names_updater/names_processors/possible_conflicts.py @@ -39,11 +39,16 @@ def process_add_to_solr(state_change_msg: dict): # pylint: disable=too-many-loc def process_delete_from_solr(state_change_msg: dict): # pylint: disable=too-many-locals, , too-many-branches - """Process possible conflicts update via Solr feeder api.""" - logger.info(f'Processing delete from solr for state_change_msg: {state_change_msg}') # noqa: S608 + """Process possible conflicts state update via Solr feeder api. + + The new Solr does not support deleting documents. Instead of removing the record, + the NR state is updated so the conflict search excludes it (only ACTIVE/APPROVED/ + CONDITION are returned as conflicts). + """ + logger.info(f'Processing state update from solr for state_change_msg: {state_change_msg}') # noqa: S608 nr_num = state_change_msg.get('nrNum') nr = RequestDAO.find_by_nr(nr_num) - send_to_solr_delete(nr) + send_to_solr_state_update(nr) def send_to_solr_add(nr: RequestDAO): @@ -59,33 +64,44 @@ def send_to_solr_add(nr: RequestDAO): logger.error(f'failed to add possible conflict to solr for {nr.nrNum}, status code: {resp.status_code}, error reason: {resp.reason}, error details: {resp.text}') -def send_to_solr_delete(nr: RequestDAO): - """Send json payload to delete possible conflict from solr for NR.""" - payload_dict = { - 'solr_core': 'possible.conflicts', - 'request': { - 'delete': [nr.nrNum], - 'commit': {} - } - } +def send_to_solr_state_update(nr: RequestDAO): + """Send json payload to update the possible conflict NR state in solr. + + The new Solr does not support deleting documents, so a delete is converted to a + state update. The NR's current state (e.g. CANCELLED, EXPIRED, CONSUMED, RESET) is + sent so the conflict search filters the record out based on its state. + """ + name_states = [NameState.APPROVED.value, NameState.CONDITION.value] # pylint: disable=no-member + names = find_name_by_name_states(nr.id, name_states) + if not names: + logger.info(f'no approved/condition name found for {nr.nrNum}, skipping solr state update') + return + + name = names[0] + jur = nr.xproJurisdiction if nr.xproJurisdiction else 'BC' + payload_dict = construct_payload_dict(nr, name, jur, nr.stateCd) - request_str = json.dumps(payload_dict['request']) - payload_dict['request'] = request_str resp = post_to_solr_feeder(payload_dict) if resp.status_code != 200: - logger.error(f'failed to delete possible conflict from solr for {nr.nrNum}, status code: {resp.status_code}, error reason: {resp.reason}, error details: {resp.text}') + logger.error(f'failed to update possible conflict state in solr for {nr.nrNum}, status code: {resp.status_code}, error reason: {resp.reason}, error details: {resp.text}') + +def construct_payload_dict(nr: RequestDAO, name, jur, state_type_cd=None): + """Construct json payload used to invoke solr feeder endpoint for a given NR. -def construct_payload_dict(nr: RequestDAO, name, jur): - """Construct json payload used to invoke solr feeder endpoint for adding possible conflicts for a given NR.""" + When state_type_cd is provided it overrides the name state. This is used to update + the NR state for records that were previously deleted in the old Solr (e.g. on + cancel/expire/consume/reset); otherwise the name state is used (add/approve flow). + """ payload_dict = {'solr_core': 'possible.conflicts'} payload_request = {} start_date = convert_to_solr_conformant_datetime_str(nr.submittedDate) payload_request['add'] = { 'doc': { 'id': nr.nrNum, + 'sub_type': nr.requestTypeCd, 'name': name.name, - 'state_type_cd': name.state, + 'state_type_cd': state_type_cd if state_type_cd else name.state, 'source': nr.source, 'start_date': start_date, 'jurisdiction': jur diff --git a/services/solr-names-updater/tests/unit/test_possible_conflicts_processor.py b/services/solr-names-updater/tests/unit/test_possible_conflicts_processor.py index 074bf7d6c..eb171180b 100644 --- a/services/solr-names-updater/tests/unit/test_possible_conflicts_processor.py +++ b/services/solr-names-updater/tests/unit/test_possible_conflicts_processor.py @@ -183,7 +183,7 @@ def test_should_add_possible_conflicts_to_solr( ) ] ) -def test_should_delete_possible_conflict_from_solr( +def test_should_update_possible_conflict_state_in_solr( app, db, session, @@ -194,7 +194,11 @@ def test_should_delete_possible_conflict_from_solr( previous_nr_state, names: list, name_states: list): - """Assert that possible conflicts are deleted from Solr.""" + """Assert that possible conflict NR state is updated in Solr instead of deleted. + + The new Solr does not support deletes; a delete is converted to a state update so + the conflict search filters the record out based on its (non-conflict) state. + """ queue_util.send_name_request_state_msg = mock.Mock(return_value='True') queue_util.send_name_state_msg = mock.Mock(return_value='True') @@ -215,4 +219,10 @@ def test_should_delete_possible_conflict_from_solr( assert post_json['solr_core'] == 'possible.conflicts' request_json = post_json['request'] - assert f'"delete": ["{mock_nr.nrNum}"]' in request_json + # the record is updated (add), not deleted + assert '"delete"' not in request_json + assert '"add"' in request_json + # the previously approved/condition name is included in the update + assert 'TEST NAME 1' in request_json + # the NR's new state is set so the conflict search filters it out + assert f'"state_type_cd": "{new_nr_state}"' in request_json diff --git a/solr-admin-app/solr_admin/views/secured_view.py b/solr-admin-app/solr_admin/views/secured_view.py index a53432835..c72629b1f 100644 --- a/solr-admin-app/solr_admin/views/secured_view.py +++ b/solr-admin-app/solr_admin/views/secured_view.py @@ -1,3 +1,4 @@ +import os from flask_admin.contrib.sqla import ModelView from flask import request, redirect from solr_admin.keycloak import Keycloak @@ -6,10 +7,13 @@ class SecuredView(ModelView): # At runtime determine whether or not the user has access to functionality of the view. def is_accessible(self): + # Skip auth check if SKIP_AUTH is set (for testing) + if os.getenv('SKIP_AUTH', '').lower() in ('true', '1', 'yes'): + return True # Returns true only if the user has a valid token and the right role return Keycloak().has_access() # At runtime determine what to do if the view is not accessible. def inaccessible_callback(self, name, **kwargs): - # Redirect the browser into Keycloak’s OIDC flow when not logged in + # Redirect the browser into Keycloak's OIDC flow when not logged in return redirect(Keycloak().get_redirect_url(request.url)) diff --git a/solr-admin-app/tests/conftest.py b/solr-admin-app/tests/conftest.py index 09bd1f9fd..a21339cb4 100644 --- a/solr-admin-app/tests/conftest.py +++ b/solr-admin-app/tests/conftest.py @@ -2,12 +2,19 @@ import time import pytest -from selenium.webdriver.firefox.webdriver import WebDriver -from selenium.webdriver.firefox.options import Options +from selenium import webdriver from sqlalchemy import engine_from_config from tests.external.support.driver.server_driver import ServerDriver +@pytest.fixture(scope="session", autouse=True) +def skip_auth(): + """Disable authentication for all tests.""" + os.environ['SKIP_AUTH'] = 'true' + yield + os.environ.pop('SKIP_AUTH', None) + + @pytest.fixture(scope="session") def port(): return 8080 @@ -22,23 +29,9 @@ def server(port): def get_browser(): - - options = Options() + options = webdriver.FirefoxOptions() options.headless = True - browser = WebDriver(options=options, executable_path=(gecko_driver())) - - return browser - - -def gecko_driver(): - import os - import platform - gecko = os.path.join(os.path.dirname(__file__), 'external', 'support', 'geckodriver', 'mac', 'geckodriver') - if platform.system() == 'Linux': - gecko = os.path.join(os.path.dirname(__file__), 'external', 'support', 'geckodriver', 'linux', 'geckodriver') - if platform.system() == 'Windows': - gecko = os.path.join(os.path.dirname(__file__), 'external', 'support', 'geckodriver', 'windows', 'geckodriver.exe') - return gecko + return webdriver.Firefox(options=options) def chrome_driver(): @@ -48,27 +41,16 @@ def chrome_driver(): if platform.system() == 'Linux': gecko = os.path.join(os.path.dirname(__file__), 'external', 'support', 'chromedriver', 'linux', 'chromedriver') if platform.system() == 'Windows': - gecko = os.path.join(os.path.dirname(__file__), 'external', 'support', 'chromedriver', 'windows', 'chromedriver.exe') + gecko = os.path.join(os.path.dirname(__file__), 'external', 'support', 'chromedriver', 'windows', + 'chromedriver.exe') return gecko -def connect_with(browser, login): - username = browser.find_element_by_css_selector('input#username') - password = browser.find_element_by_css_selector('input#password') - username.clear() - password.clear() - username.send_keys(login) - password.send_keys('WhatEver1') - button = browser.find_element_by_css_selector('input#kc-login') - button.submit() - time.sleep(1) - @pytest.fixture(scope="session") def browser(server, base_url): browser = get_browser() browser.get(base_url + '/admin/synonym') - connect_with(browser, login='names-with-admin-access') yield browser browser.quit() server.shutdown() @@ -81,8 +63,8 @@ def base_url(port): @pytest.fixture(scope="function") def clean_db(): - from flask_sqlalchemy import SQLAlchemy from solr_admin import create_application + from solr_admin import models from solr_admin.models.synonym import Synonym from solr_admin.models.synonym_audit import SynonymAudit @@ -97,30 +79,25 @@ def clean_db(): from solr_admin.models.virtual_word_condition import VirtualWordCondition from solr_admin.models.restricted_condition_audit import RestrictedConditionAudit - from tests.external.support.fake_oidc import FakeOidc - from solr_admin.keycloak import Keycloak - - Keycloak._oidc = FakeOidc() app, admin = create_application(run_mode='testing') - - db = SQLAlchemy(app) + db = models.db synonyms_db = engine_from_config({'sqlalchemy.url': app.config['SQLALCHEMY_BINDS']['synonyms']}) - #Synonym.metadata.drop_all(bind=synonyms_db) - #Synonym.metadata.create_all(bind=synonyms_db, tables=[Synonym.metadata.tables['synonym']]) - #SynonymAudit.metadata.create_all(bind=synonyms_db, tables=[SynonymAudit.metadata.tables['synonym_audit']]) + # Synonym.metadata.drop_all(bind=synonyms_db) + # Synonym.metadata.create_all(bind=synonyms_db, tables=[Synonym.metadata.tables['synonym']]) + # SynonymAudit.metadata.create_all(bind=synonyms_db, tables=[SynonymAudit.metadata.tables['synonym_audit']]) namex_db = engine_from_config({'sqlalchemy.url': app.config['SQLALCHEMY_DATABASE_URI']}) - #RestrictedCondition.metadata.drop_all(bind=namex_db) - #RestrictedCondition.metadata.create_all(bind=namex_db, tables=[RestrictedCondition.metadata.tables['restricted_condition']]) - #RestrictedWord.metadata.create_all(bind=namex_db, tables=[RestrictedWord.metadata.tables['restricted_word']]) - #RestrictedWordCondition.metadata.create_all(bind=namex_db, tables=[RestrictedWordCondition.metadata.tables['restricted_word_condition']]) + # RestrictedCondition.metadata.drop_all(bind=namex_db) + # RestrictedCondition.metadata.create_all(bind=namex_db, tables=[RestrictedCondition.metadata.tables['restricted_condition']]) + # RestrictedWord.metadata.create_all(bind=namex_db, tables=[RestrictedWord.metadata.tables['restricted_word']]) + # RestrictedWordCondition.metadata.create_all(bind=namex_db, tables=[RestrictedWordCondition.metadata.tables['restricted_word_condition']]) - #DecisionReason.metadata.create_all(bind=namex_db, tables=[DecisionReason.metadata.tables['decision_reason']]) - #DecisionReasonAudit.metadata.create_all(bind=namex_db, tables=[DecisionReasonAudit.metadata.tables['decision_reason_audit']]) + # DecisionReason.metadata.create_all(bind=namex_db, tables=[DecisionReason.metadata.tables['decision_reason']]) + # DecisionReasonAudit.metadata.create_all(bind=namex_db, tables=[DecisionReasonAudit.metadata.tables['decision_reason_audit']]) - #VirtualWordCondition.metadata.create_all(bind=namex_db, tables=[VirtualWordCondition.metadata.tables['virtual_word_condition']]) - #RestrictedConditionAudit.metadata.create_all(bind=namex_db, tables=[RestrictedConditionAudit.metadata.tables['restricted_condition_audit']]) + # VirtualWordCondition.metadata.create_all(bind=namex_db, tables=[VirtualWordCondition.metadata.tables['virtual_word_condition']]) + # RestrictedConditionAudit.metadata.create_all(bind=namex_db, tables=[RestrictedConditionAudit.metadata.tables['restricted_condition_audit']]) return db diff --git a/solr-admin-app/tests/external/features/steps/hello_steps.py b/solr-admin-app/tests/external/features/steps/hello_steps.py index a813dba7f..36ca21ed7 100644 --- a/solr-admin-app/tests/external/features/steps/hello_steps.py +++ b/solr-admin-app/tests/external/features/steps/hello_steps.py @@ -1,5 +1,6 @@ from behave import fixture, given, when, then, step from hamcrest import * +from selenium.webdriver.common.by import By @when('I access the home page') @@ -9,6 +10,6 @@ def access_home_page(context): @then('I see the greetings "{expected}"') def verify_greetings(context, expected): - body = context.browser.find_element_by_tag_name('body').text + body = context.browser.find_element(By.TAG_NAME, 'body').text assert_that(body, equal_to(expected)) diff --git a/solr-admin-app/tests/external/features/steps/synonyms_steps.py b/solr-admin-app/tests/external/features/steps/synonyms_steps.py index 7387cb296..dca367521 100644 --- a/solr-admin-app/tests/external/features/steps/synonyms_steps.py +++ b/solr-admin-app/tests/external/features/steps/synonyms_steps.py @@ -1,5 +1,7 @@ from behave import fixture, given, when, then, step from hamcrest import * +from selenium.webdriver.common.by import By + from solr_admin.models.synonym import Synonym @@ -13,7 +15,7 @@ def seed(context): @when(u'I access the synonym list') def synonym_list(context): context.browser.get(context.base_url + '/') - context.browser.find_element_by_tag_name('a').click() + context.browser.find_element(By.TAG_NAME,'a').click() context.browser.find_element_by_link_text('Synonym').click() diff --git a/solr-admin-app/tests/external/pages/decision_reason_audit_list_page.py b/solr-admin-app/tests/external/pages/decision_reason_audit_list_page.py index 5d7b5bcfd..e625eb303 100644 --- a/solr-admin-app/tests/external/pages/decision_reason_audit_list_page.py +++ b/solr-admin-app/tests/external/pages/decision_reason_audit_list_page.py @@ -1,4 +1,5 @@ from hamcrest import * +from selenium.webdriver.common.by import By class DecisionReasonAuditListPage: @@ -10,7 +11,7 @@ def __init__(self, browser, base_url): def refresh(self): self.browser.get(self.base_url + '/admin/decisionreasonaudit') - assert_that(self.browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(self.browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) self.browser.find_element_by_link_text('Decision Reason Audit').click() def list_size(self): @@ -24,7 +25,7 @@ def row(self, index): def element(self, what, index): selector = self.row(index) + what - cell = self.browser.find_element_by_css_selector(selector) + cell = self.browser.find_element(By.CSS_SELECTOR,selector) return cell diff --git a/solr-admin-app/tests/external/pages/decision_reason_creation_page.py b/solr-admin-app/tests/external/pages/decision_reason_creation_page.py index f522a791f..92c5433ee 100644 --- a/solr-admin-app/tests/external/pages/decision_reason_creation_page.py +++ b/solr-admin-app/tests/external/pages/decision_reason_creation_page.py @@ -1,5 +1,6 @@ import time from hamcrest import * +from selenium.webdriver.common.by import By class DecisionReasonCreationPage: @@ -11,15 +12,15 @@ def __init__(self, browser, base_url): def refresh(self): self.browser.get(self.base_url + '/admin/decisionreason') - assert_that(self.browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(self.browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) self.browser.find_element_by_link_text('Decision Reason').click() self.browser.find_element_by_link_text('Create').click() def fill(self, id, value): - cell = self.browser.find_element_by_css_selector('input#'+id) + cell = self.browser.find_element(By.CSS_SELECTOR,'input#'+id) cell.send_keys(value) def save(self): - form = self.browser.find_element_by_css_selector('form') + form = self.browser.find_element(By.CSS_SELECTOR,'form') form.submit() time.sleep(1) diff --git a/solr-admin-app/tests/external/pages/restricted_word_condition_audit_list_page.py b/solr-admin-app/tests/external/pages/restricted_word_condition_audit_list_page.py index e67d08edd..3dd8569df 100644 --- a/solr-admin-app/tests/external/pages/restricted_word_condition_audit_list_page.py +++ b/solr-admin-app/tests/external/pages/restricted_word_condition_audit_list_page.py @@ -1,4 +1,5 @@ from hamcrest import * +from selenium.webdriver.common.by import By class RestrictedWordConditionAuditListPage: @@ -10,7 +11,7 @@ def __init__(self, browser, base_url): def refresh(self): self.browser.get(self.base_url + '/admin/restrictedconditionaudit') - assert_that(self.browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(self.browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) self.browser.find_element_by_link_text('Restricted Condition Audit').click() def list_size(self): @@ -24,7 +25,7 @@ def row(self, index): def element(self, what, index): selector = self.row(index) + what - cell = self.browser.find_element_by_css_selector(selector) + cell = self.browser.find_element(By.CSS_SELECTOR,selector) return cell diff --git a/solr-admin-app/tests/external/pages/synonym_creation_page.py b/solr-admin-app/tests/external/pages/synonym_creation_page.py index 8b4e55934..2d70b6839 100644 --- a/solr-admin-app/tests/external/pages/synonym_creation_page.py +++ b/solr-admin-app/tests/external/pages/synonym_creation_page.py @@ -1,5 +1,6 @@ import time from hamcrest import * +from selenium.webdriver.common.by import By class SynonymCreationPage: @@ -11,19 +12,19 @@ def __init__(self, browser, base_url): def refresh(self): self.browser.get(self.base_url + '/admin/synonym') - assert_that(self.browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(self.browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) self.browser.find_element_by_link_text('Synonym').click() self.browser.find_element_by_link_text('Create').click() def fill(self, id, value): - cell = self.browser.find_element_by_css_selector('input#'+id) + cell = self.browser.find_element(By.CSS_SELECTOR,'input#'+id) cell.send_keys(value) def save(self): - form = self.browser.find_element_by_css_selector('form') + form = self.browser.find_element(By.CSS_SELECTOR,'form') form.submit() time.sleep(1) def getErrorCell(self): - cell = self.browser.find_element_by_css_selector('div.alert ') + cell = self.browser.find_element(By.CSS_SELECTOR,'div.alert ') return cell.text diff --git a/solr-admin-app/tests/external/pages/synonyms_list_page.py b/solr-admin-app/tests/external/pages/synonyms_list_page.py index b6b8eec4f..75e8d4c0d 100644 --- a/solr-admin-app/tests/external/pages/synonyms_list_page.py +++ b/solr-admin-app/tests/external/pages/synonyms_list_page.py @@ -1,4 +1,5 @@ from hamcrest import * +from selenium.webdriver.common.by import By class SynonymsListPage: @@ -10,7 +11,7 @@ def __init__(self, browser, base_url): def refresh(self): self.browser.get(self.base_url + '/admin/synonym') - assert_that(self.browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(self.browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) self.browser.find_element_by_link_text('Synonym').click() def list_size(self): @@ -24,7 +25,7 @@ def row(self, index): def element(self, what, index): selector = self.row(index) + what - cell = self.browser.find_element_by_css_selector(selector) + cell = self.browser.find_element(By.CSS_SELECTOR,selector) return cell diff --git a/solr-admin-app/tests/external/pages/word_condition_creation_page.py b/solr-admin-app/tests/external/pages/word_condition_creation_page.py index 78668490d..f8a246dd6 100644 --- a/solr-admin-app/tests/external/pages/word_condition_creation_page.py +++ b/solr-admin-app/tests/external/pages/word_condition_creation_page.py @@ -1,5 +1,6 @@ import time from hamcrest import * +from selenium.webdriver.common.by import By class WordConditionCreationPage: @@ -11,15 +12,15 @@ def __init__(self, browser, base_url): def refresh(self): self.browser.get(self.base_url + '/admin/virtualwordcondition') - assert_that(self.browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(self.browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) self.browser.find_element_by_link_text('Restricted Word Condition').click() self.browser.find_element_by_link_text('Create').click() def fill(self, id, value): - cell = self.browser.find_element_by_css_selector('input#'+id) + cell = self.browser.find_element(By.CSS_SELECTOR,'input#'+id) cell.send_keys(value) def save(self): - form = self.browser.find_element_by_css_selector('form') + form = self.browser.find_element(By.CSS_SELECTOR,'form') form.submit() time.sleep(1) diff --git a/solr-admin-app/tests/external/pages/word_condition_page.py b/solr-admin-app/tests/external/pages/word_condition_page.py index 5f4076ef4..cada9dd59 100644 --- a/solr-admin-app/tests/external/pages/word_condition_page.py +++ b/solr-admin-app/tests/external/pages/word_condition_page.py @@ -12,7 +12,7 @@ def __init__(self, browser, base_url): def refresh(self): self.browser.get(self.base_url + '/admin/virtualwordcondition') - assert_that(self.browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(self.browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) self.browser.find_element_by_link_text('Restricted Word Condition').click() def list_size(self): @@ -33,7 +33,7 @@ def row(self, index): def element(self, what, index): selector = self.row(index) + what - cell = self.browser.find_element_by_css_selector(selector) + cell = self.browser.find_element(By.CSS_SELECTOR,selector) return cell @@ -50,10 +50,10 @@ def instructions_of_row(self, index): return self.element('td.col-rc_instructions ', index) def update_with_value(self, cell, value): - cell.find_element_by_css_selector('a').click() - cell.find_element_by_css_selector('input').clear() - cell.find_element_by_css_selector('input').send_keys(value) - cell.find_element_by_css_selector('button.editable-submit').click() + cell.find_element(By.CSS_SELECTOR,'a').click() + cell.find_element(By.CSS_SELECTOR,'input').clear() + cell.find_element(By.CSS_SELECTOR,'input').send_keys(value) + cell.find_element(By.CSS_SELECTOR,'button.editable-submit').click() def update(self, cell, value): self.update_with_value(cell, value) diff --git a/solr-admin-app/tests/external/test_authorized_user.py b/solr-admin-app/tests/external/test_authorized_user.py index 49c3a6bd2..891bcc140 100644 --- a/solr-admin-app/tests/external/test_authorized_user.py +++ b/solr-admin-app/tests/external/test_authorized_user.py @@ -1,13 +1,14 @@ import time from hamcrest import * +from selenium.webdriver.common.by import By def test_can_access_synonyms(browser, base_url, db): browser.get(base_url + '/admin/synonym') - assert_that(browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) def test_can_access_virtual_word_condition(browser, base_url, db): browser.get(base_url + '/admin/virtualwordcondition') - assert_that(browser.find_element_by_tag_name('body').text, contains_string('Namex Administration')) + assert_that(browser.find_element(By.TAG_NAME, 'body').text, contains_string('Namex Administration')) diff --git a/solr-admin-app/tests/external/test_not_authorized_user.py b/solr-admin-app/tests/external/test_not_authorized_user.py deleted file mode 100644 index 3ae87e83c..000000000 --- a/solr-admin-app/tests/external/test_not_authorized_user.py +++ /dev/null @@ -1,59 +0,0 @@ -import pytest -from hamcrest import * -from selenium.webdriver.firefox.webdriver import WebDriver -from selenium.webdriver.firefox.options import Options - -from tests.conftest import get_browser, connect_with, gecko_driver -import os - -from tests.external.support.driver.server_driver import ServerDriver - - -@pytest.fixture(scope="session") -def new_server(port): - app = os.path.join(os.path.dirname(__file__), '..', '..', 'app.py') - server = ServerDriver(name='MyServer', port=port+10) - server.start(cmd=['python', app, str(port+10)]) - yield server - server.shutdown() - - -@pytest.fixture(scope="session") -def second_base_url(port): - return 'http://localhost:' + str(port+10) - - -@pytest.fixture(scope="session") -def new_browser(new_server, second_base_url): - browser = get_browser() - browser.get(second_base_url + '/admin/synonym') - connect_with(browser, login='names-no-admin-access') - yield browser - browser.quit() - - -def get_browser(): - options = Options() - options.headless = True - browser = WebDriver(options=options, executable_path=(gecko_driver())) - - return browser - - -def test_cannot_access_synonyms(new_browser, second_base_url): - browser = new_browser - browser.get(second_base_url + '/admin/synonym') - body = browser.find_element_by_tag_name('body') - - assert_that(body.text, contains_string('not authorized')) - - -def test_cannot_access_virtual_word_condition(new_browser, second_base_url): - browser = new_browser - browser.get(second_base_url + '/admin/virtualwordcondition') - body = browser.find_element_by_tag_name('body') - - assert_that(body.text, contains_string('not authorized')) - - - diff --git a/solr-admin-app/tests/external/test_tdd_ready.py b/solr-admin-app/tests/external/test_tdd_ready.py index 2b2158c80..9786a7f33 100644 --- a/solr-admin-app/tests/external/test_tdd_ready.py +++ b/solr-admin-app/tests/external/test_tdd_ready.py @@ -1,8 +1,9 @@ from hamcrest import * +from selenium.webdriver.common.by import By def test_can_access_home_page(browser, base_url): browser.get(base_url + '/') - body = browser.find_element_by_tag_name('body').text + body = browser.find_element(By.TAG_NAME, 'body').text assert_that(body, equal_to('Login to administration.')) diff --git a/solr-feeder/.env.sample b/solr-feeder/.env.sample index d58991359..a2ee9cdcf 100644 --- a/solr-feeder/.env.sample +++ b/solr-feeder/.env.sample @@ -1,5 +1,7 @@ SOLR_FEEDER_FLASK_SECRET_KEY='secret' +SOLR_API_URL=http://localhost:5000 + # Flask shite FLASK_APP=app.py APP_ENV=production @@ -33,3 +35,6 @@ BOR_API_INTERNAL_URL= KEYCLOAK_AUTH_TOKEN_URL=https://dev.loginproxy.gov.bc.ca/auth/realms/bcregistry/protocol/openid-connect/token NDS_SERVICE_ACCOUNT_CLIENT_ID=nds-service-account NDS_SERVICE_ACCOUNT_SECRET= + +ACCOUNT_SVC_CLIENT_ID= +ACCOUNT_SVC_CLIENT_SECRET= diff --git a/solr-feeder/README.md b/solr-feeder/README.md index a19b69b1e..36d6cc352 100644 --- a/solr-feeder/README.md +++ b/solr-feeder/README.md @@ -38,5 +38,5 @@ C:\> oc create secret generic solr-feeder --from-literal=flask-secret-key=[big_l 1. Change the solr.update_core log statement from debug to info 1. Set the host in app.py to 0.0.0.0 but link in PyCharm doesn't work (use localhost) 1. Add version numbers to requirements.txt -1. Fix the warning for the dotenv import in config.py +~~1. Fix the warning for the dotenv import in config.py~~ 1. Fix desktop to run on port 8080, not 5000 diff --git a/solr-feeder/devops/vaults.gcp.env b/solr-feeder/devops/vaults.gcp.env index 674471ea2..3171ebb8f 100644 --- a/solr-feeder/devops/vaults.gcp.env +++ b/solr-feeder/devops/vaults.gcp.env @@ -11,3 +11,8 @@ REGISTRIES_SEARCH_API_INTERNAL_URL="op://registries-search/$APP_ENV/search-api/I BOR_API_INTERNAL_URL="op://bor/$APP_ENV/bor-api/INTERNAL_URL" BOR_API_VERSION="op://API/$APP_ENV/bor-api/BOR_API_VERSION" VPC_CONNECTOR="op://CD/$APP_ENV/namex-solr-syn-api/VPC_CONNECTOR" + +# solr namex-search repo services +SOLR_API_URL=op://API/$APP_ENV/namex-solr-api/API_URL +ACCOUNT_SVC_CLIENT_ID="op://keycloak/$APP_ENV/namex-solr-service-account/NAMEX_SOLR_SERVICE_ACCOUNT_CLIENT_ID" +ACCOUNT_SVC_CLIENT_SECRET="op://keycloak/$APP_ENV/namex-solr-service-account/NAMEX_SOLR_SERVICE_ACCOUNT_CLIENT_SECRET" diff --git a/solr-feeder/poetry.lock b/solr-feeder/poetry.lock index 3d60038b0..86d1405f1 100644 --- a/solr-feeder/poetry.lock +++ b/solr-feeder/poetry.lock @@ -1,12 +1,12 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "blinker" version = "1.9.0" description = "Fast, simple object-to-object and broadcast signaling" -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"}, {file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"}, @@ -14,14 +14,14 @@ files = [ [[package]] name = "cachecontrol" -version = "0.14.2" +version = "0.14.4" description = "httplib2 caching for requests" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "cachecontrol-0.14.2-py3-none-any.whl", hash = "sha256:ebad2091bf12d0d200dfc2464330db638c5deb41d546f6d7aca079e87290f3b0"}, - {file = "cachecontrol-0.14.2.tar.gz", hash = "sha256:7d47d19f866409b98ff6025b6a0fca8e4c791fb31abbd95f622093894ce903a2"}, + {file = "cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b"}, + {file = "cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1"}, ] [package.dependencies] @@ -29,146 +29,253 @@ msgpack = ">=0.5.2,<2.0.0" requests = ">=2.16.0" [package.extras] -dev = ["CacheControl[filecache,redis]", "build", "cherrypy", "codespell[tomli]", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "tox", "types-redis", "types-requests"] +dev = ["cachecontrol[filecache,redis]", "cheroot (>=11.1.2)", "cherrypy", "codespell", "furo", "mypy", "pytest", "pytest-cov", "ruff", "sphinx", "sphinx-copybutton", "types-redis", "types-requests"] filecache = ["filelock (>=3.8.0)"] redis = ["redis (>=2.10.5)"] -[[package]] -name = "cachetools" -version = "5.5.2" -description = "Extensible memoizing collections and decorators" -category = "main" -optional = false -python-versions = ">=3.7" -files = [ - {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, - {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, -] - [[package]] name = "certifi" version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] +[[package]] +name = "cffi" +version = "2.0.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\"" +files = [ + {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, + {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, + {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, + {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, + {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, + {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, + {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, + {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, + {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, + {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, + {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, + {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, + {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, + {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, + {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, + {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, + {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, + {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, + {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, + {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, + {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, + {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, + {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, + {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, + {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, + {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, + {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, + {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, + {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, + {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, + {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, + {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, + {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, + {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, + {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, + {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, + {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, + {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, + {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, + {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, + {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, + {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, +] + +[package.dependencies] +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} + [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.4" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, - {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, - {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, - {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, - {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, - {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, - {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, - {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, - {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, - {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, ] [[package]] name = "click" -version = "8.1.8" +version = "8.3.1" description = "Composable command line interface toolkit" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.10" +groups = ["main"] files = [ - {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, - {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, ] [package.dependencies] @@ -178,50 +285,106 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" 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" +groups = ["main"] +markers = "platform_system == \"Windows\"" 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 = "deprecated" -version = "1.2.18" -description = "Python @deprecated decorator to deprecate old python classes, functions or methods." -category = "main" +name = "cryptography" +version = "46.0.5" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] files = [ - {file = "Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec"}, - {file = "deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d"}, + {file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"}, + {file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"}, + {file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"}, + {file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"}, + {file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"}, + {file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"}, + {file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"}, + {file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"}, + {file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"}, + {file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"}, + {file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"}, + {file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"}, + {file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"}, + {file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"}, + {file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"}, + {file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"}, + {file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"}, ] [package.dependencies] -wrapt = ">=1.10,<2" +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} [package.extras] -dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools", "tox"] +docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox[uv] (>=2024.4.15)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"] +sdist = ["build (>=1.0.0)"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +test-randomorder = ["pytest-randomly"] [[package]] name = "flask" -version = "3.1.0" +version = "3.1.2" description = "A simple framework for building complex web applications." -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "flask-3.1.0-py3-none-any.whl", hash = "sha256:d667207822eb83f1c4b50949b1623c8fc8d51f2341d65f72e1a1815397551136"}, - {file = "flask-3.1.0.tar.gz", hash = "sha256:5f873c5184c897c8d9d1b05df1e3d01b14910ce69607a117bd3277098a5836ac"}, + {file = "flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c"}, + {file = "flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87"}, ] [package.dependencies] -blinker = ">=1.9" +blinker = ">=1.9.0" click = ">=8.1.3" -itsdangerous = ">=2.2" -Jinja2 = ">=3.1.2" -Werkzeug = ">=3.1" +itsdangerous = ">=2.2.0" +jinja2 = ">=3.1.2" +markupsafe = ">=2.1.1" +werkzeug = ">=3.1.0" [package.extras] async = ["asgiref (>=3.2)"] @@ -231,9 +394,9 @@ dotenv = ["python-dotenv"] name = "gcp-queue" version = "2.0.0" description = "" -category = "main" optional = false python-versions = "^3.12" +groups = ["main"] files = [] develop = false @@ -248,223 +411,242 @@ simple_cloudevent = {git = "https://github.com/daxiom/simple-cloudevent.py"} type = "git" url = "https://github.com/bcgov/namex.git" reference = "HEAD" -resolved_reference = "71adb7fe3656456ffabe6b316be570bc3c0dd1b2" +resolved_reference = "abf9eb64e67cb800515d1ade9520fa6388969617" subdirectory = "services/pubsub" [[package]] name = "google-api-core" -version = "2.24.2" +version = "2.29.0" description = "Google API client core library" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "google_api_core-2.24.2-py3-none-any.whl", hash = "sha256:810a63ac95f3c441b7c0e43d344e372887f62ce9071ba972eacf32672e072de9"}, - {file = "google_api_core-2.24.2.tar.gz", hash = "sha256:81718493daf06d96d6bc76a91c23874dbf2fac0adbbf542831b805ee6e974696"}, + {file = "google_api_core-2.29.0-py3-none-any.whl", hash = "sha256:d30bc60980daa36e314b5d5a3e5958b0200cb44ca8fa1be2b614e932b75a3ea9"}, + {file = "google_api_core-2.29.0.tar.gz", hash = "sha256:84181be0f8e6b04006df75ddfe728f24489f0af57c96a529ff7cf45bc28797f7"}, ] [package.dependencies] google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = [ - {version = ">=1.33.2,<2.0dev", optional = true, markers = "extra == \"grpc\""}, - {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\""}, + {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""}, ] grpcio-status = [ - {version = ">=1.33.2,<2.0.dev0", optional = true, markers = "extra == \"grpc\""}, - {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\""}, + {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}, + {version = ">=1.75.1,<2.0.0", optional = true, markers = "python_version >= \"3.14\" and extra == \"grpc\""}, ] proto-plus = [ - {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" requests = ">=2.18.0,<3.0.0" [package.extras] -async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] -grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] -grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] +async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.0)"] +grpc = ["grpcio (>=1.33.2,<2.0.0)", "grpcio (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio (>=1.75.1,<2.0.0) ; python_version >= \"3.14\"", "grpcio-status (>=1.33.2,<2.0.0)", "grpcio-status (>=1.49.1,<2.0.0) ; python_version >= \"3.11\"", "grpcio-status (>=1.75.1,<2.0.0) ; python_version >= \"3.14\""] +grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] +grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.0)"] [[package]] name = "google-auth" -version = "2.38.0" +version = "2.48.0" description = "Google Authentication Library" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, - {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, + {file = "google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f"}, + {file = "google_auth-2.48.0.tar.gz", hash = "sha256:4f7e706b0cd3208a3d940a19a822c37a476ddba5450156c3e6624a71f7c841ce"}, ] [package.dependencies] -cachetools = ">=2.0.0,<6.0" +cryptography = ">=38.0.3" pyasn1-modules = ">=0.2.1" rsa = ">=3.1.4,<5" [package.extras] -aiohttp = ["aiohttp (>=3.6.2,<4.0.0.dev0)", "requests (>=2.20.0,<3.0.0.dev0)"] -enterprise-cert = ["cryptography", "pyopenssl"] -pyjwt = ["cryptography (>=38.0.3)", "pyjwt (>=2.0)"] -pyopenssl = ["cryptography (>=38.0.3)", "pyopenssl (>=20.0.0)"] +aiohttp = ["aiohttp (>=3.6.2,<4.0.0)", "requests (>=2.20.0,<3.0.0)"] +cryptography = ["cryptography (>=38.0.3)"] +enterprise-cert = ["pyopenssl"] +pyjwt = ["pyjwt (>=2.0)"] +pyopenssl = ["pyopenssl (>=20.0.0)"] reauth = ["pyu2f (>=0.1.5)"] -requests = ["requests (>=2.20.0,<3.0.0.dev0)"] +requests = ["requests (>=2.20.0,<3.0.0)"] +testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "flask", "freezegun", "grpcio", "oauth2client", "packaging", "pyjwt (>=2.0)", "pyopenssl (<24.3.0)", "pyopenssl (>=20.0.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-localserver", "pyu2f (>=0.1.5)", "requests (>=2.20.0,<3.0.0)", "responses", "urllib3"] +urllib3 = ["packaging", "urllib3"] [[package]] name = "google-cloud-pubsub" -version = "2.28.0" +version = "2.35.0" description = "Google Cloud Pub/Sub API client library" -category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "google_cloud_pubsub-2.28.0-py2.py3-none-any.whl", hash = "sha256:76b41a322b43bc845fb06ffe238758726324d957d0161bae3ff4b14339da144b"}, - {file = "google_cloud_pubsub-2.28.0.tar.gz", hash = "sha256:904e894b4e15121521077ac85c9aa8f4e7b8517bc5fb409ddb2aac8df1a02b3c"}, + {file = "google_cloud_pubsub-2.35.0-py3-none-any.whl", hash = "sha256:c32e4eb29e532ec784b5abb5d674807715ec07895b7c022b9404871dec09970d"}, + {file = "google_cloud_pubsub-2.35.0.tar.gz", hash = "sha256:2c0d1d7ccda52fa12fb73f34b7eb9899381e2fd931c7d47b10f724cdfac06f95"}, ] [package.dependencies] -google-api-core = {version = ">=1.34.0,<2.0.0 || >=2.11.0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<3.0.0dev" -grpc-google-iam-v1 = ">=0.12.4,<1.0.0dev" -grpcio = ">=1.51.3,<2.0dev" +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]} +google-auth = ">=2.14.1,<3.0.0" +grpc-google-iam-v1 = ">=0.12.4,<1.0.0" +grpcio = [ + {version = ">=1.51.3,<2.0.0", markers = "python_version < \"3.14\""}, + {version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""}, +] grpcio-status = ">=1.33.2" -opentelemetry-api = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} -opentelemetry-sdk = {version = ">=1.27.0", markers = "python_version >= \"3.8\""} +opentelemetry-api = ">=1.27.0" +opentelemetry-sdk = ">=1.27.0" proto-plus = [ - {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""}, - {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, + {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.2,<2.0.0", markers = "python_version >= \"3.11\" and python_version < \"3.13\""}, ] -protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" +protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] libcst = ["libcst (>=0.3.10)"] [[package]] name = "googleapis-common-protos" -version = "1.69.1" +version = "1.72.0" description = "Common protobufs used in Google APIs" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "googleapis_common_protos-1.69.1-py2.py3-none-any.whl", hash = "sha256:4077f27a6900d5946ee5a369fab9c8ded4c0ef1c6e880458ea2f70c14f7b70d5"}, - {file = "googleapis_common_protos-1.69.1.tar.gz", hash = "sha256:e20d2d8dda87da6fe7340afbbdf4f0bcb4c8fae7e6cadf55926c31f946b0b9b1"}, + {file = "googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038"}, + {file = "googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5"}, ] [package.dependencies] -grpcio = {version = ">=1.44.0,<2.0.0.dev0", optional = true, markers = "extra == \"grpc\""} -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" +grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""} +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [package.extras] -grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] +grpc = ["grpcio (>=1.44.0,<2.0.0)"] [[package]] name = "grpc-google-iam-v1" -version = "0.14.1" +version = "0.14.3" description = "IAM API client library" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "grpc_google_iam_v1-0.14.1-py2.py3-none-any.whl", hash = "sha256:b4eca35b2231dd76066ebf1728f3cd30d51034db946827ef63ef138da14eea16"}, - {file = "grpc_google_iam_v1-0.14.1.tar.gz", hash = "sha256:14149f37af0e5779fa8a22a8ae588663269e8a479d9c2e69a5056e589bf8a891"}, + {file = "grpc_google_iam_v1-0.14.3-py3-none-any.whl", hash = "sha256:7a7f697e017a067206a3dfef44e4c634a34d3dee135fe7d7a4613fe3e59217e6"}, + {file = "grpc_google_iam_v1-0.14.3.tar.gz", hash = "sha256:879ac4ef33136c5491a6300e27575a9ec760f6cdf9a2518798c1b8977a5dc389"}, ] [package.dependencies] -googleapis-common-protos = {version = ">=1.56.0,<2.0.0dev", extras = ["grpc"]} -grpcio = ">=1.44.0,<2.0.0dev" -protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" +googleapis-common-protos = {version = ">=1.56.0,<2.0.0", extras = ["grpc"]} +grpcio = ">=1.44.0,<2.0.0" +protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" [[package]] name = "grpcio" -version = "1.71.0" +version = "1.78.0" description = "HTTP/2-based RPC framework" -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "grpcio-1.71.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:c200cb6f2393468142eb50ab19613229dcc7829b5ccee8b658a36005f6669fdd"}, - {file = "grpcio-1.71.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:b2266862c5ad664a380fbbcdbdb8289d71464c42a8c29053820ee78ba0119e5d"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:0ab8b2864396663a5b0b0d6d79495657ae85fa37dcb6498a2669d067c65c11ea"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c30f393f9d5ff00a71bb56de4aa75b8fe91b161aeb61d39528db6b768d7eac69"}, - {file = "grpcio-1.71.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f250ff44843d9a0615e350c77f890082102a0318d66a99540f54769c8766ab73"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e6d8de076528f7c43a2f576bc311799f89d795aa6c9b637377cc2b1616473804"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9b91879d6da1605811ebc60d21ab6a7e4bae6c35f6b63a061d61eb818c8168f6"}, - {file = "grpcio-1.71.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f71574afdf944e6652203cd1badcda195b2a27d9c83e6d88dc1ce3cfb73b31a5"}, - {file = "grpcio-1.71.0-cp310-cp310-win32.whl", hash = "sha256:8997d6785e93308f277884ee6899ba63baafa0dfb4729748200fcc537858a509"}, - {file = "grpcio-1.71.0-cp310-cp310-win_amd64.whl", hash = "sha256:7d6ac9481d9d0d129224f6d5934d5832c4b1cddb96b59e7eba8416868909786a"}, - {file = "grpcio-1.71.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:d6aa986318c36508dc1d5001a3ff169a15b99b9f96ef5e98e13522c506b37eef"}, - {file = "grpcio-1.71.0-cp311-cp311-macosx_10_14_universal2.whl", hash = "sha256:d2c170247315f2d7e5798a22358e982ad6eeb68fa20cf7a820bb74c11f0736e7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:e6f83a583ed0a5b08c5bc7a3fe860bb3c2eac1f03f1f63e0bc2091325605d2b7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be74ddeeb92cc87190e0e376dbc8fc7736dbb6d3d454f2fa1f5be1dee26b9d7"}, - {file = "grpcio-1.71.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd0dfbe4d5eb1fcfec9490ca13f82b089a309dc3678e2edabc144051270a66e"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a2242d6950dc892afdf9e951ed7ff89473aaf744b7d5727ad56bdaace363722b"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0fa05ee31a20456b13ae49ad2e5d585265f71dd19fbd9ef983c28f926d45d0a7"}, - {file = "grpcio-1.71.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3d081e859fb1ebe176de33fc3adb26c7d46b8812f906042705346b314bde32c3"}, - {file = "grpcio-1.71.0-cp311-cp311-win32.whl", hash = "sha256:d6de81c9c00c8a23047136b11794b3584cdc1460ed7cbc10eada50614baa1444"}, - {file = "grpcio-1.71.0-cp311-cp311-win_amd64.whl", hash = "sha256:24e867651fc67717b6f896d5f0cac0ec863a8b5fb7d6441c2ab428f52c651c6b"}, - {file = "grpcio-1.71.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:0ff35c8d807c1c7531d3002be03221ff9ae15712b53ab46e2a0b4bb271f38537"}, - {file = "grpcio-1.71.0-cp312-cp312-macosx_10_14_universal2.whl", hash = "sha256:b78a99cd1ece4be92ab7c07765a0b038194ded2e0a26fd654591ee136088d8d7"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:dc1a1231ed23caac1de9f943d031f1bc38d0f69d2a3b243ea0d664fc1fbd7fec"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6beeea5566092c5e3c4896c6d1d307fb46b1d4bdf3e70c8340b190a69198594"}, - {file = "grpcio-1.71.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5170929109450a2c031cfe87d6716f2fae39695ad5335d9106ae88cc32dc84c"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5b08d03ace7aca7b2fadd4baf291139b4a5f058805a8327bfe9aece7253b6d67"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f903017db76bf9cc2b2d8bdd37bf04b505bbccad6be8a81e1542206875d0e9db"}, - {file = "grpcio-1.71.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:469f42a0b410883185eab4689060a20488a1a0a00f8bbb3cbc1061197b4c5a79"}, - {file = "grpcio-1.71.0-cp312-cp312-win32.whl", hash = "sha256:ad9f30838550695b5eb302add33f21f7301b882937460dd24f24b3cc5a95067a"}, - {file = "grpcio-1.71.0-cp312-cp312-win_amd64.whl", hash = "sha256:652350609332de6dac4ece254e5d7e1ff834e203d6afb769601f286886f6f3a8"}, - {file = "grpcio-1.71.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:cebc1b34ba40a312ab480ccdb396ff3c529377a2fce72c45a741f7215bfe8379"}, - {file = "grpcio-1.71.0-cp313-cp313-macosx_10_14_universal2.whl", hash = "sha256:85da336e3649a3d2171e82f696b5cad2c6231fdd5bad52616476235681bee5b3"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f9a412f55bb6e8f3bb000e020dbc1e709627dcb3a56f6431fa7076b4c1aab0db"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47be9584729534660416f6d2a3108aaeac1122f6b5bdbf9fd823e11fe6fbaa29"}, - {file = "grpcio-1.71.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9c80ac6091c916db81131d50926a93ab162a7e97e4428ffc186b6e80d6dda4"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:789d5e2a3a15419374b7b45cd680b1e83bbc1e52b9086e49308e2c0b5bbae6e3"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:1be857615e26a86d7363e8a163fade914595c81fec962b3d514a4b1e8760467b"}, - {file = "grpcio-1.71.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a76d39b5fafd79ed604c4be0a869ec3581a172a707e2a8d7a4858cb05a5a7637"}, - {file = "grpcio-1.71.0-cp313-cp313-win32.whl", hash = "sha256:74258dce215cb1995083daa17b379a1a5a87d275387b7ffe137f1d5131e2cfbb"}, - {file = "grpcio-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:22c3bc8d488c039a199f7a003a38cb7635db6656fa96437a8accde8322ce2366"}, - {file = "grpcio-1.71.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:c6a0a28450c16809f94e0b5bfe52cabff63e7e4b97b44123ebf77f448534d07d"}, - {file = "grpcio-1.71.0-cp39-cp39-macosx_10_14_universal2.whl", hash = "sha256:a371e6b6a5379d3692cc4ea1cb92754d2a47bdddeee755d3203d1f84ae08e03e"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:39983a9245d37394fd59de71e88c4b295eb510a3555e0a847d9965088cdbd033"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9182e0063112e55e74ee7584769ec5a0b4f18252c35787f48738627e23a62b97"}, - {file = "grpcio-1.71.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693bc706c031aeb848849b9d1c6b63ae6bcc64057984bb91a542332b75aa4c3d"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:20e8f653abd5ec606be69540f57289274c9ca503ed38388481e98fa396ed0b41"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:8700a2a57771cc43ea295296330daaddc0d93c088f0a35cc969292b6db959bf3"}, - {file = "grpcio-1.71.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d35a95f05a8a2cbe8e02be137740138b3b2ea5f80bd004444e4f9a1ffc511e32"}, - {file = "grpcio-1.71.0-cp39-cp39-win32.whl", hash = "sha256:f9c30c464cb2ddfbc2ddf9400287701270fdc0f14be5f08a1e3939f1e749b455"}, - {file = "grpcio-1.71.0-cp39-cp39-win_amd64.whl", hash = "sha256:63e41b91032f298b3e973b3fa4093cbbc620c875e2da7b93e249d4728b54559a"}, - {file = "grpcio-1.71.0.tar.gz", hash = "sha256:2b85f7820475ad3edec209d3d89a7909ada16caab05d3f2e08a7e8ae3200a55c"}, + {file = "grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5"}, + {file = "grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2"}, + {file = "grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d"}, + {file = "grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb"}, + {file = "grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7"}, + {file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec"}, + {file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a"}, + {file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813"}, + {file = "grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de"}, + {file = "grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf"}, + {file = "grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6"}, + {file = "grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e"}, + {file = "grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911"}, + {file = "grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e"}, + {file = "grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303"}, + {file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04"}, + {file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec"}, + {file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074"}, + {file = "grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856"}, + {file = "grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558"}, + {file = "grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97"}, + {file = "grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e"}, + {file = "grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996"}, + {file = "grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7"}, + {file = "grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9"}, + {file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383"}, + {file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6"}, + {file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce"}, + {file = "grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68"}, + {file = "grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e"}, + {file = "grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b"}, + {file = "grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a"}, + {file = "grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84"}, + {file = "grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb"}, + {file = "grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5"}, + {file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9"}, + {file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702"}, + {file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20"}, + {file = "grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670"}, + {file = "grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4"}, + {file = "grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e"}, + {file = "grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f"}, + {file = "grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724"}, + {file = "grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b"}, + {file = "grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7"}, + {file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452"}, + {file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127"}, + {file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65"}, + {file = "grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c"}, + {file = "grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb"}, + {file = "grpcio-1.78.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5"}, + {file = "grpcio-1.78.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5"}, + {file = "grpcio-1.78.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c"}, + {file = "grpcio-1.78.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1"}, + {file = "grpcio-1.78.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597"}, + {file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a"}, + {file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf"}, + {file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c"}, + {file = "grpcio-1.78.0-cp39-cp39-win32.whl", hash = "sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525"}, + {file = "grpcio-1.78.0-cp39-cp39-win_amd64.whl", hash = "sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df"}, + {file = "grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5"}, ] +[package.dependencies] +typing-extensions = ">=4.12,<5.0" + [package.extras] -protobuf = ["grpcio-tools (>=1.71.0)"] +protobuf = ["grpcio-tools (>=1.78.0)"] [[package]] name = "grpcio-status" -version = "1.71.0" +version = "1.78.0" description = "Status proto mapping for gRPC" -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "grpcio_status-1.71.0-py3-none-any.whl", hash = "sha256:843934ef8c09e3e858952887467f8256aac3910c55f077a359a65b2b3cde3e68"}, - {file = "grpcio_status-1.71.0.tar.gz", hash = "sha256:11405fed67b68f406b3f3c7c5ae5104a79d2d309666d10d61b152e91d28fb968"}, + {file = "grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34"}, + {file = "grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189"}, ] [package.dependencies] googleapis-common-protos = ">=1.5.5" -grpcio = ">=1.71.0" -protobuf = ">=5.26.1,<6.0dev" +grpcio = ">=1.78.0" +protobuf = ">=6.31.1,<7.0.0" [[package]] name = "gunicorn" version = "23.0.0" description = "WSGI HTTP Server for UNIX" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, @@ -482,14 +664,14 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "idna" -version = "3.10" +version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, ] [package.extras] @@ -497,35 +679,35 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.6.1" +version = "8.7.1" description = "Read metadata from Python packages" -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, - {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, + {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, + {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, ] [package.dependencies] zipp = ">=3.20" [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] +enabler = ["pytest-enabler (>=3.4)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] +test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] [[package]] name = "itsdangerous" version = "2.2.0" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, @@ -533,14 +715,14 @@ files = [ [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -551,220 +733,246 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, + {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, + {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, + {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, + {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, + {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, + {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, + {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, + {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, + {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, + {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, + {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, + {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, + {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, + {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, + {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, + {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, + {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, + {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, + {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, + {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, + {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, + {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, + {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, + {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, + {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, + {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, + {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, + {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, + {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, + {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, + {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, + {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, + {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, + {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, + {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, + {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, + {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, + {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, + {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, ] [[package]] name = "msgpack" -version = "1.1.0" +version = "1.1.2" description = "MessagePack serializer" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d"}, - {file = "msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e"}, - {file = "msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68"}, - {file = "msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b"}, - {file = "msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044"}, - {file = "msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa"}, - {file = "msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e59bca908d9ca0de3dc8684f21ebf9a690fe47b6be93236eb40b99af28b6ea6"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1da8f11a3dd397f0a32c76165cf0c4eb95b31013a94f6ecc0b280c05c91b59"}, - {file = "msgpack-1.1.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:452aff037287acb1d70a804ffd022b21fa2bb7c46bee884dbc864cc9024128a0"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8da4bf6d54ceed70e8861f833f83ce0814a2b72102e890cbdfe4b34764cdd66e"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:41c991beebf175faf352fb940bf2af9ad1fb77fd25f38d9142053914947cdbf6"}, - {file = "msgpack-1.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a52a1f3a5af7ba1c9ace055b659189f6c669cf3657095b50f9602af3a3ba0fe5"}, - {file = "msgpack-1.1.0-cp311-cp311-win32.whl", hash = "sha256:58638690ebd0a06427c5fe1a227bb6b8b9fdc2bd07701bec13c2335c82131a88"}, - {file = "msgpack-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd2906780f25c8ed5d7b323379f6138524ba793428db5d0e9d226d3fa6aa1788"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d46cf9e3705ea9485687aa4001a76e44748b609d260af21c4ceea7f2212a501d"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5dbad74103df937e1325cc4bfeaf57713be0b4f15e1c2da43ccdd836393e2ea2"}, - {file = "msgpack-1.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:58dfc47f8b102da61e8949708b3eafc3504509a5728f8b4ddef84bd9e16ad420"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676e5be1b472909b2ee6356ff425ebedf5142427842aa06b4dfd5117d1ca8a2"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17fb65dd0bec285907f68b15734a993ad3fc94332b5bb21b0435846228de1f39"}, - {file = "msgpack-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a51abd48c6d8ac89e0cfd4fe177c61481aca2d5e7ba42044fd218cfd8ea9899f"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2137773500afa5494a61b1208619e3871f75f27b03bcfca7b3a7023284140247"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:398b713459fea610861c8a7b62a6fec1882759f308ae0795b5413ff6a160cf3c"}, - {file = "msgpack-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:06f5fd2f6bb2a7914922d935d3b8bb4a7fff3a9a91cfce6d06c13bc42bec975b"}, - {file = "msgpack-1.1.0-cp312-cp312-win32.whl", hash = "sha256:ad33e8400e4ec17ba782f7b9cf868977d867ed784a1f5f2ab46e7ba53b6e1e1b"}, - {file = "msgpack-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:115a7af8ee9e8cddc10f87636767857e7e3717b7a2e97379dc2054712693e90f"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:071603e2f0771c45ad9bc65719291c568d4edf120b44eb36324dcb02a13bfddf"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0f92a83b84e7c0749e3f12821949d79485971f087604178026085f60ce109330"}, - {file = "msgpack-1.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1964df7b81285d00a84da4e70cb1383f2e665e0f1f2a7027e683956d04b734"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59caf6a4ed0d164055ccff8fe31eddc0ebc07cf7326a2aaa0dbf7a4001cd823e"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0907e1a7119b337971a689153665764adc34e89175f9a34793307d9def08e6ca"}, - {file = "msgpack-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65553c9b6da8166e819a6aa90ad15288599b340f91d18f60b2061f402b9a4915"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7a946a8992941fea80ed4beae6bff74ffd7ee129a90b4dd5cf9c476a30e9708d"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4b51405e36e075193bc051315dbf29168d6141ae2500ba8cd80a522964e31434"}, - {file = "msgpack-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4c01941fd2ff87c2a934ee6055bda4ed353a7846b8d4f341c428109e9fcde8c"}, - {file = "msgpack-1.1.0-cp313-cp313-win32.whl", hash = "sha256:7c9a35ce2c2573bada929e0b7b3576de647b0defbd25f5139dcdaba0ae35a4cc"}, - {file = "msgpack-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:bce7d9e614a04d0883af0b3d4d501171fbfca038f12c77fa838d9f198147a23f"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c40ffa9a15d74e05ba1fe2681ea33b9caffd886675412612d93ab17b58ea2fec"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ba6136e650898082d9d5a5217d5906d1e138024f836ff48691784bbe1adf96"}, - {file = "msgpack-1.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0856a2b7e8dcb874be44fea031d22e5b3a19121be92a1e098f46068a11b0870"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:471e27a5787a2e3f974ba023f9e265a8c7cfd373632247deb225617e3100a3c7"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:646afc8102935a388ffc3914b336d22d1c2d6209c773f3eb5dd4d6d3b6f8c1cb"}, - {file = "msgpack-1.1.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13599f8829cfbe0158f6456374e9eea9f44eee08076291771d8ae93eda56607f"}, - {file = "msgpack-1.1.0-cp38-cp38-win32.whl", hash = "sha256:8a84efb768fb968381e525eeeb3d92857e4985aacc39f3c47ffd00eb4509315b"}, - {file = "msgpack-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:879a7b7b0ad82481c52d3c7eb99bf6f0645dbdec5134a4bddbd16f3506947feb"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:53258eeb7a80fc46f62fd59c876957a2d0e15e6449a9e71842b6d24419d88ca1"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e7b853bbc44fb03fbdba34feb4bd414322180135e2cb5164f20ce1c9795ee48"}, - {file = "msgpack-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3e9b4936df53b970513eac1758f3882c88658a220b58dcc1e39606dccaaf01c"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46c34e99110762a76e3911fc923222472c9d681f1094096ac4102c18319e6468"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a706d1e74dd3dea05cb54580d9bd8b2880e9264856ce5068027eed09680aa74"}, - {file = "msgpack-1.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:534480ee5690ab3cbed89d4c8971a5c631b69a8c0883ecfea96c19118510c846"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8cf9e8c3a2153934a23ac160cc4cba0ec035f6867c8013cc6077a79823370346"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3180065ec2abbe13a4ad37688b61b99d7f9e012a535b930e0e683ad6bc30155b"}, - {file = "msgpack-1.1.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c5a91481a3cc573ac8c0d9aace09345d989dc4a0202b7fcb312c88c26d4e71a8"}, - {file = "msgpack-1.1.0-cp39-cp39-win32.whl", hash = "sha256:f80bc7d47f76089633763f952e67f8214cb7b3ee6bfa489b3cb6a84cfac114cd"}, - {file = "msgpack-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:4d1b7ff2d6146e16e8bd665ac726a89c74163ef8cd39fa8c1087d4e52d3a2325"}, - {file = "msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e"}, + {file = "msgpack-1.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0051fffef5a37ca2cd16978ae4f0aef92f164df86823871b5162812bebecd8e2"}, + {file = "msgpack-1.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a605409040f2da88676e9c9e5853b3449ba8011973616189ea5ee55ddbc5bc87"}, + {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b696e83c9f1532b4af884045ba7f3aa741a63b2bc22617293a2c6a7c645f251"}, + {file = "msgpack-1.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:365c0bbe981a27d8932da71af63ef86acc59ed5c01ad929e09a0b88c6294e28a"}, + {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:41d1a5d875680166d3ac5c38573896453bbbea7092936d2e107214daf43b1d4f"}, + {file = "msgpack-1.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:354e81bcdebaab427c3df4281187edc765d5d76bfb3a7c125af9da7a27e8458f"}, + {file = "msgpack-1.1.2-cp310-cp310-win32.whl", hash = "sha256:e64c8d2f5e5d5fda7b842f55dec6133260ea8f53c4257d64494c534f306bf7a9"}, + {file = "msgpack-1.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:db6192777d943bdaaafb6ba66d44bf65aa0e9c5616fa1d2da9bb08828c6b39aa"}, + {file = "msgpack-1.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2e86a607e558d22985d856948c12a3fa7b42efad264dca8a3ebbcfa2735d786c"}, + {file = "msgpack-1.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:283ae72fc89da59aa004ba147e8fc2f766647b1251500182fac0350d8af299c0"}, + {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:61c8aa3bd513d87c72ed0b37b53dd5c5a0f58f2ff9f26e1555d3bd7948fb7296"}, + {file = "msgpack-1.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:454e29e186285d2ebe65be34629fa0e8605202c60fbc7c4c650ccd41870896ef"}, + {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7bc8813f88417599564fafa59fd6f95be417179f76b40325b500b3c98409757c"}, + {file = "msgpack-1.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bafca952dc13907bdfdedfc6a5f579bf4f292bdd506fadb38389afa3ac5b208e"}, + {file = "msgpack-1.1.2-cp311-cp311-win32.whl", hash = "sha256:602b6740e95ffc55bfb078172d279de3773d7b7db1f703b2f1323566b878b90e"}, + {file = "msgpack-1.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:d198d275222dc54244bf3327eb8cbe00307d220241d9cec4d306d49a44e85f68"}, + {file = "msgpack-1.1.2-cp311-cp311-win_arm64.whl", hash = "sha256:86f8136dfa5c116365a8a651a7d7484b65b13339731dd6faebb9a0242151c406"}, + {file = "msgpack-1.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:70a0dff9d1f8da25179ffcf880e10cf1aad55fdb63cd59c9a49a1b82290062aa"}, + {file = "msgpack-1.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:446abdd8b94b55c800ac34b102dffd2f6aa0ce643c55dfc017ad89347db3dbdb"}, + {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c63eea553c69ab05b6747901b97d620bb2a690633c77f23feb0c6a947a8a7b8f"}, + {file = "msgpack-1.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:372839311ccf6bdaf39b00b61288e0557916c3729529b301c52c2d88842add42"}, + {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2929af52106ca73fcb28576218476ffbb531a036c2adbcf54a3664de124303e9"}, + {file = "msgpack-1.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:be52a8fc79e45b0364210eef5234a7cf8d330836d0a64dfbb878efa903d84620"}, + {file = "msgpack-1.1.2-cp312-cp312-win32.whl", hash = "sha256:1fff3d825d7859ac888b0fbda39a42d59193543920eda9d9bea44d958a878029"}, + {file = "msgpack-1.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:1de460f0403172cff81169a30b9a92b260cb809c4cb7e2fc79ae8d0510c78b6b"}, + {file = "msgpack-1.1.2-cp312-cp312-win_arm64.whl", hash = "sha256:be5980f3ee0e6bd44f3a9e9dea01054f175b50c3e6cdb692bc9424c0bbb8bf69"}, + {file = "msgpack-1.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4efd7b5979ccb539c221a4c4e16aac1a533efc97f3b759bb5a5ac9f6d10383bf"}, + {file = "msgpack-1.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:42eefe2c3e2af97ed470eec850facbe1b5ad1d6eacdbadc42ec98e7dcf68b4b7"}, + {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1fdf7d83102bf09e7ce3357de96c59b627395352a4024f6e2458501f158bf999"}, + {file = "msgpack-1.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fac4be746328f90caa3cd4bc67e6fe36ca2bf61d5c6eb6d895b6527e3f05071e"}, + {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fffee09044073e69f2bad787071aeec727183e7580443dfeb8556cbf1978d162"}, + {file = "msgpack-1.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5928604de9b032bc17f5099496417f113c45bc6bc21b5c6920caf34b3c428794"}, + {file = "msgpack-1.1.2-cp313-cp313-win32.whl", hash = "sha256:a7787d353595c7c7e145e2331abf8b7ff1e6673a6b974ded96e6d4ec09f00c8c"}, + {file = "msgpack-1.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:a465f0dceb8e13a487e54c07d04ae3ba131c7c5b95e2612596eafde1dccf64a9"}, + {file = "msgpack-1.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:e69b39f8c0aa5ec24b57737ebee40be647035158f14ed4b40e6f150077e21a84"}, + {file = "msgpack-1.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e23ce8d5f7aa6ea6d2a2b326b4ba46c985dbb204523759984430db7114f8aa00"}, + {file = "msgpack-1.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c15b7d74c939ebe620dd8e559384be806204d73b4f9356320632d783d1f7939"}, + {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e2cb7b9031568a2a5c73aa077180f93dd2e95b4f8d3b8e14a73ae94a9e667e"}, + {file = "msgpack-1.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:180759d89a057eab503cf62eeec0aa61c4ea1200dee709f3a8e9397dbb3b6931"}, + {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:04fb995247a6e83830b62f0b07bf36540c213f6eac8e851166d8d86d83cbd014"}, + {file = "msgpack-1.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8e22ab046fa7ede9e36eeb4cfad44d46450f37bb05d5ec482b02868f451c95e2"}, + {file = "msgpack-1.1.2-cp314-cp314-win32.whl", hash = "sha256:80a0ff7d4abf5fecb995fcf235d4064b9a9a8a40a3ab80999e6ac1e30b702717"}, + {file = "msgpack-1.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:9ade919fac6a3e7260b7f64cea89df6bec59104987cbea34d34a2fa15d74310b"}, + {file = "msgpack-1.1.2-cp314-cp314-win_arm64.whl", hash = "sha256:59415c6076b1e30e563eb732e23b994a61c159cec44deaf584e5cc1dd662f2af"}, + {file = "msgpack-1.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:897c478140877e5307760b0ea66e0932738879e7aa68144d9b78ea4c8302a84a"}, + {file = "msgpack-1.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a668204fa43e6d02f89dbe79a30b0d67238d9ec4c5bd8a940fc3a004a47b721b"}, + {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5559d03930d3aa0f3aacb4c42c776af1a2ace2611871c84a75afe436695e6245"}, + {file = "msgpack-1.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70c5a7a9fea7f036b716191c29047374c10721c389c21e9ffafad04df8c52c90"}, + {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f2cb069d8b981abc72b41aea1c580ce92d57c673ec61af4c500153a626cb9e20"}, + {file = "msgpack-1.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d62ce1f483f355f61adb5433ebfd8868c5f078d1a52d042b0a998682b4fa8c27"}, + {file = "msgpack-1.1.2-cp314-cp314t-win32.whl", hash = "sha256:1d1418482b1ee984625d88aa9585db570180c286d942da463533b238b98b812b"}, + {file = "msgpack-1.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:5a46bf7e831d09470ad92dff02b8b1ac92175ca36b087f904a0519857c6be3ff"}, + {file = "msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46"}, + {file = "msgpack-1.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea5405c46e690122a76531ab97a079e184c0daf491e588592d6a23d3e32af99e"}, + {file = "msgpack-1.1.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9fba231af7a933400238cb357ecccf8ab5d51535ea95d94fc35b7806218ff844"}, + {file = "msgpack-1.1.2-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a8f6e7d30253714751aa0b0c84ae28948e852ee7fb0524082e6716769124bc23"}, + {file = "msgpack-1.1.2-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94fd7dc7d8cb0a54432f296f2246bc39474e017204ca6f4ff345941d4ed285a7"}, + {file = "msgpack-1.1.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:350ad5353a467d9e3b126d8d1b90fe05ad081e2e1cef5753f8c345217c37e7b8"}, + {file = "msgpack-1.1.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6bde749afe671dc44893f8d08e83bf475a1a14570d67c4bb5cec5573463c8833"}, + {file = "msgpack-1.1.2-cp39-cp39-win32.whl", hash = "sha256:ad09b984828d6b7bb52d1d1d0c9be68ad781fa004ca39216c8a1e63c0f34ba3c"}, + {file = "msgpack-1.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:67016ae8c8965124fdede9d3769528ad8284f14d635337ffa6a713a580f6c030"}, + {file = "msgpack-1.1.2.tar.gz", hash = "sha256:3b60763c1373dd60f398488069bcdc703cd08a711477b5d480eecc9f9626f47e"}, ] [[package]] name = "opentelemetry-api" -version = "1.31.0" +version = "1.39.1" description = "OpenTelemetry Python API" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "opentelemetry_api-1.31.0-py3-none-any.whl", hash = "sha256:145b72c6c16977c005c568ec32f4946054ab793d8474a17fd884b0397582c5f2"}, - {file = "opentelemetry_api-1.31.0.tar.gz", hash = "sha256:d8da59e83e8e3993b4726e4c1023cd46f57c4d5a73142e239247e7d814309de1"}, + {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, + {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, ] [package.dependencies] -deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<8.7.0" +importlib-metadata = ">=6.0,<8.8.0" +typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-sdk" -version = "1.31.0" +version = "1.39.1" description = "OpenTelemetry Python SDK" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "opentelemetry_sdk-1.31.0-py3-none-any.whl", hash = "sha256:97c9a03865e69723725fb64fe04343a488c3e61e684eb804bd7d6da2215dfc60"}, - {file = "opentelemetry_sdk-1.31.0.tar.gz", hash = "sha256:452d7d5b3c1db2e5e4cb64abede0ddd20690cb244a559c73a59652fdf6726070"}, + {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, + {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, ] [package.dependencies] -opentelemetry-api = "1.31.0" -opentelemetry-semantic-conventions = "0.52b0" -typing-extensions = ">=3.7.4" +opentelemetry-api = "1.39.1" +opentelemetry-semantic-conventions = "0.60b1" +typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.52b0" +version = "0.60b1" description = "OpenTelemetry Semantic Conventions" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "opentelemetry_semantic_conventions-0.52b0-py3-none-any.whl", hash = "sha256:4d843652ae1f9f3c0d4d8df0bfef740627c90495ac043fc33f0a04bad3b606e2"}, - {file = "opentelemetry_semantic_conventions-0.52b0.tar.gz", hash = "sha256:f8bc8873a69d0a2f45746c31980baad2bb10ccee16b1816497ccf99417770386"}, + {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, + {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, ] [package.dependencies] -deprecated = ">=1.2.6" -opentelemetry-api = "1.31.0" +opentelemetry-api = "1.39.1" +typing-extensions = ">=4.5.0" [[package]] name = "packaging" -version = "24.2" +version = "26.0" description = "Core utilities for Python packages" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] [[package]] name = "proto-plus" -version = "1.26.1" +version = "1.27.1" description = "Beautiful, Pythonic protocol buffers" -category = "main" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ - {file = "proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66"}, - {file = "proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012"}, + {file = "proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc"}, + {file = "proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147"}, ] [package.dependencies] @@ -775,62 +983,74 @@ testing = ["google-api-core (>=1.31.5)"] [[package]] name = "protobuf" -version = "5.29.3" +version = "6.33.5" description = "" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, - {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, - {file = "protobuf-5.29.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:a8434404bbf139aa9e1300dbf989667a83d42ddda9153d8ab76e0d5dcaca484e"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:daaf63f70f25e8689c072cfad4334ca0ac1d1e05a92fc15c54eb9cf23c3efd84"}, - {file = "protobuf-5.29.3-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:c027e08a08be10b67c06bf2370b99c811c466398c357e615ca88c91c07f0910f"}, - {file = "protobuf-5.29.3-cp38-cp38-win32.whl", hash = "sha256:84a57163a0ccef3f96e4b6a20516cedcf5bb3a95a657131c5c3ac62200d23252"}, - {file = "protobuf-5.29.3-cp38-cp38-win_amd64.whl", hash = "sha256:b89c115d877892a512f79a8114564fb435943b59067615894c3b13cd3e1fa107"}, - {file = "protobuf-5.29.3-cp39-cp39-win32.whl", hash = "sha256:0eb32bfa5219fc8d4111803e9a690658aa2e6366384fd0851064b963b6d1f2a7"}, - {file = "protobuf-5.29.3-cp39-cp39-win_amd64.whl", hash = "sha256:6ce8cc3389a20693bfde6c6562e03474c40851b44975c9b2bf6df7d8c4f864da"}, - {file = "protobuf-5.29.3-py3-none-any.whl", hash = "sha256:0a18ed4a24198528f2333802eb075e59dea9d679ab7a6c5efb017a59004d849f"}, - {file = "protobuf-5.29.3.tar.gz", hash = "sha256:5da0f41edaf117bde316404bad1a486cb4ededf8e4a54891296f648e8e076620"}, + {file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"}, + {file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"}, + {file = "protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5"}, + {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190"}, + {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd"}, + {file = "protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0"}, + {file = "protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c"}, + {file = "protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a"}, + {file = "protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02"}, + {file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"}, ] [[package]] name = "pyasn1" -version = "0.6.1" +version = "0.6.2" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, - {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, + {file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"}, + {file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"}, ] [[package]] name = "pyasn1-modules" -version = "0.4.1" +version = "0.4.2" description = "A collection of ASN.1-based protocols modules" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, - {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, + {file = "pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a"}, + {file = "pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6"}, ] [package.dependencies] -pyasn1 = ">=0.4.6,<0.7.0" +pyasn1 = ">=0.6.1,<0.7.0" + +[[package]] +name = "pycparser" +version = "3.0" +description = "C parser in Python" +optional = false +python-versions = ">=3.10" +groups = ["main"] +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" +files = [ + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, +] [[package]] name = "python-dotenv" -version = "1.0.1" +version = "1.2.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, ] [package.extras] @@ -838,19 +1058,19 @@ cli = ["click (>=5.0)"] [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" description = "Python HTTP for Humans." -category = "main" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -860,14 +1080,14 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rsa" -version = "4.9" +version = "4.9.1" description = "Pure-Python RSA implementation" -category = "main" optional = false -python-versions = ">=3.6,<4" +python-versions = "<4,>=3.6" +groups = ["main"] files = [ - {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, - {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, + {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, + {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, ] [package.dependencies] @@ -877,9 +1097,9 @@ pyasn1 = ">=0.1.3" name = "simple-cloudevent" version = "0.0.2" description = "A short description of the project" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [] develop = false @@ -896,9 +1116,9 @@ resolved_reference = "447cabb988202206ac69e71177d7cd11b6c0b002" name = "strict-rfc3339" version = "0.7" description = "Strict, simple, lightweight RFC3339 functions" -category = "main" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "strict-rfc3339-0.7.tar.gz", hash = "sha256:5cad17bedfc3af57b399db0fed32771f18fc54bbd917e85546088607ac5e1277"}, ] @@ -907,9 +1127,9 @@ files = [ name = "structlog" version = "24.4.0" description = "Structured Logging for Python" -category = "main" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "structlog-24.4.0-py3-none-any.whl", hash = "sha256:597f61e80a91cc0749a9fd2a098ed76715a1c8a01f73e336b746504d1aad7610"}, {file = "structlog-24.4.0.tar.gz", hash = "sha256:b27bfecede327a6d2da5fbc96bd859f114ecc398a6389d664f62085ee7ae6fc4"}, @@ -925,9 +1145,9 @@ typing = ["mypy (>=1.4)", "rich", "twisted"] name = "structured-logging" version = "0.4.0" description = "" -category = "main" optional = false python-versions = "^3.9" +groups = ["main"] files = [] develop = false @@ -944,162 +1164,73 @@ subdirectory = "python/structured-logging" [[package]] name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, ] [[package]] name = "urllib3" -version = "2.3.0" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, - {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, + {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, + {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "werkzeug" -version = "3.1.3" +version = "3.1.5" description = "The comprehensive WSGI web application library." -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, - {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, + {file = "werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc"}, + {file = "werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67"}, ] [package.dependencies] -MarkupSafe = ">=2.1.1" +markupsafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] -[[package]] -name = "wrapt" -version = "1.17.2" -description = "Module for decorators, wrappers and monkey patching." -category = "main" -optional = false -python-versions = ">=3.8" -files = [ - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3d57c572081fed831ad2d26fd430d565b76aa277ed1d30ff4d40670b1c0dd984"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b5e251054542ae57ac7f3fba5d10bfff615b6c2fb09abeb37d2f1463f841ae22"}, - {file = "wrapt-1.17.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80dd7db6a7cb57ffbc279c4394246414ec99537ae81ffd702443335a61dbf3a7"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a6e821770cf99cc586d33833b2ff32faebdbe886bd6322395606cf55153246c"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b60fb58b90c6d63779cb0c0c54eeb38941bae3ecf7a73c764c52c88c2dcb9d72"}, - {file = "wrapt-1.17.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b870b5df5b71d8c3359d21be8f0d6c485fa0ebdb6477dda51a1ea54a9b558061"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4011d137b9955791f9084749cba9a367c68d50ab8d11d64c50ba1688c9b457f2"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1473400e5b2733e58b396a04eb7f35f541e1fb976d0c0724d0223dd607e0f74c"}, - {file = "wrapt-1.17.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3cedbfa9c940fdad3e6e941db7138e26ce8aad38ab5fe9dcfadfed9db7a54e62"}, - {file = "wrapt-1.17.2-cp310-cp310-win32.whl", hash = "sha256:582530701bff1dec6779efa00c516496968edd851fba224fbd86e46cc6b73563"}, - {file = "wrapt-1.17.2-cp310-cp310-win_amd64.whl", hash = "sha256:58705da316756681ad3c9c73fd15499aa4d8c69f9fd38dc8a35e06c12468582f"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ff04ef6eec3eee8a5efef2401495967a916feaa353643defcc03fc74fe213b58"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4db983e7bca53819efdbd64590ee96c9213894272c776966ca6306b73e4affda"}, - {file = "wrapt-1.17.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9abc77a4ce4c6f2a3168ff34b1da9b0f311a8f1cfd694ec96b0603dff1c79438"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b929ac182f5ace000d459c59c2c9c33047e20e935f8e39371fa6e3b85d56f4a"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f09b286faeff3c750a879d336fb6d8713206fc97af3adc14def0cdd349df6000"}, - {file = "wrapt-1.17.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a7ed2d9d039bd41e889f6fb9364554052ca21ce823580f6a07c4ec245c1f5d6"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:129a150f5c445165ff941fc02ee27df65940fcb8a22a61828b1853c98763a64b"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1fb5699e4464afe5c7e65fa51d4f99e0b2eadcc176e4aa33600a3df7801d6662"}, - {file = "wrapt-1.17.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9a2bce789a5ea90e51a02dfcc39e31b7f1e662bc3317979aa7e5538e3a034f72"}, - {file = "wrapt-1.17.2-cp311-cp311-win32.whl", hash = "sha256:4afd5814270fdf6380616b321fd31435a462019d834f83c8611a0ce7484c7317"}, - {file = "wrapt-1.17.2-cp311-cp311-win_amd64.whl", hash = "sha256:acc130bc0375999da18e3d19e5a86403667ac0c4042a094fefb7eec8ebac7cf3"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d5e2439eecc762cd85e7bd37161d4714aa03a33c5ba884e26c81559817ca0925"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fc7cb4c1c744f8c05cd5f9438a3caa6ab94ce8344e952d7c45a8ed59dd88392"}, - {file = "wrapt-1.17.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fdbdb757d5390f7c675e558fd3186d590973244fab0c5fe63d373ade3e99d40"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b"}, - {file = "wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae"}, - {file = "wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9"}, - {file = "wrapt-1.17.2-cp312-cp312-win32.whl", hash = "sha256:468090021f391fe0056ad3e807e3d9034e0fd01adcd3bdfba977b6fdf4213ea9"}, - {file = "wrapt-1.17.2-cp312-cp312-win_amd64.whl", hash = "sha256:ec89ed91f2fa8e3f52ae53cd3cf640d6feff92ba90d62236a81e4e563ac0e991"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6ed6ffac43aecfe6d86ec5b74b06a5be33d5bb9243d055141e8cabb12aa08125"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35621ae4c00e056adb0009f8e86e28eb4a41a4bfa8f9bfa9fca7d343fe94f998"}, - {file = "wrapt-1.17.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a604bf7a053f8362d27eb9fefd2097f82600b856d5abe996d623babd067b1ab5"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cbabee4f083b6b4cd282f5b817a867cf0b1028c54d445b7ec7cfe6505057cf8"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:49703ce2ddc220df165bd2962f8e03b84c89fee2d65e1c24a7defff6f988f4d6"}, - {file = "wrapt-1.17.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8112e52c5822fc4253f3901b676c55ddf288614dc7011634e2719718eaa187dc"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9fee687dce376205d9a494e9c121e27183b2a3df18037f89d69bd7b35bcf59e2"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:18983c537e04d11cf027fbb60a1e8dfd5190e2b60cc27bc0808e653e7b218d1b"}, - {file = "wrapt-1.17.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:703919b1633412ab54bcf920ab388735832fdcb9f9a00ae49387f0fe67dad504"}, - {file = "wrapt-1.17.2-cp313-cp313-win32.whl", hash = "sha256:abbb9e76177c35d4e8568e58650aa6926040d6a9f6f03435b7a522bf1c487f9a"}, - {file = "wrapt-1.17.2-cp313-cp313-win_amd64.whl", hash = "sha256:69606d7bb691b50a4240ce6b22ebb319c1cfb164e5f6569835058196e0f3a845"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4a721d3c943dae44f8e243b380cb645a709ba5bd35d3ad27bc2ed947e9c68192"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:766d8bbefcb9e00c3ac3b000d9acc51f1b399513f44d77dfe0eb026ad7c9a19b"}, - {file = "wrapt-1.17.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e496a8ce2c256da1eb98bd15803a79bee00fc351f5dfb9ea82594a3f058309e0"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d615e4fe22f4ad3528448c193b218e077656ca9ccb22ce2cb20db730f8d306"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a5aaeff38654462bc4b09023918b7f21790efb807f54c000a39d41d69cf552cb"}, - {file = "wrapt-1.17.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7d15bbd2bc99e92e39f49a04653062ee6085c0e18b3b7512a4f2fe91f2d681"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e3890b508a23299083e065f435a492b5435eba6e304a7114d2f919d400888cc6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c8b293cd65ad716d13d8dd3624e42e5a19cc2a2f1acc74b30c2c13f15cb61a6"}, - {file = "wrapt-1.17.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c82b8785d98cdd9fed4cac84d765d234ed3251bd6afe34cb7ac523cb93e8b4f"}, - {file = "wrapt-1.17.2-cp313-cp313t-win32.whl", hash = "sha256:13e6afb7fe71fe7485a4550a8844cc9ffbe263c0f1a1eea569bc7091d4898555"}, - {file = "wrapt-1.17.2-cp313-cp313t-win_amd64.whl", hash = "sha256:eaf675418ed6b3b31c7a989fd007fa7c3be66ce14e5c3b27336383604c9da85c"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5c803c401ea1c1c18de70a06a6f79fcc9c5acfc79133e9869e730ad7f8ad8ef9"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f917c1180fdb8623c2b75a99192f4025e412597c50b2ac870f156de8fb101119"}, - {file = "wrapt-1.17.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ecc840861360ba9d176d413a5489b9a0aff6d6303d7e733e2c4623cfa26904a6"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb87745b2e6dc56361bfde481d5a378dc314b252a98d7dd19a651a3fa58f24a9"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58455b79ec2661c3600e65c0a716955adc2410f7383755d537584b0de41b1d8a"}, - {file = "wrapt-1.17.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e42a40a5e164cbfdb7b386c966a588b1047558a990981ace551ed7e12ca9c2"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:91bd7d1773e64019f9288b7a5101f3ae50d3d8e6b1de7edee9c2ccc1d32f0c0a"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:bb90fb8bda722a1b9d48ac1e6c38f923ea757b3baf8ebd0c82e09c5c1a0e7a04"}, - {file = "wrapt-1.17.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:08e7ce672e35efa54c5024936e559469436f8b8096253404faeb54d2a878416f"}, - {file = "wrapt-1.17.2-cp38-cp38-win32.whl", hash = "sha256:410a92fefd2e0e10d26210e1dfb4a876ddaf8439ef60d6434f21ef8d87efc5b7"}, - {file = "wrapt-1.17.2-cp38-cp38-win_amd64.whl", hash = "sha256:95c658736ec15602da0ed73f312d410117723914a5c91a14ee4cdd72f1d790b3"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99039fa9e6306880572915728d7f6c24a86ec57b0a83f6b2491e1d8ab0235b9a"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2696993ee1eebd20b8e4ee4356483c4cb696066ddc24bd70bcbb80fa56ff9061"}, - {file = "wrapt-1.17.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:612dff5db80beef9e649c6d803a8d50c409082f1fedc9dbcdfde2983b2025b82"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62c2caa1585c82b3f7a7ab56afef7b3602021d6da34fbc1cf234ff139fed3cd9"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c958bcfd59bacc2d0249dcfe575e71da54f9dcf4a8bdf89c4cb9a68a1170d73f"}, - {file = "wrapt-1.17.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc78a84e2dfbc27afe4b2bd7c80c8db9bca75cc5b85df52bfe634596a1da846b"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ba0f0eb61ef00ea10e00eb53a9129501f52385c44853dbd6c4ad3f403603083f"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1e1fe0e6ab7775fd842bc39e86f6dcfc4507ab0ffe206093e76d61cde37225c8"}, - {file = "wrapt-1.17.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c86563182421896d73858e08e1db93afdd2b947a70064b813d515d66549e15f9"}, - {file = "wrapt-1.17.2-cp39-cp39-win32.whl", hash = "sha256:f393cda562f79828f38a819f4788641ac7c4085f30f1ce1a68672baa686482bb"}, - {file = "wrapt-1.17.2-cp39-cp39-win_amd64.whl", hash = "sha256:36ccae62f64235cf8ddb682073a60519426fdd4725524ae38874adf72b5f2aeb"}, - {file = "wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8"}, - {file = "wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3"}, -] - [[package]] name = "zipp" -version = "3.21.0" +version = "3.23.0" description = "Backport of pathlib-compatible object wrapper for zip files" -category = "main" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, - {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, + {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, + {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] +test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] type = ["pytest-mypy"] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = "^3.12" content-hash = "c2b0dd7f2fe5d35f8dad62779d57d1215cb18056773d5224fb4a5c51c5f1cc7f" diff --git a/solr-feeder/solr_feeder/config.py b/solr-feeder/solr_feeder/config.py index c195d8fed..90cdeaa4c 100644 --- a/solr-feeder/solr_feeder/config.py +++ b/solr-feeder/solr_feeder/config.py @@ -22,8 +22,12 @@ """ import os +from dotenv import load_dotenv -class Config(): # pylint: disable=too-few-public-methods; +load_dotenv() + + +class Config: # pylint: disable=too-few-public-methods; """Base class configuration that should set reasonable defaults. Used as the base for all the other configurations. @@ -34,12 +38,14 @@ class Config(): # pylint: disable=too-few-public-methods; POD_NAMESPACE = os.getenv('POD_NAMESPACE', 'unknown') COLIN_API_URL = os.getenv('COLIN_API_URL', 'http://') + \ - os.getenv('COLIN_API_VERSION', '/api/v1') + os.getenv('COLIN_API_VERSION', '/api/v1') SEARCH_API_URL = os.getenv('REGISTRIES_SEARCH_API_INTERNAL_URL', 'http://') - BOR_API_URL = os.getenv('BOR_API_INTERNAL_URL', '') \ - + os.getenv('BOR_API_VERSION', '') + BOR_API_URL = os.getenv('BOR_API_INTERNAL_URL', '') + \ + os.getenv('BOR_API_VERSION', '') + + SOLR_API_URL = os.getenv('SOLR_API_URL', '') # External API Timeouts try: @@ -68,6 +74,9 @@ class Config(): # pylint: disable=too-few-public-methods; KEYCLOAK_SERVICE_ACCOUNT_ID = os.getenv('NDS_SERVICE_ACCOUNT_CLIENT_ID') KEYCLOAK_SERVICE_ACCOUNT_SECRET = os.getenv('NDS_SERVICE_ACCOUNT_SECRET') + ACCOUNT_SVC_CLIENT_ID = os.getenv('ACCOUNT_SVC_CLIENT_ID') + ACCOUNT_SVC_CLIENT_SECRET = os.getenv('ACCOUNT_SVC_CLIENT_SECRET') + DEBUG = False TESTING = False diff --git a/solr-feeder/solr_feeder/services/auth.py b/solr-feeder/solr_feeder/services/auth.py index b16e3ce32..46b9623a2 100644 --- a/solr-feeder/solr_feeder/services/auth.py +++ b/solr-feeder/solr_feeder/services/auth.py @@ -27,6 +27,19 @@ def get_bearer_token() -> tuple[str, dict]: client_secret = current_app.config.get('KEYCLOAK_SERVICE_ACCOUNT_SECRET') auth_api_timeout = current_app.config.get('AUTH_API_TIMEOUT') + return _get_bearer_token(token_url, client_id, client_secret, auth_api_timeout) + + +def get_search_bearer_token() -> tuple[str, dict]: + """Get a valid Bearer token for the search service to use.""" + token_url = current_app.config.get('KEYCLOAK_AUTH_TOKEN_URL') + client_id = current_app.config.get('ACCOUNT_SVC_CLIENT_ID') + client_secret = current_app.config.get('ACCOUNT_SVC_CLIENT_SECRET') + auth_api_timeout = current_app.config.get('AUTH_API_TIMEOUT') + + return _get_bearer_token(token_url, client_id, client_secret, auth_api_timeout) + +def _get_bearer_token(token_url, client_id, client_secret, timeout) -> tuple[str | None, dict | None]: data = 'grant_type=client_credentials' # get service account token @@ -35,7 +48,7 @@ def get_bearer_token() -> tuple[str, dict]: data=data, headers={'content-type': 'application/x-www-form-urlencoded'}, auth=(client_id, client_secret), - timeout=auth_api_timeout) + timeout=timeout) if res.status_code != HTTPStatus.OK: return None, {'message': res.json(), 'status_code': res.status_code} diff --git a/solr-feeder/solr_feeder/solr.py b/solr-feeder/solr_feeder/solr.py index 8f9ee84b3..5cf8b1045 100644 --- a/solr-feeder/solr_feeder/solr.py +++ b/solr-feeder/solr_feeder/solr.py @@ -17,10 +17,11 @@ import requests from flask import current_app +from solr_feeder.services.auth import get_search_bearer_token +from solr_feeder.utilities.converters import convert_solr_doc __all__ = ['update_core'] - _SOLR_INSTANCE = os.getenv('SOLR_FEEDER_SOLR_INSTANCE', 'http://localhost:8393/solr') _SOLR_URL = _SOLR_INSTANCE + '/{}/update/json' @@ -29,18 +30,28 @@ def update_core(core_name: str, json_string: str): """Update the core with the given data.""" current_app.logger.debug('json Solr command: %s', json_string) - response = requests.post(_SOLR_URL.format(core_name), - data=json_string, - timeout=current_app.config['NAMEX_SOLR_TIMEOUT']) + converted_request = convert_solr_doc(json_string) + + bearer_token, token_err = get_search_bearer_token() + headers = { + 'Authorization': f'Bearer {bearer_token}', + 'Content-Type': 'application/json', + } + + solr_api_url = current_app.config['SOLR_API_URL'].rstrip('/') + '/internal/solr/update' + + resp = requests.put(solr_api_url, + json=converted_request, + headers=headers + ) - # By the way, if your request is mangled, Solr will sometimes happily return a 200 with a responseHeader['status'] - # value of 0 (meaning all is good). - if response.status_code != 200: - current_app.logger.error('%s core: %s', core_name, response.json()) + if resp.status_code > 299: + current_app.logger.error('Solr update for ' + json_string + ' failed.') + current_app.logger.error('Failed to update solr', resp.json()) return { - 'message': f"{core_name} core: {response.json()['error']['msg']}", - 'status_code': response.status_code + 'message': f"{core_name} core: {resp.json()['error']['msg']}", + 'status_code': resp.status_code } return None diff --git a/api/tests/python/solr/__init__.py b/solr-feeder/solr_feeder/utilities/__init__.py similarity index 100% rename from api/tests/python/solr/__init__.py rename to solr-feeder/solr_feeder/utilities/__init__.py diff --git a/solr-feeder/solr_feeder/utilities/converters.py b/solr-feeder/solr_feeder/utilities/converters.py new file mode 100644 index 000000000..b6e3e204a --- /dev/null +++ b/solr-feeder/solr_feeder/utilities/converters.py @@ -0,0 +1,97 @@ +# Copyright © 2023 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Converters for transforming Solr document formats.""" +import json +from datetime import datetime + + +def convert_solr_doc(json_string: str) -> dict: + """Convert a Solr add document to the target format. + + Args: + json_string: JSON string in Solr add format. + + Returns: + Converted dictionary in NR or CORP format. + """ + data = json.loads(json_string) + doc = data.get('add', {}).get('doc', {}) + source = doc.get('source', '').upper() + + if source == 'NAMEREQUEST': + return _convert_nr_doc(doc) + elif source == 'CORP': + return _convert_corp_doc(doc) + else: + raise ValueError(f"Unknown source type: {source}") + + +def _convert_nr_doc(doc: dict) -> dict: + """Convert a name request Solr doc to target NR format.""" + # Convert "NR 0664756" -> "NR0664756" (remove space) + nr_num = doc.get('id', '').replace(' ', '') + + # Convert ISO datetime to date only + start_date = _extract_date(doc.get('start_date', '')) + + return { + 'nr_num': nr_num, + 'start_date': start_date, + 'jurisdiction': doc.get('jurisdiction', ''), + 'state': doc.get('state_type_cd', ''), + 'type': 'NR', + 'sub_type': doc.get('sub_type', '-'), + 'names': [ + { + 'choice': doc.get('choice', -1), + 'name': doc.get('name', ''), + 'name_state': 'A', # Default to Approved + 'submit_count': 1 + } + ] + } + + +def _convert_corp_doc(doc: dict) -> dict: + """Convert a corporation Solr doc to target CORP format.""" + # Convert ISO datetime to date only + # start_date = _extract_date(doc.get('start_date', '')) + start_date = doc.get('start_date', '') + + return { + 'corp_num': doc.get('id', ''), + 'start_date': start_date, + 'jurisdiction': doc.get('jurisdiction', ''), + 'state': doc.get('state_type_cd', ''), + 'type': 'CORP', + 'name': doc.get('name', '') + } + + +def _extract_date(iso_datetime: str) -> str: + """Extract date portion from ISO datetime string. + + Args: + iso_datetime: ISO format datetime (e.g., "2026-01-27T14:52:31Z") + + Returns: + Date string in YYYY-MM-DD format. + """ + if not iso_datetime: + return '' + try: + dt = datetime.fromisoformat(iso_datetime.replace('Z', '+00:00')) + return dt.strftime('%Y-%m-%d') + except ValueError: + return iso_datetime[:10] if len(iso_datetime) >= 10 else iso_datetime