From baf8c2440ea398055863582ba50f6869eec82f0f Mon Sep 17 00:00:00 2001 From: Ketaki Deodhar Date: Wed, 18 Feb 2026 12:27:38 -0800 Subject: [PATCH 01/46] modify notebooks --- jobs/correction-ben-statement/.env.example | 9 + .../add_corrections_alterations.ipynb | 169 +++++++++--------- .../add_corrections_ia.ipynb | 163 ++++++++--------- .../add_registrars_notation_alteration.ipynb | 162 ++++++++--------- .../add_registrars_notation_historical.ipynb | 74 ++++---- .../add_registrars_notation_ia.ipynb | 165 ++++++++--------- 6 files changed, 380 insertions(+), 362 deletions(-) create mode 100644 jobs/correction-ben-statement/.env.example diff --git a/jobs/correction-ben-statement/.env.example b/jobs/correction-ben-statement/.env.example new file mode 100644 index 0000000000..2d2cbecb8b --- /dev/null +++ b/jobs/correction-ben-statement/.env.example @@ -0,0 +1,9 @@ +ACCOUNT_SVC_AUTH_URL= +ACCOUNT_SVC_CLIENT_ID= +ACCOUNT_SVC_CLIENT_SECRET= +LEGAL_API_BASE_URL= +ENTITY_DATABASE_USERNAME= +ENTITY_DATABASE_PASSWORD= +ENTITY_DATABASE_HOST= +ENTITY_DATABASE_NAME= +ENTITY_DATABASE_PORT= diff --git a/jobs/correction-ben-statement/add_corrections_alterations.ipynb b/jobs/correction-ben-statement/add_corrections_alterations.ipynb index 129683660c..f2d1af40fb 100644 --- a/jobs/correction-ben-statement/add_corrections_alterations.ipynb +++ b/jobs/correction-ben-statement/add_corrections_alterations.ipynb @@ -26,8 +26,7 @@ "source": [ "import os\n", "from dotenv import load_dotenv, find_dotenv\n", - "import psycopg2\n", - "import pandas as pd\n", + "from sqlalchemy import create_engine, text\n", "\n", "# this will load all the envars from a .env file located in the project root (api)\n", "load_dotenv(find_dotenv())\n", @@ -45,19 +44,11 @@ " os.getenv('ENTITY_DATABASE_USERNAME', '') + \":\" + os.getenv('ENTITY_DATABASE_PASSWORD', '') +'@' + \\\n", " os.getenv('ENTITY_DATABASE_HOST', '') + ':' + os.getenv('ENTITY_DATABASE_PORT', '5432') + '/' + \\\n", " os.getenv('ENTITY_DATABASE_NAME', '');\n", - "connect_to_db\n", - " \n", - "%sql $connect_to_db" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%sql \n", - "select now() AT TIME ZONE 'PST' as current_date" + "engine = create_engine(connect_to_db)\n", + "\n", + "# Test connection\n", + "with engine.connect() as conn:\n", + " print(\"Connected successfully!\")" ] }, { @@ -129,76 +120,88 @@ "skipped_identifiers = []\n", "\n", "# loop through list of businesses to create filing\n", - "for identifier in businesses:\n", - " filing_details = %sql \\\n", - " SELECT f.id, f.filing_date \\\n", - " FROM businesses b \\\n", - " JOIN filings f ON b.id = f.business_id \\\n", - " WHERE f.filing_type = 'alteration' \\\n", - " AND f.meta_data->'alteration'->>'fromLegalType' IN ('BC', 'ULC', 'CC', 'C', 'CUL', 'CCC') \\\n", - " AND f.meta_data->'alteration'->>'toLegalType' IN ('BEN', 'CBEN') \\\n", - " AND b.identifier = :identifier\n", - " \n", - " if filing_details:\n", - " filing_id = filing_details[0]['id']\n", - " filing_date = filing_details[0]['filing_date']\n", - "\n", - " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", - " \n", - " draft_details = %sql \\\n", - " SELECT b.state, \\\n", - " (SELECT COUNT(1) \\\n", - " FROM filings f \\\n", - " WHERE f.business_id = b.id \\\n", - " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft \\\n", - " FROM businesses b \\\n", - " WHERE b.identifier = :identifier\n", - " state = None\n", - " has_draft = None\n", - " if draft_details:\n", - " state = draft_details[0]['state']\n", - " has_draft = draft_details[0]['has_draft']\n", - " \n", - " if state != 'ACTIVE' or has_draft:\n", - " skipped_identifiers.append(identifier)\n", - " continue\n", - " \n", - " correction_filing_data = {\n", - " \"filing\": {\n", - " \"header\": {\n", - " \"name\": \"correction\",\n", - " \"date\": current_date,\n", - " \"certifiedBy\": \"system\",\n", - " \"correctionBenStatement\": True,\n", - " \"waiveFees\": True\n", - " },\n", - " \"business\": {\n", - " \"identifier\": identifier,\n", - " \"legalType\": \"BEN\"\n", - " },\n", - " \"correction\": {\n", - " \"details\": \"BEN Correction statement\",\n", - " \"correctedFilingId\": filing_id,\n", - " \"correctedFilingType\": \"alteration\",\n", - " \"commentOnly\": True,\n", - " \"comment\": f\"\"\"Correction for Alteration filed on {formatted_filing_date} \\n{correction_statement}\"\"\"\n", + "with engine.connect() as conn:\n", + " for identifier in businesses:\n", + " filing_details_query = text(\"\"\"\n", + " SELECT f.id, f.filing_date \n", + " FROM businesses b \n", + " JOIN filings f ON b.id = f.business_id \n", + " WHERE f.filing_type = 'alteration' \n", + " AND f.meta_data->'alteration'->>'fromLegalType' IN ('BC', 'ULC', 'CC', 'C', 'CUL', 'CCC') \n", + " AND f.meta_data->'alteration'->>'toLegalType' IN ('BEN', 'CBEN') \n", + " AND b.identifier = :identifier\n", + " \"\"\")\n", + " filing_details_query_result = conn.execute(filing_details_query, {\"identifier\": identifier})\n", + " filing_details = filing_details_query_result.mappings().fetchone()\n", + " \n", + " if filing_details:\n", + " filing_id = filing_details[0]['id']\n", + " filing_date = filing_details[0]['filing_date']\n", + "\n", + " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", + " \n", + " draft_details_query = text(\"\"\"\n", + " SELECT b.state,\n", + " (SELECT COUNT(1)\n", + " FROM filings f\n", + " WHERE f.business_id = b.id\n", + " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft\n", + " FROM businesses b\n", + " WHERE b.identifier = :identifier\n", + " \"\"\")\n", + " \n", + " draft_details_query_result = conn.execute(draft_details_query, {\"identifier\": identifier})\n", + " draft_details = draft_details_query_result.mappings().fetchone()\n", + " \n", + " state = None\n", + " has_draft = None\n", + " if draft_details:\n", + " state = draft_details[0]['state']\n", + " has_draft = draft_details[0]['has_draft']\n", + " \n", + " if state != 'ACTIVE' or has_draft:\n", + " skipped_identifiers.append(identifier)\n", + " continue\n", + " \n", + " correction_filing_data = {\n", + " \"filing\": {\n", + " \"header\": {\n", + " \"name\": \"correction\",\n", + " \"date\": current_date,\n", + " \"certifiedBy\": \"system\",\n", + " \"correctionBenStatement\": True,\n", + " \"waiveFees\": True\n", + " },\n", + " \"business\": {\n", + " \"identifier\": identifier,\n", + " \"legalType\": \"BEN\"\n", + " },\n", + " \"correction\": {\n", + " \"details\": \"BEN Correction statement\",\n", + " \"correctedFilingId\": filing_id,\n", + " \"correctedFilingType\": \"alteration\",\n", + " \"commentOnly\": True,\n", + " \"comment\": f\"\"\"Correction for Alteration filed on {formatted_filing_date} \\n{correction_statement}\"\"\"\n", + " }\n", " }\n", " }\n", - " }\n", - "\n", - " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", - " rv = requests.post(filing_url, headers=headers, json=correction_filing_data)\n", - "\n", - " # Check the status code of the response\n", - " if rv.status_code == 201:\n", - " correction_filing_id = rv.json()[\"filing\"][\"header\"][\"filingId\"]\n", - " successful_identifiers.append(identifier)\n", - " else:\n", - " failed_identifiers.append(identifier)\n", - " print(f\"Failed to make POST request. Status code: {rv.status_code}: {rv.text} for {identifier}\")\n", - "print('Successfully filed Corrections for:', successful_identifiers) # Print the error message if the request fails \n", - "print('Failed to file Corrections for:', failed_identifiers) # Print the error message if the request fails\n", - "print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers\n" + "\n", + " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", + " rv = requests.post(filing_url, headers=headers, json=correction_filing_data)\n", + "\n", + " # Check the status code of the response\n", + " if rv.status_code == 201:\n", + " correction_filing_id = rv.json()[\"filing\"][\"header\"][\"filingId\"]\n", + " successful_identifiers.append(identifier)\n", + " else:\n", + " failed_identifiers.append(identifier)\n", + " print(f\"Failed to make POST request. Status code: {rv.status_code}: {rv.text} for {identifier}\")\n", + " print('Successfully filed Corrections for:', successful_identifiers) # Print the error message if the request fails \n", + " print('Failed to file Corrections for:', failed_identifiers) # Print the error message if the request fails\n", + " print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers\n", + "\n", + "#close the engine connection\n", + "conn.close()" ] } ], diff --git a/jobs/correction-ben-statement/add_corrections_ia.ipynb b/jobs/correction-ben-statement/add_corrections_ia.ipynb index dc330cc548..9053874037 100644 --- a/jobs/correction-ben-statement/add_corrections_ia.ipynb +++ b/jobs/correction-ben-statement/add_corrections_ia.ipynb @@ -26,8 +26,7 @@ "source": [ "import os\n", "from dotenv import load_dotenv, find_dotenv\n", - "import psycopg2\n", - "import pandas as pd\n", + "from sqlalchemy import create_engine, text\n", "\n", "# this will load all the envars from a .env file located in the project root (api)\n", "load_dotenv(find_dotenv())\n", @@ -45,19 +44,11 @@ " os.getenv('ENTITY_DATABASE_USERNAME', '') + \":\" + os.getenv('ENTITY_DATABASE_PASSWORD', '') +'@' + \\\n", " os.getenv('ENTITY_DATABASE_HOST', '') + ':' + os.getenv('ENTITY_DATABASE_PORT', '5432') + '/' + \\\n", " os.getenv('ENTITY_DATABASE_NAME', '');\n", - "connect_to_db\n", - " \n", - "%sql $connect_to_db" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%sql \n", - "select now() AT TIME ZONE 'PST' as current_date" + "engine = create_engine(connect_to_db)\n", + "\n", + "# Test connection\n", + "with engine.connect() as conn:\n", + " print(\"Connected successfully!\")" ] }, { @@ -129,74 +120,84 @@ "skipped_identifiers = []\n", "\n", "# loop through list of businesses to create filing\n", - "for identifier in businesses:\n", - " filing_details = %sql \\\n", - " SELECT f.id, f.filing_date \\\n", - " FROM businesses b \\\n", - " JOIN filings f ON b.id = f.business_id \\\n", - " WHERE f.filing_type = 'incorporationApplication' \\\n", - " AND b.identifier = :identifier\n", - " \n", - " if filing_details:\n", - " filing_id = filing_details[0]['id']\n", - " filing_date = filing_details[0]['filing_date']\n", - "\n", - " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", - " \n", - " draft_details = %sql \\\n", - " SELECT b.state, \\\n", - " (SELECT COUNT(1) \\\n", - " FROM filings f \\\n", - " WHERE f.business_id = b.id \\\n", - " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft \\\n", - " FROM businesses b \\\n", - " WHERE b.identifier = :identifier\n", - " \n", - " state = None\n", - " has_draft = None\n", - " if draft_details:\n", - " state = draft_details[0]['state']\n", - " has_draft = draft_details[0]['has_draft']\n", - " \n", - " if state == 'HISTORICAL' or has_draft:\n", - " skipped_identifiers.append(identifier)\n", - " continue\n", - " \n", - " correction_filing_data = {\n", - " \"filing\": {\n", - " \"header\": {\n", - " \"name\": \"correction\",\n", - " \"date\": current_date,\n", - " \"certifiedBy\": \"system\",\n", - " \"correctionBenStatement\": True,\n", - " \"waiveFees\": True\n", - " },\n", - " \"business\": {\n", - " \"identifier\": identifier,\n", - " \"legalType\": \"BEN\"\n", - " },\n", - " \"correction\": {\n", - " \"details\": \"BEN Correction statement\",\n", - " \"correctedFilingId\": filing_id,\n", - " \"correctedFilingType\": \"incorporationApplication\",\n", - " \"commentOnly\": True,\n", - " \"comment\": f\"\"\"Correction for Incorporation Application filed on {formatted_filing_date} \\n{correction_statement}\"\"\"\n", + "with engine.connect() as conn:\n", + " for identifier in businesses:\n", + " filing_details_query = text(\"\"\"\n", + " SELECT f.id, f.filing_date \n", + " FROM businesses b \n", + " JOIN filings f ON b.id = f.business_id \n", + " WHERE f.filing_type = 'incorporationApplication' \n", + " AND b.identifier = :identifier\n", + " \"\"\")\n", + " filing_details_query_result = conn.execute(filing_details_query, {\"identifier\": identifier})\n", + " filing_details = filing_details_query_result.mappings().fetchone()\n", + " \n", + " if filing_details:\n", + " filing_id = filing_details[0]['id']\n", + " filing_date = filing_details[0]['filing_date']\n", + "\n", + " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", + " \n", + " draft_details_query = text(\"\"\"\n", + " SELECT b.state, \n", + " (SELECT COUNT(1) \n", + " FROM filings f \n", + " WHERE f.business_id = b.id \n", + " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft \n", + " FROM businesses b \n", + " WHERE b.identifier = :identifier\n", + " \"\"\")\n", + " draft_details_query_result = conn.execute(draft_details_query, {\"identifier\": identifier})\n", + " draft_details = draft_details_query_result.mappings().fetchone()\n", + " \n", + " state = None\n", + " has_draft = None\n", + " if draft_details:\n", + " state = draft_details[0]['state']\n", + " has_draft = draft_details[0]['has_draft']\n", + " \n", + " if state == 'HISTORICAL' or has_draft:\n", + " skipped_identifiers.append(identifier)\n", + " continue\n", + " \n", + " correction_filing_data = {\n", + " \"filing\": {\n", + " \"header\": {\n", + " \"name\": \"correction\",\n", + " \"date\": current_date,\n", + " \"certifiedBy\": \"system\",\n", + " \"correctionBenStatement\": True,\n", + " \"waiveFees\": True\n", + " },\n", + " \"business\": {\n", + " \"identifier\": identifier,\n", + " \"legalType\": \"BEN\"\n", + " },\n", + " \"correction\": {\n", + " \"details\": \"BEN Correction statement\",\n", + " \"correctedFilingId\": filing_id,\n", + " \"correctedFilingType\": \"incorporationApplication\",\n", + " \"commentOnly\": True,\n", + " \"comment\": f\"\"\"Correction for Incorporation Application filed on {formatted_filing_date} \\n{correction_statement}\"\"\"\n", + " }\n", " }\n", " }\n", - " }\n", - "\n", - " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", - " rv = requests.post(filing_url, headers=headers, json=correction_filing_data)\n", - "\n", - " # Check the status code of the response\n", - " if rv.status_code == 201:\n", - " successful_identifiers.append(identifier)\n", - " else:\n", - " failed_identifiers.append(identifier)\n", - " print(f\"Failed to make POST request. Status code: {rv.status_code}: {rv.text} for {identifier}\")\n", - "print('Successfully filed Corrections for:', successful_identifiers) # Print the successful identifiers\n", - "print('Failed to file Corrections for:', failed_identifiers) # Print the failed identifiers\n", - "print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers\n" + "\n", + " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", + " rv = requests.post(filing_url, headers=headers, json=correction_filing_data)\n", + "\n", + " # Check the status code of the response\n", + " if rv.status_code == 201:\n", + " successful_identifiers.append(identifier)\n", + " else:\n", + " failed_identifiers.append(identifier)\n", + " print(f\"Failed to make POST request. Status code: {rv.status_code}: {rv.text} for {identifier}\")\n", + " print('Successfully filed Corrections for:', successful_identifiers) # Print the successful identifiers\n", + " print('Failed to file Corrections for:', failed_identifiers) # Print the failed identifiers\n", + " print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers\n", + "\n", + "#close the engine connection\n", + "conn.close()" ] } ], diff --git a/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb b/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb index 24ded64ccf..218c710993 100644 --- a/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb +++ b/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb @@ -26,8 +26,7 @@ "source": [ "import os\n", "from dotenv import load_dotenv, find_dotenv\n", - "import psycopg2\n", - "import pandas as pd\n", + "from sqlalchemy import create_engine, text\n", "\n", "# this will load all the envars from a .env file located in the project root (api)\n", "load_dotenv(find_dotenv())\n", @@ -45,19 +44,11 @@ " os.getenv('ENTITY_DATABASE_USERNAME', '') + \":\" + os.getenv('ENTITY_DATABASE_PASSWORD', '') +'@' + \\\n", " os.getenv('ENTITY_DATABASE_HOST', '') + ':' + os.getenv('ENTITY_DATABASE_PORT', '5432') + '/' + \\\n", " os.getenv('ENTITY_DATABASE_NAME', '');\n", - "connect_to_db\n", - " \n", - "%sql $connect_to_db" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%sql \n", - "select now() AT TIME ZONE 'PST' as current_date" + "engine = create_engine(connect_to_db)\n", + "\n", + "# Test connection\n", + "with engine.connect() as conn:\n", + " print(\"Connected successfully!\")" ] }, { @@ -121,73 +112,84 @@ "skipped_identifiers = []\n", "\n", "# loop through list of businesses to create filing\n", - "for identifier in businesses:\n", - " filing_details = %sql \\\n", - " SELECT f.id, f.filing_date \\\n", - " FROM businesses b \\\n", - " JOIN filings f ON b.id = f.business_id \\\n", - " WHERE f.filing_type = 'alteration' \\\n", - " AND f.meta_data->'alteration'->>'fromLegalType' IN ('BC', 'ULC', 'CC', 'C', 'CUL', 'CCC') \\\n", - " AND f.meta_data->'alteration'->>'toLegalType' IN ('BEN', 'CBEN') \\\n", - " AND b.identifier = :identifier\n", - " \n", - " if filing_details:\n", - " filing_id = filing_details[0]['id']\n", - " filing_date = filing_details[0]['filing_date']\n", - "\n", - " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", - " \n", - " draft_details = %sql \\\n", - " SELECT b.state, \\\n", - " (SELECT COUNT(1) \\\n", - " FROM filings f \\\n", - " WHERE f.business_id = b.id \\\n", - " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft \\\n", - " FROM businesses b \\\n", - " WHERE b.identifier = :identifier\n", - " \n", - " if draft_details:\n", - " state = draft_details[0]['state']\n", - " has_draft = draft_details[0]['has_draft']\n", - " \n", - " if state == 'HISTORICAL' or has_draft:\n", - " skipped_identifiers.append(identifier)\n", - " continue\n", - " \n", - " filing_data = {\n", - " \"filing\": {\n", - " \"header\": {\n", - " \"name\": \"registrarsNotation\",\n", - " \"date\": current_date,\n", - " \"certifiedBy\": \"system\"\n", - " },\n", - " \"business\": {\n", - " \"identifier\": identifier,\n", - " \"legalType\": \"BEN\"\n", - " },\n", - " \"registrarsNotation\": {\n", - " \"orderDetails\": \"BC benefit company statement contained in notice of articles as required under \" + \n", - " \"section 51.992 of the Business Corporations Act corrected from \" +\n", - " \"\\\"This company is a benefit company and, as such, has purposes that include conducting its business \" +\n", - " \" in a responsible and sustainable manner and promoting one or more public benefits\\\" to \" + \n", - " \"\\\"This company is a benefit company and, as such, is committed to conducting its business in a \" + \n", - " \"responsible and sustainable manner and promoting one or more public benefits\\\".\"\n", + "with engine.connect() as conn:\n", + " for identifier in businesses:\n", + " filing_details_query = text(\"\"\"\n", + " SELECT f.id, f.filing_date\n", + " FROM businesses b\n", + " JOIN filings f ON b.id = f.business_id\n", + " WHERE f.filing_type = 'alteration'\n", + " AND f.meta_data->'alteration'->>'fromLegalType' IN ('BC', 'ULC', 'CC', 'C', 'CUL', 'CCC')\n", + " AND f.meta_data->'alteration'->>'toLegalType' IN ('BEN', 'CBEN')\n", + " AND b.identifier = :identifier\n", + " \"\"\")\n", + " filing_details_query_result = conn.execute(filing_details_query, {\"identifier\": identifier})\n", + " filing_details = filing_details_query_result.mappings().fetchone()\n", + " \n", + " if filing_details:\n", + " filing_id = filing_details[0]['id']\n", + " filing_date = filing_details[0]['filing_date']\n", + "\n", + " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", + " \n", + " draft_details_query = text(\"\"\"\n", + " SELECT b.state,\n", + " (SELECT COUNT(1)\n", + " FROM filings f\n", + " WHERE f.business_id = b.id\n", + " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft\n", + " FROM businesses b\n", + " WHERE b.identifier = :identifier\n", + " \"\"\")\n", + " \n", + " draft_details_query_result = conn.execute(draft_details_query, {\"identifier\": identifier})\n", + " draft_details = draft_details_query_result.mappings().fetchone()\n", + " \n", + " if draft_details:\n", + " state = draft_details[0]['state']\n", + " has_draft = draft_details[0]['has_draft']\n", + " \n", + " if state == 'HISTORICAL' or has_draft:\n", + " skipped_identifiers.append(identifier)\n", + " continue\n", + " \n", + " filing_data = {\n", + " \"filing\": {\n", + " \"header\": {\n", + " \"name\": \"registrarsNotation\",\n", + " \"date\": current_date,\n", + " \"certifiedBy\": \"system\"\n", + " },\n", + " \"business\": {\n", + " \"identifier\": identifier,\n", + " \"legalType\": \"BEN\"\n", + " },\n", + " \"registrarsNotation\": {\n", + " \"orderDetails\": \"BC benefit company statement contained in notice of articles as required under \" + \n", + " \"section 51.992 of the Business Corporations Act corrected from \" +\n", + " \"\\\"This company is a benefit company and, as such, has purposes that include conducting its business \" +\n", + " \" in a responsible and sustainable manner and promoting one or more public benefits\\\" to \" + \n", + " \"\\\"This company is a benefit company and, as such, is committed to conducting its business in a \" + \n", + " \"responsible and sustainable manner and promoting one or more public benefits\\\".\"\n", + " }\n", " }\n", " }\n", - " }\n", - "\n", - " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", - " response = requests.post(filing_url, headers=headers, json=filing_data)\n", - "\n", - " # Check the status code of the response\n", - " if response.status_code == 201:\n", - " successful_identifiers.append(identifier)\n", - " else:\n", - " failed_identifiers.append(identifier)\n", - " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", - "print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", - "print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", - "print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n" + "\n", + " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", + " response = requests.post(filing_url, headers=headers, json=filing_data)\n", + "\n", + " # Check the status code of the response\n", + " if response.status_code == 201:\n", + " successful_identifiers.append(identifier)\n", + " else:\n", + " failed_identifiers.append(identifier)\n", + " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", + " print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", + " print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", + " print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n", + "\n", + "#close the engine connection\n", + "conn.close()" ] } ], diff --git a/jobs/correction-ben-statement/add_registrars_notation_historical.ipynb b/jobs/correction-ben-statement/add_registrars_notation_historical.ipynb index fb5ad05dd3..af2548bde8 100644 --- a/jobs/correction-ben-statement/add_registrars_notation_historical.ipynb +++ b/jobs/correction-ben-statement/add_registrars_notation_historical.ipynb @@ -26,8 +26,7 @@ "source": [ "import os\n", "from dotenv import load_dotenv, find_dotenv\n", - "import psycopg2\n", - "import pandas as pd\n", + "from sqlalchemy import create_engine, text\n", "\n", "# this will load all the envars from a .env file located in the project root (api)\n", "load_dotenv(find_dotenv())\n", @@ -45,9 +44,11 @@ " os.getenv('ENTITY_DATABASE_USERNAME', '') + \":\" + os.getenv('ENTITY_DATABASE_PASSWORD', '') +'@' + \\\n", " os.getenv('ENTITY_DATABASE_HOST', '') + ':' + os.getenv('ENTITY_DATABASE_PORT', '5434') + '/' + \\\n", " os.getenv('ENTITY_DATABASE_NAME', '');\n", - "connect_to_db\n", - " \n", - "%sql $connect_to_db" + "engine = create_engine(connect_to_db)\n", + "\n", + "# Test connection\n", + "with engine.connect() as conn:\n", + " print(\"Connected successfully!\")" ] }, { @@ -121,41 +122,42 @@ "skipped_identifiers = []\n", "\n", "# loop through list of businesses to create filing\n", - "for identifier in businesses: \n", - " filing_data = {\n", - " \"filing\": {\n", - " \"header\": {\n", - " \"name\": \"registrarsNotation\",\n", - " \"date\": current_date,\n", - " \"certifiedBy\": \"system\"\n", - " },\n", - " \"business\": {\n", - " \"identifier\": identifier,\n", - " \"legalType\": \"BEN\"\n", - " },\n", - " \"registrarsNotation\": {\n", - " \"orderDetails\": \"BC benefit company statement contained in notice of articles as required under \" + \n", - " \"section 51.992 of the Business Corporations Act corrected from \" +\n", - " \"\\\"This company is a benefit company and, as such, has purposes that include conducting its business \" +\n", - " \" in a responsible and sustainable manner and promoting one or more public benefits\\\" to \" + \n", - " \"\\\"This company is a benefit company and, as such, is committed to conducting its business in a \" + \n", - " \"responsible and sustainable manner and promoting one or more public benefits\\\".\"\n", + "with engine.connect() as conn:\n", + " for identifier in businesses: \n", + " filing_data = {\n", + " \"filing\": {\n", + " \"header\": {\n", + " \"name\": \"registrarsNotation\",\n", + " \"date\": current_date,\n", + " \"certifiedBy\": \"system\"\n", + " },\n", + " \"business\": {\n", + " \"identifier\": identifier,\n", + " \"legalType\": \"BEN\"\n", + " },\n", + " \"registrarsNotation\": {\n", + " \"orderDetails\": \"BC benefit company statement contained in notice of articles as required under \" + \n", + " \"section 51.992 of the Business Corporations Act corrected from \" +\n", + " \"\\\"This company is a benefit company and, as such, has purposes that include conducting its business \" +\n", + " \" in a responsible and sustainable manner and promoting one or more public benefits\\\" to \" + \n", + " \"\\\"This company is a benefit company and, as such, is committed to conducting its business in a \" + \n", + " \"responsible and sustainable manner and promoting one or more public benefits\\\".\"\n", + " }\n", " }\n", " }\n", - " }\n", "\n", - " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", - " response = requests.post(filing_url, headers=headers, json=filing_data)\n", + " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", + " response = requests.post(filing_url, headers=headers, json=filing_data)\n", "\n", - " # Check the status code of the response\n", - " if response.status_code == 201:\n", - " successful_identifiers.append(identifier)\n", - " else:\n", - " failed_identifiers.append(identifier)\n", - " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", - "print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", - "print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", - "print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n" + " # Check the status code of the response\n", + " if response.status_code == 201:\n", + " successful_identifiers.append(identifier)\n", + " else:\n", + " failed_identifiers.append(identifier)\n", + " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", + " print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", + " print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", + " print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n" ] } ], diff --git a/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb b/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb index 292656df85..4c06bde2c5 100644 --- a/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb +++ b/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb @@ -26,8 +26,7 @@ "source": [ "import os\n", "from dotenv import load_dotenv, find_dotenv\n", - "import psycopg2\n", - "import pandas as pd\n", + "from sqlalchemy import create_engine, text\n", "\n", "# this will load all the envars from a .env file located in the project root (api)\n", "load_dotenv(find_dotenv())\n", @@ -45,19 +44,11 @@ " os.getenv('ENTITY_DATABASE_USERNAME', '') + \":\" + os.getenv('ENTITY_DATABASE_PASSWORD', '') +'@' + \\\n", " os.getenv('ENTITY_DATABASE_HOST', '') + ':' + os.getenv('ENTITY_DATABASE_PORT', '5432') + '/' + \\\n", " os.getenv('ENTITY_DATABASE_NAME', '');\n", - "connect_to_db\n", - " \n", - "%sql $connect_to_db" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%%sql \n", - "select now() AT TIME ZONE 'PST' as current_date" + "engine = create_engine(connect_to_db)\n", + "\n", + "# Test connection\n", + "with engine.connect() as conn:\n", + " print(\"Connected successfully!\")" ] }, { @@ -121,80 +112,90 @@ "skipped_identifiers = []\n", "\n", "# loop through list of businesses to create filing\n", - "for identifier in businesses:\n", - " filing_details = %sql \\\n", - " SELECT f.id, f.filing_date \\\n", - " FROM businesses b \\\n", - " JOIN filings f ON b.id = f.business_id \\\n", - " WHERE f.filing_type = 'incorporationApplication' \\\n", - " AND b.identifier = :identifier\n", - " \n", - " if filing_details:\n", - " filing_id = filing_details[0]['id']\n", - " filing_date = filing_details[0]['filing_date']\n", - "\n", - " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", - " \n", - " draft_details = %sql \\\n", - " SELECT b.state, \\\n", - " (SELECT COUNT(1) \\\n", - " FROM filings f \\\n", - " WHERE f.business_id = b.id \\\n", - " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft \\\n", - " FROM businesses b \\\n", - " WHERE b.identifier = :identifier\n", - " \n", - " state = None\n", - " has_draft = None\n", - " \n", - " if draft_details:\n", - " state = draft_details[0]['state']\n", - " has_draft = draft_details[0]['has_draft']\n", - " \n", - " if state == 'HISTORICAL' or has_draft:\n", - " skipped_identifiers.append(identifier)\n", - " continue\n", + "with engine.connect() as conn:\n", + " for identifier in businesses:\n", + " filing_details_query = text(\"\"\"\n", + " SELECT f.id, f.filing_date \n", + " FROM businesses b \n", + " JOIN filings f ON b.id = f.business_id \n", + " WHERE f.filing_type = 'incorporationApplication' \n", + " AND b.identifier = :identifier\n", + " \"\"\")\n", + " filing_details_query_result = conn.execute(filing_details_query, {\"identifier\": identifier})\n", + " filing_details = filing_details_query_result.mappings().fetchone()\n", + " \n", + " if filing_details:\n", + " filing_id = filing_details['id']\n", + " filing_date = filing_details['filing_date']\n", + "\n", + " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", + " \n", + " draft_details_query = text(\"\"\"\n", + " SELECT b.state, \n", + " (SELECT COUNT(1) \n", + " FROM filings f \n", + " WHERE f.business_id = b.id \n", + " AND f.status in ('DRAFT', 'PENDING')) <> 0 AS has_draft \n", + " FROM businesses b \n", + " WHERE b.identifier = :identifier\n", + " \"\"\")\n", + " draft_details_query_result = conn.execute(draft_details_query, {\"identifier\": identifier})\n", + " draft_details = draft_details_query_result.mappings().fetchone()\n", + " \n", + " state = None\n", + " has_draft = None\n", + " \n", + " if draft_details:\n", + " state = draft_details['state']\n", + " has_draft = draft_details['has_draft']\n", + " \n", + " if state == 'HISTORICAL' or has_draft:\n", + " skipped_identifiers.append(identifier)\n", + " continue\n", " \n", - " filing_data = {\n", - " \"filing\": {\n", - " \"header\": {\n", - " \"name\": \"registrarsNotation\",\n", - " \"date\": current_date,\n", - " \"certifiedBy\": \"system\"\n", - " },\n", - " \"business\": {\n", - " \"identifier\": identifier,\n", - " \"legalType\": \"BEN\"\n", - " },\n", - " \"registrarsNotation\": {\n", - " \"orderDetails\": \"BC benefit company statement contained in notice of articles as required under \" + \n", - " \"section 51.992 of the Business Corporations Act corrected from \" +\n", - " \"\\\"This company is a benefit company and, as such, has purposes that include conducting its business \" +\n", - " \" in a responsible and sustainable manner and promoting one or more public benefits\\\" to \" + \n", - " \"\\\"This company is a benefit company and, as such, is committed to conducting its business in a \" + \n", - " \"responsible and sustainable manner and promoting one or more public benefits\\\".\"\n", + " filing_data = {\n", + " \"filing\": {\n", + " \"header\": {\n", + " \"name\": \"registrarsNotation\",\n", + " \"date\": current_date,\n", + " \"certifiedBy\": \"system\"\n", + " },\n", + " \"business\": {\n", + " \"identifier\": identifier,\n", + " \"legalType\": \"BEN\"\n", + " },\n", + " \"registrarsNotation\": {\n", + " \"orderDetails\": \"BC benefit company statement contained in notice of articles as required under \" + \n", + " \"section 51.992 of the Business Corporations Act corrected from \" +\n", + " \"\\\"This company is a benefit company and, as such, has purposes that include conducting its business \" +\n", + " \" in a responsible and sustainable manner and promoting one or more public benefits\\\" to \" + \n", + " \"\\\"This company is a benefit company and, as such, is committed to conducting its business in a \" + \n", + " \"responsible and sustainable manner and promoting one or more public benefits\\\".\"\n", + " }\n", " }\n", " }\n", - " }\n", - "\n", - " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", - " response = requests.post(filing_url, headers=headers, json=filing_data)\n", - "\n", - " # Check the status code of the response\n", - " if response.status_code == 201:\n", - " successful_identifiers.append(identifier)\n", - " else:\n", - " failed_identifiers.append(identifier)\n", - " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", - "print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", - "print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", - "print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n" + "\n", + " filing_url = urljoin(base_url, f\"/api/v2/businesses/{identifier}/filings\")\n", + " response = requests.post(filing_url, headers=headers, json=filing_data)\n", + "\n", + " # Check the status code of the response\n", + " if response.status_code == 201:\n", + " successful_identifiers.append(identifier)\n", + " else:\n", + " failed_identifiers.append(identifier)\n", + " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", + " print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", + " print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", + " print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n", + "\n", + "#close the engine connection\n", + "conn.close()" ] } ], "metadata": { "kernelspec": { - "display_name": "3.8.17", + "display_name": "3.11.7", "language": "python", "name": "python3" }, @@ -208,7 +209,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.17" + "version": "3.11.7" } }, "nbformat": 4, From 53904078316c01687abcbac0e674093c1ad2751f Mon Sep 17 00:00:00 2001 From: Ketaki Deodhar Date: Tue, 3 Mar 2026 12:14:36 -0800 Subject: [PATCH 02/46] modify correction notebooks --- .../add_corrections_alterations.ipynb | 8 ++++---- .../add_corrections_ia.ipynb | 12 ++++++------ .../add_registrars_notation_alteration.ipynb | 8 ++++---- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/jobs/correction-ben-statement/add_corrections_alterations.ipynb b/jobs/correction-ben-statement/add_corrections_alterations.ipynb index f2d1af40fb..112a5874e3 100644 --- a/jobs/correction-ben-statement/add_corrections_alterations.ipynb +++ b/jobs/correction-ben-statement/add_corrections_alterations.ipynb @@ -135,8 +135,8 @@ " filing_details = filing_details_query_result.mappings().fetchone()\n", " \n", " if filing_details:\n", - " filing_id = filing_details[0]['id']\n", - " filing_date = filing_details[0]['filing_date']\n", + " filing_id = filing_details['id']\n", + " filing_date = filing_details['filing_date']\n", "\n", " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", " \n", @@ -156,8 +156,8 @@ " state = None\n", " has_draft = None\n", " if draft_details:\n", - " state = draft_details[0]['state']\n", - " has_draft = draft_details[0]['has_draft']\n", + " state = draft_details['state']\n", + " has_draft = draft_details['has_draft']\n", " \n", " if state != 'ACTIVE' or has_draft:\n", " skipped_identifiers.append(identifier)\n", diff --git a/jobs/correction-ben-statement/add_corrections_ia.ipynb b/jobs/correction-ben-statement/add_corrections_ia.ipynb index 9053874037..0e1184c624 100644 --- a/jobs/correction-ben-statement/add_corrections_ia.ipynb +++ b/jobs/correction-ben-statement/add_corrections_ia.ipynb @@ -133,8 +133,8 @@ " filing_details = filing_details_query_result.mappings().fetchone()\n", " \n", " if filing_details:\n", - " filing_id = filing_details[0]['id']\n", - " filing_date = filing_details[0]['filing_date']\n", + " filing_id = filing_details['id']\n", + " filing_date = filing_details['filing_date']\n", "\n", " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", " \n", @@ -153,8 +153,8 @@ " state = None\n", " has_draft = None\n", " if draft_details:\n", - " state = draft_details[0]['state']\n", - " has_draft = draft_details[0]['has_draft']\n", + " state = draft_details['state']\n", + " has_draft = draft_details['has_draft']\n", " \n", " if state == 'HISTORICAL' or has_draft:\n", " skipped_identifiers.append(identifier)\n", @@ -203,7 +203,7 @@ ], "metadata": { "kernelspec": { - "display_name": "3.8.17", + "display_name": "3.11.7", "language": "python", "name": "python3" }, @@ -217,7 +217,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.17" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb b/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb index 218c710993..29e647052f 100644 --- a/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb +++ b/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb @@ -127,8 +127,8 @@ " filing_details = filing_details_query_result.mappings().fetchone()\n", " \n", " if filing_details:\n", - " filing_id = filing_details[0]['id']\n", - " filing_date = filing_details[0]['filing_date']\n", + " filing_id = filing_details['id']\n", + " filing_date = filing_details['filing_date']\n", "\n", " formatted_filing_date = filing_date.strftime(\"%B %d, %Y\")\n", " \n", @@ -146,8 +146,8 @@ " draft_details = draft_details_query_result.mappings().fetchone()\n", " \n", " if draft_details:\n", - " state = draft_details[0]['state']\n", - " has_draft = draft_details[0]['has_draft']\n", + " state = draft_details['state']\n", + " has_draft = draft_details['has_draft']\n", " \n", " if state == 'HISTORICAL' or has_draft:\n", " skipped_identifiers.append(identifier)\n", From 83a3ee25c0b264db0734a7ff50ea13e896a36587 Mon Sep 17 00:00:00 2001 From: Ketaki Deodhar Date: Tue, 3 Mar 2026 12:37:03 -0800 Subject: [PATCH 03/46] remove close connection --- .../add_corrections_alterations.ipynb | 9 +++------ jobs/correction-ben-statement/add_corrections_ia.ipynb | 5 +---- .../add_registrars_notation_alteration.ipynb | 9 +++------ .../add_registrars_notation_ia.ipynb | 5 +---- 4 files changed, 8 insertions(+), 20 deletions(-) diff --git a/jobs/correction-ben-statement/add_corrections_alterations.ipynb b/jobs/correction-ben-statement/add_corrections_alterations.ipynb index 112a5874e3..a23bdc1ea1 100644 --- a/jobs/correction-ben-statement/add_corrections_alterations.ipynb +++ b/jobs/correction-ben-statement/add_corrections_alterations.ipynb @@ -198,16 +198,13 @@ " print(f\"Failed to make POST request. Status code: {rv.status_code}: {rv.text} for {identifier}\")\n", " print('Successfully filed Corrections for:', successful_identifiers) # Print the error message if the request fails \n", " print('Failed to file Corrections for:', failed_identifiers) # Print the error message if the request fails\n", - " print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers\n", - "\n", - "#close the engine connection\n", - "conn.close()" + " print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers" ] } ], "metadata": { "kernelspec": { - "display_name": "3.8.17", + "display_name": "3.11.7", "language": "python", "name": "python3" }, @@ -221,7 +218,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.17" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/jobs/correction-ben-statement/add_corrections_ia.ipynb b/jobs/correction-ben-statement/add_corrections_ia.ipynb index 0e1184c624..e8b5966705 100644 --- a/jobs/correction-ben-statement/add_corrections_ia.ipynb +++ b/jobs/correction-ben-statement/add_corrections_ia.ipynb @@ -194,10 +194,7 @@ " print(f\"Failed to make POST request. Status code: {rv.status_code}: {rv.text} for {identifier}\")\n", " print('Successfully filed Corrections for:', successful_identifiers) # Print the successful identifiers\n", " print('Failed to file Corrections for:', failed_identifiers) # Print the failed identifiers\n", - " print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers\n", - "\n", - "#close the engine connection\n", - "conn.close()" + " print('Skipped to file Corrections for:', skipped_identifiers) # Print the skipped identifiers" ] } ], diff --git a/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb b/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb index 29e647052f..d6e98bc715 100644 --- a/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb +++ b/jobs/correction-ben-statement/add_registrars_notation_alteration.ipynb @@ -186,16 +186,13 @@ " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", " print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", " print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", - " print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n", - "\n", - "#close the engine connection\n", - "conn.close()" + " print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers" ] } ], "metadata": { "kernelspec": { - "display_name": "3.8.17", + "display_name": "3.11.7", "language": "python", "name": "python3" }, @@ -209,7 +206,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.17" + "version": "3.11.7" } }, "nbformat": 4, diff --git a/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb b/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb index 4c06bde2c5..9c13f189bf 100644 --- a/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb +++ b/jobs/correction-ben-statement/add_registrars_notation_ia.ipynb @@ -186,10 +186,7 @@ " print(f\"Failed to make POST request. Status code: {response.status_code} for {identifier}\")\n", " print('Successfully filed Registrar Notation for:', successful_identifiers) # Print the successful identifiers\n", " print('Failed to file Registrar Notation for:', failed_identifiers) # Print the failed identifiers\n", - " print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers\n", - "\n", - "#close the engine connection\n", - "conn.close()" + " print('Skipped to file Registrar Notation for:', skipped_identifiers) # Print the skipped identifiers" ] } ], From 5a20308319d4db883f10015f2ffcb1df700080d7 Mon Sep 17 00:00:00 2001 From: meawong Date: Tue, 3 Mar 2026 16:04:54 -0800 Subject: [PATCH 04/46] =?UTF-8?q?Revert=20"32594=20-=20Add=20validation=20?= =?UTF-8?q?for=20no=20contact=20point=20in=20limited=20restorations=20(?= =?UTF-8?q?=E2=80=A6"=20(#4132)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 3db205e54019b03354fcf1d8c9f575ead4e79f33. --- legal-api/poetry.lock | 6 +- legal-api/pyproject.toml | 2 +- .../filings/validations/restoration.py | 9 --- .../filings/validations/test_restoration.py | 66 ------------------- 4 files changed, 4 insertions(+), 79 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 96b6097bee..2ca5c10dc4 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -2231,8 +2231,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.63" -resolved_reference = "0e584cc385d1cd00722f96032af346b48f875d72" +reference = "2.18.62" +resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" [[package]] name = "reportlab" @@ -3134,4 +3134,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9.22,<3.10" -content-hash = "1cb0b50b3e444405eec28df9a200178315601fa43e355662e85a1b1db1a4c03e" +content-hash = "5425c99816e9059bc80ed201c71960e55dc2ac19956193e5f33acd3134b0390f" diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index 6a0c91de97..d895f566e4 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "blinker (==1.4)", "pyjwt (==2.8.0)", - "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.63#egg=registry_schemas", + "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.62#egg=registry_schemas", "sql-versioning @ git+https://github.com/bcgov/lear.git@main#subdirectory=python/common/sql-versioning", "gcp-queue @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/gcp-queue", "structured-logging @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/structured-logging" diff --git a/legal-api/src/legal_api/services/filings/validations/restoration.py b/legal-api/src/legal_api/services/filings/validations/restoration.py index 39b58b6941..06a19ba760 100644 --- a/legal-api/src/legal_api/services/filings/validations/restoration.py +++ b/legal-api/src/legal_api/services/filings/validations/restoration.py @@ -51,7 +51,6 @@ def validate(business: Business, restoration: dict) -> Optional[Error]: "limitedRestoration") if restoration_type in ("limitedRestoration", "limitedRestorationExtension"): msg.extend(validate_expiry_date(business, restoration, restoration_type)) - msg.extend(validate_contact_point(restoration)) elif restoration_type in ("fullRestoration", "limitedRestorationToFull"): msg.extend(validate_relationship(restoration)) @@ -81,14 +80,6 @@ def validate(business: Business, restoration: dict) -> Optional[Error]: return Error(HTTPStatus.BAD_REQUEST, msg) return None -def validate_contact_point(filing: dict) -> list: - """Validate that no contact point is provided for limited restoration filings.""" - msg = [] - if filing.get("filing", {}).get("restoration", {}).get("contactPoint", None): - msg.append({"error": "Contact point must not be provided. " - "The corporation will be restored with the contact information it had prior to dissolution.", - "path": "/filing/restoration/contactPoint"}) - return msg def validate_expiry_date(business: Business, filing: dict, restoration_type: str) -> list: """Validate expiry date.""" diff --git a/legal-api/tests/unit/services/filings/validations/test_restoration.py b/legal-api/tests/unit/services/filings/validations/test_restoration.py index ffc7e1b1b4..8df3e4f38d 100644 --- a/legal-api/tests/unit/services/filings/validations/test_restoration.py +++ b/legal-api/tests/unit/services/filings/validations/test_restoration.py @@ -95,9 +95,6 @@ def execute_test_restoration_nr(mocker, filing_sub_type, legal_type, nr_number, mock_nr_response = MockResponse(temp_nr_response, HTTPStatus.OK) mocker.patch('legal_api.services.NameXService.query_nr_number', return_value=mock_nr_response) - - if filing['filing']['restoration']['type'] in ('limitedRestoration', 'limitedRestorationExtension'): - del filing['filing']['restoration']['contactPoint'] with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -144,10 +141,6 @@ def test_validate_party(session, test_name, party_role, expected_msg): filing['filing']['restoration']['parties'][0]['roles'][0]['roleType'] = party_role else: filing['filing']['restoration']['parties'] = [] - - if filing['filing']['restoration']['type'] in ('limitedRestoration', 'limitedRestorationExtension'): - del filing['filing']['restoration']['contactPoint'] - err = validate(business, filing) if expected_msg: @@ -184,7 +177,6 @@ def test_validate_relationship(session, test_status, restoration_type, expected_ filing['filing']['restoration']['nameRequest']['legalName'] = legal_name if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): - del filing['filing']['restoration']['contactPoint'] expiry_date = LegislationDatetime.now() + relativedelta(months=1) filing['filing']['restoration']['expiry'] = expiry_date.strftime(date_format) elif test_status == 'SUCCESS' and restoration_type in ('fullRestoration', 'limitedRestorationToFull'): @@ -241,9 +233,6 @@ def test_validate_expiry_date(session, test_name, restoration_type, delta_date, filing['filing']['restoration']['type'] = restoration_type if delta_date: filing['filing']['restoration']['expiry'] = expiry_date.strftime(date_format) - - if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): - del filing['filing']['restoration']['contactPoint'] with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -294,9 +283,6 @@ def test_approval_type(session, test_status, restoration_types, legal_types, app filing['filing']['restoration']['applicationDate'] = '2023-03-30' filing['filing']['restoration']['noticeDate'] = '2023-03-30' - if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): - del filing['filing']['restoration']['contactPoint'] - with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -353,9 +339,6 @@ def test_restoration_court_orders(session, test_status, restoration_types, legal else: del filing['filing']['restoration']['courtOrder'] - if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): - del filing['filing']['restoration']['contactPoint'] - with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -413,9 +396,6 @@ def test_restoration_registrar(session, test_status, restoration_types, legal_ty if notice_date: filing['filing']['restoration']['noticeDate'] = notice_date - if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): - del filing['filing']['restoration']['contactPoint'] - with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -553,49 +533,3 @@ def test_validate_restoration_name_translation(session, test_name, name_translat if err: print(err, err.code, err.msg) assert err is None - -@pytest.mark.parametrize( - 'test_status, restoration_type, has_contact_point, expected_code, expected_msg', - [ - ('SUCCESS', 'limitedRestoration', False, None, None), - ('SUCCESS', 'limitedRestorationExtension', False, None, None), - ('FAIL', 'limitedRestoration', True, HTTPStatus.BAD_REQUEST, - 'Contact point must not be provided. ' - 'The corporation will be restored with the contact information it had prior to dissolution.'), - ('FAIL', 'limitedRestorationExtension', True, HTTPStatus.BAD_REQUEST, - 'Contact point must not be provided. ' - 'The corporation will be restored with the contact information it had prior to dissolution.'), - ] -) -def test_validate_contact_point(session, test_status, restoration_type, has_contact_point, expected_code, expected_msg): - """Assert that contact point is not allowed for limited restoration filings.""" - business = Business(identifier='BC1234567', legal_type='BC', restoration_expiry_date=LegislationDatetime.now()) - limited_restoration_filing = None - if restoration_type == 'limitedRestorationExtension': - limited_restoration_filing = factory_limited_restoration_filing() - - filing = copy.deepcopy(FILING_HEADER) - filing['filing']['restoration'] = copy.deepcopy(RESTORATION) - filing['filing']['header']['name'] = 'restoration' - filing['filing']['restoration']['type'] = restoration_type - - expiry_date = LegislationDatetime.now() + relativedelta(months=1) - filing['filing']['restoration']['expiry'] = expiry_date.strftime(date_format) - - if restoration_type == 'limitedRestorationExtension': - del filing['filing']['restoration']['nameRequest'] - else: - filing['filing']['restoration']['nameRequest']['legalName'] = legal_name - - if not has_contact_point: - del filing['filing']['restoration']['contactPoint'] - - with patch.object(Filing, 'get_most_recent_filing', - return_value=limited_restoration_filing): - err = validate(business, filing) - - if expected_code: - assert expected_code == err.code - assert expected_msg == err.msg[0]['error'] - else: - assert not err From f2e037a9b0583516af71ab6260d809f4883c1de6 Mon Sep 17 00:00:00 2001 From: meawong Date: Wed, 4 Mar 2026 08:47:13 -0800 Subject: [PATCH 05/46] 32594 - Update registry schema version (#4135) --- legal-api/poetry.lock | 8 ++++---- legal-api/pyproject.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 2ca5c10dc4..200c993859 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -2213,7 +2213,7 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "registry_schemas" -version = "2.18.62" +version = "2.18.63" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -2231,8 +2231,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.62" -resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" +reference = "2.18.63" +resolved_reference = "0e584cc385d1cd00722f96032af346b48f875d72" [[package]] name = "reportlab" @@ -3134,4 +3134,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9.22,<3.10" -content-hash = "5425c99816e9059bc80ed201c71960e55dc2ac19956193e5f33acd3134b0390f" +content-hash = "1cb0b50b3e444405eec28df9a200178315601fa43e355662e85a1b1db1a4c03e" diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index d895f566e4..6a0c91de97 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "blinker (==1.4)", "pyjwt (==2.8.0)", - "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.62#egg=registry_schemas", + "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.63#egg=registry_schemas", "sql-versioning @ git+https://github.com/bcgov/lear.git@main#subdirectory=python/common/sql-versioning", "gcp-queue @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/gcp-queue", "structured-logging @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/structured-logging" From 23dd8fe1741929690f5b2a59d80440f0fd0a6730 Mon Sep 17 00:00:00 2001 From: Lucas O'Neil Date: Wed, 4 Mar 2026 09:08:40 -0800 Subject: [PATCH 06/46] Add pytest ini with addopts for code cov GHA fix (#4134) Signed-off-by: Lucas --- queue_services/business-digital-credentials/Makefile | 2 +- queue_services/business-digital-credentials/pyproject.toml | 3 +++ queue_services/business-emailer/pyproject.toml | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/queue_services/business-digital-credentials/Makefile b/queue_services/business-digital-credentials/Makefile index 5fc3bd1611..cfe7a679a6 100644 --- a/queue_services/business-digital-credentials/Makefile +++ b/queue_services/business-digital-credentials/Makefile @@ -53,7 +53,7 @@ run: ## Run the project in local business-digital-credentials test: ## Unit testing - poetry run pytest --cov=src --cov-report=xml --cov-report=html --cov-report=term --cov-report=term-missing + poetry run pytest --cov-report=xml --cov-report=html --cov-report=term-missing coverage-check: poetry run pytest --cov=src --cov-fail-under=85 diff --git a/queue_services/business-digital-credentials/pyproject.toml b/queue_services/business-digital-credentials/pyproject.toml index 16f8ab8795..e2f9977b9b 100644 --- a/queue_services/business-digital-credentials/pyproject.toml +++ b/queue_services/business-digital-credentials/pyproject.toml @@ -130,6 +130,9 @@ docstring-quotes = "double" [tool.ruff.lint.extend-per-file-ignores] "**/__init__.py" = ["F401"] # used for imports +[tool.pytest.ini_options] +addopts = "--cov=src" + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/queue_services/business-emailer/pyproject.toml b/queue_services/business-emailer/pyproject.toml index 9da95847f8..d1035e6e89 100644 --- a/queue_services/business-emailer/pyproject.toml +++ b/queue_services/business-emailer/pyproject.toml @@ -199,6 +199,9 @@ docstring-quotes = "double" ] "src/business_emailer/services/namex.py" = ["I001"] # ignoring 'unordered' imports +[tool.pytest.ini_options] +addopts = "--cov=src" + [build-system] requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" From d844169681baed690b77df9617897414cd5c42ff Mon Sep 17 00:00:00 2001 From: Lucas O'Neil Date: Wed, 4 Mar 2026 14:48:31 -0800 Subject: [PATCH 07/46] Remove CoD effective date validation (#4136) Signed-off-by: Lucas --- .../validations/change_of_directors.py | 10 +-- .../test_validation_effective_date.py | 87 ------------------- 2 files changed, 1 insertion(+), 96 deletions(-) diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_directors.py b/legal-api/src/legal_api/services/filings/validations/change_of_directors.py index 05e57780b1..ebe7e3062e 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_directors.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_directors.py @@ -18,7 +18,7 @@ from flask_babel import _ as babel # importing camelcase '_' as a name from legal_api.errors import Error -from legal_api.models import Address, Business, Filing +from legal_api.models import Address, Business from legal_api.services.filings.validations.common_validations import PARTY_NAME_MAX_LENGTH, validate_parties_addresses from legal_api.services.utils import get_str from legal_api.utils.datetime import date, datetime @@ -260,14 +260,6 @@ def validate_effective_date(business: Business, cod: dict) -> list: if effective_date_leg < founding_date_leg: msg.append({"error": babel("Effective date cannot be before businesses founding date.")}) - # check if effective date is before their most recent COD or AR date - last_cod_filing = Filing.get_most_recent_legal_filing(business.id, - Filing.FILINGS["changeOfDirectors"]["name"]) - if last_cod_filing: - last_cod_date_leg = LegislationDatetime.as_legislation_timezone(last_cod_filing.effective_date).date() - if effective_date_leg < last_cod_date_leg: - msg.append({"error": babel("Effective date cannot be before another Change of Director filing.")}) - return msg def validate_directors_name(cod: dict) -> list: diff --git a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py index 454c5ec32f..ded2b04eba 100644 --- a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py +++ b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py @@ -150,93 +150,6 @@ def test_validate_effective_date_not_before_founding(session): assert not err -def test_validate_effective_date_not_before_other_COD(session): # noqa: N802; COD is an acronym - """Assert that the effective date of change cannot be before a previous COD filing.""" - # setup - identifier = 'CP1234567' - founding_date = datetime(2000, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) - business = Business(identifier=identifier, - founding_date=founding_date) - business.save() - now = datetime(2020, 7, 30, 12, 0, 0, 0, tzinfo=timezone.utc) - - # create the previous COD filing - filing_cod = copy.deepcopy(FILING_HEADER) - filing_cod['filing']['header']['name'] = 'changeOfDirectors' - filing_cod['filing']['changeOfDirectors'] = copy.deepcopy(CHANGE_OF_DIRECTORS) - filing_date = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) - factory_completed_filing(business=business, - data_dict=filing_cod, - filing_date=filing_date) - - # The effective date _cannot_ be before the previous COD. - before = datetime(2010, 8, 4, 12, 0, 0, 0, tzinfo=timezone.utc) - effective_date = as_effective_date(before) - filing_cod['filing']['header']['effectiveDate'] = effective_date.isoformat() - with freeze_time(now): - err = validate_effective_date(business, filing_cod) - assert err - - # The effective date _can_ be on the same date as the previous COD. - on = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) - effective_date = as_effective_date(on) - filing_cod['filing']['header']['effectiveDate'] = effective_date.isoformat() - with freeze_time(now): - err = validate_effective_date(business, filing_cod) - assert not err - - # The effective date _can_ be after the previous COD. - after = datetime(2010, 8, 6, 12, 0, 0, 0, tzinfo=timezone.utc) - effective_date = as_effective_date(after) - filing_cod['filing']['header']['effectiveDate'] = effective_date.isoformat() - with freeze_time(now): - err = validate_effective_date(business, filing_cod) - assert not err - - -def test_validate_effective_date_not_before_other_AR_with_COD(session): # noqa: N802; COD is an acronym - """Assert that the effective date of change cannot be before a previous AR filing.""" - # setup - identifier = 'CP1234567' - founding_date = datetime(2000, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) - business = Business(identifier=identifier, - founding_date=founding_date) - business.save() - now = datetime(2020, 7, 30, 12, 0, 0, 0, tzinfo=timezone.utc) - - # create the previous AR filing - filing_ar = copy.deepcopy(ANNUAL_REPORT) - filing_ar['filing']['changeOfDirectors'] = copy.deepcopy(CHANGE_OF_DIRECTORS) - filing_date = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) - factory_completed_filing(business=business, - data_dict=filing_ar, - filing_date=filing_date) - - # The effective date _cannot_ be before the previous AR. - before = datetime(2010, 8, 4, 12, 0, 0, 0, tzinfo=timezone.utc) - effective_date = as_effective_date(before) - filing_ar['filing']['header']['effectiveDate'] = effective_date.isoformat() - with freeze_time(now): - err = validate_effective_date(business, filing_ar) - assert err - - # The effective date _can_ be on the same date as the previous AR. - on = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) - effective_date = as_effective_date(on) - filing_ar['filing']['header']['effectiveDate'] = effective_date.isoformat() - with freeze_time(now): - err = validate_effective_date(business, filing_ar) - assert not err - - # The effective date _can_ be after the previous AR. - after = datetime(2010, 8, 6, 12, 0, 0, 0, tzinfo=timezone.utc) - effective_date = as_effective_date(after) - filing_ar['filing']['header']['effectiveDate'] = effective_date.isoformat() - with freeze_time(now): - err = validate_effective_date(business, filing_ar) - assert not err - - def as_effective_date(date_time: datetime) -> datetime: """Convert date_time to an effective date with 0 time in the legislation timezone, same as the UI does.""" # 1. convert to legislation datetime From 05b634e21cd8dd77255308b3cf82f39c98ea1542 Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Thu, 5 Mar 2026 12:46:58 -0800 Subject: [PATCH 08/46] 32690 bump version up (#4137) --- legal-api/src/legal_api/version.py | 2 +- queue_services/business-emailer/pyproject.toml | 2 +- queue_services/business-filer/pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/legal-api/src/legal_api/version.py b/legal-api/src/legal_api/version.py index 4e0260754d..3b53010500 100644 --- a/legal-api/src/legal_api/version.py +++ b/legal-api/src/legal_api/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = "2.169.0" # pylint: disable=invalid-name +__version__ = "2.170.0" # pylint: disable=invalid-name diff --git a/queue_services/business-emailer/pyproject.toml b/queue_services/business-emailer/pyproject.toml index d1035e6e89..2269660ee5 100644 --- a/queue_services/business-emailer/pyproject.toml +++ b/queue_services/business-emailer/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "business-emailer" -version = "0.1.3" +version = "0.1.4" description = "This module is the service worker for sending emails about entity related events." authors = ["Hrvoje Fekete "] license = "BSD-3-Clause" diff --git a/queue_services/business-filer/pyproject.toml b/queue_services/business-filer/pyproject.toml index 042fecd04a..84049a43ef 100644 --- a/queue_services/business-filer/pyproject.toml +++ b/queue_services/business-filer/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "business-filer" -version = "3.0.10" +version = "3.0.11" description = "Business Registry Filer Service" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} From 099fae6a088fc6b528bc8c4aa8b260634541ff5e Mon Sep 17 00:00:00 2001 From: Kial Date: Fri, 6 Mar 2026 12:10:18 -0500 Subject: [PATCH 09/46] Business API/model - liquidation report updates (#4138) * Business API/model - liquidation report updates Signed-off-by: Kial Jinnah * chore: fix lint Signed-off-by: Kial Jinnah * chore: update common model with missed api model change Signed-off-by: Kial Jinnah --------- Signed-off-by: Kial Jinnah --- ...467e09986f2_business_liquidation_values.py | 34 ++++ legal-api/src/legal_api/core/meta/filing.py | 2 +- legal-api/src/legal_api/models/business.py | 24 +++ legal-api/src/legal_api/models/filing.py | 1 + .../resources/v2/business/business_tasks.py | 2 +- legal-api/src/legal_api/services/authz.py | 21 +- .../services/involuntary_dissolution.py | 2 +- .../business/business_checks/__init__.py | 3 + .../business/business_checks/business.py | 14 +- legal-api/tests/unit/models/__init__.py | 9 +- .../tests/unit/resources/v2/test_business.py | 2 +- .../unit/resources/v2/test_business_tasks.py | 4 +- .../tests/unit/services/test_authorization.py | 183 +++++++++++------- .../business_checks/test_in_liquidation.py | 64 ++++++ .../test_involuntary_dissolution.py | 46 +++-- .../business-registry-model/pyproject.toml | 2 +- .../src/business_model/models/business.py | 24 +++ .../src/business_model/models/filing.py | 1 + ...467e09986f2_business_liquidation_values.py | 34 ++++ .../tests/models/__init__.py | 9 +- .../tests/models/test_business.py | 49 +++-- 21 files changed, 413 insertions(+), 117 deletions(-) create mode 100644 legal-api/migrations/versions/2467e09986f2_business_liquidation_values.py create mode 100644 legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py create mode 100644 python/common/business-registry-model/src/business_model_migrations/versions/2467e09986f2_business_liquidation_values.py diff --git a/legal-api/migrations/versions/2467e09986f2_business_liquidation_values.py b/legal-api/migrations/versions/2467e09986f2_business_liquidation_values.py new file mode 100644 index 0000000000..9eb19b2cd2 --- /dev/null +++ b/legal-api/migrations/versions/2467e09986f2_business_liquidation_values.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 2467e09986f2 +Revises: 4a27dc3cf7bc +Create Date: 2026-03-05 10:14:34.122569 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '2467e09986f2' +down_revision = '4a27dc3cf7bc' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('businesses', sa.Column('last_lr_year', sa.Integer(), nullable=True)) + op.add_column('businesses', sa.Column('in_liquidation_date', sa.DateTime(timezone=True), nullable=True)) + op.add_column('businesses_version', sa.Column('last_lr_year', sa.Integer(), autoincrement=False, nullable=True)) + op.add_column('businesses_version', sa.Column('in_liquidation_date', sa.DateTime(timezone=True), autoincrement=False, nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('businesses_version', 'in_liquidation_date') + op.drop_column('businesses_version', 'last_lr_year') + op.drop_column('businesses', 'in_liquidation_date') + op.drop_column('businesses', 'last_lr_year') + # ### end Alembic commands ### diff --git a/legal-api/src/legal_api/core/meta/filing.py b/legal-api/src/legal_api/core/meta/filing.py index 5573641602..3909f7ff34 100644 --- a/legal-api/src/legal_api/core/meta/filing.py +++ b/legal-api/src/legal_api/core/meta/filing.py @@ -852,7 +852,7 @@ class FilingTitles(str, Enum): "transition": { "name": "transition", "title": "Transition", - "displayName": "Transition Application", + "displayName": "Post Restoration Transition Application", "codes": { "BC": "TRANP", "BEN": "TRANP", diff --git a/legal-api/src/legal_api/models/business.py b/legal-api/src/legal_api/models/business.py index 9640a24ac0..edfb1dc341 100644 --- a/legal-api/src/legal_api/models/business.py +++ b/legal-api/src/legal_api/models/business.py @@ -207,6 +207,7 @@ class AssociationTypes(Enum): "last_cod_date", "last_ledger_id", "last_ledger_timestamp", + "last_lr_year", "last_modified", "last_remote_ledger_id", "last_tr_year", @@ -248,14 +249,19 @@ class AssociationTypes(Enum): tax_id = db.Column("tax_id", db.String(15), index=True) fiscal_year_end_date = db.Column("fiscal_year_end_date", db.DateTime(timezone=True), default=datetime.utcnow) restriction_ind = db.Column("restriction_ind", db.Boolean, unique=False, default=False) + # last annual report filing year last_ar_year = db.Column("last_ar_year", db.Integer) last_ar_reminder_year = db.Column("last_ar_reminder_year", db.Integer) + # last liquidation report filing year + last_lr_year = db.Column("last_lr_year", db.Integer) + # last transparency register filing year last_tr_year = db.Column("last_tr_year", db.Integer) association_type = db.Column("association_type", db.String(50)) state = db.Column("state", db.Enum(State), default=State.ACTIVE.value) state_filing_id = db.Column("state_filing_id", db.Integer) admin_freeze = db.Column("admin_freeze", db.Boolean, unique=False, default=False) in_liquidation = db.Column("in_liquidation", db.Boolean, unique=False, default=False) + in_liquidation_date = db.Column("in_liquidation_date", db.DateTime(timezone=True)) submitter_userid = db.Column("submitter_userid", db.Integer, db.ForeignKey("users.id")) submitter = db.relationship("User", backref=backref("submitter", uselist=False), foreign_keys=[submitter_userid]) send_ar_ind = db.Column("send_ar_ind", db.Boolean, unique=False, default=True) @@ -418,6 +424,20 @@ def next_annual_tr_due_datetime(self) -> datetime: # return as this date at 23:59:59 return due_datetime.replace(hour=23, minute=59, second=59, microsecond=0) + @property + def next_lr_min_date(self): + """Retrieve the next minimum date that a Liquidation Report can be filed.""" + if self.in_liquidation and self.in_liquidation_date: + founding_date = LegislationDatetime.as_legislation_timezone(self.founding_date).date() + in_liquidation_date = LegislationDatetime.as_legislation_timezone(self.in_liquidation_date).date() + + next_lr_year = in_liquidation_date.year + 1 + if self.last_lr_year: + next_lr_year = self.last_lr_year + 1 + + # min date for liquidation report is based on the anniversary date + return founding_date.replace(year=next_lr_year) + def get_ar_dates(self, next_ar_year): """Get ar min and max date for the specific year.""" ar_min_date = datetime(next_ar_year, 1, 1).date() @@ -486,6 +506,10 @@ def good_standing(self): if self.is_firm: return True + # A business in liquidation is always not in good standing + if self.in_liquidation: + return False + # check transition filing for corps if self.legal_type in self.CORPS and self.transition_needed_but_not_filed(): return False diff --git a/legal-api/src/legal_api/models/filing.py b/legal-api/src/legal_api/models/filing.py index 001c67c372..f51533537d 100644 --- a/legal-api/src/legal_api/models/filing.py +++ b/legal-api/src/legal_api/models/filing.py @@ -663,6 +663,7 @@ class Source(Enum): "transition": { "name": "transition", "title": "Transition", + "displayName": "Post Restoration Transition Application", "codes": { "BC": "TRANP", "BEN": "TRANP", diff --git a/legal-api/src/legal_api/resources/v2/business/business_tasks.py b/legal-api/src/legal_api/resources/v2/business/business_tasks.py index 49726e0340..6af19c60ee 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_tasks.py +++ b/legal-api/src/legal_api/resources/v2/business/business_tasks.py @@ -150,7 +150,7 @@ def construct_task_list(business: Business): # noqa: PLR0915 tasks.append(task) order += 1 - if business.legal_type not in entity_types_no_ar: + if business.legal_type not in entity_types_no_ar and not business.in_liquidation: # If this is the first calendar year since incorporation, there is no previous ar year. next_ar_year = (business.last_ar_year if business.last_ar_year else business.founding_date.year) + 1 diff --git a/legal-api/src/legal_api/services/authz.py b/legal-api/src/legal_api/services/authz.py index 95bdba33f8..0351f68f14 100644 --- a/legal-api/src/legal_api/services/authz.py +++ b/legal-api/src/legal_api/services/authz.py @@ -34,6 +34,7 @@ ) from legal_api.services.request_context import get_request_context from legal_api.services.warnings.business.business_checks import WarningType +from legal_api.utils.legislation_datetime import LegislationDatetime SYSTEM_ROLE = "system" SBC_STAFF_ROLE = "sbc_staff" @@ -63,6 +64,7 @@ class BusinessBlocker(str, Enum): IN_LIQUIDATION = "IN_LIQUIDATION" FILING_WITHDRAWAL = "FILING_WITHDRAWAL" MAX_DISSOLUTION_DELAYS_REACHED = "MAX_DISSOLUTION_DELAYS_REACHED" + MIN_LR_DATE_REACHED = "MIN_LR_DATE_REACHED" class BusinessRequirement(str, Enum): @@ -229,7 +231,7 @@ def get_allowable_filings_dict(is_authorization: bool = False): "annualReport": { "legalTypes": ["CP", "BEN", "BC", "ULC", "CC", "C", "CBEN", "CUL", "CCC"], "blockerChecks": { - "business": [BusinessBlocker.DEFAULT] + "business": [BusinessBlocker.DEFAULT, BusinessBlocker.IN_LIQUIDATION] } }, "changeOfAddress": { @@ -245,7 +247,6 @@ def get_allowable_filings_dict(is_authorization: bool = False): } }, "changeOfLiquidators": { - # FUTURE: narrow down blocker checks, needs BA / design input - will be done in #31714 "appointLiquidator": { "legalTypes": ["BC", "BEN", "ULC", "CC", "C", "CBEN", "CUL", "CCC"], "blockerChecks": { @@ -255,13 +256,15 @@ def get_allowable_filings_dict(is_authorization: bool = False): "ceaseLiquidator": { "legalTypes": ["BC", "BEN", "ULC", "CC", "C", "CBEN", "CUL", "CCC"], "blockerChecks": { - "business": [BusinessBlocker.DEFAULT] + "business": [BusinessBlocker.DEFAULT], + "validBusiness": [BusinessBlocker.IN_LIQUIDATION] } }, "changeAddressLiquidator": { "legalTypes": ["BC", "BEN", "ULC", "CC", "C", "CBEN", "CUL", "CCC"], "blockerChecks": { - "business": [BusinessBlocker.DEFAULT] + "business": [BusinessBlocker.DEFAULT], + "validBusiness": [BusinessBlocker.IN_LIQUIDATION] } }, "intentToLiquidate": { @@ -273,7 +276,8 @@ def get_allowable_filings_dict(is_authorization: bool = False): "liquidationReport": { "legalTypes": ["BC", "BEN", "ULC", "CC", "C", "CBEN", "CUL", "CCC"], "blockerChecks": { - "business": [BusinessBlocker.DEFAULT] + "business": [BusinessBlocker.DEFAULT], + "validBusiness": [BusinessBlocker.IN_LIQUIDATION, BusinessBlocker.MIN_LR_DATE_REACHED] } }, }, @@ -525,7 +529,7 @@ def get_allowable_filings_dict(is_authorization: bool = False): "annualReport": { "legalTypes": ["CP", "BEN", "BC", "ULC", "CC", "C", "CBEN", "CUL", "CCC"], "blockerChecks": { - "business": [BusinessBlocker.DEFAULT] + "business": [BusinessBlocker.DEFAULT, BusinessBlocker.IN_LIQUIDATION] } }, "changeOfAddress": { @@ -946,7 +950,8 @@ def business_blocker_check(business: Business, is_ignore_draft_blockers: bool = BusinessBlocker.IN_DISSOLUTION: False, BusinessBlocker.IN_LIQUIDATION: False, BusinessBlocker.FILING_WITHDRAWAL: False, - BusinessBlocker.MAX_DISSOLUTION_DELAYS_REACHED: False + BusinessBlocker.MAX_DISSOLUTION_DELAYS_REACHED: False, + BusinessBlocker.MIN_LR_DATE_REACHED: False } if not business: @@ -971,6 +976,8 @@ def business_blocker_check(business: Business, is_ignore_draft_blockers: bool = if business.in_liquidation: business_blocker_checks[BusinessBlocker.IN_LIQUIDATION] = True + if LegislationDatetime.datenow() >= business.next_lr_min_date: + business_blocker_checks[BusinessBlocker.MIN_LR_DATE_REACHED] = True if has_notice_of_withdrawal_filing_blocker(business, is_ignore_draft_blockers): business_blocker_checks[BusinessBlocker.FILING_WITHDRAWAL] = True diff --git a/legal-api/src/legal_api/services/involuntary_dissolution.py b/legal-api/src/legal_api/services/involuntary_dissolution.py index 0184598f9b..16e9095f31 100644 --- a/legal-api/src/legal_api/services/involuntary_dissolution.py +++ b/legal-api/src/legal_api/services/involuntary_dissolution.py @@ -142,7 +142,7 @@ def _get_businesses_eligible_query(eligibility_filters: Optional[EligibilityFilt query = query.filter( or_( - specific_filing_overdue, + and_(specific_filing_overdue, not_(Business.in_liquidation.is_(True))), no_transition_filed_after_restoration ) ).\ diff --git a/legal-api/src/legal_api/services/warnings/business/business_checks/__init__.py b/legal-api/src/legal_api/services/warnings/business/business_checks/__init__.py index 36b47d3ee4..d00e55b859 100644 --- a/legal-api/src/legal_api/services/warnings/business/business_checks/__init__.py +++ b/legal-api/src/legal_api/services/warnings/business/business_checks/__init__.py @@ -22,6 +22,7 @@ class WarningType(str, Enum): FUTURE_EFFECTIVE_AMALGAMATION = "FUTURE_EFFECTIVE_AMALGAMATION" NOT_IN_GOOD_STANDING = "NOT_IN_GOOD_STANDING" INVOLUNTARY_DISSOLUTION = "INVOLUNTARY_DISSOLUTION" + LIQUIDATION = "LIQUIDATION" class BusinessWarningCodes(str, Enum): @@ -76,6 +77,8 @@ class BusinessWarningCodes(str, Enum): DISSOLUTION_IN_PROGRESS = "DISSOLUTION_IN_PROGRESS" + LIQUIDATION_IN_PROGRESS = "LIQUIDATION_IN_PROGRESS" + class BusinessWarningReferers(str, Enum): """Enum for business warning referers.""" diff --git a/legal-api/src/legal_api/services/warnings/business/business_checks/business.py b/legal-api/src/legal_api/services/warnings/business/business_checks/business.py index 2abd91d93d..e72339e7b0 100644 --- a/legal-api/src/legal_api/services/warnings/business/business_checks/business.py +++ b/legal-api/src/legal_api/services/warnings/business/business_checks/business.py @@ -16,12 +16,13 @@ from legal_api.models import Business from legal_api.services.involuntary_dissolution import InvoluntaryDissolutionService +from . import BusinessWarningCodes, WarningType from .corps import check_business as corps_check from .firms import check_business as firms_check from .involuntary_dissolution import check_business as involuntary_dissolution_check -def check_business(business: any) -> list: +def check_business(business: Business) -> list: """Check business for warnings.""" result = [] @@ -35,4 +36,15 @@ def check_business(business: any) -> list: if business.legal_type in InvoluntaryDissolutionService.ELIGIBLE_TYPES: result.extend(involuntary_dissolution_check(business)) + if business.in_liquidation: + result.append({ + "code": BusinessWarningCodes.LIQUIDATION_IN_PROGRESS.value, + "data": { + "inLiquidationDate": business.in_liquidation_date, + "lastLiquidationReportYear": business.last_lr_year, + "nextLiquidationReportMinDate": business.next_lr_min_date + }, + "message": "This business is in the process of Liquidation.", + "warningType": WarningType.LIQUIDATION + }) return result diff --git a/legal-api/tests/unit/models/__init__.py b/legal-api/tests/unit/models/__init__.py index a393ea2012..5b32830f09 100644 --- a/legal-api/tests/unit/models/__init__.py +++ b/legal-api/tests/unit/models/__init__.py @@ -139,7 +139,9 @@ def factory_business(identifier, naics_code=None, naics_desc=None, admin_freeze=False, - no_dissolution=False): + no_dissolution=False, + in_liquidation_date=None, + last_lr_year=None): """Create a business entity with a versioned business.""" last_ar_year = None if last_ar_date: @@ -158,7 +160,10 @@ def factory_business(identifier, naics_code=naics_code, naics_description=naics_desc, admin_freeze=admin_freeze, - no_dissolution=no_dissolution) + no_dissolution=no_dissolution, + in_liquidation=in_liquidation_date != None, + in_liquidation_date=in_liquidation_date, + last_lr_year=last_lr_year) # Versioning business VersioningProxy.get_transaction_id(db.session()) diff --git a/legal-api/tests/unit/resources/v2/test_business.py b/legal-api/tests/unit/resources/v2/test_business.py index 7765c15f2c..cc67e6c207 100644 --- a/legal-api/tests/unit/resources/v2/test_business.py +++ b/legal-api/tests/unit/resources/v2/test_business.py @@ -896,7 +896,7 @@ def test_get_could_file(session, client, jwt, monkeypatch): "name": "registrarsOrder" }, { - "displayName": "Transition Application", + "displayName": "Post Restoration Transition Application", "name": "transition" }, { diff --git a/legal-api/tests/unit/resources/v2/test_business_tasks.py b/legal-api/tests/unit/resources/v2/test_business_tasks.py index 68529cad57..192515a810 100644 --- a/legal-api/tests/unit/resources/v2/test_business_tasks.py +++ b/legal-api/tests/unit/resources/v2/test_business_tasks.py @@ -265,6 +265,7 @@ def test_get_tasks_pending_correction_filings(session, client, jwt): ('BC first AR to be issued', 'BC1234567', '2021-07-02', None, Business.LegalTypes.COMP.value, 1), ('BC no AR due yet', 'BC1234567', '2021-07-03', None, Business.LegalTypes.COMP.value, 0), ('BC 3 ARs overdue', 'BC1234567', '2019-05-15', None, Business.LegalTypes.COMP.value, 3), + ('BC in liquidation no ARs due', 'BC1234567', '2019-05-15', None, Business.LegalTypes.COMP.value, 0), ('BC current AR year issued', 'BC1234567', '1900-07-01', '2022-03-03', Business.LegalTypes.COMP.value, 0), ('ULC first AR to be issued', 'BC1234567', '2021-07-02', None, Business.LegalTypes.BC_ULC_COMPANY.value, 1), @@ -292,8 +293,9 @@ def test_construct_task_list_ar(session, client, jwt, test_name, identifier, fou with patch('legal_api.resources.v2.business.business_tasks.check_warnings', return_value=[]): previous_ar_datetime = datetime.fromisoformat(previous_ar_date) if previous_ar_date else None founding_date = datetime.fromisoformat(founding_date + 'T12:00:00+00:00') + in_liquidation_date = datetime.now() if test_name.startswith('BC in liquidation') else None business = factory_business( - identifier, founding_date, previous_ar_datetime, legal_type) + identifier, founding_date, previous_ar_datetime, legal_type, in_liquidation_date=in_liquidation_date) tasks = construct_task_list(business) assert len(tasks) == tasks_length diff --git a/legal-api/tests/unit/services/test_authorization.py b/legal-api/tests/unit/services/test_authorization.py index 573b37af1e..498255fd69 100644 --- a/legal-api/tests/unit/services/test_authorization.py +++ b/legal-api/tests/unit/services/test_authorization.py @@ -219,7 +219,7 @@ class FilingKey(str, Enum): FilingKey.CONSENT_CONTINUATION_OUT: {'displayName': '6-Month Consent to Continue Out', 'feeCode': 'CONTO', 'name': 'consentContinuationOut'}, FilingKey.CONTINUATION_OUT: {'displayName': 'Continuation Out', 'feeCode': 'COUTI', 'name': 'continuationOut'}, - FilingKey.TRANSITION: {'displayName': 'Transition Application', 'feeCode': 'TRANP', 'name': 'transition'}, + FilingKey.TRANSITION: {'displayName': 'Post Restoration Transition Application', 'feeCode': 'TRANP', 'name': 'transition'}, FilingKey.IA_CP: {'displayName': 'Incorporation Application', 'feeCode': 'OTINC', 'name': 'incorporationApplication'}, FilingKey.IA_BC: {'displayName': 'BC Limited Company Incorporation Application', 'feeCode': 'BCINC', @@ -311,7 +311,7 @@ class FilingKey(str, Enum): FilingKey.CONSENT_CONTINUATION_OUT: {'displayName': '6-Month Consent to Continue Out', 'feeCode': 'CONTO', 'name': 'consentContinuationOut'}, FilingKey.CONTINUATION_OUT: {'displayName': 'Continuation Out', 'feeCode': 'COUTI', 'name': 'continuationOut'}, - FilingKey.TRANSITION: {'displayName': 'Transition Application', 'feeCode': 'TRANP', 'name': 'transition'}, + FilingKey.TRANSITION: {'displayName': 'Post Restoration Transition Application', 'feeCode': 'TRANP', 'name': 'transition'}, FilingKey.IA_CP: {'displayName': 'Incorporation Application', 'feeCode': 'OTINC', 'name': 'incorporationApplication'}, FilingKey.IA_BC: {'displayName': 'BC Limited Company Incorporation Application', 'feeCode': 'BCINC', @@ -1067,10 +1067,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -1099,10 +1096,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -1413,10 +1407,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -1445,10 +1436,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -1795,10 +1783,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -1820,10 +1805,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -2323,10 +2305,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -2354,10 +2333,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -2526,10 +2502,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -2561,10 +2534,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -2595,10 +2565,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -2627,10 +2594,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -2986,10 +2950,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3027,10 +2988,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3080,10 +3038,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3174,10 +3129,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3198,10 +3150,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3367,10 +3316,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3420,10 +3366,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3594,8 +3537,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, FilingKey.COO_CORPS, FilingKey.CORRCTN, @@ -3646,10 +3587,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3673,10 +3611,7 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me FilingKey.COA_CORPS, FilingKey.COD_CORPS, FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, - FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, - FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, - FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, FilingKey.COO_CORPS, FilingKey.CHANGE_OF_RECEIVERS_AMEND, FilingKey.CHANGE_OF_RECEIVERS_APPOINT, @@ -3809,6 +3744,120 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me assert filing_types == expected +@pytest.mark.parametrize( + 'test_name,username,roles,in_liquidation_date,last_lr_year,expected', + [ + ('staff_allowed_all_1', 'staff', [STAFF_ROLE], datetime.now() - datedelta(years=2), None, + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.ALTERATION, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, + FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, + FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, + FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, + FilingKey.CHANGE_OF_LIQUIDATORS_REPORT, + FilingKey.COO_CORPS, + FilingKey.CHANGE_OF_RECEIVERS_AMEND, + FilingKey.CHANGE_OF_RECEIVERS_APPOINT, + FilingKey.CHANGE_OF_RECEIVERS_CEASE, + FilingKey.CHANGE_OF_RECEIVERS_ADDRESS, + FilingKey.CORRCTN, + FilingKey.COURT_ORDER, + FilingKey.VOL_DISS, + FilingKey.ADM_DISS, + FilingKey.PUT_BACK_OFF, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION, + ])), + ('staff_allowed_no_lr_1', 'staff', [STAFF_ROLE], datetime.now() - datedelta(years=2), datetime.now().year, + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.ALTERATION, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, + FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, + FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, + FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, + FilingKey.COO_CORPS, + FilingKey.CHANGE_OF_RECEIVERS_AMEND, + FilingKey.CHANGE_OF_RECEIVERS_APPOINT, + FilingKey.CHANGE_OF_RECEIVERS_CEASE, + FilingKey.CHANGE_OF_RECEIVERS_ADDRESS, + FilingKey.CORRCTN, + FilingKey.COURT_ORDER, + FilingKey.VOL_DISS, + FilingKey.ADM_DISS, + FilingKey.PUT_BACK_OFF, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION, + ])), + ('staff_allowed_no_lr_2', 'staff', [STAFF_ROLE], datetime.now(), None, + expected_lookup([FilingKey.ADMN_FRZE, + FilingKey.ALTERATION, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.CHANGE_OF_LIQUIDATORS_APPOINT, + FilingKey.CHANGE_OF_LIQUIDATORS_CEASE, + FilingKey.CHANGE_OF_LIQUIDATORS_ADDRESS, + FilingKey.CHANGE_OF_LIQUIDATORS_INTENT, + FilingKey.COO_CORPS, + FilingKey.CHANGE_OF_RECEIVERS_AMEND, + FilingKey.CHANGE_OF_RECEIVERS_APPOINT, + FilingKey.CHANGE_OF_RECEIVERS_CEASE, + FilingKey.CHANGE_OF_RECEIVERS_ADDRESS, + FilingKey.CORRCTN, + FilingKey.COURT_ORDER, + FilingKey.VOL_DISS, + FilingKey.ADM_DISS, + FilingKey.PUT_BACK_OFF, + FilingKey.REGISTRARS_NOTATION, + FilingKey.REGISTRARS_ORDER, + FilingKey.TRANSITION, + ])), + ('basic_user_not_allowed', + 'basic_user', + [BASIC_USER], + datetime.now() - datedelta(years=2), + None, + expected_lookup([FilingKey.ALTERATION, + FilingKey.COA_CORPS, + FilingKey.COD_CORPS, + FilingKey.COO_CORPS, + FilingKey.TRANSITION, + FilingKey.TRANSPARENCY_REGISTER_ANNUAL, + FilingKey.TRANSPARENCY_REGISTER_CHANGE, + FilingKey.TRANSPARENCY_REGISTER_INITIAL])) + ] +) +def test_get_allowed_filings_in_liquidation(monkeypatch, app, session, jwt, test_name, username, roles, in_liquidation_date, last_lr_year, expected): + """Assert that get allowed returns valid filings when business is in liquidation.""" + token = helper_create_jwt(jwt, roles=roles, username=username) + headers = {'Authorization': 'Bearer ' + token, 'Account-Id': 1} + + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] + + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + monkeypatch.setattr( + 'legal_api.services.flags.value', + lambda flag, _user, _account_id: "changeOfLiquidators.appointLiquidator,changeOfLiquidators.ceaseLiquidator,changeOfLiquidators.changeAddressLiquidator,changeOfLiquidators.intentToLiquidate,changeOfLiquidators.liquidationReport,changeOfReceivers.amendReceiver,changeOfReceivers.appointReceiver,changeOfReceivers.ceaseReceiver,changeOfReceivers.changeAddressReceiver,dissolution.delay,transition" + if flag == 'enabled-specific-filings' else {} + ) + monkeypatch.setattr( + 'legal_api.models.User.get_or_create_user_by_jwt', + lambda _: None + ) + + identifier = 'BC7654321' + business: Business = factory_business(identifier=identifier, entity_type=Business.LegalTypes.COMP, in_liquidation_date=in_liquidation_date, last_lr_year=last_lr_year) + filing_types = get_allowed_filings(business, Business.State.ACTIVE, Business.LegalTypes.COMP, jwt) + assert filing_types == expected + + def create_incomplete_filing(business, filing_name, filing_status, diff --git a/legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py b/legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py new file mode 100644 index 0000000000..1c2738162b --- /dev/null +++ b/legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py @@ -0,0 +1,64 @@ +# Copyright © 2026 Province of British Columbia +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in business 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. +"""Test suite to ensure Corpse business checks work correctly for businesses in liquidation.""" +import copy + +import pytest +from datedelta import datedelta + +from legal_api.models import Business +from legal_api.services.warnings.business import check_business +from legal_api.services.warnings.business.business_checks import WarningType, BusinessWarningCodes +from legal_api.utils.datetime import datetime, date +from tests.unit.models import factory_business + +FOUNDING_DATE = datetime(2023, 3, 3) +IN_LIQUIDATION_DATE = datetime(2024, 6, 10) + +@pytest.mark.parametrize('test_name, founding_date, in_liquidation_date, last_lr_year, expected_meta_data', [ + ('NOT_IN_LIQUIDATION', datetime.now(), None, None, {}), + ('IN_LIQUIDATION', FOUNDING_DATE, IN_LIQUIDATION_DATE, None, + { + 'inLiquidationDate': IN_LIQUIDATION_DATE, + 'lastLiquidationReportYear': None, + 'nextLiquidationReportMinDate': date(2025, 3, 2)} + ), + ('IN_LIQUIDATION_lr_year', FOUNDING_DATE, IN_LIQUIDATION_DATE, 2025, + { + 'inLiquidationDate': IN_LIQUIDATION_DATE, + 'lastLiquidationReportYear': 2025, + 'nextLiquidationReportMinDate': date(2026, 3, 2)} + ) +]) +def test_check_business(session, test_name, founding_date, in_liquidation_date, last_lr_year, expected_meta_data): + """Test the check_business function.""" + identifier = 'BC7654321' + business = factory_business(identifier=identifier, + entity_type=Business.LegalTypes.COMP.value, + founding_date=founding_date, + in_liquidation_date=in_liquidation_date, + last_lr_year=last_lr_year) + + result = check_business(business) + + if test_name == 'NOT_IN_LIQUIDATION': + assert len(result) == 0 + else: + assert len(result) == 1 + assert result[0]['code'] == BusinessWarningCodes.LIQUIDATION_IN_PROGRESS + assert result[0]['message'] == 'This business is in the process of Liquidation.' + assert result[0]['warningType'] == WarningType.LIQUIDATION + + res_meta_data = result[0]['data'] + assert res_meta_data == expected_meta_data diff --git a/legal-api/tests/unit/services/warnings/business/business_checks/test_involuntary_dissolution.py b/legal-api/tests/unit/services/warnings/business/business_checks/test_involuntary_dissolution.py index 7794613c5f..4b631fca00 100644 --- a/legal-api/tests/unit/services/warnings/business/business_checks/test_involuntary_dissolution.py +++ b/legal-api/tests/unit/services/warnings/business/business_checks/test_involuntary_dissolution.py @@ -44,19 +44,22 @@ PAST_TRIGGER_DATE = datetime.utcnow() + datedelta(days=-10) -@pytest.mark.parametrize('test_name, no_dissolution, batch_status, batch_processing_status', [ - ('NOT_ELIGIBLE', True, None, None), - ('ELIGIBLE_AR_OVERDUE', False, 'PROCESSING', 'COMPLETED'), - ('ELIGIBLE_TRANSITION_OVERDUE', False, 'PROCESSING', 'COMPLETED'), - ('ELIGIBLE_AR_OVERDUE_FUTURE_EFFECTIVE_FILING_HAS_WARNINGS', False, 'PROCESSING', 'COMPLETED'), - ('IN_DISSOLUTION_AR_OVERDUE', False, 'PROCESSING', 'PROCESSING'), - ('IN_DISSOLUTION_TRANSITION_OVERDUE', False, 'PROCESSING', 'PROCESSING'), - ('IN_DISSOLUTION_AR_OVERDUE_FUTURE_EFFECTIVE_FILING_HAS_WARNINGS', False, 'PROCESSING', 'PROCESSING'), +@pytest.mark.parametrize('test_name, no_dissolution, in_liquidation, batch_status, batch_processing_status', [ + ('NOT_ELIGIBLE', True, False, None, None), + ('ELIGIBLE_AR_OVERDUE', False, False, 'PROCESSING', 'COMPLETED'), + ('ELIGIBLE_AR_OVERDUE_liquidation', False, True, 'PROCESSING', 'COMPLETED'), + ('ELIGIBLE_TRANSITION_OVERDUE', False, False, 'PROCESSING', 'COMPLETED'), + ('ELIGIBLE_TRANSITION_OVERDUE_liquidation', False, True, 'PROCESSING', 'COMPLETED'), + ('ELIGIBLE_AR_OVERDUE_FUTURE_EFFECTIVE_FILING_HAS_WARNINGS', False, False, 'PROCESSING', 'COMPLETED'), + ('IN_DISSOLUTION_AR_OVERDUE', False, False, 'PROCESSING', 'PROCESSING'), + ('IN_DISSOLUTION_TRANSITION_OVERDUE', False, False, 'PROCESSING', 'PROCESSING'), + ('IN_DISSOLUTION_AR_OVERDUE_FUTURE_EFFECTIVE_FILING_HAS_WARNINGS', False, False, 'PROCESSING', 'PROCESSING'), ]) -def test_check_business(session, test_name, no_dissolution, batch_status, batch_processing_status): +def test_check_business(session, test_name, no_dissolution, in_liquidation, batch_status, batch_processing_status): """Test the check_business function.""" identifier = 'BC7654321' - business = factory_business(identifier=identifier, entity_type=Business.LegalTypes.COMP.value, no_dissolution=no_dissolution) + in_liquidation_date = datetime.utcnow() if in_liquidation else None + business = factory_business(identifier=identifier, entity_type=Business.LegalTypes.COMP.value, no_dissolution=no_dissolution, in_liquidation_date=in_liquidation_date) target_date = datetime.utcnow() + datedelta(days=72) meta_data = { 'overdueARs': True, @@ -107,17 +110,20 @@ def test_check_business(session, test_name, no_dissolution, batch_status, batch_ else: assert res_meta_data['overdueARs'] == True else: - assert len(result) == 1 + if test_name.startswith('ELIGIBLE_AR_OVERDUE') and in_liquidation: + assert len(result) == 0 + else: + assert len(result) == 1 - warning = result[0] - if 'TRANSITION_OVERDUE' in test_name: - assert warning['code'] == BusinessWarningCodes.TRANSITION_NOT_FILED_AFTER_12_MONTH_RESTORATION.value - assert warning['message'] == 'Transition filing not filed. Eligible for involuntary dissolution.' - assert warning['warningType'] == WarningType.NOT_IN_GOOD_STANDING - else: - assert warning['code'] == 'MULTIPLE_ANNUAL_REPORTS_NOT_FILED' - assert warning['message'] == 'Multiple annual reports not filed. Eligible for involuntary dissolution.' - assert warning['warningType'] == WarningType.NOT_IN_GOOD_STANDING + warning = result[0] + if 'TRANSITION_OVERDUE' in test_name: + assert warning['code'] == BusinessWarningCodes.TRANSITION_NOT_FILED_AFTER_12_MONTH_RESTORATION.value + assert warning['message'] == 'Transition filing not filed. Eligible for involuntary dissolution.' + assert warning['warningType'] == WarningType.NOT_IN_GOOD_STANDING + else: + assert warning['code'] == 'MULTIPLE_ANNUAL_REPORTS_NOT_FILED' + assert warning['message'] == 'Multiple annual reports not filed. Eligible for involuntary dissolution.' + assert warning['warningType'] == WarningType.NOT_IN_GOOD_STANDING @pytest.mark.parametrize('test_name, batch_processing_step, trigger_date, expected_warning_date', [ diff --git a/python/common/business-registry-model/pyproject.toml b/python/common/business-registry-model/pyproject.toml index 625ac88a8a..cfb6a4a05d 100644 --- a/python/common/business-registry-model/pyproject.toml +++ b/python/common/business-registry-model/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "business-model" -version = "3.3.21" +version = "3.3.22" description = "" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/python/common/business-registry-model/src/business_model/models/business.py b/python/common/business-registry-model/src/business_model/models/business.py index 8f00f1044c..95dc3b478c 100644 --- a/python/common/business-registry-model/src/business_model/models/business.py +++ b/python/common/business-registry-model/src/business_model/models/business.py @@ -231,6 +231,7 @@ class AssociationTypes(Enum): 'last_cod_date', 'last_ledger_id', 'last_ledger_timestamp', + 'last_lr_year', 'last_modified', 'last_remote_ledger_id', 'last_tr_year', @@ -272,14 +273,19 @@ class AssociationTypes(Enum): tax_id = db.Column('tax_id', db.String(15), index=True) fiscal_year_end_date = db.Column('fiscal_year_end_date', db.DateTime(timezone=True), default=func.now()) restriction_ind = db.Column('restriction_ind', db.Boolean, unique=False, default=False) + # last annual report filing year last_ar_year = db.Column('last_ar_year', db.Integer) last_ar_reminder_year = db.Column('last_ar_reminder_year', db.Integer) + # last liquidation report filing year + last_lr_year = db.Column('last_lr_year', db.Integer) + # last transparency register filing year last_tr_year = db.Column('last_tr_year', db.Integer) association_type = db.Column('association_type', db.String(50)) state = db.Column('state', db.Enum(State), default=State.ACTIVE.value) state_filing_id = db.Column('state_filing_id', db.Integer) admin_freeze = db.Column('admin_freeze', db.Boolean, unique=False, default=False) in_liquidation = db.Column('in_liquidation', db.Boolean, unique=False, default=False) + in_liquidation_date = db.Column('in_liquidation_date', db.DateTime(timezone=True)) submitter_userid = db.Column('submitter_userid', db.Integer, db.ForeignKey('users.id')) submitter = db.relationship('User', backref=backref('submitter', uselist=False), foreign_keys=[submitter_userid]) send_ar_ind = db.Column('send_ar_ind', db.Boolean, unique=False, default=True) @@ -444,6 +450,20 @@ def next_annual_tr_due_datetime(self) -> datetime: # return as this date at 23:59:59 return due_datetime.replace(hour=23, minute=59, second=59, microsecond=0) + @property + def next_lr_min_date(self) -> datetime | None: + """Retrieve the next minimum date that a Liquidation Report can be filed.""" + if self.in_liquidation and self.in_liquidation_date: + founding_date = LegislationDatetime.as_legislation_timezone(self.founding_date).date() + in_liquidation_date = LegislationDatetime.as_legislation_timezone(self.in_liquidation_date).date() + + next_lr_year = in_liquidation_date.year + 1 + if self.last_lr_year: + next_lr_year = self.last_lr_year + 1 + + # min date for liquidation report is based on the anniversary date + return founding_date.replace(year=next_lr_year) + def get_ar_dates(self, next_ar_year): """Get ar min and max date for the specific year.""" ar_min_date = datetime(next_ar_year, 1, 1).date() @@ -511,6 +531,10 @@ def good_standing(self): if self.is_firm: return True + # A business in liquidation is always not in good standing + if self.in_liquidation: + return False + # check transition filing for corps if self.legal_type in self.CORPS and self.transition_needed_but_not_filed(): return False diff --git a/python/common/business-registry-model/src/business_model/models/filing.py b/python/common/business-registry-model/src/business_model/models/filing.py index 156d5735bd..225e37fd30 100644 --- a/python/common/business-registry-model/src/business_model/models/filing.py +++ b/python/common/business-registry-model/src/business_model/models/filing.py @@ -664,6 +664,7 @@ class Source(Enum): 'transition': { 'name': 'transition', 'title': 'Transition', + 'displayName': 'Post Restoration Transition Application', 'codes': { 'BC': 'TRANP', 'BEN': 'TRANP', diff --git a/python/common/business-registry-model/src/business_model_migrations/versions/2467e09986f2_business_liquidation_values.py b/python/common/business-registry-model/src/business_model_migrations/versions/2467e09986f2_business_liquidation_values.py new file mode 100644 index 0000000000..bd83109329 --- /dev/null +++ b/python/common/business-registry-model/src/business_model_migrations/versions/2467e09986f2_business_liquidation_values.py @@ -0,0 +1,34 @@ +"""empty message + +Revision ID: 2467e09986f2 +Revises: 4a27dc3cf7bc +Create Date: 2026-03-05 10:14:34.122569 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '2467e09986f2' +down_revision = '4a27dc3cf7bc' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('businesses', sa.Column('last_lr_year', sa.Integer(), nullable=True)) + op.add_column('businesses', sa.Column('in_liquidation_date', sa.DateTime(timezone=True), nullable=True)) + op.add_column('businesses_version', sa.Column('last_lr_year', sa.Integer(), autoincrement=False, nullable=True)) + op.add_column('businesses_version', sa.Column('in_liquidation_date', sa.DateTime(timezone=True), autoincrement=False, nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('businesses_version', 'in_liquidation_date') + op.drop_column('businesses_version', 'last_lr_year') + op.drop_column('businesses', 'in_liquidation_date') + op.drop_column('businesses', 'last_lr_year') + # ### end Alembic commands ### diff --git a/python/common/business-registry-model/tests/models/__init__.py b/python/common/business-registry-model/tests/models/__init__.py index 79cb540f0f..7a21b85951 100644 --- a/python/common/business-registry-model/tests/models/__init__.py +++ b/python/common/business-registry-model/tests/models/__init__.py @@ -138,7 +138,9 @@ def factory_business(identifier, naics_code=None, naics_desc=None, admin_freeze=False, - no_dissolution=False): + no_dissolution=False, + in_liquidation_date=None, + last_lr_year=None): """Create a business entity with a versioned business.""" last_ar_year = None if last_ar_date: @@ -157,7 +159,10 @@ def factory_business(identifier, naics_code=naics_code, naics_description=naics_desc, admin_freeze=admin_freeze, - no_dissolution=no_dissolution) + no_dissolution=no_dissolution, + in_liquidation=in_liquidation_date != None, + in_liquidation_date=in_liquidation_date, + last_lr_year=last_lr_year) # Versioning business VersioningProxy.get_transaction_id(db.session()) diff --git a/python/common/business-registry-model/tests/models/test_business.py b/python/common/business-registry-model/tests/models/test_business.py index 60f06f2966..367c95a8a6 100644 --- a/python/common/business-registry-model/tests/models/test_business.py +++ b/python/common/business-registry-model/tests/models/test_business.py @@ -19,7 +19,7 @@ import base64 import copy import uuid -from datetime import UTC, datetime +from datetime import UTC, date, datetime import datedelta import pytest @@ -97,6 +97,9 @@ def test_business_identifier(session): ('AB0000001', False), ] +FOUNDING_DATE = datetime(2023, 3, 3) +IN_LIQUIDATION_DATE = datetime(2024, 6, 10) + @pytest.mark.parametrize('identifier,expected', TEST_IDENTIFIER_DATA) def test_business_validate_identifier(identifier, expected): @@ -250,19 +253,21 @@ def test_business_find_by_identifier_no_identifier(session): TEST_GOOD_STANDING_DATA = [ - (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.ACTIVE.value, False, True), - (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.ACTIVE.value, True, False), - (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.HISTORICAL.value, False, True), - (datetime.now() - datedelta.datedelta(years=1, months=6), Business.LegalTypes.COMP, Business.State.ACTIVE.value, False, False), - (datetime.now() - datedelta.datedelta(years=1, months=6), Business.LegalTypes.SOLE_PROP, Business.State.ACTIVE.value, False, True), - (datetime.now() - datedelta.datedelta(years=1, months=6), Business.LegalTypes.PARTNERSHIP, Business.State.ACTIVE.value, False, True), - (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.SOLE_PROP, Business.State.ACTIVE.value, False, True), - (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.PARTNERSHIP, Business.State.ACTIVE.value, False, True) + (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.ACTIVE.value, False, False, True), + (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.ACTIVE.value, False, True, False), + (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.ACTIVE.value, True, False, False), + (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.HISTORICAL.value, False, False, True), + (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.COMP, Business.State.HISTORICAL.value, False, True, False), + (datetime.now() - datedelta.datedelta(years=1, months=6), Business.LegalTypes.COMP, Business.State.ACTIVE.value, False, False, False), + (datetime.now() - datedelta.datedelta(years=1, months=6), Business.LegalTypes.SOLE_PROP, Business.State.ACTIVE.value, False, False, True), + (datetime.now() - datedelta.datedelta(years=1, months=6), Business.LegalTypes.PARTNERSHIP, Business.State.ACTIVE.value, False, False, True), + (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.SOLE_PROP, Business.State.ACTIVE.value, False, False, True), + (datetime.now() - datedelta.datedelta(months=6), Business.LegalTypes.PARTNERSHIP, Business.State.ACTIVE.value, False, False, True) ] -@pytest.mark.parametrize('last_ar_date, legal_type, state, limited_restoration, expected', TEST_GOOD_STANDING_DATA) -def test_good_standing(session, last_ar_date, legal_type, state, limited_restoration, expected): +@pytest.mark.parametrize('last_ar_date, legal_type, state, limited_restoration, in_liquidation, expected', TEST_GOOD_STANDING_DATA) +def test_good_standing(session, last_ar_date, legal_type, state, limited_restoration, in_liquidation, expected): """Assert that the business is in good standing when conditions are met.""" designation = '001' business = Business(legal_name=f'legal_name-{designation}', @@ -275,7 +280,9 @@ def test_good_standing(session, last_ar_date, legal_type, state, limited_restora tax_id=f'BN0000{designation}', fiscal_year_end_date=datetime(2001, 8, 5, 7, 7, 58, 272362), last_ar_date=last_ar_date, - restoration_expiry_date=datetime.utcnow() if limited_restoration else None) + in_liquidation=in_liquidation, + in_liquidation_date=datetime.now(UTC) if in_liquidation else None, + restoration_expiry_date=datetime.now(UTC) if limited_restoration else None) business.save() assert business.good_standing is expected @@ -1005,3 +1012,21 @@ def test_public_user_dod_filings(session, test_name, dods, expected_dod_indxs): assert len(business.public_user_dod_filings) == len(expected_ids) assert sorted([filing.id for filing in business.public_user_dod_filings]) == sorted(expected_ids) + + +@pytest.mark.parametrize('test_name, founding_date, in_liquidation_date, last_lr_year, expected_lr_min_date', [ + ('NOT_IN_LIQUIDATION', datetime.now(), None, None, None), + ('IN_LIQUIDATION', FOUNDING_DATE, IN_LIQUIDATION_DATE, None, date(2025, 3, 2)), + ('IN_LIQUIDATION_lr_year', FOUNDING_DATE, IN_LIQUIDATION_DATE, 2025, date(2026, 3, 2)) +]) +def test_next_lr_min_date(session, test_name, founding_date, in_liquidation_date, last_lr_year, expected_lr_min_date): + """Assert next_lr_min_date works as expected.""" + identifier = 'BC7654321' + business = factory_business_from_tests(identifier=identifier, + entity_type=Business.LegalTypes.COMP.value, + founding_date=founding_date, + in_liquidation_date=in_liquidation_date, + last_lr_year=last_lr_year) + + assert business.next_lr_min_date == expected_lr_min_date + From 0078d2e1d22319c9875e06ad77ee680973c09552 Mon Sep 17 00:00:00 2001 From: Lucas O'Neil Date: Fri, 6 Mar 2026 09:20:47 -0800 Subject: [PATCH 10/46] Flag for CoD backdating for Out of Order Director Logic (#4139) * Flag for CoD backdating Signed-off-by: Lucas * Fix from review Signed-off-by: Lucas --------- Signed-off-by: Lucas --- legal-api/flags.json | 3 +- .../validations/change_of_directors.py | 29 +++- .../test_validation_directors_dates.py | 132 ++++++++++++++++++ .../test_validation_effective_date.py | 117 ++++++++++++++++ 4 files changed, 274 insertions(+), 7 deletions(-) diff --git a/legal-api/flags.json b/legal-api/flags.json index bd91fab7be..59fe6a498f 100644 --- a/legal-api/flags.json +++ b/legal-api/flags.json @@ -43,6 +43,7 @@ "support-agm-extension-entities": "BC BEN CC ULC C CBEN CCC CUL", "supported-agm-location-change-entities": "BC BEN CC ULC C CBEN CCC CUL", "supported-continuation-out-entities": "BC BEN CC ULC C CBEN CCC CUL", - "supported-consent-continuation-out-entities": "BC BEN CC ULC C CBEN CCC CUL" + "supported-consent-continuation-out-entities": "BC BEN CC ULC C CBEN CCC CUL", + "enable-backdated-cod": false } } diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_directors.py b/legal-api/src/legal_api/services/filings/validations/change_of_directors.py index ebe7e3062e..9244ba4c24 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_directors.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_directors.py @@ -18,7 +18,8 @@ from flask_babel import _ as babel # importing camelcase '_' as a name from legal_api.errors import Error -from legal_api.models import Address, Business +from legal_api.models import Address, Business, Filing +from legal_api.services import flags from legal_api.services.filings.validations.common_validations import PARTY_NAME_MAX_LENGTH, validate_parties_addresses from legal_api.services.utils import get_str from legal_api.utils.datetime import date, datetime @@ -56,12 +57,19 @@ def validate(business: Business, cod: dict) -> Error: return None def get_cod_date_bounds(business: Business) -> tuple[date, date]: - """Return (earliest_allowed_date_leg, today_leg) for COD date validation.""" - earliest_allowed_date_leg = LegislationDatetime.as_legislation_timezone( - business.last_cod_date or business.founding_date - ).date() - today_leg = LegislationDatetime.datenow() + """Return (earliest_allowed_date_leg, today_leg) for COD date validation. + + When enable-backdated-cod flag is on, only the founding_date is used as the lower bound. + When the flag is off, the earliest allowed date is last_cod_date (if present) or founding_date. + """ + founding_date_leg = LegislationDatetime.as_legislation_timezone(business.founding_date).date() + if flags.is_on("enable-backdated-cod") or not business.last_cod_date: + earliest_allowed_date_leg = founding_date_leg + else: + earliest_allowed_date_leg = LegislationDatetime.as_legislation_timezone(business.last_cod_date).date() + + today_leg = LegislationDatetime.datenow() return earliest_allowed_date_leg, today_leg @@ -260,6 +268,15 @@ def validate_effective_date(business: Business, cod: dict) -> list: if effective_date_leg < founding_date_leg: msg.append({"error": babel("Effective date cannot be before businesses founding date.")}) + # check if effective date is before their most recent COD or AR date + if not flags.is_on("enable-backdated-cod"): + last_cod_filing = Filing.get_most_recent_legal_filing(business.id, + Filing.FILINGS["changeOfDirectors"]["name"]) + if last_cod_filing: + last_cod_date_leg = LegislationDatetime.as_legislation_timezone(last_cod_filing.effective_date).date() + if effective_date_leg < last_cod_date_leg: + msg.append({"error": babel("Effective date cannot be before another Change of Directors filing.")}) + return msg def validate_directors_name(cod: dict) -> list: diff --git a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_dates.py b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_dates.py index b915cb5bc4..30bf7b3493 100644 --- a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_dates.py +++ b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_directors_dates.py @@ -22,8 +22,11 @@ from registry_schemas.example_data import CHANGE_OF_DIRECTORS, FILING_HEADER from legal_api.models import Business +from legal_api.services import flags from legal_api.services.filings import validate +from legal_api.services.filings.validations.change_of_directors import get_cod_date_bounds from legal_api.utils.datetime import datetime, timezone +from legal_api.utils.legislation_datetime import LegislationDatetime from tests.unit.services.filings.validations import lists_are_equal NOW = datetime(2025, 1, 1, 12, 0, tzinfo=timezone.utc) @@ -254,3 +257,132 @@ def test_validate_cod_director_appointment_date(session, test_name, actions, app assert lists_are_equal(err.msg, expected_msg) else: assert err is None + + +def test_validate_cessation_date_before_last_cod_with_flag_on(session, mocker): + """Assert that cessation date before last_cod_date but after founding passes when flag is on.""" + mocker.patch.object(flags, 'is_on', side_effect=lambda flag, **kwargs: flag == 'enable-backdated-cod') + # Use a date between founding and last_cod_date + between_date = (FOUNDING_DATE + datedelta.DAY).date().isoformat() + business, f = common_setup_cod( + "CP7654321", + actions=["ceased"], + cessationDate=between_date, + last_cod_date=LAST_COD_DATE_AFTER_FOUNDING + ) + + with freeze_time(NOW): + err = validate(business, f) + + assert err is None + + +def test_validate_cessation_date_before_founding_with_flag_on(session, mocker): + """Assert that cessation date before founding_date still fails even when flag is on.""" + mocker.patch.object(flags, 'is_on', side_effect=lambda flag, **kwargs: flag == 'enable-backdated-cod') + business, f = common_setup_cod( + "CP7654321", + actions=["ceased"], + cessationDate="2023-01-01", # before founding_date + last_cod_date=LAST_COD_DATE_SAME_AS_FOUNDING + ) + + with freeze_time(NOW): + err = validate(business, f) + + assert err.code == HTTPStatus.BAD_REQUEST + assert lists_are_equal(err.msg, [ + { + "error": "Cessation date cannot be before the business founding date " + "or the most recent Change of Directors filing.", + "path": "/filing/changeOfDirectors/directors/0/cessationDate" + } + ]) + + +def test_validate_appointment_date_before_last_cod_with_flag_on(session, mocker): + """Assert that appointment date before last_cod_date but after founding passes when flag is on.""" + mocker.patch.object(flags, 'is_on', side_effect=lambda flag, **kwargs: flag == 'enable-backdated-cod') + between_date = (FOUNDING_DATE + datedelta.DAY).date().isoformat() + business, f = common_setup_cod( + "CP7654321", + actions=["appointed"], + appointmentDate=between_date, + last_cod_date=LAST_COD_DATE_AFTER_FOUNDING + ) + + with freeze_time(NOW): + err = validate(business, f) + + assert err is None + + +def test_validate_appointment_date_before_founding_with_flag_on(session, mocker): + """Assert that appointment date before founding_date still fails even when flag is on.""" + mocker.patch.object(flags, 'is_on', side_effect=lambda flag, **kwargs: flag == 'enable-backdated-cod') + business, f = common_setup_cod( + "CP7654321", + actions=["appointed"], + appointmentDate="2023-01-01", # before founding_date + last_cod_date=LAST_COD_DATE_SAME_AS_FOUNDING + ) + + with freeze_time(NOW): + err = validate(business, f) + + assert err.code == HTTPStatus.BAD_REQUEST + assert lists_are_equal(err.msg, [ + { + "error": "Appointment date cannot be before the business founding date " + "or the most recent Change of Directors filing.", + "path": "/filing/changeOfDirectors/directors/0/appointmentDate" + } + ]) + + +class TestGetCodDateBounds: + """Tests for get_cod_date_bounds.""" + + def test_flag_off_no_last_cod_date(self, session, mocker): + """Flag off, no last_cod_date -> returns founding_date.""" + mocker.patch.object(flags, 'is_on', return_value=False) + business = Business(founding_date=FOUNDING_DATE, last_cod_date=None) + + with freeze_time(NOW): + earliest, today = get_cod_date_bounds(business) + expected = LegislationDatetime.as_legislation_timezone(FOUNDING_DATE).date() + assert earliest == expected + assert today == LegislationDatetime.datenow() + + def test_flag_off_with_last_cod_date(self, session, mocker): + """Flag off, last_cod_date after founding -> returns last_cod_date.""" + mocker.patch.object(flags, 'is_on', return_value=False) + business = Business(founding_date=FOUNDING_DATE, last_cod_date=LAST_COD_DATE_AFTER_FOUNDING) + + with freeze_time(NOW): + earliest, _ = get_cod_date_bounds(business) + + expected = LegislationDatetime.as_legislation_timezone(LAST_COD_DATE_AFTER_FOUNDING).date() + assert earliest == expected + + def test_flag_on_with_last_cod_date(self, session, mocker): + """Flag on, last_cod_date after founding -> returns founding_date (ignores last_cod_date).""" + mocker.patch.object(flags, 'is_on', side_effect=lambda flag, **kwargs: flag == 'enable-backdated-cod') + business = Business(founding_date=FOUNDING_DATE, last_cod_date=LAST_COD_DATE_AFTER_FOUNDING) + + with freeze_time(NOW): + earliest, _ = get_cod_date_bounds(business) + + expected = LegislationDatetime.as_legislation_timezone(FOUNDING_DATE).date() + assert earliest == expected + + def test_flag_on_no_last_cod_date(self, session, mocker): + """Flag on, no last_cod_date -> returns founding_date.""" + mocker.patch.object(flags, 'is_on', side_effect=lambda flag, **kwargs: flag == 'enable-backdated-cod') + business = Business(founding_date=FOUNDING_DATE, last_cod_date=None) + + with freeze_time(NOW): + earliest, _ = get_cod_date_bounds(business) + + expected = LegislationDatetime.as_legislation_timezone(FOUNDING_DATE).date() + assert earliest == expected diff --git a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py index ded2b04eba..32671d2698 100644 --- a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py +++ b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_effective_date.py @@ -19,6 +19,7 @@ from registry_schemas.example_data import ANNUAL_REPORT, CHANGE_OF_DIRECTORS, FILING_HEADER from legal_api.models import Business +from legal_api.services import flags from legal_api.services.filings.validations.change_of_directors import validate_effective_date from legal_api.utils.datetime import datetime, timezone from legal_api.utils.legislation_datetime import LegislationDatetime @@ -150,6 +151,122 @@ def test_validate_effective_date_not_before_founding(session): assert not err +def test_validate_effective_date_not_before_other_cod(session): + """Assert that the effective date of change cannot be before a previous COD filing.""" + # setup + identifier = 'CP1234567' + founding_date = datetime(2000, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + business = Business(identifier=identifier, + founding_date=founding_date) + business.save() + now = datetime(2020, 7, 30, 12, 0, 0, 0, tzinfo=timezone.utc) + + # create the previous COD filing + filing_cod = copy.deepcopy(FILING_HEADER) + filing_cod['filing']['header']['name'] = 'changeOfDirectors' + filing_cod['filing']['changeOfDirectors'] = copy.deepcopy(CHANGE_OF_DIRECTORS) + filing_date = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + factory_completed_filing(business=business, + data_dict=filing_cod, + filing_date=filing_date) + + # The effective date _cannot_ be before the previous COD. + before = datetime(2010, 8, 4, 12, 0, 0, 0, tzinfo=timezone.utc) + effective_date = as_effective_date(before) + filing_cod['filing']['header']['effectiveDate'] = effective_date.isoformat() + with freeze_time(now): + err = validate_effective_date(business, filing_cod) + assert err + + # The effective date _can_ be on the same date as the previous COD. + on = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + effective_date = as_effective_date(on) + filing_cod['filing']['header']['effectiveDate'] = effective_date.isoformat() + with freeze_time(now): + err = validate_effective_date(business, filing_cod) + assert not err + + # The effective date _can_ be after the previous COD. + after = datetime(2010, 8, 6, 12, 0, 0, 0, tzinfo=timezone.utc) + effective_date = as_effective_date(after) + filing_cod['filing']['header']['effectiveDate'] = effective_date.isoformat() + with freeze_time(now): + err = validate_effective_date(business, filing_cod) + assert not err + + +def test_validate_effective_date_not_before_other_ar_with_cod(session): + """Assert that the effective date of change cannot be before a previous AR filing.""" + # setup + identifier = 'CP1234567' + founding_date = datetime(2000, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + business = Business(identifier=identifier, + founding_date=founding_date) + business.save() + now = datetime(2020, 7, 30, 12, 0, 0, 0, tzinfo=timezone.utc) + + # create the previous AR filing + filing_ar = copy.deepcopy(ANNUAL_REPORT) + filing_ar['filing']['changeOfDirectors'] = copy.deepcopy(CHANGE_OF_DIRECTORS) + filing_date = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + factory_completed_filing(business=business, + data_dict=filing_ar, + filing_date=filing_date) + + # The effective date _cannot_ be before the previous AR. + before = datetime(2010, 8, 4, 12, 0, 0, 0, tzinfo=timezone.utc) + effective_date = as_effective_date(before) + filing_ar['filing']['header']['effectiveDate'] = effective_date.isoformat() + with freeze_time(now): + err = validate_effective_date(business, filing_ar) + assert err + + # The effective date _can_ be on the same date as the previous AR. + on = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + effective_date = as_effective_date(on) + filing_ar['filing']['header']['effectiveDate'] = effective_date.isoformat() + with freeze_time(now): + err = validate_effective_date(business, filing_ar) + assert not err + + # The effective date _can_ be after the previous AR. + after = datetime(2010, 8, 6, 12, 0, 0, 0, tzinfo=timezone.utc) + effective_date = as_effective_date(after) + filing_ar['filing']['header']['effectiveDate'] = effective_date.isoformat() + with freeze_time(now): + err = validate_effective_date(business, filing_ar) + assert not err + + +def test_validate_effective_date_before_other_cod_with_flag_on(session, mocker): + """Assert that effective date before a previous COD passes when enable-backdated-cod flag is on.""" + mocker.patch.object(flags, 'is_on', side_effect=lambda flag, **kwargs: flag == 'enable-backdated-cod') + + identifier = 'CP1234567' + founding_date = datetime(2000, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + business = Business(identifier=identifier, + founding_date=founding_date) + business.save() + now = datetime(2020, 7, 30, 12, 0, 0, 0, tzinfo=timezone.utc) + + # create the previous COD filing + filing_cod = copy.deepcopy(FILING_HEADER) + filing_cod['filing']['header']['name'] = 'changeOfDirectors' + filing_cod['filing']['changeOfDirectors'] = copy.deepcopy(CHANGE_OF_DIRECTORS) + filing_date = datetime(2010, 8, 5, 12, 0, 0, 0, tzinfo=timezone.utc) + factory_completed_filing(business=business, + data_dict=filing_cod, + filing_date=filing_date) + + # The effective date _can_ be before the previous COD when flag is on. + before = datetime(2010, 8, 4, 12, 0, 0, 0, tzinfo=timezone.utc) + effective_date = as_effective_date(before) + filing_cod['filing']['header']['effectiveDate'] = effective_date.isoformat() + with freeze_time(now): + err = validate_effective_date(business, filing_cod) + assert not err + + def as_effective_date(date_time: datetime) -> datetime: """Convert date_time to an effective date with 0 time in the legislation timezone, same as the UI does.""" # 1. convert to legislation datetime From 6ad7e1989f612f541c3468d459ccfff784bb6a29 Mon Sep 17 00:00:00 2001 From: Kial Date: Fri, 6 Mar 2026 14:13:09 -0500 Subject: [PATCH 11/46] Email reminder + common dissolution - liquidation updates (#4141) Signed-off-by: Kial Jinnah --- gcp-jobs/email-reminder/poetry.lock | 40 +++++++++++----- gcp-jobs/email-reminder/pyproject.toml | 2 +- .../src/email_reminder/worker.py | 2 + .../email-reminder/tests/unit/__init__.py | 6 ++- .../email-reminder/tests/unit/test_worker.py | 28 +++++++---- .../business-registry-dissolution/poetry.lock | 47 +++++++++++++------ .../pyproject.toml | 2 +- .../involuntary_dissolution.py | 3 +- .../tests/__init__.py | 9 +++- .../unit/test_involuntary_dissolution.py | 6 ++- 10 files changed, 99 insertions(+), 46 deletions(-) diff --git a/gcp-jobs/email-reminder/poetry.lock b/gcp-jobs/email-reminder/poetry.lock index ebb29c347e..ebdc683a2d 100644 --- a/gcp-jobs/email-reminder/poetry.lock +++ b/gcp-jobs/email-reminder/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "alembic" @@ -113,10 +113,10 @@ files = [ [[package]] name = "business-model" -version = "3.0.0" +version = "3.3.22" description = "" optional = false -python-versions = ">=3.9,<4" +python-versions = ">=3.13,<3.14" groups = ["main"] files = [] develop = false @@ -124,6 +124,8 @@ develop = false [package.dependencies] croniter = ">=6.0.0,<7.0.0" datedelta = ">=1.4,<2.0" +dotenv = ">=0.9.9,<0.10.0" +flask = ">=3.1.0,<4.0.0" flask-babel = ">=4.0.0,<5.0.0" flask-migrate = ">=4.1.0,<5.0.0" flask-sqlalchemy = ">=3.1.1,<4.0.0" @@ -131,14 +133,14 @@ pg8000 = ">=1.31.2,<2.0.0" pycountry = ">=24.6.1,<25.0.0" pydantic = ">=2.10.6,<3.0.0" pytz = ">=2025.1,<2026.0" -registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.39"} +registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.62"} sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdirectory = "python/common/sql-versioning-alt"} [package.source] type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "a18fd64eeac8736a8481b90e50409188a91cc601" +resolved_reference = "0078d2e1d22319c9875e06ad77ee680973c09552" subdirectory = "python/common/business-registry-model" [[package]] @@ -614,6 +616,20 @@ wrapt = ">=1.10,<2" [package.extras] dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "setuptools ; python_version >= \"3.12\"", "tox"] +[[package]] +name = "dotenv" +version = "0.9.9" +description = "Deprecated package" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"}, +] + +[package.dependencies] +python-dotenv = "*" + [[package]] name = "expiringdict" version = "1.2.2" @@ -822,7 +838,7 @@ files = [ [package.dependencies] google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" -grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""} 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" @@ -830,7 +846,7 @@ 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) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] +grpc = ["grpcio (>=1.33.2,<2.0.dev0)", "grpcio (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -1088,7 +1104,7 @@ files = [ [package.dependencies] googleapis-common-protos = ">=1.5.5" grpcio = ">=1.71.0" -protobuf = ">=5.26.1,<6.0dev" +protobuf = ">=5.26.1,<6.0.dev0" [[package]] name = "idna" @@ -1216,7 +1232,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} @@ -1867,7 +1883,7 @@ rpds-py = ">=0.7.0" [[package]] name = "registry_schemas" -version = "2.18.39" +version = "2.18.62" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -1885,8 +1901,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.39" -resolved_reference = "c0aeb44d47f8cd126c85b6ca9a8a747f33266db1" +reference = "2.18.62" +resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" [[package]] name = "requests" diff --git a/gcp-jobs/email-reminder/pyproject.toml b/gcp-jobs/email-reminder/pyproject.toml index dffa003a4d..a2a198e811 100644 --- a/gcp-jobs/email-reminder/pyproject.toml +++ b/gcp-jobs/email-reminder/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "email-reminder" -version = "0.1.0" +version = "0.1.1" description = "" authors = [ {name = "SBC-Connect"} diff --git a/gcp-jobs/email-reminder/src/email_reminder/worker.py b/gcp-jobs/email-reminder/src/email_reminder/worker.py index a0ff15394a..b803e80fd2 100644 --- a/gcp-jobs/email-reminder/src/email_reminder/worker.py +++ b/gcp-jobs/email-reminder/src/email_reminder/worker.py @@ -111,6 +111,8 @@ def get_businesses(legal_types: list) -> Pagination: Business.state == Business.State.ACTIVE, # restoration_expiry_date will have a value for limitedRestoration and limitedRestorationExtension Business.restoration_expiry_date == None, + # businesses in liquidation do not file annual reports + Business.in_liquidation == False, where_clause ).order_by(Business.id).paginate(per_page=20) diff --git a/gcp-jobs/email-reminder/tests/unit/__init__.py b/gcp-jobs/email-reminder/tests/unit/__init__.py index fa7ae4a9fc..5e85d06bc9 100644 --- a/gcp-jobs/email-reminder/tests/unit/__init__.py +++ b/gcp-jobs/email-reminder/tests/unit/__init__.py @@ -54,7 +54,8 @@ def factory_business(identifier, admin_freeze=False, no_dissolution=False, last_ar_reminder_year=None, - restoration_expiry_date=None): + restoration_expiry_date=None, + in_liquidation=False): """Create a business.""" last_ar_year = None if last_ar_date: @@ -69,7 +70,8 @@ def factory_business(identifier, admin_freeze=admin_freeze, no_dissolution=no_dissolution, last_ar_reminder_year=last_ar_reminder_year, - restoration_expiry_date=restoration_expiry_date) + restoration_expiry_date=restoration_expiry_date, + in_liquidation=in_liquidation) business.save() return business diff --git a/gcp-jobs/email-reminder/tests/unit/test_worker.py b/gcp-jobs/email-reminder/tests/unit/test_worker.py index f5633dd56c..dd82ec4569 100644 --- a/gcp-jobs/email-reminder/tests/unit/test_worker.py +++ b/gcp-jobs/email-reminder/tests/unit/test_worker.py @@ -49,22 +49,30 @@ def test_send_email(app: Flask): email_publish_mock.assert_called() assert_publish_mock(app, email_publish_mock, business_id, ar_fee, ar_year) -@pytest.mark.parametrize('_test_name, founding_date, reminder_year, rest_expiry_date, state, expected',[ - ('active_new_business', datetime.now(UTC), None, None, Business.State.ACTIVE, False), - ('active_year_old_business', datetime.now(UTC) - timedelta(days=365), None, None, Business.State.ACTIVE, True), - ('active_year_old_business_with_reminder_year', datetime.now(UTC) - timedelta(days=365), datetime.now(UTC).year, None, Business.State.ACTIVE, False), - ('active_2year_old_business_with_old_reminder_year', datetime.now(UTC) - timedelta(days=730), (datetime.now(UTC) - timedelta(days=730)).year, None, Business.State.ACTIVE, True), - ('active_2year_old_business_with_reminder_year', datetime.now(UTC) - timedelta(days=730), (datetime.now(UTC) - timedelta(days=364)).year, None, Business.State.ACTIVE, False), - ('active_year_old_business_with_restoration', datetime.now(UTC) - timedelta(days=365), None, datetime.now(UTC) + timedelta(days=10), Business.State.ACTIVE, False), - ('historical_year_old_business', datetime.now(UTC) - timedelta(days=365), None, None, Business.State.HISTORICAL, False), +@pytest.mark.parametrize('_test_name, founding_date, reminder_year, rest_expiry_date, in_liquidation, state, expected',[ + ('active_new_business', datetime.now(UTC), None, None, False, Business.State.ACTIVE, False), + ('active_year_old_business', datetime.now(UTC) - timedelta(days=365), None, None, False, Business.State.ACTIVE, True), + ('active_year_old_business_with_reminder_year', datetime.now(UTC) - timedelta(days=365), datetime.now(UTC).year, None, False, Business.State.ACTIVE, False), + ('active_2year_old_business_with_old_reminder_year', datetime.now(UTC) - timedelta(days=730), (datetime.now(UTC) - timedelta(days=730)).year, None, False, Business.State.ACTIVE, True), + ('active_2year_old_business_with_reminder_year', datetime.now(UTC) - timedelta(days=730), (datetime.now(UTC) - timedelta(days=364)).year, None, False, Business.State.ACTIVE, True), + ('active_2year_old_business_with_current_reminder_year', datetime.now(UTC) - timedelta(days=730), (datetime.now(UTC)).year, None, False, Business.State.ACTIVE, False), + ('active_year_old_business_with_restoration', datetime.now(UTC) - timedelta(days=365), None, datetime.now(UTC) + timedelta(days=10), False, Business.State.ACTIVE, False), + ('active_year_old_business_in_liquidation', datetime.now(UTC) - timedelta(days=365), None, None, True, Business.State.ACTIVE, False), + ('historical_year_old_business', datetime.now(UTC) - timedelta(days=365), None, None, False, Business.State.HISTORICAL, False), ]) -def test_get_businesses(app, session, _test_name, founding_date, reminder_year, rest_expiry_date, state, expected): +def test_get_businesses(app, session, _test_name, founding_date, reminder_year, rest_expiry_date, in_liquidation, state, expected): """Assert the get_businesses method works as expected.""" business = factory_business(identifier='BC1234567', founding_date=founding_date, last_ar_reminder_year=reminder_year, restoration_expiry_date=rest_expiry_date, - state=state) + state=state, + in_liquidation=in_liquidation) + print(business.founding_date) + print(business.last_ar_reminder_year) + print(business.restoration_expiry_date) + print(business.in_liquidation) + print(expected) resp = get_businesses([business.legal_type]) if expected: assert len(resp.items) == 1 diff --git a/python/common/business-registry-dissolution/poetry.lock b/python/common/business-registry-dissolution/poetry.lock index caf6ed3edc..fe97ac1293 100644 --- a/python/common/business-registry-dissolution/poetry.lock +++ b/python/common/business-registry-dissolution/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "alembic" @@ -158,10 +158,10 @@ files = [ [[package]] name = "business-model" -version = "3.0.0" +version = "3.3.22" description = "" optional = false -python-versions = ">=3.9,<4" +python-versions = ">=3.13,<3.14" groups = ["main"] files = [] develop = false @@ -169,20 +169,23 @@ develop = false [package.dependencies] croniter = ">=6.0.0,<7.0.0" datedelta = ">=1.4,<2.0" +dotenv = ">=0.9.9,<0.10.0" +flask = ">=3.1.0,<4.0.0" flask-babel = ">=4.0.0,<5.0.0" flask-migrate = ">=4.1.0,<5.0.0" +flask-sqlalchemy = ">=3.1.1,<4.0.0" pg8000 = ">=1.31.2,<2.0.0" pycountry = ">=24.6.1,<25.0.0" pydantic = ">=2.10.6,<3.0.0" pytz = ">=2025.1,<2026.0" -registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.39"} +registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.62"} sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdirectory = "python/common/sql-versioning-alt"} [package.source] type = "git" url = "https://github.com/bcgov/lear.git" reference = "HEAD" -resolved_reference = "67cc9c452dcc602a9c0d740ebe059fd694180616" +resolved_reference = "0078d2e1d22319c9875e06ad77ee680973c09552" subdirectory = "python/common/business-registry-model" [[package]] @@ -215,7 +218,7 @@ name = "business-registry-common" version = "0.1.0" description = "" optional = false -python-versions = "^3.13" +python-versions = ">=3.9,<4.0" groups = ["main"] files = [] develop = false @@ -226,9 +229,9 @@ pytz = "^2025.1" [package.source] type = "git" -url = "https://github.com/BrandonSharratt/lear.git" +url = "https://github.com/bcgov/lear.git" reference = "HEAD" -resolved_reference = "a18723796cee0a5ea1d8022d31847214b09f1c64" +resolved_reference = "d844169681baed690b77df9617897414cd5c42ff" subdirectory = "python/common/business-registry-common" [[package]] @@ -552,6 +555,20 @@ files = [ {file = "datedelta-1.4.tar.gz", hash = "sha256:3f1ef319ead642a76a3cab731917bf14a0ced0d91943f33ff57ae615837cab97"}, ] +[[package]] +name = "dotenv" +version = "0.9.9" +description = "Deprecated package" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9"}, +] + +[package.dependencies] +python-dotenv = "*" + [[package]] name = "flake8" version = "7.1.2" @@ -717,7 +734,7 @@ description = "Lightweight in-process concurrent programming" optional = false python-versions = ">=3.7" groups = ["main"] -markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")" +markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" files = [ {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, @@ -900,7 +917,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} @@ -1417,7 +1434,7 @@ rpds-py = ">=0.7.0" [[package]] name = "registry_schemas" -version = "2.18.39" +version = "2.18.62" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -1435,8 +1452,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.39" -resolved_reference = "c0aeb44d47f8cd126c85b6ca9a8a747f33266db1" +reference = "2.18.62" +resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" [[package]] name = "requests" @@ -1934,5 +1951,5 @@ tomli = "*" [metadata] lock-version = "2.1" -python-versions = "^3.13" -content-hash = "5db1b20b151054bc384812f7806c0fb9326d15bfd4e95b84f8eea5e254f6f411" +python-versions = ">=3.13,<3.14" +content-hash = "6fdda1f3bc2f1f4fb12258fd2f1b170538480b3ad14138f002f45a99fd37dacc" diff --git a/python/common/business-registry-dissolution/pyproject.toml b/python/common/business-registry-dissolution/pyproject.toml index 50e2c6a2f3..f5451c8ee6 100644 --- a/python/common/business-registry-dissolution/pyproject.toml +++ b/python/common/business-registry-dissolution/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" packages = [{include = "dissolution_service", from = "src"}] [tool.poetry.dependencies] -python = "^3.13" +python = ">=3.13,<3.14" flask = "^3.1.0" sqlalchemy = "^2.0.39" datedelta = "^1.4" diff --git a/python/common/business-registry-dissolution/src/dissolution_service/involuntary_dissolution.py b/python/common/business-registry-dissolution/src/dissolution_service/involuntary_dissolution.py index 5a9bff034e..30eb1c8cd6 100644 --- a/python/common/business-registry-dissolution/src/dissolution_service/involuntary_dissolution.py +++ b/python/common/business-registry-dissolution/src/dissolution_service/involuntary_dissolution.py @@ -142,7 +142,8 @@ def _get_businesses_eligible_query(eligibility_filters: EligibilityFilters = Eli query = query.filter( or_( - specific_filing_overdue, + # businesses in liquidation do not file ARs + and_(specific_filing_overdue, not_(Business.in_liquidation.is_(True))), no_transition_filed_after_restoration ) ).\ diff --git a/python/common/business-registry-dissolution/tests/__init__.py b/python/common/business-registry-dissolution/tests/__init__.py index 1331dd7489..d910c002eb 100644 --- a/python/common/business-registry-dissolution/tests/__init__.py +++ b/python/common/business-registry-dissolution/tests/__init__.py @@ -76,7 +76,9 @@ def factory_business(identifier, naics_code=None, naics_desc=None, admin_freeze=False, - no_dissolution=False): + no_dissolution=False, + in_liquidation_date=None, + last_lr_year=None): """Create a business entity with a versioned business.""" last_ar_year = None if last_ar_date: @@ -95,7 +97,10 @@ def factory_business(identifier, naics_code=naics_code, naics_description=naics_desc, admin_freeze=admin_freeze, - no_dissolution=no_dissolution) + no_dissolution=no_dissolution, + in_liquidation=in_liquidation_date != None, + in_liquidation_date=in_liquidation_date, + last_lr_year=last_lr_year) # Versioning business VersioningProxy.get_transaction_id(db.session()) diff --git a/python/common/business-registry-dissolution/tests/unit/test_involuntary_dissolution.py b/python/common/business-registry-dissolution/tests/unit/test_involuntary_dissolution.py index 2a0bce8863..77ba8e8a67 100644 --- a/python/common/business-registry-dissolution/tests/unit/test_involuntary_dissolution.py +++ b/python/common/business-registry-dissolution/tests/unit/test_involuntary_dissolution.py @@ -166,6 +166,7 @@ def test_get_businesses_eligible_query_in_dissolution(session, test_name, batch_ ('RECOGNITION_OVERDUE', False), ('RESTORATION_OVERDUE', False), ('AR_OVERDUE', False), + ('AR_OVERDUE_IN_LIQUIDATION', True), ('NO_OVERDUE', True) ] ) @@ -177,9 +178,10 @@ def test_get_businesses_eligible_query_specific_filing_overdue(session, test_nam business = factory_business(identifier='BC1234567', entity_type=Business.LegalTypes.COMP.value) effective_date = datetime.utcnow() - datedelta(years=3) factory_completed_filing(business, RESTORATION_FILING, filing_type='restoration', filing_date=effective_date) - elif test_name == 'AR_OVERDUE': + elif test_name.startswith('AR_OVERDUE'): last_ar_date = datetime.utcnow() - datedelta(years=3) - business = factory_business(identifier='BC1234567', entity_type=Business.LegalTypes.COMP.value, last_ar_date=last_ar_date) + in_liquidation_date = datetime.utcnow() if test_name == 'AR_OVERDUE_IN_LIQUIDATION' else None + business = factory_business(identifier='BC1234567', entity_type=Business.LegalTypes.COMP.value, last_ar_date=last_ar_date, in_liquidation_date=in_liquidation_date) else: business = factory_business(identifier='BC1234567', entity_type=Business.LegalTypes.COMP.value, founding_date=datetime.utcnow()) From dd6eb4b54ef45c9f5b8cb2e950ca7406e6182a51 Mon Sep 17 00:00:00 2001 From: Kial Date: Mon, 9 Mar 2026 15:55:06 -0400 Subject: [PATCH 12/46] 32567 32565 business filer liquidation updates (#4142) * Business Filer - liquidation processing updates Signed-off-by: Kial Jinnah * chore: improve test Signed-off-by: Kial Jinnah * chore: ruff fix Signed-off-by: Kial Jinnah * chore: reduce duplication Signed-off-by: Kial Jinnah * chore: sonar cloud fixes Signed-off-by: Kial Jinnah --------- Signed-off-by: Kial Jinnah --- queue_services/business-filer/poetry.lock | 4 +- .../change_of_liquidators.py | 28 ++- .../filing_processors/intent_to_liquidate.py | 55 ------ .../src/business_filer/services/filer.py | 4 - .../test_filer/test_change_of_liquidators.py | 167 +++++++++++++----- 5 files changed, 144 insertions(+), 114 deletions(-) delete mode 100644 queue_services/business-filer/src/business_filer/filing_processors/intent_to_liquidate.py diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index 337e0468ab..f7fc95e170 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -113,7 +113,7 @@ files = [ [[package]] name = "business-model" -version = "3.3.20" +version = "3.3.22" description = "" optional = false python-versions = ">=3.13,<3.14" @@ -140,7 +140,7 @@ sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdi type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "b029c78337ef6bcc83d91b335185b4a770cb1d33" +resolved_reference = "6ad7e1989f612f541c3468d459ccfff784bb6a29" subdirectory = "python/common/business-registry-model" [[package]] diff --git a/queue_services/business-filer/src/business_filer/filing_processors/change_of_liquidators.py b/queue_services/business-filer/src/business_filer/filing_processors/change_of_liquidators.py index 061e9654e0..e05bc25cb2 100644 --- a/queue_services/business-filer/src/business_filer/filing_processors/change_of_liquidators.py +++ b/queue_services/business-filer/src/business_filer/filing_processors/change_of_liquidators.py @@ -33,8 +33,10 @@ # POSSIBILITY OF SUCH DAMAGE. """File processing rules and actions for change of liquidators filings.""" import copy +from datetime import UTC, datetime from business_model.models import Business, Filing, PartyRole +from business_model.utils.legislation_datetime import LegislationDatetime from business_filer.filing_meta import FilingMeta from business_filer.filing_processors.filing_components.filings import update_filing_court_order @@ -46,20 +48,33 @@ ) +def _init_liquidation(business: Business, filing_meta: FilingMeta): + """Put the business into liquidation.""" + if not business.in_liquidation: + business.in_liquidation = True + business.in_liquidation_date = filing_meta.application_date or datetime.now(UTC) + + +def _update_last_lr_year(business: Business): + """Update the business last liquidation report year.""" + if business.last_lr_year: + business.last_lr_year += 1 + else: + in_liquidation_date = LegislationDatetime.as_legislation_timezone(business.in_liquidation_date).date() + business.last_lr_year = in_liquidation_date.year + 1 + + def process(business: Business, filing_rec: Filing, filing_meta: FilingMeta): """Render the changeOfLiquidators onto the business model objects.""" filing_json = copy.deepcopy(filing_rec.filing_json) relationships = filing_json["filing"]["changeOfLiquidators"].get("relationships", []) offices = filing_json["filing"]["changeOfLiquidators"].get("offices", {}) - if filing_rec.filing_sub_type == "intentToLiquidate": + if filing_rec.filing_sub_type in ["intentToLiquidate", "appointLiquidator"]: create_relationships(relationships, business, filing_rec) update_or_create_offices(business, offices) - business.in_liquidation = True + _init_liquidation(business, filing_meta) - elif filing_rec.filing_sub_type == "appointLiquidator": - create_relationships(relationships, business, filing_rec) - elif filing_rec.filing_sub_type == "ceaseLiquidator": cease_relationships(relationships, business, [PartyRole.RoleTypes.LIQUIDATOR.value], filing_meta.application_date) @@ -68,8 +83,7 @@ def process(business: Business, filing_rec: Filing, filing_meta: FilingMeta): update_or_create_offices(business, offices) elif filing_rec.filing_sub_type == "liquidationReport": - # FUTURE: updates for this will be made as part of #31714 - pass + _update_last_lr_year(business) # update court order, if any is present if court_order := filing_json["filing"]["changeOfLiquidators"].get("courtOrder"): diff --git a/queue_services/business-filer/src/business_filer/filing_processors/intent_to_liquidate.py b/queue_services/business-filer/src/business_filer/filing_processors/intent_to_liquidate.py deleted file mode 100644 index bbde2f6f04..0000000000 --- a/queue_services/business-filer/src/business_filer/filing_processors/intent_to_liquidate.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright © 2025 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. -"""File processing rules and actions for the Intent to Liquidate filing.""" -from business_model.models import Business, Comment, Filing -from flask import current_app - -from business_filer.exceptions import QueueException -from business_filer.filing_meta import FilingMeta -from business_filer.filing_processors.filing_components import filings - - -def process(business: Business, - filing: dict, - filing_rec: Filing, - filing_meta: FilingMeta): - """Render the intent to liquidate filing unto the model objects.""" - if not (intent_to_liquidate := filing.get("intentToLiquidate")): - current_app.logger.error("Could not find intentToLiquidate in: %s", filing) - raise QueueException(f"legal_filing:intentToLiquidate missing from {filing}") - - current_app.logger.debug("processing intentToLiquidate: %s", filing) - - liquidation_date = intent_to_liquidate.get("dateOfCommencementOfLiquidation") - - filing_meta.intent_to_liquidate = {} - filing_meta.intent_to_liquidate = { - **filing_meta.intent_to_liquidate, - "dateOfCommencementOfLiquidation": liquidation_date - } - - # Add comment about liquidation date - filing_rec.comments.append( - Comment( - comment=f"Liquidation is scheduled to commence on {liquidation_date}.", - staff_id=filing_rec.submitter_id - ) - ) - - # Update business in_liquidation flag - business.in_liquidation = True - - # update court order, if any is present - if court_order := intent_to_liquidate.get("courtOrder"): - filings.update_filing_court_order(filing_rec, court_order) diff --git a/queue_services/business-filer/src/business_filer/services/filer.py b/queue_services/business-filer/src/business_filer/services/filer.py index c4a1336e89..6a3a3d4b5d 100644 --- a/queue_services/business-filer/src/business_filer/services/filer.py +++ b/queue_services/business-filer/src/business_filer/services/filer.py @@ -67,7 +67,6 @@ court_order, dissolution, incorporation_filing, - intent_to_liquidate, notice_of_withdrawal, put_back_off, put_back_on, @@ -222,9 +221,6 @@ def process_filing(filing_message: FilingMessage): # noqa: PLR0915, PLR0912 filing_submission.json, filing_submission, filing_meta) - - case "intentToLiquidate": - intent_to_liquidate.process(business, filing, filing_submission, filing_meta) case "noticeOfWithdrawal": notice_of_withdrawal.process(filing_submission, filing, filing_meta) diff --git a/queue_services/business-filer/tests/unit/test_filer/test_change_of_liquidators.py b/queue_services/business-filer/tests/unit/test_filer/test_change_of_liquidators.py index b9613737bc..ffbe915c48 100644 --- a/queue_services/business-filer/tests/unit/test_filer/test_change_of_liquidators.py +++ b/queue_services/business-filer/tests/unit/test_filer/test_change_of_liquidators.py @@ -122,6 +122,7 @@ def _assert_party_roles_addresses(party_roles, party_id, expected_delivery, expe return def _assert_office_addresses(offices, expected_office): + # Should have registered office and liquidations office assert len(offices) == 2 has_liquidation_office = False @@ -140,24 +141,38 @@ def _assert_office_addresses(offices, expected_office): assert address.street == expected_mailing_street assert has_liquidation_office -def test_process_col_filing(app, session, mocker): - """Assert that all COL filings can be applied to the model correctly.""" - payment_id = str(random.SystemRandom().getrandbits(0x58)) - effective_date = datetime(2023, 10, 10, 10, 0, 0, tzinfo=timezone.utc) - identifier = f'BC{random.randint(1000000, 9999999)}' - drs_publish_mock = mocker.patch('business_filer.services.gcp_queue.publish', return_value=None) - - business = create_business(identifier) +def _assert_common_data(business: Business, filing: Filing, expected_date, expected_lr_year, expected_next_lr_yr): + """Assert the expected common data was updated by the liquidation filing processing.""" + assert filing.transaction_id + assert filing.business_id == business.id + assert filing.status == Filing.Status.COMPLETED.value + assert business.in_liquidation == True + assert business.in_liquidation_date == expected_date + assert business.last_lr_year == expected_lr_year + assert business.next_lr_min_date.year == expected_next_lr_yr +def _get_liquidation_filing(sub_type: str, effective_date: datetime, identifier = 'BC1234567'): + """Return a valid change of liquidators filing for the sub type.""" + payment_id = str(random.SystemRandom().getrandbits(0x58)) filing = copy.deepcopy(FILING_TEMPLATE) filing['filing']['header']['name'] = 'changeOfLiquidators' filing['filing']['header']['effectiveDate'] = effective_date.isoformat() filing['filing']['business']['identifier'] = identifier filing['filing']['business']['legalType'] = 'BC' filing['filing']['changeOfLiquidators'] = copy.deepcopy(CHANGE_OF_LIQUIDATORS_INTENT) + return filing, payment_id, identifier + + +def test_process_col_filing(app, session, mocker): + """Assert that all COL filings can be applied to the model correctly.""" + # NOTE: this is actually in 2023 pacific time + expected_in_liquidation_date = datetime(2024, 1, 1, 1, 0, 0, tzinfo=timezone.utc) + drs_publish_mock = mocker.patch('business_filer.services.gcp_queue.publish', return_value=None) + filing, payment_id, identifier = _get_liquidation_filing('intentToLiquidate', expected_in_liquidation_date) + business = create_business(identifier) filing_rec = create_filing(payment_id, filing, business.id) - filing_rec.effective_date = effective_date + filing_rec.effective_date = expected_in_liquidation_date filing_rec.save() # setup @@ -171,10 +186,7 @@ def test_process_col_filing(app, session, mocker): business: Business = Business.find_by_internal_id(business.id) # assert changes - assert intent_filing.transaction_id - assert intent_filing.business_id == business.id - assert intent_filing.status == Filing.Status.COMPLETED.value - assert business.in_liquidation == True + _assert_common_data(business, intent_filing, expected_in_liquidation_date, None, 2024) check_drs_publish(drs_publish_mock, app, business, intent_filing, '') drs_publish_mock.reset_mock() @@ -186,23 +198,7 @@ def test_process_col_filing(app, session, mocker): assert role.role == PartyRole.RoleTypes.LIQUIDATOR.value offices: list[Office] = business.offices.all() - # NOTE: will have a registered office too - assert len(offices) == 2 - has_liquidation_office = False - for office in offices: - if office.office_type == OfficeType.LIQUIDATION: - has_liquidation_office = True - officeAddresses: list[Address] = office.addresses.all() - assert len(officeAddresses) == 2 - expected_delivery_street = filing['filing']['changeOfLiquidators']['offices']['liquidationRecordsOffice']['deliveryAddress']['streetAddress'] - expected_mailing_street = filing['filing']['changeOfLiquidators']['offices']['liquidationRecordsOffice']['mailingAddress']['streetAddress'] - for address in officeAddresses: - if address.address_type == Address.DELIVERY: - assert address.street == expected_delivery_street - else: - assert address.address_type == Address.MAILING - assert address.street == expected_mailing_street - assert has_liquidation_office + _assert_office_addresses(offices, filing['filing']['changeOfLiquidators']['offices']['liquidationRecordsOffice']) # Test cease liquidator @@ -241,9 +237,7 @@ def test_process_col_filing(app, session, mocker): business: Business = Business.find_by_internal_id(business.id) # assert changes - assert cease_filing.transaction_id - assert cease_filing.business_id == business.id - assert cease_filing.status == Filing.Status.COMPLETED.value + _assert_common_data(business, cease_filing, expected_in_liquidation_date, None, 2024) check_drs_publish(drs_publish_mock, app, business, cease_filing, '') drs_publish_mock.reset_mock() @@ -305,9 +299,7 @@ def test_process_col_filing(app, session, mocker): business: Business = Business.find_by_internal_id(business.id) # assert changes - assert change_address_filing.transaction_id - assert change_address_filing.business_id == business.id - assert change_address_filing.status == Filing.Status.COMPLETED.value + _assert_common_data(business, change_address_filing, expected_in_liquidation_date, None, 2024) check_drs_publish(drs_publish_mock, app, business, change_address_filing, '') drs_publish_mock.reset_mock() @@ -353,9 +345,7 @@ def test_process_col_filing(app, session, mocker): business: Business = Business.find_by_internal_id(business.id) # assert changes - assert change_address_filing.transaction_id - assert change_address_filing.business_id == business.id - assert change_address_filing.status == Filing.Status.COMPLETED.value + _assert_common_data(business, change_address_filing, expected_in_liquidation_date, None, 2024) check_drs_publish(drs_publish_mock, app, business, change_address_filing, '') drs_publish_mock.reset_mock() @@ -398,9 +388,7 @@ def test_process_col_filing(app, session, mocker): business: Business = Business.find_by_internal_id(business.id) # assert changes - assert change_address_filing.transaction_id - assert change_address_filing.business_id == business.id - assert change_address_filing.status == Filing.Status.COMPLETED.value + _assert_common_data(business, change_address_filing, expected_in_liquidation_date, None, 2024) check_drs_publish(drs_publish_mock, app, business, change_address_filing, '') drs_publish_mock.reset_mock() @@ -462,9 +450,7 @@ def test_process_col_filing(app, session, mocker): business: Business = Business.find_by_internal_id(business.id) # assert changes - assert appoint_filing.transaction_id - assert appoint_filing.business_id == business.id - assert appoint_filing.status == Filing.Status.COMPLETED.value + _assert_common_data(business, appoint_filing, expected_in_liquidation_date, None, 2024) check_drs_publish(drs_publish_mock, app, business, appoint_filing, new_document_id) drs_publish_mock.reset_mock() @@ -486,4 +472,93 @@ def test_process_col_filing(app, session, mocker): assert mailing_address.address_type == 'mailing' assert mailing_address.street == new_relationship['mailingAddress']['streetAddress'] - # FUTURE: liquidation report - will be done in #31714 \ No newline at end of file + # liquidation report + expected_last_lr_year = 2024 + + filing['filing']['changeOfLiquidators'] = { + 'type': 'liquidationReport', + } + payment_id = str(random.SystemRandom().getrandbits(0x58)) + effective_date = datetime(2025, 11, 10, 10, 0, 0, tzinfo=timezone.utc) + + filing_rec = create_filing(payment_id, filing, business.id) + filing_rec.effective_date = effective_date + filing_rec.save() + filing_msg = FilingMessage(filing_identifier=filing_rec.id) + + # TEST + process_filing(filing_msg) + + # Get modified data + lr_filing_1: Filing = Filing.find_by_id(filing_rec.id) + business: Business = Business.find_by_internal_id(business.id) + + # assert changes + _assert_common_data(business, lr_filing_1, expected_in_liquidation_date, expected_last_lr_year, 2025) + check_drs_publish(drs_publish_mock, app, business, lr_filing_1, '') + drs_publish_mock.reset_mock() + + # 2nd liquidation report + second_expected_last_lr_year = expected_last_lr_year + 1 + filing['filing']['changeOfLiquidators'] = { + 'type': 'liquidationReport', + } + payment_id = str(random.SystemRandom().getrandbits(0x58)) + effective_date = datetime(2025, 11, 10, 10, 0, 0, tzinfo=timezone.utc) + + filing_rec = create_filing(payment_id, filing, business.id) + filing_rec.effective_date = effective_date + filing_rec.save() + filing_msg = FilingMessage(filing_identifier=filing_rec.id) + + # TEST + process_filing(filing_msg) + + # Get modified data + lr_filing_2: Filing = Filing.find_by_id(filing_rec.id) + business: Business = Business.find_by_internal_id(business.id) + + # assert changes + _assert_common_data(business, lr_filing_2, expected_in_liquidation_date, second_expected_last_lr_year, 2026) + check_drs_publish(drs_publish_mock, app, business, lr_filing_2, '') + drs_publish_mock.reset_mock() + + +def test_process_col_filing_initiated_with_appoint(app, session, mocker): + """Assert that appointLiquidator filings can put the business into liquidation and can include liquidation office.""" + expected_in_liquidation_date = datetime(2023, 10, 10, 10, 0, 0, tzinfo=timezone.utc) + filing, payment_id, identifier = _get_liquidation_filing('appointLiquidator', expected_in_liquidation_date) + drs_publish_mock = mocker.patch('business_filer.services.gcp_queue.publish', return_value=None) + + business = create_business(identifier) + + filing_rec = create_filing(payment_id, filing, business.id) + filing_rec.effective_date = expected_in_liquidation_date + filing_rec.save() + + # setup + filing_msg = FilingMessage(filing_identifier=filing_rec.id) + + # TEST + process_filing(filing_msg) + + # Get modified data + appoint_filing: Filing = Filing.find_by_id(filing_rec.id) + business: Business = Business.find_by_internal_id(business.id) + + # assert changes + _assert_common_data(business, appoint_filing, expected_in_liquidation_date, None, 2024) + check_drs_publish(drs_publish_mock, app, business, appoint_filing, '') + drs_publish_mock.reset_mock() + + party_roles: list[PartyRole] = business.party_roles.all() + assert len(party_roles) == 2 + for role in party_roles: + assert role.appointment_date + assert not role.cessation_date + assert role.role == PartyRole.RoleTypes.LIQUIDATOR.value + + offices: list[Office] = business.offices.all() + # NOTE: will have a registered office too + assert len(offices) == 2 + _assert_office_addresses(offices, filing['filing']['changeOfLiquidators']['offices']['liquidationRecordsOffice']) From e6dadd1b5cc0cf5113cbd5374eb355d944eb0766 Mon Sep 17 00:00:00 2001 From: meawong Date: Mon, 9 Mar 2026 15:16:31 -0700 Subject: [PATCH 13/46] 32594 - Add validation and unit tests (#4143) --- .../filings/validations/restoration.py | 11 ++++ .../filings/validations/test_restoration.py | 66 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/legal-api/src/legal_api/services/filings/validations/restoration.py b/legal-api/src/legal_api/services/filings/validations/restoration.py index 06a19ba760..5bd814ba36 100644 --- a/legal-api/src/legal_api/services/filings/validations/restoration.py +++ b/legal-api/src/legal_api/services/filings/validations/restoration.py @@ -51,6 +51,7 @@ def validate(business: Business, restoration: dict) -> Optional[Error]: "limitedRestoration") if restoration_type in ("limitedRestoration", "limitedRestorationExtension"): msg.extend(validate_expiry_date(business, restoration, restoration_type)) + msg.extend(validate_contact_point(restoration)) elif restoration_type in ("fullRestoration", "limitedRestorationToFull"): msg.extend(validate_relationship(restoration)) @@ -81,6 +82,16 @@ def validate(business: Business, restoration: dict) -> Optional[Error]: return None +def validate_contact_point(filing: dict) -> list: + """Validate that no contact point is provided for limited restoration filings.""" + msg = [] + if filing.get("filing", {}).get("restoration", {}).get("contactPoint", None): + msg.append({"error": "Contact point must not be provided. " + "The corporation will be restored with the contact information it had prior to dissolution.", + "path": "/filing/restoration/contactPoint"}) + return msg + + def validate_expiry_date(business: Business, filing: dict, restoration_type: str) -> list: """Validate expiry date.""" msg = [] diff --git a/legal-api/tests/unit/services/filings/validations/test_restoration.py b/legal-api/tests/unit/services/filings/validations/test_restoration.py index 8df3e4f38d..ffc7e1b1b4 100644 --- a/legal-api/tests/unit/services/filings/validations/test_restoration.py +++ b/legal-api/tests/unit/services/filings/validations/test_restoration.py @@ -95,6 +95,9 @@ def execute_test_restoration_nr(mocker, filing_sub_type, legal_type, nr_number, mock_nr_response = MockResponse(temp_nr_response, HTTPStatus.OK) mocker.patch('legal_api.services.NameXService.query_nr_number', return_value=mock_nr_response) + + if filing['filing']['restoration']['type'] in ('limitedRestoration', 'limitedRestorationExtension'): + del filing['filing']['restoration']['contactPoint'] with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -141,6 +144,10 @@ def test_validate_party(session, test_name, party_role, expected_msg): filing['filing']['restoration']['parties'][0]['roles'][0]['roleType'] = party_role else: filing['filing']['restoration']['parties'] = [] + + if filing['filing']['restoration']['type'] in ('limitedRestoration', 'limitedRestorationExtension'): + del filing['filing']['restoration']['contactPoint'] + err = validate(business, filing) if expected_msg: @@ -177,6 +184,7 @@ def test_validate_relationship(session, test_status, restoration_type, expected_ filing['filing']['restoration']['nameRequest']['legalName'] = legal_name if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): + del filing['filing']['restoration']['contactPoint'] expiry_date = LegislationDatetime.now() + relativedelta(months=1) filing['filing']['restoration']['expiry'] = expiry_date.strftime(date_format) elif test_status == 'SUCCESS' and restoration_type in ('fullRestoration', 'limitedRestorationToFull'): @@ -233,6 +241,9 @@ def test_validate_expiry_date(session, test_name, restoration_type, delta_date, filing['filing']['restoration']['type'] = restoration_type if delta_date: filing['filing']['restoration']['expiry'] = expiry_date.strftime(date_format) + + if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): + del filing['filing']['restoration']['contactPoint'] with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -283,6 +294,9 @@ def test_approval_type(session, test_status, restoration_types, legal_types, app filing['filing']['restoration']['applicationDate'] = '2023-03-30' filing['filing']['restoration']['noticeDate'] = '2023-03-30' + if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): + del filing['filing']['restoration']['contactPoint'] + with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -339,6 +353,9 @@ def test_restoration_court_orders(session, test_status, restoration_types, legal else: del filing['filing']['restoration']['courtOrder'] + if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): + del filing['filing']['restoration']['contactPoint'] + with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -396,6 +413,9 @@ def test_restoration_registrar(session, test_status, restoration_types, legal_ty if notice_date: filing['filing']['restoration']['noticeDate'] = notice_date + if restoration_type in ('limitedRestoration', 'limitedRestorationExtension'): + del filing['filing']['restoration']['contactPoint'] + with patch.object(Filing, 'get_most_recent_filing', return_value=limited_restoration_filing): err = validate(business, filing) @@ -533,3 +553,49 @@ def test_validate_restoration_name_translation(session, test_name, name_translat if err: print(err, err.code, err.msg) assert err is None + +@pytest.mark.parametrize( + 'test_status, restoration_type, has_contact_point, expected_code, expected_msg', + [ + ('SUCCESS', 'limitedRestoration', False, None, None), + ('SUCCESS', 'limitedRestorationExtension', False, None, None), + ('FAIL', 'limitedRestoration', True, HTTPStatus.BAD_REQUEST, + 'Contact point must not be provided. ' + 'The corporation will be restored with the contact information it had prior to dissolution.'), + ('FAIL', 'limitedRestorationExtension', True, HTTPStatus.BAD_REQUEST, + 'Contact point must not be provided. ' + 'The corporation will be restored with the contact information it had prior to dissolution.'), + ] +) +def test_validate_contact_point(session, test_status, restoration_type, has_contact_point, expected_code, expected_msg): + """Assert that contact point is not allowed for limited restoration filings.""" + business = Business(identifier='BC1234567', legal_type='BC', restoration_expiry_date=LegislationDatetime.now()) + limited_restoration_filing = None + if restoration_type == 'limitedRestorationExtension': + limited_restoration_filing = factory_limited_restoration_filing() + + filing = copy.deepcopy(FILING_HEADER) + filing['filing']['restoration'] = copy.deepcopy(RESTORATION) + filing['filing']['header']['name'] = 'restoration' + filing['filing']['restoration']['type'] = restoration_type + + expiry_date = LegislationDatetime.now() + relativedelta(months=1) + filing['filing']['restoration']['expiry'] = expiry_date.strftime(date_format) + + if restoration_type == 'limitedRestorationExtension': + del filing['filing']['restoration']['nameRequest'] + else: + filing['filing']['restoration']['nameRequest']['legalName'] = legal_name + + if not has_contact_point: + del filing['filing']['restoration']['contactPoint'] + + with patch.object(Filing, 'get_most_recent_filing', + return_value=limited_restoration_filing): + err = validate(business, filing) + + if expected_code: + assert expected_code == err.code + assert expected_msg == err.msg[0]['error'] + else: + assert not err From 5c537c4c4d7743378dbc3b75b9a3077492bc1a98 Mon Sep 17 00:00:00 2001 From: Argus Chiu Date: Tue, 10 Mar 2026 10:12:34 -0700 Subject: [PATCH 14/46] 32736 Update /search/affiliation_mappings endpoint to support migrated businesses (#4140) * 32736 Update /search/affiliation_mappings endpoint to support migrated businesses * updated business identifer logic * updated * updated query filters for colin firms * updated test --------- Co-authored-by: Rajandeep Co-authored-by: Rajandeep Kaur <144159721+Rajandeep98@users.noreply.github.com> --- legal-api/src/legal_api/core/filing.py | 1 + .../src/legal_api/services/search_service.py | 49 +++-- .../tests/unit/resources/v2/test_business.py | 169 +++++++++++++++++- 3 files changed, 206 insertions(+), 13 deletions(-) diff --git a/legal-api/src/legal_api/core/filing.py b/legal-api/src/legal_api/core/filing.py index 082c553363..3c450e43cc 100644 --- a/legal-api/src/legal_api/core/filing.py +++ b/legal-api/src/legal_api/core/filing.py @@ -102,6 +102,7 @@ class FilingTypes(str, Enum): SPECIALRESOLUTION = "specialResolution" TRANSITION = "transition" TRANSPARENCY_REGISTER = "transparencyRegister" + TOMBSTONE = "lear_tombstone" class FilingTypesCompact(str, Enum): """Render enum for filing types with sub-types.""" diff --git a/legal-api/src/legal_api/services/search_service.py b/legal-api/src/legal_api/services/search_service.py index 93d4e649fd..1af3c4c529 100644 --- a/legal-api/src/legal_api/services/search_service.py +++ b/legal-api/src/legal_api/services/search_service.py @@ -16,13 +16,14 @@ # pylint: disable=singleton-comparison ; pylint does not recognize sqlalchemy == from dataclasses import dataclass from datetime import datetime, timezone -from operator import and_ +from operator import and_, or_ from typing import Final, Optional from flask import current_app from requests import Request from sqlalchemy import func +from legal_api.core.filing import Filing as CoreFiling from legal_api.models import Business, Filing, RegistrationBootstrap, db @@ -86,7 +87,6 @@ class BusinessSearchService: # pylint: disable=too-many-public-methods EXCLUDED_FILINGS_STATUS: Final = [ Filing.Status.WITHDRAWN.value ] - @staticmethod def check_and_get_respective_values(codes): """Check if codes belong to BUSINESS_TEMP_FILINGS_CORP_CODES and return the matching ones.""" @@ -316,19 +316,44 @@ def get_affiliation_mapping_results(identifiers): nr_identifiers.append(identifier) else: business_identifiers.append(identifier) - conditions = [] + draft_conditions = [] if business_identifiers: - conditions.append(Business._identifier.in_(business_identifiers)) # pylint: disable=protected-access + draft_conditions.append(Business._identifier.in_(business_identifiers)) # pylint: disable=protected-access if nr_identifiers: - conditions.append(Filing + draft_conditions.append(Filing .filing_json["filing"][Filing._filing_type] # pylint: disable=protected-access ["nameRequest"]["nrNumber"] .astext.in_(nr_identifiers)) if temp_identifiers: - conditions.append(RegistrationBootstrap._identifier.in_(identifiers)) # pylint: disable=protected-access - query = query.filter(db.or_(*conditions)) - - rows = query.all() - result_list = [dict(row) for row in rows] - - return result_list + draft_conditions.append(RegistrationBootstrap._identifier.in_(identifiers)) # pylint: disable=protected-access + + if not draft_conditions: + return [] + + draft_query = query.filter(db.or_(*draft_conditions)) + + draft_rows = draft_query.all() + result_list = [] + for row in draft_rows: + result_list.append({ + "identifier": row.identifier, + "nrNumber": row.nrNumber, + "bootstrapIdentifier": row.bootstrapIdentifier + }) + migrated_identifiers = db.session.query( Business._identifier.label("identifier"), # pylint: disable=protected-access + Filing + ._filing_type # pylint: disable=protected-access + .label("filing_type"), + ).select_from(Filing) \ + .join(Business, Filing.business_id == Business.id).filter(Business._identifier.in_(business_identifiers)) + migrated_rows = migrated_identifiers.filter( + or_(Filing._filing_type == CoreFiling.FilingTypes.TOMBSTONE.value, + and_(Business.legal_type.in_(["SP", "GP"]),Business.identifier.like("FM0%"))) + ).all() + for row in migrated_rows: + result_list.append({ + "identifier": row.identifier, + "nrNumber": None, + "bootstrapIdentifier": None + }) + return result_list \ No newline at end of file diff --git a/legal-api/tests/unit/resources/v2/test_business.py b/legal-api/tests/unit/resources/v2/test_business.py index cc67e6c207..ee601b5ecd 100644 --- a/legal-api/tests/unit/resources/v2/test_business.py +++ b/legal-api/tests/unit/resources/v2/test_business.py @@ -36,7 +36,7 @@ from legal_api.services import flags from legal_api.utils.datetime import datetime from tests import integration_affiliation -from tests.unit.models import factory_batch, factory_batch_processing, factory_business, factory_pending_filing +from tests.unit.models import factory_batch, factory_batch_processing, factory_business, factory_filing, factory_pending_filing from tests.unit.services.warnings import create_business from tests.unit.services.utils import create_header from tests.unit.models import factory_completed_filing @@ -732,6 +732,173 @@ def test_post_affiliated_businesses_invalid(session, client, jwt): assert rv.status_code == HTTPStatus.BAD_REQUEST +def _create_affiliation_mapping_draft(identifier, + filing_name='registration', + legal_type=Business.LegalTypes.SOLE_PROP.value, + nr_number=None, + legal_name='Test NR Name'): + """Create a draft filing with a temp registration for affiliation mapping tests.""" + temp_reg = RegistrationBootstrap() + temp_reg._identifier = identifier + temp_reg.save() + + json_data = copy.deepcopy(FILING_HEADER) + json_data['filing']['header']['name'] = filing_name + json_data['filing']['header']['identifier'] = identifier + del json_data['filing']['business'] + json_data['filing'][filing_name] = { + 'nameRequest': { + 'legalType': legal_type + } + } + + if nr_number: + json_data['filing'][filing_name]['nameRequest'] = { + **json_data['filing'][filing_name]['nameRequest'], + 'nrNumber': nr_number, + 'legalName': legal_name + } + + filing = factory_pending_filing(None, json_data) + filing.temp_reg = identifier + filing.save() + + +def test_post_affiliation_mappings_migrated_business_without_bootstrap(session, client, jwt): + """Assert that direct business identifiers return a mapping even without bootstrap-linked filings.""" + identifier = 'BC1328381' + business = factory_business_model(legal_name=identifier + 'name', + identifier=identifier, + founding_date=datetime.utcfromtimestamp(0), + last_ledger_timestamp=datetime.utcfromtimestamp(0), + last_modified=datetime.utcfromtimestamp(0), + fiscal_year_end_date=None, + tax_id=None, + dissolution_date=None, + legal_type=Business.LegalTypes.BCOMP.value) + factory_filing(business,{'filing': { + 'header': { + 'name': 'lear_tombstone' + } + }}, filing_type='lear_tombstone') + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [identifier]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 1 + assert rv.json['entityDetails'] == [{ + 'identifier': identifier, + 'nrNumber': None, + 'bootstrapIdentifier': None + }] + + +def test_post_affiliation_mappings_temp_identifier(session, client, jwt): + """Assert that temp identifiers still resolve through the filing/bootstrap path.""" + identifier = 'Tb31yQIuC1' + _create_affiliation_mapping_draft(identifier=identifier, + filing_name='incorporationApplication', + legal_type=Business.LegalTypes.BCOMP.value) + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [identifier]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 1 + detail = rv.json['entityDetails'][0] + assert detail['bootstrapIdentifier'] == identifier + assert detail['nrNumber'] is None + + +def test_post_affiliation_mappings_nr_identifier(session, client, jwt): + """Assert that NR identifiers still resolve through the filing/bootstrap path.""" + bootstrap_identifier = 'Tb31yQIuC2' + nr_number = 'NR 1234567' + _create_affiliation_mapping_draft(identifier=bootstrap_identifier, + filing_name='registration', + legal_type=Business.LegalTypes.SOLE_PROP.value, + nr_number=nr_number, + legal_name='Test NR Name') + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [nr_number]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 1 + detail = rv.json['entityDetails'][0] + assert detail['nrNumber'] == nr_number + assert detail['bootstrapIdentifier'] == bootstrap_identifier + + +def test_post_affiliation_mappings_mixed_direct_and_nr_identifiers(session, client, jwt): + """Assert that direct business and draft-backed NR lookups coexist in a single request.""" + business_identifier = 'BC7654321' + nr_number = 'NR 1234568' + bootstrap_identifier = 'Tb31yQIuC3' + + business = factory_business_model(legal_name=business_identifier + 'name', + identifier=business_identifier, + founding_date=datetime.utcfromtimestamp(0), + last_ledger_timestamp=datetime.utcfromtimestamp(0), + last_modified=datetime.utcfromtimestamp(0), + fiscal_year_end_date=None, + tax_id=None, + dissolution_date=None, + legal_type=Business.LegalTypes.BCOMP.value) + + factory_filing(business,{'filing': { + 'header': { + 'name': 'lear_tombstone' + } + }}, filing_type='lear_tombstone') + + _create_affiliation_mapping_draft(identifier=bootstrap_identifier, + filing_name='registration', + legal_type=Business.LegalTypes.SOLE_PROP.value, + nr_number=nr_number, + legal_name='Mixed NR Name') + + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': [business_identifier, nr_number]}, + headers=create_header(jwt, [SYSTEM_ROLE])) + + assert rv.status_code == HTTPStatus.OK + assert rv.json['count'] == 2 + + business_detail = next( + detail for detail in rv.json['entityDetails'] + if detail['identifier'] == business_identifier + ) + nr_detail = next( + detail for detail in rv.json['entityDetails'] + if detail['nrNumber'] == nr_number + ) + + assert business_detail['bootstrapIdentifier'] is None + assert business_detail['nrNumber'] is None + assert nr_detail['bootstrapIdentifier'] == bootstrap_identifier + + +def test_post_affiliation_mappings_unauthorized(session, client, jwt): + """Assert that the affiliation mappings endpoint is unauthorized for non-system tokens.""" + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={'identifiers': ['BC1234567']}, + headers=create_header(jwt, [STAFF_ROLE])) + assert rv.status_code == HTTPStatus.UNAUTHORIZED + + +def test_post_affiliation_mappings_invalid(session, client, jwt): + """Assert that the affiliation mappings endpoint is bad request when identifiers are not given.""" + rv = client.post('/api/v2/businesses/search/affiliation_mappings', + json={}, + headers=create_header(jwt, [SYSTEM_ROLE])) + assert rv.status_code == HTTPStatus.BAD_REQUEST + + def test_get_could_file(session, client, jwt, monkeypatch): """Assert that the cold file is returned.""" monkeypatch.setattr( From b745e248a60f21f23171b495cb5e1098ccf08079 Mon Sep 17 00:00:00 2001 From: Kial Date: Wed, 11 Mar 2026 12:46:08 -0400 Subject: [PATCH 15/46] Business API - fix for liquidation businesses (#4144) Signed-off-by: Kial Jinnah --- legal-api/src/legal_api/models/business.py | 1 + legal-api/src/legal_api/services/authz.py | 2 +- .../business_checks/test_in_liquidation.py | 14 +++++++++++++- .../common/business-registry-model/pyproject.toml | 2 +- .../src/business_model/models/business.py | 1 + 5 files changed, 17 insertions(+), 3 deletions(-) diff --git a/legal-api/src/legal_api/models/business.py b/legal-api/src/legal_api/models/business.py index edfb1dc341..9f3470b42a 100644 --- a/legal-api/src/legal_api/models/business.py +++ b/legal-api/src/legal_api/models/business.py @@ -198,6 +198,7 @@ class AssociationTypes(Enum): "founding_date", "identifier", "in_liquidation", + "in_liquidation_date", "jurisdiction", "last_agm_date", "last_ar_date", diff --git a/legal-api/src/legal_api/services/authz.py b/legal-api/src/legal_api/services/authz.py index 0351f68f14..3675782d99 100644 --- a/legal-api/src/legal_api/services/authz.py +++ b/legal-api/src/legal_api/services/authz.py @@ -976,7 +976,7 @@ def business_blocker_check(business: Business, is_ignore_draft_blockers: bool = if business.in_liquidation: business_blocker_checks[BusinessBlocker.IN_LIQUIDATION] = True - if LegislationDatetime.datenow() >= business.next_lr_min_date: + if business.next_lr_min_date and LegislationDatetime.datenow() >= business.next_lr_min_date: business_blocker_checks[BusinessBlocker.MIN_LR_DATE_REACHED] = True if has_notice_of_withdrawal_filing_blocker(business, is_ignore_draft_blockers): diff --git a/legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py b/legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py index 1c2738162b..91a3e915c1 100644 --- a/legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py +++ b/legal-api/tests/unit/services/warnings/business/business_checks/test_in_liquidation.py @@ -15,6 +15,7 @@ import copy import pytest +from psycopg2.tz import FixedOffsetTimezone from datedelta import datedelta from legal_api.models import Business @@ -24,7 +25,7 @@ from tests.unit.models import factory_business FOUNDING_DATE = datetime(2023, 3, 3) -IN_LIQUIDATION_DATE = datetime(2024, 6, 10) +IN_LIQUIDATION_DATE = datetime(2024, 6, 10, 0, 0, tzinfo=FixedOffsetTimezone(0)) @pytest.mark.parametrize('test_name, founding_date, in_liquidation_date, last_lr_year, expected_meta_data', [ ('NOT_IN_LIQUIDATION', datetime.now(), None, None, {}), @@ -39,6 +40,13 @@ 'inLiquidationDate': IN_LIQUIDATION_DATE, 'lastLiquidationReportYear': 2025, 'nextLiquidationReportMinDate': date(2026, 3, 2)} + ), + # Should not happen, but need to make sure it still returns for this case + ('IN_LIQUIDATION_no_in_liquidation_date', FOUNDING_DATE, None, None, + { + 'inLiquidationDate': None, + 'lastLiquidationReportYear': None, + 'nextLiquidationReportMinDate': None} ) ]) def test_check_business(session, test_name, founding_date, in_liquidation_date, last_lr_year, expected_meta_data): @@ -50,6 +58,10 @@ def test_check_business(session, test_name, founding_date, in_liquidation_date, in_liquidation_date=in_liquidation_date, last_lr_year=last_lr_year) + if test_name != 'NOT_IN_LIQUIDATION': + business.in_liquidation = True + business.save() + result = check_business(business) if test_name == 'NOT_IN_LIQUIDATION': diff --git a/python/common/business-registry-model/pyproject.toml b/python/common/business-registry-model/pyproject.toml index cfb6a4a05d..498ee8f35b 100644 --- a/python/common/business-registry-model/pyproject.toml +++ b/python/common/business-registry-model/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "business-model" -version = "3.3.22" +version = "3.3.23" description = "" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/python/common/business-registry-model/src/business_model/models/business.py b/python/common/business-registry-model/src/business_model/models/business.py index 95dc3b478c..dff2af68fe 100644 --- a/python/common/business-registry-model/src/business_model/models/business.py +++ b/python/common/business-registry-model/src/business_model/models/business.py @@ -222,6 +222,7 @@ class AssociationTypes(Enum): 'founding_date', 'identifier', 'in_liquidation', + 'in_liquidation_date', 'jurisdiction', 'last_agm_date', 'last_ar_date', From 9f961fb7646faab1ef1a5e5ed249fc8cf1fd515d Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Thu, 12 Mar 2026 12:49:28 -0700 Subject: [PATCH 16/46] 32773-32774 trim whitespace in bn template (#4146) --- queue_services/business-bn/poetry.lock | 2431 +++++++++-------- queue_services/business-bn/pyproject.toml | 2 +- .../src/business_bn/bn_processors/__init__.py | 7 +- 3 files changed, 1302 insertions(+), 1138 deletions(-) diff --git a/queue_services/business-bn/poetry.lock b/queue_services/business-bn/poetry.lock index becb38ac86..761745a5b8 100644 --- a/queue_services/business-bn/poetry.lock +++ b/queue_services/business-bn/poetry.lock @@ -1,20 +1,20 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "alembic" -version = "1.15.2" +version = "1.18.4" description = "A database migration tool for SQLAlchemy." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53"}, - {file = "alembic-1.15.2.tar.gz", hash = "sha256:1c72391bbdeffccfe317eefba686cb9a3c078005478885413b95c3b26c57a8a7"}, + {file = "alembic-1.18.4-py3-none-any.whl", hash = "sha256:a5ed4adcf6d8a4cb575f3d759f071b03cd6e5c7618eb796cb52497be25bfe19a"}, + {file = "alembic-1.18.4.tar.gz", hash = "sha256:cb6e1fd84b6174ab8dbb2329f86d631ba9559dd78df550b57804d607672cedbc"}, ] [package.dependencies] Mako = "*" -SQLAlchemy = ">=1.4.0" +SQLAlchemy = ">=1.4.23" typing-extensions = ">=4.12" [package.extras] @@ -34,38 +34,38 @@ files = [ [[package]] name = "arrow" -version = "1.3.0" +version = "1.4.0" description = "Better dates & times for Python" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80"}, - {file = "arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85"}, + {file = "arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205"}, + {file = "arrow-1.4.0.tar.gz", hash = "sha256:ed0cc050e98001b8779e84d461b0098c4ac597e88704a655582b21d116e526d7"}, ] [package.dependencies] python-dateutil = ">=2.7.0" -types-python-dateutil = ">=2.8.10" +tzdata = {version = "*", markers = "python_version >= \"3.9\""} [package.extras] doc = ["doc8", "sphinx (>=7.0.0)", "sphinx-autobuild", "sphinx-autodoc-typehints", "sphinx_rtd_theme (>=1.3.0)"] -test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2021.1)", "simplejson (==3.*)"] +test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock", "pytz (==2025.2)", "simplejson (==3.*)"] [[package]] name = "asgiref" -version = "3.8.1" +version = "3.11.1" description = "ASGI specs, helper code, and adapters" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"}, - {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"}, + {file = "asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133"}, + {file = "asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce"}, ] [package.extras] -tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] [[package]] name = "asn1crypto" @@ -81,34 +81,26 @@ files = [ [[package]] name = "attrs" -version = "25.3.0" +version = "25.4.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, + {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, + {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, ] -[package.extras] -benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] - [[package]] name = "babel" -version = "2.17.0" +version = "2.18.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, + {file = "babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35"}, + {file = "babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d"}, ] [package.extras] @@ -128,10 +120,10 @@ files = [ [[package]] name = "business-model" -version = "3.0.0" +version = "3.3.23" description = "" optional = false -python-versions = ">=3.9,<4" +python-versions = ">=3.13,<3.14" groups = ["main"] files = [] develop = false @@ -148,14 +140,14 @@ pg8000 = ">=1.31.2,<2.0.0" pycountry = ">=24.6.1,<25.0.0" pydantic = ">=2.10.6,<3.0.0" pytz = ">=2025.1,<2026.0" -registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.39"} +registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.62"} sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdirectory = "python/common/sql-versioning-alt"} [package.source] type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "83d2e5880b212566b488a17de44d7aff29967a89" +resolved_reference = "b745e248a60f21f23171b495cb5e1098ccf08079" subdirectory = "python/common/business-registry-model" [[package]] @@ -180,7 +172,7 @@ requests = "^2.32.3" type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "83d2e5880b212566b488a17de44d7aff29967a89" +resolved_reference = "b745e248a60f21f23171b495cb5e1098ccf08079" subdirectory = "python/common/business-registry-account" [[package]] @@ -188,7 +180,7 @@ name = "business-registry-common" version = "0.1.0" description = "" optional = false -python-versions = "^3.13" +python-versions = ">=3.9,<4.0" groups = ["main"] files = [] develop = false @@ -201,7 +193,7 @@ pytz = "^2025.1" type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "83d2e5880b212566b488a17de44d7aff29967a89" +resolved_reference = "b745e248a60f21f23171b495cb5e1098ccf08079" subdirectory = "python/common/business-registry-common" [[package]] @@ -216,223 +208,249 @@ files = [ {file = "cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48"}, ] -[[package]] -name = "cachetools" -version = "5.5.2" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -groups = ["main"] -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 = "2025.1.31" +version = "2025.11.12" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, - {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, + {file = "certifi-2025.11.12-py3-none-any.whl", hash = "sha256:97de8790030bbd5c2d96b7ec782fc2f7820ef8dba6db909ccf95449f2d062d4b"}, + {file = "certifi-2025.11.12.tar.gz", hash = "sha256:d8ab5478f2ecd78af242878415affce761ca6bc54a22a27e026d7c25357c3316"}, ] [[package]] name = "cffi" -version = "1.17.1" +version = "2.0.0" description = "Foreign Function Interface for Python calling C code." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] markers = "platform_python_implementation != \"PyPy\"" files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, + {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 = "*" +pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.5" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 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.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05"}, + {file = "charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a"}, + {file = "charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636"}, + {file = "charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7"}, + {file = "charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa"}, + {file = "charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win32.whl", hash = "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c"}, + {file = "charset_normalizer-3.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win32.whl", hash = "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc"}, + {file = "charset_normalizer-3.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4"}, + {file = "charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0"}, + {file = "charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644"}, ] [[package]] name = "click" -version = "8.1.8" +version = "8.3.1" description = "Composable command line interface toolkit" 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] @@ -453,75 +471,118 @@ markers = {main = "platform_system == \"Windows\"", test = "sys_platform == \"wi [[package]] name = "coverage" -version = "7.8.0" +version = "7.13.4" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"}, - {file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"}, - {file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"}, - {file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"}, - {file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"}, - {file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"}, - {file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"}, - {file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"}, - {file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"}, - {file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"}, - {file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"}, - {file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"}, - {file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"}, - {file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"}, - {file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"}, - {file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"}, - {file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"}, - {file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"}, - {file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"}, - {file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"}, - {file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"}, - {file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"}, - {file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"}, - {file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"}, - {file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"}, - {file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"}, - {file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"}, - {file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"}, - {file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"}, - {file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"}, - {file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"}, - {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"}, - {file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"}, - {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"}, - {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"}, - {file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"}, - {file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"}, - {file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"}, - {file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"}, - {file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"}, - {file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, + {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, + {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, + {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, + {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, + {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, + {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, + {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, + {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, + {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, + {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, + {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, + {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, + {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, + {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, + {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, + {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, + {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, + {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, + {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, + {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, + {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, + {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, ] [package.extras] @@ -545,60 +606,74 @@ pytz = ">2021.1" [[package]] name = "cryptography" -version = "44.0.2" +version = "46.0.5" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = "!=3.9.0,!=3.9.1,>=3.7" -groups = ["main"] -files = [ - {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a"}, - {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308"}, - {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688"}, - {file = "cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7"}, - {file = "cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79"}, - {file = "cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa"}, - {file = "cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9"}, - {file = "cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23"}, - {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922"}, - {file = "cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4"}, - {file = "cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5"}, - {file = "cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:af4ff3e388f2fa7bff9f7f2b31b87d5651c45731d3e8cfa0944be43dff5cfbdb"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0529b1d5a0105dd3731fa65680b45ce49da4d8115ea76e9da77a875396727b41"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:7ca25849404be2f8e4b3c59483d9d3c51298a22c1c61a0e84415104dacaf5562"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:268e4e9b177c76d569e8a145a6939eca9a5fec658c932348598818acf31ae9a5"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:9eb9d22b0a5d8fd9925a7764a054dca914000607dff201a24c791ff5c799e1fa"}, - {file = "cryptography-44.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2bf7bf75f7df9715f810d1b038870309342bff3069c5bd8c6b96128cb158668d"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:909c97ab43a9c0c0b0ada7a1281430e4e5ec0458e6d9244c0e821bbf152f061d"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:96e7a5e9d6e71f9f4fca8eebfd603f8e86c5225bb18eb621b2c1e50b290a9471"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d1b3031093a366ac767b3feb8bcddb596671b3aaff82d4050f984da0c248b615"}, - {file = "cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390"}, - {file = "cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.8" +groups = ["main"] +files = [ + {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] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} +cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] +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 (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] -pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +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 (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] +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]] @@ -656,14 +731,14 @@ tests = ["coverage", "coveralls", "dill", "mock", "nose"] [[package]] name = "flake8-import-order" -version = "0.18.2" +version = "0.19.2" description = "Flake8 and pylama plugin that checks the ordering of import statements." optional = false python-versions = "*" groups = ["main"] files = [ - {file = "flake8-import-order-0.18.2.tar.gz", hash = "sha256:e23941f892da3e0c09d711babbb0c73bc735242e9b216b726616758a920d900e"}, - {file = "flake8_import_order-0.18.2-py2.py3-none-any.whl", hash = "sha256:82ed59f1083b629b030ee9d3928d9e06b6213eb196fe745b3a7d4af2168130df"}, + {file = "flake8_import_order-0.19.2-py3-none-any.whl", hash = "sha256:2dfe60175e7195cf36d4c573861fd2e3258cd6650cbd7616da3c6b8193b29b7c"}, + {file = "flake8_import_order-0.19.2.tar.gz", hash = "sha256:133b3c55497631e4235074fc98a95078bba817832379f22a31f0ad2455bcb0b2"}, ] [package.dependencies] @@ -672,23 +747,24 @@ setuptools = "*" [[package]] name = "flask" -version = "3.1.0" +version = "3.1.3" description = "A simple framework for building complex web applications." 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.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"}, + {file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"}, ] [package.dependencies] asgiref = {version = ">=3.2", optional = true, markers = "extra == \"async\""} -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)"] @@ -801,7 +877,7 @@ simple-cloudevent = {git = "https://github.com/daxiom/simple-cloudevent.py.git"} type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "7f1cc0ea4a374310ac558ff435fa6b7ea7bb2f8b" +resolved_reference = "2a3d5a2d5b5ff3c905626f3193db5ed0af6d4952" subdirectory = "python/gcp-queue" [[package]] @@ -831,29 +907,30 @@ grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0dev)"] [[package]] name = "google-auth" -version = "2.39.0" +version = "2.49.0" description = "Google Authentication Library" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" groups = ["main"] files = [ - {file = "google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2"}, - {file = "google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7"}, + {file = "google_auth-2.49.0-py3-none-any.whl", hash = "sha256:f893ef7307f19cf53700b7e2f61b5a6affe3aa0edf9943b13788920ab92d8d87"}, + {file = "google_auth-2.49.0.tar.gz", hash = "sha256:9cc2d9259d3700d7a257681f81052db6737495a1a46b610597f4b8bafe5286ae"}, ] [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)", "requests (>=2.20.0,<3.0.0)"] -enterprise-cert = ["cryptography", "pyopenssl"] -pyjwt = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyjwt (>=2.0)"] -pyopenssl = ["cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "pyopenssl (>=20.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)"] -testing = ["aiohttp (<3.10.0)", "aiohttp (>=3.6.2,<4.0.0)", "aioresponses", "cryptography (<39.0.0) ; python_version < \"3.8\"", "cryptography (>=38.0.3)", "flask", "freezegun", "grpcio", "mock", "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"] +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]] @@ -901,73 +978,71 @@ grpc = ["grpcio (>=1.44.0,<2.0.0.dev0)"] [[package]] name = "greenlet" -version = "3.2.0" +version = "3.3.2" description = "Lightweight in-process concurrent programming" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\"" files = [ - {file = "greenlet-3.2.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:b7a7b7f2bad3ca72eb2fa14643f1c4ca11d115614047299d89bc24a3b11ddd09"}, - {file = "greenlet-3.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60e77242e38e99ecaede853755bbd8165e0b20a2f1f3abcaa6f0dceb826a7411"}, - {file = "greenlet-3.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d3f32d7c70b1c26844fd0e4e56a1da852b493e4e1c30df7b07274a1e5a9b599e"}, - {file = "greenlet-3.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97bc1be4bad83b70d8b8627ada6724091af41139616696e59b7088f358583b9"}, - {file = "greenlet-3.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23f56a0103deb5570c8d6a0bb4ddf8a7a28931973ad7ed7a883460a67e599b32"}, - {file = "greenlet-3.2.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2919b126eeb63ca5fa971501cd20cd6cdb5522369a8e39548bbc73a3e10b8b41"}, - {file = "greenlet-3.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:844acfd479ee380f3810415e682c9ee941725fb90b45e139bb7fd6f85c6c9a30"}, - {file = "greenlet-3.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2b986f1a6467710e7ffeeeac1777da0318c95bbfcc467acbd0bd35abc775f558"}, - {file = "greenlet-3.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:29449a2b82ed7ce11f8668c31ef20d31e9d88cd8329eb933098fab5a8608a93a"}, - {file = "greenlet-3.2.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:b99de16560097b9984409ded0032f101f9555e1ab029440fc6a8b5e76dbba7ac"}, - {file = "greenlet-3.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0bc5776ac2831c022e029839bf1b9d3052332dcf5f431bb88c8503e27398e31"}, - {file = "greenlet-3.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dcb1108449b55ff6bc0edac9616468f71db261a4571f27c47ccf3530a7f8b97"}, - {file = "greenlet-3.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a68a25a08f51fc8b66b113d1d9863ee123cdb0e8f1439aed9fc795cd6f85cf"}, - {file = "greenlet-3.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fee6f518868e8206c617f4084a83ad4d7a3750b541bf04e692dfa02e52e805d"}, - {file = "greenlet-3.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6fad8a9ca98b37951a053d7d2d2553569b151cd8c4ede744806b94d50d7f8f73"}, - {file = "greenlet-3.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e14541f9024a280adb9645143d6a0a51fda6f7c5695fd96cb4d542bb563442f"}, - {file = "greenlet-3.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7f163d04f777e7bd229a50b937ecc1ae2a5b25296e6001445e5433e4f51f5191"}, - {file = "greenlet-3.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:39801e633a978c3f829f21022501e7b0c3872683d7495c1850558d1a6fb95ed0"}, - {file = "greenlet-3.2.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7d08b88ee8d506ca1f5b2a58744e934d33c6a1686dd83b81e7999dfc704a912f"}, - {file = "greenlet-3.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58ef3d637c54e2f079064ca936556c4af3989144e4154d80cfd4e2a59fc3769c"}, - {file = "greenlet-3.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:33ea7e7269d6f7275ce31f593d6dcfedd97539c01f63fbdc8d84e493e20b1b2c"}, - {file = "greenlet-3.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e61d426969b68b2170a9f853cc36d5318030494576e9ec0bfe2dc2e2afa15a68"}, - {file = "greenlet-3.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04e781447a4722e30b4861af728cb878d73a3df79509dc19ea498090cea5d204"}, - {file = "greenlet-3.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b2392cc41eeed4055978c6b52549ccd9effd263bb780ffd639c0e1e7e2055ab0"}, - {file = "greenlet-3.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:430cba962c85e339767235a93450a6aaffed6f9c567e73874ea2075f5aae51e1"}, - {file = "greenlet-3.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5e57ff52315bfc0c5493917f328b8ba3ae0c0515d94524453c4d24e7638cbb53"}, - {file = "greenlet-3.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:211a9721f540e454a02e62db7956263e9a28a6cf776d4b9a7213844e36426333"}, - {file = "greenlet-3.2.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b86a3ccc865ae601f446af042707b749eebc297928ea7bd0c5f60c56525850be"}, - {file = "greenlet-3.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144283ad88ed77f3ebd74710dd419b55dd15d18704b0ae05935766a93f5671c5"}, - {file = "greenlet-3.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5be69cd50994b8465c3ad1467f9e63001f76e53a89440ad4440d1b6d52591280"}, - {file = "greenlet-3.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47aeadd1e8fbdef8fdceb8fb4edc0cbb398a57568d56fd68f2bc00d0d809e6b6"}, - {file = "greenlet-3.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18adc14ab154ca6e53eecc9dc50ff17aeb7ba70b7e14779b26e16d71efa90038"}, - {file = "greenlet-3.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8622b33d8694ec373ad55050c3d4e49818132b44852158442e1931bb02af336"}, - {file = "greenlet-3.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e8ac9a2c20fbff3d0b853e9ef705cdedb70d9276af977d1ec1cde86a87a4c821"}, - {file = "greenlet-3.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:cd37273dc7ca1d5da149b58c8b3ce0711181672ba1b09969663905a765affe21"}, - {file = "greenlet-3.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8a8940a8d301828acd8b9f3f85db23069a692ff2933358861b19936e29946b95"}, - {file = "greenlet-3.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee59db626760f1ca8da697a086454210d36a19f7abecc9922a2374c04b47735b"}, - {file = "greenlet-3.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7154b13ef87a8b62fc05419f12d75532d7783586ad016c57b5de8a1c6feeb517"}, - {file = "greenlet-3.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:199453d64b02d0c9d139e36d29681efd0e407ed8e2c0bf89d88878d6a787c28f"}, - {file = "greenlet-3.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0010e928e1901d36625f21d008618273f9dda26b516dbdecf873937d39c9dff0"}, - {file = "greenlet-3.2.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6005f7a86de836a1dc4b8d824a2339cdd5a1ca7cb1af55ea92575401f9952f4c"}, - {file = "greenlet-3.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:17fd241c0d50bacb7ce8ff77a30f94a2d0ca69434ba2e0187cf95a5414aeb7e1"}, - {file = "greenlet-3.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:7b17a26abc6a1890bf77d5d6b71c0999705386b00060d15c10b8182679ff2790"}, - {file = "greenlet-3.2.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:397b6bbda06f8fe895893d96218cd6f6d855a6701dc45012ebe12262423cec8b"}, - {file = "greenlet-3.2.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:4174fa6fa214e8924cedf332b6f2395ba2b9879f250dacd3c361b2fca86f58af"}, - {file = "greenlet-3.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6017a4d430fad5229e397ad464db504ae70cb7b903757c4688cee6c25d6ce8d8"}, - {file = "greenlet-3.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78b721dfadc60e3639141c0e1f19d23953c5b4b98bfcaf04ce40f79e4f01751c"}, - {file = "greenlet-3.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fd2583024ff6cd5d4f842d446d001de4c4fe1264fdb5f28ddea28f6488866df"}, - {file = "greenlet-3.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598da3bd464c2cc411b723e3d4afc27b13c219ac077ba897bac88443ae45f5ec"}, - {file = "greenlet-3.2.0-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2688b3bd3198cc4bad7a79648a95fee088c24a0f6abd05d3639e6c3040ded015"}, - {file = "greenlet-3.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1cf89e2d92bae0d7e2d6093ce0bed26feeaf59a5d588e3984e35fcd46fc41090"}, - {file = "greenlet-3.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8b3538711e7c0efd5f7a8fc1096c4db9598d6ed99dc87286b31e4ce9f8a8da67"}, - {file = "greenlet-3.2.0-cp39-cp39-win32.whl", hash = "sha256:ce531d7c424ef327a391de7a9777a6c93a38e1f89e18efa903a1c4ba11f85905"}, - {file = "greenlet-3.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:7b162de2fb61b4c7f4b5d749408bf3280cae65db9b5a6aaf7f922ac829faa67c"}, - {file = "greenlet-3.2.0.tar.gz", hash = "sha256:1d2d43bd711a43db8d9b9187500e6432ddb4fafe112d082ffabca8660a9e01a7"}, + {file = "greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7"}, + {file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef"}, + {file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca"}, + {file = "greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f"}, + {file = "greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2"}, + {file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99"}, + {file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be"}, + {file = "greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5"}, + {file = "greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd"}, + {file = "greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb"}, + {file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79"}, + {file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395"}, + {file = "greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f"}, + {file = "greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643"}, + {file = "greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd"}, + {file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a"}, + {file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b"}, + {file = "greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124"}, + {file = "greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327"}, + {file = "greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9"}, + {file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce"}, + {file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5"}, + {file = "greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492"}, + {file = "greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71"}, + {file = "greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf"}, + {file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727"}, + {file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e"}, + {file = "greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a"}, + {file = "greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2"}, ] [package.extras] docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil"] +test = ["objgraph", "psutil", "setuptools"] [[package]] name = "grpc-google-iam-v1" @@ -988,67 +1063,80 @@ protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.1 || >4.21.1,<4 [[package]] name = "grpcio" -version = "1.71.0" +version = "1.78.0" description = "HTTP/2-based RPC framework" 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" @@ -1091,14 +1179,14 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "idna" -version = "3.10" +version = "3.11" description = "Internationalized Domain Names in Applications (IDNA)" 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] @@ -1106,14 +1194,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "iniconfig" -version = "2.1.0" +version = "2.3.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["test"] files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] [[package]] @@ -1175,14 +1263,14 @@ files = [ [[package]] name = "jsonschema" -version = "4.23.0" +version = "4.26.0" description = "An implementation of JSON Schema validation for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"}, - {file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"}, + {file = "jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce"}, + {file = "jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326"}, ] [package.dependencies] @@ -1195,24 +1283,24 @@ jsonschema-specifications = ">=2023.03.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} -rpds-py = ">=0.7.1" +rpds-py = ">=0.25.0" uri-template = {version = "*", optional = true, markers = "extra == \"format\""} webcolors = {version = ">=1.11", optional = true, markers = "extra == \"format\""} [package.extras] format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"] +format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] [[package]] name = "jsonschema-specifications" -version = "2024.10.1" +version = "2025.9.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"}, - {file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"}, + {file = "jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe"}, + {file = "jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d"}, ] [package.dependencies] @@ -1220,14 +1308,14 @@ referencing = ">=0.31.0" [[package]] name = "launchdarkly-eventsource" -version = "1.2.2" +version = "1.5.1" description = "LaunchDarkly SSE Client" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "launchdarkly_eventsource-1.2.2-py3-none-any.whl", hash = "sha256:3c47c1ce77418d978ca4753bc3ff61eaf5c4191088376c0584c70d1571eb2aaf"}, - {file = "launchdarkly_eventsource-1.2.2.tar.gz", hash = "sha256:6f7cc74c1dc01ac3fe1cec09d71a7f46c2da92ab3051c0d3406d90311a64d963"}, + {file = "launchdarkly_eventsource-1.5.1-py3-none-any.whl", hash = "sha256:43dfbc14a3962c9bce252320d690cdcbfda0ca00501226d517ae77e60f570d44"}, + {file = "launchdarkly_eventsource-1.5.1.tar.gz", hash = "sha256:f122f80b36db6ea1ab20af62c82b8b2668682259b415053c94400dd6c07922a7"}, ] [package.dependencies] @@ -1235,20 +1323,20 @@ urllib3 = ">=1.26.0,<3" [[package]] name = "launchdarkly-server-sdk" -version = "9.11.0" +version = "9.15.0" description = "LaunchDarkly SDK for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "launchdarkly_server_sdk-9.11.0-py3-none-any.whl", hash = "sha256:2409d298b3614165d755a325d102f8bf5948da5876268eb161443864b18a6ae5"}, - {file = "launchdarkly_server_sdk-9.11.0.tar.gz", hash = "sha256:5106f0c574c529108217fe00b862329c381f5f415dde0ef62a7106f42ec64e22"}, + {file = "launchdarkly_server_sdk-9.15.0-py3-none-any.whl", hash = "sha256:c267e29bfa3fb5e2a06a208448ada6ed5557a2924979b8d79c970b45d227c668"}, + {file = "launchdarkly_server_sdk-9.15.0.tar.gz", hash = "sha256:f31441b74bc1a69c381db57c33116509e407a2612628ad6dff0a7dbb39d5020b"}, ] [package.dependencies] certifi = ">=2018.4.16" expiringdict = ">=1.1.4" -launchdarkly-eventsource = ">=1.1.0,<2.0.0" +launchdarkly-eventsource = ">=1.5.0,<2.0.0" pyRFC3339 = ">=1.0" semver = ">=2.10.2" urllib3 = ">=1.26.0,<3" @@ -1281,97 +1369,125 @@ testing = ["pytest"] [[package]] name = "markupsafe" -version = "3.0.2" +version = "3.0.3" description = "Safely add untrusted strings to HTML/XML markup." 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 = "packaging" -version = "24.2" +version = "26.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" groups = ["main", "test"] 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 = "pg8000" -version = "1.31.2" +version = "1.31.5" description = "PostgreSQL interface library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pg8000-1.31.2-py3-none-any.whl", hash = "sha256:436c771ede71af4d4c22ba867a30add0bc5c942d7ab27fadbb6934a487ecc8f6"}, - {file = "pg8000-1.31.2.tar.gz", hash = "sha256:1ea46cf09d8eca07fe7eaadefd7951e37bee7fabe675df164f1a572ffb300876"}, + {file = "pg8000-1.31.5-py3-none-any.whl", hash = "sha256:0af2c1926b153307639868d2ee5cef6cd3a7d07448e12736989b10e1d491e201"}, + {file = "pg8000-1.31.5.tar.gz", hash = "sha256:46ebb03be52b7a77c03c725c79da2ca281d6e8f59577ca66b17c9009618cae78"}, ] [package.dependencies] @@ -1380,30 +1496,30 @@ scramp = ">=1.4.5" [[package]] name = "pluggy" -version = "1.5.0" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["test"] files = [ - {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, - {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "proto-plus" -version = "1.26.1" +version = "1.27.1" description = "Beautiful, Pythonic protocol buffers" 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] @@ -1527,14 +1643,14 @@ files = [ [[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)" 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]] @@ -1554,14 +1670,14 @@ pyasn1 = ">=0.6.1,<0.7.0" [[package]] name = "pycodestyle" -version = "2.13.0" +version = "2.14.0" description = "Python style guide checker" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pycodestyle-2.13.0-py2.py3-none-any.whl", hash = "sha256:35863c5974a271c7a726ed228a14a4f6daf49df369d8c50cd9a6f58a5e143ba9"}, - {file = "pycodestyle-2.13.0.tar.gz", hash = "sha256:c8415bf09abe81d9c7f872502a6eee881fbe85d8763dd5b9924bb0a01d67efae"}, + {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, + {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, ] [[package]] @@ -1578,34 +1694,34 @@ files = [ [[package]] name = "pycparser" -version = "2.22" +version = "3.0" description = "C parser in Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" +markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\"" files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, + {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, + {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, ] [[package]] name = "pydantic" -version = "2.11.3" +version = "2.12.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"}, - {file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"}, + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.33.1" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" [package.extras] email = ["email-validator (>=2.0.0)"] @@ -1613,178 +1729,216 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.33.1" +version = "2.41.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"}, - {file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5183e4f6a2d468787243ebcd70cf4098c247e60d73fb7d68d5bc1e1beaa0c4db"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:398a38d323f37714023be1e0285765f0a27243a8b1506b7b7de87b647b517e48"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87d3776f0001b43acebfa86f8c64019c043b55cc5a6a2e313d728b5c95b46969"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c566dd9c5f63d22226409553531f89de0cac55397f2ab8d97d6f06cfce6d947e"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0d5f3acc81452c56895e90643a625302bd6be351e7010664151cc55b7b97f89"}, - {file = "pydantic_core-2.33.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3a07fadec2a13274a8d861d3d37c61e97a816beae717efccaa4b36dfcaadcde"}, - {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f99aeda58dce827f76963ee87a0ebe75e648c72ff9ba1174a253f6744f518f65"}, - {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:902dbc832141aa0ec374f4310f1e4e7febeebc3256f00dc359a9ac3f264a45dc"}, - {file = "pydantic_core-2.33.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe44d56aa0b00d66640aa84a3cbe80b7a3ccdc6f0b1ca71090696a6d4777c091"}, - {file = "pydantic_core-2.33.1-cp310-cp310-win32.whl", hash = "sha256:ed3eb16d51257c763539bde21e011092f127a2202692afaeaccb50db55a31383"}, - {file = "pydantic_core-2.33.1-cp310-cp310-win_amd64.whl", hash = "sha256:694ad99a7f6718c1a498dc170ca430687a39894a60327f548e02a9c7ee4b6504"}, - {file = "pydantic_core-2.33.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e966fc3caaf9f1d96b349b0341c70c8d6573bf1bac7261f7b0ba88f96c56c24"}, - {file = "pydantic_core-2.33.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bfd0adeee563d59c598ceabddf2c92eec77abcb3f4a391b19aa7366170bd9e30"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91815221101ad3c6b507804178a7bb5cb7b2ead9ecd600041669c8d805ebd595"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fea9c1869bb4742d174a57b4700c6dadea951df8b06de40c2fedb4f02931c2e"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d20eb4861329bb2484c021b9d9a977566ab16d84000a57e28061151c62b349a"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb935c5591573ae3201640579f30128ccc10739b45663f93c06796854405505"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c964fd24e6166420d18fb53996d8c9fd6eac9bf5ae3ec3d03015be4414ce497f"}, - {file = "pydantic_core-2.33.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:681d65e9011f7392db5aa002b7423cc442d6a673c635668c227c6c8d0e5a4f77"}, - {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e100c52f7355a48413e2999bfb4e139d2977a904495441b374f3d4fb4a170961"}, - {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:048831bd363490be79acdd3232f74a0e9951b11b2b4cc058aeb72b22fdc3abe1"}, - {file = "pydantic_core-2.33.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bdc84017d28459c00db6f918a7272a5190bec3090058334e43a76afb279eac7c"}, - {file = "pydantic_core-2.33.1-cp311-cp311-win32.whl", hash = "sha256:32cd11c5914d1179df70406427097c7dcde19fddf1418c787540f4b730289896"}, - {file = "pydantic_core-2.33.1-cp311-cp311-win_amd64.whl", hash = "sha256:2ea62419ba8c397e7da28a9170a16219d310d2cf4970dbc65c32faf20d828c83"}, - {file = "pydantic_core-2.33.1-cp311-cp311-win_arm64.whl", hash = "sha256:fc903512177361e868bc1f5b80ac8c8a6e05fcdd574a5fb5ffeac5a9982b9e89"}, - {file = "pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8"}, - {file = "pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d"}, - {file = "pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b"}, - {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39"}, - {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a"}, - {file = "pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db"}, - {file = "pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda"}, - {file = "pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4"}, - {file = "pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea"}, - {file = "pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a"}, - {file = "pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d"}, - {file = "pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e"}, - {file = "pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40"}, - {file = "pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c"}, - {file = "pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18"}, - {file = "pydantic_core-2.33.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:5ab77f45d33d264de66e1884fca158bc920cb5e27fd0764a72f72f5756ae8bdb"}, - {file = "pydantic_core-2.33.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7aaba1b4b03aaea7bb59e1b5856d734be011d3e6d98f5bcaa98cb30f375f2ad"}, - {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fb66263e9ba8fea2aa85e1e5578980d127fb37d7f2e292773e7bc3a38fb0c7b"}, - {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f2648b9262607a7fb41d782cc263b48032ff7a03a835581abbf7a3bec62bcf5"}, - {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723c5630c4259400818b4ad096735a829074601805d07f8cafc366d95786d331"}, - {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d100e3ae783d2167782391e0c1c7a20a31f55f8015f3293647544df3f9c67824"}, - {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177d50460bc976a0369920b6c744d927b0ecb8606fb56858ff542560251b19e5"}, - {file = "pydantic_core-2.33.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a3edde68d1a1f9af1273b2fe798997b33f90308fb6d44d8550c89fc6a3647cf6"}, - {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a62c3c3ef6a7e2c45f7853b10b5bc4ddefd6ee3cd31024754a1a5842da7d598d"}, - {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:c91dbb0ab683fa0cd64a6e81907c8ff41d6497c346890e26b23de7ee55353f96"}, - {file = "pydantic_core-2.33.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9f466e8bf0a62dc43e068c12166281c2eca72121dd2adc1040f3aa1e21ef8599"}, - {file = "pydantic_core-2.33.1-cp39-cp39-win32.whl", hash = "sha256:ab0277cedb698749caada82e5d099dc9fed3f906a30d4c382d1a21725777a1e5"}, - {file = "pydantic_core-2.33.1-cp39-cp39-win_amd64.whl", hash = "sha256:5773da0ee2d17136b1f1c6fbde543398d452a6ad2a7b54ea1033e2daa739b8d2"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c834f54f8f4640fd7e4b193f80eb25a0602bba9e19b3cd2fc7ffe8199f5ae02"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:049e0de24cf23766f12cc5cc71d8abc07d4a9deb9061b334b62093dedc7cb068"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a28239037b3d6f16916a4c831a5a0eadf856bdd6d2e92c10a0da3a59eadcf3e"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d3da303ab5f378a268fa7d45f37d7d85c3ec19769f28d2cc0c61826a8de21fe"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:25626fb37b3c543818c14821afe0fd3830bc327a43953bc88db924b68c5723f1"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3ab2d36e20fbfcce8f02d73c33a8a7362980cff717926bbae030b93ae46b56c7"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2f9284e11c751b003fd4215ad92d325d92c9cb19ee6729ebd87e3250072cdcde"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:048c01eee07d37cbd066fc512b9d8b5ea88ceeb4e629ab94b3e56965ad655add"}, - {file = "pydantic_core-2.33.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5ccd429694cf26af7997595d627dd2637e7932214486f55b8a357edaac9dae8c"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3a371dc00282c4b84246509a5ddc808e61b9864aa1eae9ecc92bb1268b82db4a"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f59295ecc75a1788af8ba92f2e8c6eeaa5a94c22fc4d151e8d9638814f85c8fc"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08530b8ac922003033f399128505f513e30ca770527cc8bbacf75a84fcc2c74b"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae370459da6a5466978c0eacf90690cb57ec9d533f8e63e564ef3822bfa04fe"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e3de2777e3b9f4d603112f78006f4ae0acb936e95f06da6cb1a45fbad6bdb4b5"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3a64e81e8cba118e108d7126362ea30e021291b7805d47e4896e52c791be2761"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:52928d8c1b6bda03cc6d811e8923dffc87a2d3c8b3bfd2ce16471c7147a24850"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1b30d92c9412beb5ac6b10a3eb7ef92ccb14e3f2a8d7732e2d739f58b3aa7544"}, - {file = "pydantic_core-2.33.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f995719707e0e29f0f41a8aa3bcea6e761a36c9136104d3189eafb83f5cec5e5"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7edbc454a29fc6aeae1e1eecba4f07b63b8d76e76a748532233c4c167b4cb9ea"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ad05b683963f69a1d5d2c2bdab1274a31221ca737dbbceaa32bcb67359453cdd"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df6a94bf9452c6da9b5d76ed229a5683d0306ccb91cca8e1eea883189780d568"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7965c13b3967909a09ecc91f21d09cfc4576bf78140b988904e94f130f188396"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3f1fdb790440a34f6ecf7679e1863b825cb5ffde858a9197f851168ed08371e5"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:5277aec8d879f8d05168fdd17ae811dd313b8ff894aeeaf7cd34ad28b4d77e33"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8ab581d3530611897d863d1a649fb0644b860286b4718db919bfd51ece41f10b"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0483847fa9ad5e3412265c1bd72aad35235512d9ce9d27d81a56d935ef489672"}, - {file = "pydantic_core-2.33.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:de9e06abe3cc5ec6a2d5f75bc99b0bdca4f5c719a5b34026f8c57efbdecd2ee3"}, - {file = "pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, ] [package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +typing-extensions = ">=4.14.1" [[package]] name = "pyflakes" -version = "3.3.2" +version = "3.4.0" description = "passive checker of Python programs" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyflakes-3.3.2-py2.py3-none-any.whl", hash = "sha256:5039c8339cbb1944045f4ee5466908906180f13cc99cc9949348d10f82a5c32a"}, - {file = "pyflakes-3.3.2.tar.gz", hash = "sha256:6dfd61d87b97fba5dcfaaf781171ac16be16453be6d816147989e7f6e6a9576b"}, + {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, + {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, ] +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["test"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pyjwt" -version = "2.10.1" +version = "2.11.0" description = "JSON Web Token implementation in Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, + {file = "pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469"}, + {file = "pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623"}, ] [package.extras] crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] +tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] [[package]] name = "pyrfc3339" -version = "2.0.1" +version = "2.1.0" description = "Generate and parse RFC 3339 timestamps" optional = false -python-versions = "*" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyRFC3339-2.0.1-py3-none-any.whl", hash = "sha256:30b70a366acac3df7386b558c21af871522560ed7f3f73cf344b8c2cbb8b0c9d"}, - {file = "pyrfc3339-2.0.1.tar.gz", hash = "sha256:e47843379ea35c1296c3b6c67a948a1a490ae0584edfcbdea0eaffb5dd29960b"}, + {file = "pyrfc3339-2.1.0-py3-none-any.whl", hash = "sha256:560f3f972e339f579513fe1396974352fd575ef27caff160a38b312252fcddf3"}, + {file = "pyrfc3339-2.1.0.tar.gz", hash = "sha256:c569a9714faf115cdb20b51e830e798c1f4de8dabb07f6ff25d221b5d09d8d7f"}, ] [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.2" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["test"] files = [ - {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, - {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.2" [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" @@ -1807,14 +1961,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-mock" -version = "3.14.0" +version = "3.15.1" description = "Thin-wrapper around the mock package for easier use with pytest" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["test"] files = [ - {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, - {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, + {file = "pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d"}, + {file = "pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f"}, ] [package.dependencies] @@ -1840,14 +1994,14 @@ six = ">=1.5" [[package]] name = "python-dotenv" -version = "1.1.0" +version = "1.2.2" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"}, - {file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"}, + {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, + {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, ] [package.extras] @@ -1867,14 +2021,14 @@ files = [ [[package]] name = "referencing" -version = "0.36.2" +version = "0.37.0" description = "JSON Referencing + Python" optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, + {file = "referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231"}, + {file = "referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8"}, ] [package.dependencies] @@ -1883,148 +2037,131 @@ rpds-py = ">=0.7.0" [[package]] name = "regex" -version = "2026.1.15" +version = "2026.2.28" description = "Alternative regular expression module, to replace re." optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f"}, - {file = "regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618"}, - {file = "regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13"}, - {file = "regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3"}, - {file = "regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218"}, - {file = "regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a"}, - {file = "regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f"}, - {file = "regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026"}, - {file = "regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2"}, - {file = "regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1"}, - {file = "regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569"}, - {file = "regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7"}, - {file = "regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681"}, - {file = "regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5"}, - {file = "regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d"}, - {file = "regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22"}, - {file = "regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913"}, - {file = "regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a"}, - {file = "regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10"}, - {file = "regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6"}, - {file = "regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31"}, - {file = "regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3"}, - {file = "regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f"}, - {file = "regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e"}, - {file = "regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8"}, - {file = "regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09"}, - {file = "regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2"}, - {file = "regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60"}, - {file = "regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952"}, - {file = "regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10"}, - {file = "regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6"}, - {file = "regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde"}, - {file = "regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160"}, - {file = "regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1"}, - {file = "regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1"}, - {file = "regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903"}, - {file = "regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf"}, - {file = "regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a"}, - {file = "regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521"}, - {file = "regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db"}, - {file = "regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e"}, - {file = "regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf"}, - {file = "regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:55b4ea996a8e4458dd7b584a2f89863b1655dd3d17b88b46cbb9becc495a0ec5"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e1e28be779884189cdd57735e997f282b64fd7ccf6e2eef3e16e57d7a34a815"}, - {file = "regex-2026.1.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0057de9eaef45783ff69fa94ae9f0fd906d629d0bd4c3217048f46d1daa32e9b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc7cd0b2be0f0269283a45c0d8b2c35e149d1319dcb4a43c9c3689fa935c1ee6"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8db052bbd981e1666f09e957f3790ed74080c2229007c1dd67afdbf0b469c48b"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:343db82cb3712c31ddf720f097ef17c11dab2f67f7a3e7be976c4f82eba4e6df"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55e9d0118d97794367309635df398bdfd7c33b93e2fdfa0b239661cd74b4c14e"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:008b185f235acd1e53787333e5690082e4f156c44c87d894f880056089e9bc7c"}, - {file = "regex-2026.1.15-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fd65af65e2aaf9474e468f9e571bd7b189e1df3a61caa59dcbabd0000e4ea839"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f42e68301ff4afee63e365a5fc302b81bb8ba31af625a671d7acb19d10168a8c"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:f7792f27d3ee6e0244ea4697d92b825f9a329ab5230a78c1a68bd274e64b5077"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:dbaf3c3c37ef190439981648ccbf0c02ed99ae066087dd117fcb616d80b010a4"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:adc97a9077c2696501443d8ad3fa1b4fc6d131fc8fd7dfefd1a723f89071cf0a"}, - {file = "regex-2026.1.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:069f56a7bf71d286a6ff932a9e6fb878f151c998ebb2519a9f6d1cee4bffdba3"}, - {file = "regex-2026.1.15-cp39-cp39-win32.whl", hash = "sha256:ea4e6b3566127fda5e007e90a8fd5a4169f0cf0619506ed426db647f19c8454a"}, - {file = "regex-2026.1.15-cp39-cp39-win_amd64.whl", hash = "sha256:cda1ed70d2b264952e88adaa52eea653a33a1b98ac907ae2f86508eb44f65cdc"}, - {file = "regex-2026.1.15-cp39-cp39-win_arm64.whl", hash = "sha256:b325d4714c3c48277bfea1accd94e193ad6ed42b4bad79ad64f3b8f8a31260a5"}, - {file = "regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5"}, +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "regex-2026.2.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fc48c500838be6882b32748f60a15229d2dea96e59ef341eaa96ec83538f498d"}, + {file = "regex-2026.2.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2afa673660928d0b63d84353c6c08a8a476ddfc4a47e11742949d182e6863ce8"}, + {file = "regex-2026.2.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ab218076eb0944549e7fe74cf0e2b83a82edb27e81cc87411f76240865e04d5"}, + {file = "regex-2026.2.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d63db12e45a9b9f064bfe4800cefefc7e5f182052e4c1b774d46a40ab1d9bb"}, + {file = "regex-2026.2.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195237dc327858a7721bf8b0bbbef797554bc13563c3591e91cd0767bacbe359"}, + {file = "regex-2026.2.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b387a0d092dac157fb026d737dde35ff3e49ef27f285343e7c6401851239df27"}, + {file = "regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3935174fa4d9f70525a4367aaff3cb8bc0548129d114260c29d9dfa4a5b41692"}, + {file = "regex-2026.2.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b2b23587b26496ff5fd40df4278becdf386813ec00dc3533fa43a4cf0e2ad3c"}, + {file = "regex-2026.2.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3b24bd7e9d85dc7c6a8bd2aa14ecd234274a0248335a02adeb25448aecdd420d"}, + {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bd477d5f79920338107f04aa645f094032d9e3030cc55be581df3d1ef61aa318"}, + {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b49eb78048c6354f49e91e4b77da21257fecb92256b6d599ae44403cab30b05b"}, + {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a25c7701e4f7a70021db9aaf4a4a0a67033c6318752146e03d1b94d32006217e"}, + {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9dd450db6458387167e033cfa80887a34c99c81d26da1bf8b0b41bf8c9cac88e"}, + {file = "regex-2026.2.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2954379dd20752e82d22accf3ff465311cbb2bac6c1f92c4afd400e1757f7451"}, + {file = "regex-2026.2.28-cp310-cp310-win32.whl", hash = "sha256:1f8b17be5c27a684ea6759983c13506bd77bfc7c0347dff41b18ce5ddd2ee09a"}, + {file = "regex-2026.2.28-cp310-cp310-win_amd64.whl", hash = "sha256:dd8847c4978bc3c7e6c826fb745f5570e518b8459ac2892151ce6627c7bc00d5"}, + {file = "regex-2026.2.28-cp310-cp310-win_arm64.whl", hash = "sha256:73cdcdbba8028167ea81490c7f45280113e41db2c7afb65a276f4711fa3bcbff"}, + {file = "regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9"}, + {file = "regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97"}, + {file = "regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703"}, + {file = "regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098"}, + {file = "regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2"}, + {file = "regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64"}, + {file = "regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022"}, + {file = "regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1"}, + {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a"}, + {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27"}, + {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae"}, + {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea"}, + {file = "regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b"}, + {file = "regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15"}, + {file = "regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61"}, + {file = "regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a"}, + {file = "regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7"}, + {file = "regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d"}, + {file = "regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d"}, + {file = "regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc"}, + {file = "regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8"}, + {file = "regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d"}, + {file = "regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4"}, + {file = "regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05"}, + {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5"}, + {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59"}, + {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf"}, + {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae"}, + {file = "regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b"}, + {file = "regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c"}, + {file = "regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4"}, + {file = "regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952"}, + {file = "regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784"}, + {file = "regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a"}, + {file = "regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d"}, + {file = "regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95"}, + {file = "regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472"}, + {file = "regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96"}, + {file = "regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92"}, + {file = "regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11"}, + {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881"}, + {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3"}, + {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215"}, + {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944"}, + {file = "regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768"}, + {file = "regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081"}, + {file = "regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff"}, + {file = "regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e"}, + {file = "regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f"}, + {file = "regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b"}, + {file = "regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8"}, + {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb"}, + {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1"}, + {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2"}, + {file = "regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a"}, + {file = "regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341"}, + {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25"}, + {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c"}, + {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b"}, + {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f"}, + {file = "regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550"}, + {file = "regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc"}, + {file = "regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8"}, + {file = "regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b"}, + {file = "regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc"}, + {file = "regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd"}, + {file = "regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff"}, + {file = "regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911"}, + {file = "regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33"}, + {file = "regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117"}, + {file = "regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d"}, + {file = "regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a"}, + {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf"}, + {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952"}, + {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8"}, + {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07"}, + {file = "regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6"}, + {file = "regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6"}, + {file = "regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7"}, + {file = "regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d"}, + {file = "regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e"}, + {file = "regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c"}, + {file = "regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7"}, + {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e"}, + {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc"}, + {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8"}, + {file = "regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0"}, + {file = "regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b"}, + {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b"}, + {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033"}, + {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43"}, + {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18"}, + {file = "regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a"}, + {file = "regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e"}, + {file = "regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9"}, + {file = "regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec"}, + {file = "regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2"}, ] [[package]] name = "registry_schemas" -version = "2.18.39" +version = "2.18.62" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -2042,24 +2179,24 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.39" -resolved_reference = "c0aeb44d47f8cd126c85b6ca9a8a747f33266db1" +reference = "2.18.62" +resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" [[package]] name = "requests" -version = "2.32.3" +version = "2.32.5" description = "Python HTTP for Humans." 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" @@ -2096,126 +2233,127 @@ files = [ [[package]] name = "rpds-py" -version = "0.24.0" +version = "0.30.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"}, - {file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8acd55bd5b071156bae57b555f5d33697998752673b9de554dd82f5b5352727"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7e80d375134ddb04231a53800503752093dbb65dad8dabacce2c84cccc78e964"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60748789e028d2a46fc1c70750454f83c6bdd0d05db50f5ae83e2db500b34da5"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e1daf5bf6c2be39654beae83ee6b9a12347cb5aced9a29eecf12a2d25fff664"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b221c2457d92a1fb3c97bee9095c874144d196f47c038462ae6e4a14436f7bc"}, - {file = "rpds_py-0.24.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:66420986c9afff67ef0c5d1e4cdc2d0e5262f53ad11e4f90e5e22448df485bf0"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:43dba99f00f1d37b2a0265a259592d05fcc8e7c19d140fe51c6e6f16faabeb1f"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a88c0d17d039333a41d9bf4616bd062f0bd7aa0edeb6cafe00a2fc2a804e944f"}, - {file = "rpds_py-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc31e13ce212e14a539d430428cd365e74f8b2d534f8bc22dd4c9c55b277b875"}, - {file = "rpds_py-0.24.0-cp310-cp310-win32.whl", hash = "sha256:fc2c1e1b00f88317d9de6b2c2b39b012ebbfe35fe5e7bef980fd2a91f6100a07"}, - {file = "rpds_py-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0145295ca415668420ad142ee42189f78d27af806fcf1f32a18e51d47dd2052"}, - {file = "rpds_py-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:2d3ee4615df36ab8eb16c2507b11e764dcc11fd350bbf4da16d09cda11fcedef"}, - {file = "rpds_py-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e13ae74a8a3a0c2f22f450f773e35f893484fcfacb00bb4344a7e0f4f48e1f97"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf86f72d705fc2ef776bb7dd9e5fbba79d7e1f3e258bf9377f8204ad0fc1c51e"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c43583ea8517ed2e780a345dd9960896afc1327e8cf3ac8239c167530397440d"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4cd031e63bc5f05bdcda120646a0d32f6d729486d0067f09d79c8db5368f4586"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34d90ad8c045df9a4259c47d2e16a3f21fdb396665c94520dbfe8766e62187a4"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e838bf2bb0b91ee67bf2b889a1a841e5ecac06dd7a2b1ef4e6151e2ce155c7ae"}, - {file = "rpds_py-0.24.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04ecf5c1ff4d589987b4d9882872f80ba13da7d42427234fce8f22efb43133bc"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:630d3d8ea77eabd6cbcd2ea712e1c5cecb5b558d39547ac988351195db433f6c"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ebcb786b9ff30b994d5969213a8430cbb984cdd7ea9fd6df06663194bd3c450c"}, - {file = "rpds_py-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:174e46569968ddbbeb8a806d9922f17cd2b524aa753b468f35b97ff9c19cb718"}, - {file = "rpds_py-0.24.0-cp311-cp311-win32.whl", hash = "sha256:5ef877fa3bbfb40b388a5ae1cb00636a624690dcb9a29a65267054c9ea86d88a"}, - {file = "rpds_py-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:e274f62cbd274359eff63e5c7e7274c913e8e09620f6a57aae66744b3df046d6"}, - {file = "rpds_py-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d8551e733626afec514b5d15befabea0dd70a343a9f23322860c4f16a9430205"}, - {file = "rpds_py-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e374c0ce0ca82e5b67cd61fb964077d40ec177dd2c4eda67dba130de09085c7"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d69d003296df4840bd445a5d15fa5b6ff6ac40496f956a221c4d1f6f7b4bc4d9"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8212ff58ac6dfde49946bea57474a386cca3f7706fc72c25b772b9ca4af6b79e"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:528927e63a70b4d5f3f5ccc1fa988a35456eb5d15f804d276709c33fc2f19bda"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a824d2c7a703ba6daaca848f9c3d5cb93af0505be505de70e7e66829affd676e"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44d51febb7a114293ffd56c6cf4736cb31cd68c0fddd6aa303ed09ea5a48e029"}, - {file = "rpds_py-0.24.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3fab5f4a2c64a8fb64fc13b3d139848817a64d467dd6ed60dcdd6b479e7febc9"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9be4f99bee42ac107870c61dfdb294d912bf81c3c6d45538aad7aecab468b6b7"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:564c96b6076a98215af52f55efa90d8419cc2ef45d99e314fddefe816bc24f91"}, - {file = "rpds_py-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:75a810b7664c17f24bf2ffd7f92416c00ec84b49bb68e6a0d93e542406336b56"}, - {file = "rpds_py-0.24.0-cp312-cp312-win32.whl", hash = "sha256:f6016bd950be4dcd047b7475fdf55fb1e1f59fc7403f387be0e8123e4a576d30"}, - {file = "rpds_py-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:998c01b8e71cf051c28f5d6f1187abbdf5cf45fc0efce5da6c06447cba997034"}, - {file = "rpds_py-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2d8e4508e15fc05b31285c4b00ddf2e0eb94259c2dc896771966a163122a0c"}, - {file = "rpds_py-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f00c16e089282ad68a3820fd0c831c35d3194b7cdc31d6e469511d9bffc535c"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951cc481c0c395c4a08639a469d53b7d4afa252529a085418b82a6b43c45c240"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c9ca89938dff18828a328af41ffdf3902405a19f4131c88e22e776a8e228c5a8"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0ef550042a8dbcd657dfb284a8ee00f0ba269d3f2286b0493b15a5694f9fe8"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b2356688e5d958c4d5cb964af865bea84db29971d3e563fb78e46e20fe1848b"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78884d155fd15d9f64f5d6124b486f3d3f7fd7cd71a78e9670a0f6f6ca06fb2d"}, - {file = "rpds_py-0.24.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6a4a535013aeeef13c5532f802708cecae8d66c282babb5cd916379b72110cf7"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:84e0566f15cf4d769dade9b366b7b87c959be472c92dffb70462dd0844d7cbad"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:823e74ab6fbaa028ec89615ff6acb409e90ff45580c45920d4dfdddb069f2120"}, - {file = "rpds_py-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c61a2cb0085c8783906b2f8b1f16a7e65777823c7f4d0a6aaffe26dc0d358dd9"}, - {file = "rpds_py-0.24.0-cp313-cp313-win32.whl", hash = "sha256:60d9b630c8025b9458a9d114e3af579a2c54bd32df601c4581bd054e85258143"}, - {file = "rpds_py-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:6eea559077d29486c68218178ea946263b87f1c41ae7f996b1f30a983c476a5a"}, - {file = "rpds_py-0.24.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:d09dc82af2d3c17e7dd17120b202a79b578d79f2b5424bda209d9966efeed114"}, - {file = "rpds_py-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5fc13b44de6419d1e7a7e592a4885b323fbc2f46e1f22151e3a8ed3b8b920405"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c347a20d79cedc0a7bd51c4d4b7dbc613ca4e65a756b5c3e57ec84bd43505b47"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20f2712bd1cc26a3cc16c5a1bfee9ed1abc33d4cdf1aabd297fe0eb724df4272"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aad911555286884be1e427ef0dc0ba3929e6821cbeca2194b13dc415a462c7fd"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0aeb3329c1721c43c58cae274d7d2ca85c1690d89485d9c63a006cb79a85771a"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a0f156e9509cee987283abd2296ec816225145a13ed0391df8f71bf1d789e2d"}, - {file = "rpds_py-0.24.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa6800adc8204ce898c8a424303969b7aa6a5e4ad2789c13f8648739830323b7"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a18fc371e900a21d7392517c6f60fe859e802547309e94313cd8181ad9db004d"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9168764133fd919f8dcca2ead66de0105f4ef5659cbb4fa044f7014bed9a1797"}, - {file = "rpds_py-0.24.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f6e3cec44ba05ee5cbdebe92d052f69b63ae792e7d05f1020ac5e964394080c"}, - {file = "rpds_py-0.24.0-cp313-cp313t-win32.whl", hash = "sha256:8ebc7e65ca4b111d928b669713865f021b7773350eeac4a31d3e70144297baba"}, - {file = "rpds_py-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:675269d407a257b8c00a6b58205b72eec8231656506c56fd429d924ca00bb350"}, - {file = "rpds_py-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a36b452abbf29f68527cf52e181fced56685731c86b52e852053e38d8b60bc8d"}, - {file = "rpds_py-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b3b397eefecec8e8e39fa65c630ef70a24b09141a6f9fc17b3c3a50bed6b50e"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdabcd3beb2a6dca7027007473d8ef1c3b053347c76f685f5f060a00327b8b65"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5db385bacd0c43f24be92b60c857cf760b7f10d8234f4bd4be67b5b20a7c0b6b"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8097b3422d020ff1c44effc40ae58e67d93e60d540a65649d2cdaf9466030791"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:493fe54318bed7d124ce272fc36adbf59d46729659b2c792e87c3b95649cdee9"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8aa362811ccdc1f8dadcc916c6d47e554169ab79559319ae9fae7d7752d0d60c"}, - {file = "rpds_py-0.24.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d8f9a6e7fd5434817526815f09ea27f2746c4a51ee11bb3439065f5fc754db58"}, - {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8205ee14463248d3349131bb8099efe15cd3ce83b8ef3ace63c7e976998e7124"}, - {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:921ae54f9ecba3b6325df425cf72c074cd469dea843fb5743a26ca7fb2ccb149"}, - {file = "rpds_py-0.24.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:32bab0a56eac685828e00cc2f5d1200c548f8bc11f2e44abf311d6b548ce2e45"}, - {file = "rpds_py-0.24.0-cp39-cp39-win32.whl", hash = "sha256:f5c0ed12926dec1dfe7d645333ea59cf93f4d07750986a586f511c0bc61fe103"}, - {file = "rpds_py-0.24.0-cp39-cp39-win_amd64.whl", hash = "sha256:afc6e35f344490faa8276b5f2f7cbf71f88bc2cda4328e00553bd451728c571f"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:619ca56a5468f933d940e1bf431c6f4e13bef8e688698b067ae68eb4f9b30e3a"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b28e5122829181de1898c2c97f81c0b3246d49f585f22743a1246420bb8d399"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e5ab32cf9eb3647450bc74eb201b27c185d3857276162c101c0f8c6374e098"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:208b3a70a98cf3710e97cabdc308a51cd4f28aa6e7bb11de3d56cd8b74bab98d"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbc4362e06f950c62cad3d4abf1191021b2ffaf0b31ac230fbf0526453eee75e"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ebea2821cdb5f9fef44933617be76185b80150632736f3d76e54829ab4a3b4d1"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4df06c35465ef4d81799999bba810c68d29972bf1c31db61bfdb81dd9d5bb"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d3aa13bdf38630da298f2e0d77aca967b200b8cc1473ea05248f6c5e9c9bdb44"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:041f00419e1da7a03c46042453598479f45be3d787eb837af382bfc169c0db33"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:d8754d872a5dfc3c5bf9c0e059e8107451364a30d9fd50f1f1a85c4fb9481164"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:896c41007931217a343eff197c34513c154267636c8056fb409eafd494c3dcdc"}, - {file = "rpds_py-0.24.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:92558d37d872e808944c3c96d0423b8604879a3d1c86fdad508d7ed91ea547d5"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f9e0057a509e096e47c87f753136c9b10d7a91842d8042c2ee6866899a717c0d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6e109a454412ab82979c5b1b3aee0604eca4bbf9a02693bb9df027af2bfa91a"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1c892b1ec1f8cbd5da8de287577b455e388d9c328ad592eabbdcb6fc93bee5"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9c39438c55983d48f4bb3487734d040e22dad200dab22c41e331cee145e7a50d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d7e8ce990ae17dda686f7e82fd41a055c668e13ddcf058e7fb5e9da20b57793"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ea7f4174d2e4194289cb0c4e172d83e79a6404297ff95f2875cf9ac9bced8ba"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb2954155bb8f63bb19d56d80e5e5320b61d71084617ed89efedb861a684baea"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04f2b712a2206e13800a8136b07aaedc23af3facab84918e7aa89e4be0260032"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:eda5c1e2a715a4cbbca2d6d304988460942551e4e5e3b7457b50943cd741626d"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:9abc80fe8c1f87218db116016de575a7998ab1629078c90840e8d11ab423ee25"}, - {file = "rpds_py-0.24.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6a727fd083009bc83eb83d6950f0c32b3c94c8b80a9b667c87f4bd1274ca30ba"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e0f3ef95795efcd3b2ec3fe0a5bcfb5dadf5e3996ea2117427e524d4fbf309c6"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2c13777ecdbbba2077670285dd1fe50828c8742f6a4119dbef6f83ea13ad10fb"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79e8d804c2ccd618417e96720ad5cd076a86fa3f8cb310ea386a3e6229bae7d1"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd822f019ccccd75c832deb7aa040bb02d70a92eb15a2f16c7987b7ad4ee8d83"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0047638c3aa0dbcd0ab99ed1e549bbf0e142c9ecc173b6492868432d8989a046"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a5b66d1b201cc71bc3081bc2f1fc36b0c1f268b773e03bbc39066651b9e18391"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbcbb6db5582ea33ce46a5d20a5793134b5365110d84df4e30b9d37c6fd40ad3"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:63981feca3f110ed132fd217bf7768ee8ed738a55549883628ee3da75bb9cb78"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:3a55fc10fdcbf1a4bd3c018eea422c52cf08700cf99c28b5cb10fe97ab77a0d3"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:c30ff468163a48535ee7e9bf21bd14c7a81147c0e58a36c1078289a8ca7af0bd"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:369d9c6d4c714e36d4a03957b4783217a3ccd1e222cdd67d464a3a479fc17796"}, - {file = "rpds_py-0.24.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:24795c099453e3721fda5d8ddd45f5dfcc8e5a547ce7b8e9da06fecc3832e26f"}, - {file = "rpds_py-0.24.0.tar.gz", hash = "sha256:772cc1b2cd963e7e17e6cc55fe0371fb9c704d63e44cacec7b9b7f523b78919e"}, +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "rpds_py-0.30.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:679ae98e00c0e8d68a7fda324e16b90fd5260945b45d3b824c892cec9eea3288"}, + {file = "rpds_py-0.30.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4cc2206b76b4f576934f0ed374b10d7ca5f457858b157ca52064bdfc26b9fc00"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:389a2d49eded1896c3d48b0136ead37c48e221b391c052fba3f4055c367f60a6"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32c8528634e1bf7121f3de08fa85b138f4e0dc47657866630611b03967f041d7"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f207f69853edd6f6700b86efb84999651baf3789e78a466431df1331608e5324"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:67b02ec25ba7a9e8fa74c63b6ca44cf5707f2fbfadae3ee8e7494297d56aa9df"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0e95f6819a19965ff420f65578bacb0b00f251fefe2c8b23347c37174271f3"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:a452763cc5198f2f98898eb98f7569649fe5da666c2dc6b5ddb10fde5a574221"}, + {file = "rpds_py-0.30.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e0b65193a413ccc930671c55153a03ee57cecb49e6227204b04fae512eb657a7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:858738e9c32147f78b3ac24dc0edb6610000e56dc0f700fd5f651d0a0f0eb9ff"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:da279aa314f00acbb803da1e76fa18666778e8a8f83484fba94526da5de2cba7"}, + {file = "rpds_py-0.30.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7c64d38fb49b6cdeda16ab49e35fe0da2e1e9b34bc38bd78386530f218b37139"}, + {file = "rpds_py-0.30.0-cp310-cp310-win32.whl", hash = "sha256:6de2a32a1665b93233cde140ff8b3467bdb9e2af2b91079f0333a0974d12d464"}, + {file = "rpds_py-0.30.0-cp310-cp310-win_amd64.whl", hash = "sha256:1726859cd0de969f88dc8673bdd954185b9104e05806be64bcd87badbe313169"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a2bffea6a4ca9f01b3f8e548302470306689684e61602aa3d141e34da06cf425"}, + {file = "rpds_py-0.30.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dc4f992dfe1e2bc3ebc7444f6c7051b4bc13cd8e33e43511e8ffd13bf407010d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:422c3cb9856d80b09d30d2eb255d0754b23e090034e1deb4083f8004bd0761e4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07ae8a593e1c3c6b82ca3292efbe73c30b61332fd612e05abee07c79359f292f"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12f90dd7557b6bd57f40abe7747e81e0c0b119bef015ea7726e69fe550e394a4"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99b47d6ad9a6da00bec6aabe5a6279ecd3c06a329d4aa4771034a21e335c3a97"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33f559f3104504506a44bb666b93a33f5d33133765b0c216a5bf2f1e1503af89"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:946fe926af6e44f3697abbc305ea168c2c31d3e3ef1058cf68f379bf0335a78d"}, + {file = "rpds_py-0.30.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:495aeca4b93d465efde585977365187149e75383ad2684f81519f504f5c13038"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9a0ca5da0386dee0655b4ccdf46119df60e0f10da268d04fe7cc87886872ba7"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8d6d1cc13664ec13c1b84241204ff3b12f9bb82464b8ad6e7a5d3486975c2eed"}, + {file = "rpds_py-0.30.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3896fa1be39912cf0757753826bc8bdc8ca331a28a7c4ae46b7a21280b06bb85"}, + {file = "rpds_py-0.30.0-cp311-cp311-win32.whl", hash = "sha256:55f66022632205940f1827effeff17c4fa7ae1953d2b74a8581baaefb7d16f8c"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_amd64.whl", hash = "sha256:a51033ff701fca756439d641c0ad09a41d9242fa69121c7d8769604a0a629825"}, + {file = "rpds_py-0.30.0-cp311-cp311-win_arm64.whl", hash = "sha256:47b0ef6231c58f506ef0b74d44e330405caa8428e770fec25329ed2cb971a229"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a161f20d9a43006833cd7068375a94d035714d73a172b681d8881820600abfad"}, + {file = "rpds_py-0.30.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6abc8880d9d036ecaafe709079969f56e876fcf107f7a8e9920ba6d5a3878d05"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca28829ae5f5d569bb62a79512c842a03a12576375d5ece7d2cadf8abe96ec28"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a1010ed9524c73b94d15919ca4d41d8780980e1765babf85f9a2f90d247153dd"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8d1736cfb49381ba528cd5baa46f82fdc65c06e843dab24dd70b63d09121b3f"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d948b135c4693daff7bc2dcfc4ec57237a29bd37e60c2fabf5aff2bbacf3e2f1"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47f236970bccb2233267d89173d3ad2703cd36a0e2a6e92d0560d333871a3d23"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:2e6ecb5a5bcacf59c3f912155044479af1d0b6681280048b338b28e364aca1f6"}, + {file = "rpds_py-0.30.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a8fa71a2e078c527c3e9dc9fc5a98c9db40bcc8a92b4e8858e36d329f8684b51"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73c67f2db7bc334e518d097c6d1e6fed021bbc9b7d678d6cc433478365d1d5f5"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5ba103fb455be00f3b1c2076c9d4264bfcb037c976167a6047ed82f23153f02e"}, + {file = "rpds_py-0.30.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7cee9c752c0364588353e627da8a7e808a66873672bcb5f52890c33fd965b394"}, + {file = "rpds_py-0.30.0-cp312-cp312-win32.whl", hash = "sha256:1ab5b83dbcf55acc8b08fc62b796ef672c457b17dbd7820a11d6c52c06839bdf"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_amd64.whl", hash = "sha256:a090322ca841abd453d43456ac34db46e8b05fd9b3b4ac0c78bcde8b089f959b"}, + {file = "rpds_py-0.30.0-cp312-cp312-win_arm64.whl", hash = "sha256:669b1805bd639dd2989b281be2cfd951c6121b65e729d9b843e9639ef1fd555e"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f83424d738204d9770830d35290ff3273fbb02b41f919870479fab14b9d303b2"}, + {file = "rpds_py-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7536cd91353c5273434b4e003cbda89034d67e7710eab8761fd918ec6c69cf8"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2771c6c15973347f50fece41fc447c054b7ac2ae0502388ce3b6738cd366e3d4"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0a59119fc6e3f460315fe9d08149f8102aa322299deaa5cab5b40092345c2136"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76fec018282b4ead0364022e3c54b60bf368b9d926877957a8624b58419169b7"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bef75a5525db97318e8cd061542b5a79812d711ea03dbc1f6f8dbb0c5f0d2"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9027da1ce107104c50c81383cae773ef5c24d296dd11c99e2629dbd7967a20c6"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:9cf69cdda1f5968a30a359aba2f7f9aa648a9ce4b580d6826437f2b291cfc86e"}, + {file = "rpds_py-0.30.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a4796a717bf12b9da9d3ad002519a86063dcac8988b030e405704ef7d74d2d9d"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5d4c2aa7c50ad4728a094ebd5eb46c452e9cb7edbfdb18f9e1221f597a73e1e7"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba81a9203d07805435eb06f536d95a266c21e5b2dfbf6517748ca40c98d19e31"}, + {file = "rpds_py-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:945dccface01af02675628334f7cf49c2af4c1c904748efc5cf7bbdf0b579f95"}, + {file = "rpds_py-0.30.0-cp313-cp313-win32.whl", hash = "sha256:b40fb160a2db369a194cb27943582b38f79fc4887291417685f3ad693c5a1d5d"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:806f36b1b605e2d6a72716f321f20036b9489d29c51c91f4dd29a3e3afb73b15"}, + {file = "rpds_py-0.30.0-cp313-cp313-win_arm64.whl", hash = "sha256:d96c2086587c7c30d44f31f42eae4eac89b60dabbac18c7669be3700f13c3ce1"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:eb0b93f2e5c2189ee831ee43f156ed34e2a89a78a66b98cadad955972548be5a"}, + {file = "rpds_py-0.30.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:922e10f31f303c7c920da8981051ff6d8c1a56207dbdf330d9047f6d30b70e5e"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdc62c8286ba9bf7f47befdcea13ea0e26bf294bda99758fd90535cbaf408000"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47f9a91efc418b54fb8190a6b4aa7813a23fb79c51f4bb84e418f5476c38b8db"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1f3587eb9b17f3789ad50824084fa6f81921bbf9a795826570bda82cb3ed91f2"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:39c02563fc592411c2c61d26b6c5fe1e51eaa44a75aa2c8735ca88b0d9599daa"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51a1234d8febafdfd33a42d97da7a43f5dcb120c1060e352a3fbc0c6d36e2083"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:eb2c4071ab598733724c08221091e8d80e89064cd472819285a9ab0f24bcedb9"}, + {file = "rpds_py-0.30.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bdfdb946967d816e6adf9a3d8201bfad269c67efe6cefd7093ef959683c8de0"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c77afbd5f5250bf27bf516c7c4a016813eb2d3e116139aed0096940c5982da94"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:61046904275472a76c8c90c9ccee9013d70a6d0f73eecefd38c1ae7c39045a08"}, + {file = "rpds_py-0.30.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4c5f36a861bc4b7da6516dbdf302c55313afa09b81931e8280361a4f6c9a2d27"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win32.whl", hash = "sha256:3d4a69de7a3e50ffc214ae16d79d8fbb0922972da0356dcf4d0fdca2878559c6"}, + {file = "rpds_py-0.30.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f14fc5df50a716f7ece6a80b6c78bb35ea2ca47c499e422aa4463455dd96d56d"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:68f19c879420aa08f61203801423f6cd5ac5f0ac4ac82a2368a9fcd6a9a075e0"}, + {file = "rpds_py-0.30.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ec7c4490c672c1a0389d319b3a9cfcd098dcdc4783991553c332a15acf7249be"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f251c812357a3fed308d684a5079ddfb9d933860fc6de89f2b7ab00da481e65f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac98b175585ecf4c0348fd7b29c3864bda53b805c773cbf7bfdaffc8070c976f"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3e62880792319dbeb7eb866547f2e35973289e7d5696c6e295476448f5b63c87"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e7fc54e0900ab35d041b0601431b0a0eb495f0851a0639b6ef90f7741b39a18"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47e77dc9822d3ad616c3d5759ea5631a75e5809d5a28707744ef79d7a1bcfcad"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:b4dc1a6ff022ff85ecafef7979a2c6eb423430e05f1165d6688234e62ba99a07"}, + {file = "rpds_py-0.30.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4559c972db3a360808309e06a74628b95eaccbf961c335c8fe0d590cf587456f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0ed177ed9bded28f8deb6ab40c183cd1192aa0de40c12f38be4d59cd33cb5c65"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ad1fa8db769b76ea911cb4e10f049d80bf518c104f15b3edb2371cc65375c46f"}, + {file = "rpds_py-0.30.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:46e83c697b1f1c72b50e5ee5adb4353eef7406fb3f2043d64c33f20ad1c2fc53"}, + {file = "rpds_py-0.30.0-cp314-cp314-win32.whl", hash = "sha256:ee454b2a007d57363c2dfd5b6ca4a5d7e2c518938f8ed3b706e37e5d470801ed"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_amd64.whl", hash = "sha256:95f0802447ac2d10bcc69f6dc28fe95fdf17940367b21d34e34c737870758950"}, + {file = "rpds_py-0.30.0-cp314-cp314-win_arm64.whl", hash = "sha256:613aa4771c99f03346e54c3f038e4cc574ac09a3ddfb0e8878487335e96dead6"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:7e6ecfcb62edfd632e56983964e6884851786443739dbfe3582947e87274f7cb"}, + {file = "rpds_py-0.30.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a1d0bc22a7cdc173fedebb73ef81e07faef93692b8c1ad3733b67e31e1b6e1b8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d08f00679177226c4cb8c5265012eea897c8ca3b93f429e546600c971bcbae7"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5965af57d5848192c13534f90f9dd16464f3c37aaf166cc1da1cae1fd5a34898"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a4e86e34e9ab6b667c27f3211ca48f73dba7cd3d90f8d5b11be56e5dbc3fb4e"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5d3e6b26f2c785d65cc25ef1e5267ccbe1b069c5c21b8cc724efee290554419"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:626a7433c34566535b6e56a1b39a7b17ba961e97ce3b80ec62e6f1312c025551"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:acd7eb3f4471577b9b5a41baf02a978e8bdeb08b4b355273994f8b87032000a8"}, + {file = "rpds_py-0.30.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fe5fa731a1fa8a0a56b0977413f8cacac1768dad38d16b3a296712709476fbd5"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:74a3243a411126362712ee1524dfc90c650a503502f135d54d1b352bd01f2404"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3e8eeb0544f2eb0d2581774be4c3410356eba189529a6b3e36bbbf9696175856"}, + {file = "rpds_py-0.30.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dbd936cde57abfee19ab3213cf9c26be06d60750e60a8e4dd85d1ab12c8b1f40"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win32.whl", hash = "sha256:dc824125c72246d924f7f796b4f63c1e9dc810c7d9e2355864b3c3a73d59ade0"}, + {file = "rpds_py-0.30.0-cp314-cp314t-win_amd64.whl", hash = "sha256:27f4b0e92de5bfbc6f86e43959e6edd1425c33b5e69aab0984a72047f2bcf1e3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c2262bdba0ad4fc6fb5545660673925c2d2a5d9e2e0fb603aad545427be0fc58"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ee6af14263f25eedc3bb918a3c04245106a42dfd4f5c2285ea6f997b1fc3f89a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3adbb8179ce342d235c31ab8ec511e66c73faa27a47e076ccc92421add53e2bb"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:250fa00e9543ac9b97ac258bd37367ff5256666122c2d0f2bc97577c60a1818c"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9854cf4f488b3d57b9aaeb105f06d78e5529d3145b1e4a41750167e8c213c6d3"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:993914b8e560023bc0a8bf742c5f303551992dcb85e247b1e5c7f4a7d145bda5"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58edca431fb9b29950807e301826586e5bbf24163677732429770a697ffe6738"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:dea5b552272a944763b34394d04577cf0f9bd013207bc32323b5a89a53cf9c2f"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ba3af48635eb83d03f6c9735dfb21785303e73d22ad03d489e88adae6eab8877"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:dff13836529b921e22f15cb099751209a60009731a68519630a24d61f0b1b30a"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b151685b23929ab7beec71080a8889d4d6d9fa9a983d213f07121205d48e2c4"}, + {file = "rpds_py-0.30.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:ac37f9f516c51e5753f27dfdef11a88330f04de2d564be3991384b2f3535d02e"}, + {file = "rpds_py-0.30.0.tar.gz", hash = "sha256:dd8ff7cf90014af0c0f787eea34794ebf6415242ee1d6fa91eaba725cc441e84"}, ] [[package]] @@ -2235,42 +2373,42 @@ pyasn1 = ">=0.1.3" [[package]] name = "ruff" -version = "0.11.5" +version = "0.11.13" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" groups = ["dev"] files = [ - {file = "ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b"}, - {file = "ruff-0.11.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac12884b9e005c12d0bd121f56ccf8033e1614f736f766c118ad60780882a077"}, - {file = "ruff-0.11.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4bfd80a6ec559a5eeb96c33f832418bf0fb96752de0539905cf7b0cc1d31d779"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470"}, - {file = "ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159"}, - {file = "ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783"}, - {file = "ruff-0.11.5-py3-none-win32.whl", hash = "sha256:e268da7b40f56e3eca571508a7e567e794f9bfcc0f412c4b607931d3af9c4afe"}, - {file = "ruff-0.11.5-py3-none-win_amd64.whl", hash = "sha256:6c6dc38af3cfe2863213ea25b6dc616d679205732dc0fb673356c2d69608f800"}, - {file = "ruff-0.11.5-py3-none-win_arm64.whl", hash = "sha256:67e241b4314f4eacf14a601d586026a962f4002a475aa702c69980a38087aa4e"}, - {file = "ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef"}, + {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, + {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, + {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, + {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, + {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, + {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, + {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, + {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, + {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, ] [[package]] name = "scramp" -version = "1.4.5" +version = "1.4.8" description = "An implementation of the SCRAM protocol." optional = false -python-versions = ">=3.8" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "scramp-1.4.5-py3-none-any.whl", hash = "sha256:50e37c464fc67f37994e35bee4151e3d8f9320e9c204fca83a5d313c121bbbe7"}, - {file = "scramp-1.4.5.tar.gz", hash = "sha256:be3fbe774ca577a7a658117dca014e5d254d158cecae3dd60332dfe33ce6d78e"}, + {file = "scramp-1.4.8-py3-none-any.whl", hash = "sha256:87c2f15976845a2872fe5490a06097f0d01813cceb53774ea168c911f2ad025c"}, + {file = "scramp-1.4.8.tar.gz", hash = "sha256:bd018fabfe46343cceeb9f1c3e8d23f55770271e777e3accbfaee3ff0a316e71"}, ] [package.dependencies] @@ -2290,24 +2428,24 @@ files = [ [[package]] name = "setuptools" -version = "78.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" +version = "82.0.1" +description = "Most extensible Python build backend with support for C/C++ extension modules" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8"}, - {file = "setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54"}, + {file = "setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb"}, + {file = "setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] [[package]] name = "simple-cloudevent" @@ -2357,78 +2495,84 @@ sqlalchemy = ">=2.0.39,<3.0.0" type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "83d2e5880b212566b488a17de44d7aff29967a89" +resolved_reference = "b745e248a60f21f23171b495cb5e1098ccf08079" subdirectory = "python/common/sql-versioning-alt" [[package]] name = "sqlalchemy" -version = "2.0.40" +version = "2.0.48" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"}, - {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"}, - {file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bece9527f5a98466d67fb5d34dc560c4da964240d8b09024bb21c1246545e04e"}, - {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:8bb131ffd2165fae48162c7bbd0d97c84ab961deea9b8bab16366543deeab625"}, - {file = "SQLAlchemy-2.0.40-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:9408fd453d5f8990405cc9def9af46bfbe3183e6110401b407c2d073c3388f47"}, - {file = "SQLAlchemy-2.0.40-cp37-cp37m-win32.whl", hash = "sha256:00a494ea6f42a44c326477b5bee4e0fc75f6a80c01570a32b57e89cf0fbef85a"}, - {file = "SQLAlchemy-2.0.40-cp37-cp37m-win_amd64.whl", hash = "sha256:c7b927155112ac858357ccf9d255dd8c044fd9ad2dc6ce4c4149527c901fa4c3"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1ea21bef99c703f44444ad29c2c1b6bd55d202750b6de8e06a955380f4725d7"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:afe63b208153f3a7a2d1a5b9df452b0673082588933e54e7c8aac457cf35e758"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8aae085ea549a1eddbc9298b113cffb75e514eadbb542133dd2b99b5fb3b6af"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ea9181284754d37db15156eb7be09c86e16e50fbe77610e9e7bee09291771a1"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5434223b795be5c5ef8244e5ac98056e290d3a99bdcc539b916e282b160dda00"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15d08d5ef1b779af6a0909b97be6c1fd4298057504eb6461be88bd1696cb438e"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-win32.whl", hash = "sha256:cd2f75598ae70bcfca9117d9e51a3b06fe29edd972fdd7fd57cc97b4dbf3b08a"}, - {file = "sqlalchemy-2.0.40-cp310-cp310-win_amd64.whl", hash = "sha256:2cbafc8d39ff1abdfdda96435f38fab141892dc759a2165947d1a8fffa7ef596"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6bacab7514de6146a1976bc56e1545bee247242fab030b89e5f70336fc0003e"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5654d1ac34e922b6c5711631f2da497d3a7bffd6f9f87ac23b35feea56098011"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35904d63412db21088739510216e9349e335f142ce4a04b69e2528020ee19ed4"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7a80ed86d6aaacb8160a1caef6680d4ddd03c944d985aecee940d168c411d1"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:519624685a51525ddaa7d8ba8265a1540442a2ec71476f0e75241eb8263d6f51"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2ee5f9999a5b0e9689bed96e60ee53c3384f1a05c2dd8068cc2e8361b0df5b7a"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-win32.whl", hash = "sha256:c0cae71e20e3c02c52f6b9e9722bca70e4a90a466d59477822739dc31ac18b4b"}, - {file = "sqlalchemy-2.0.40-cp311-cp311-win_amd64.whl", hash = "sha256:574aea2c54d8f1dd1699449f332c7d9b71c339e04ae50163a3eb5ce4c4325ee4"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9d3b31d0a1c44b74d3ae27a3de422dfccd2b8f0b75e51ecb2faa2bf65ab1ba0d"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:37f7a0f506cf78c80450ed1e816978643d3969f99c4ac6b01104a6fe95c5490a"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bb933a650323e476a2e4fbef8997a10d0003d4da996aad3fd7873e962fdde4d"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6959738971b4745eea16f818a2cd086fb35081383b078272c35ece2b07012716"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:110179728e442dae85dd39591beb74072ae4ad55a44eda2acc6ec98ead80d5f2"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8040680eaacdce4d635f12c55c714f3d4c7f57da2bc47a01229d115bd319191"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-win32.whl", hash = "sha256:650490653b110905c10adac69408380688cefc1f536a137d0d69aca1069dc1d1"}, - {file = "sqlalchemy-2.0.40-cp312-cp312-win_amd64.whl", hash = "sha256:2be94d75ee06548d2fc591a3513422b873490efb124048f50556369a834853b0"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:915866fd50dd868fdcc18d61d8258db1bf9ed7fbd6dfec960ba43365952f3b01"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a4c5a2905a9ccdc67a8963e24abd2f7afcd4348829412483695c59e0af9a705"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55028d7a3ebdf7ace492fab9895cbc5270153f75442a0472d8516e03159ab364"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6cfedff6878b0e0d1d0a50666a817ecd85051d12d56b43d9d425455e608b5ba0"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bb19e30fdae77d357ce92192a3504579abe48a66877f476880238a962e5b96db"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:16d325ea898f74b26ffcd1cf8c593b0beed8714f0317df2bed0d8d1de05a8f26"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-win32.whl", hash = "sha256:a669cbe5be3c63f75bcbee0b266779706f1a54bcb1000f302685b87d1b8c1500"}, - {file = "sqlalchemy-2.0.40-cp313-cp313-win_amd64.whl", hash = "sha256:641ee2e0834812d657862f3a7de95e0048bdcb6c55496f39c6fa3d435f6ac6ad"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:50f5885bbed261fc97e2e66c5156244f9704083a674b8d17f24c72217d29baf5"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cf0e99cdb600eabcd1d65cdba0d3c91418fee21c4aa1d28db47d095b1064a7d8"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe147fcd85aaed53ce90645c91ed5fca0cc88a797314c70dfd9d35925bd5d106"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baf7cee56bd552385c1ee39af360772fbfc2f43be005c78d1140204ad6148438"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4aeb939bcac234b88e2d25d5381655e8353fe06b4e50b1c55ecffe56951d18c2"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c268b5100cfeaa222c40f55e169d484efa1384b44bf9ca415eae6d556f02cb08"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-win32.whl", hash = "sha256:46628ebcec4f23a1584fb52f2abe12ddb00f3bb3b7b337618b80fc1b51177aff"}, - {file = "sqlalchemy-2.0.40-cp38-cp38-win_amd64.whl", hash = "sha256:7e0505719939e52a7b0c65d20e84a6044eb3712bb6f239c6b1db77ba8e173a37"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c884de19528e0fcd9dc34ee94c810581dd6e74aef75437ff17e696c2bfefae3e"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1abb387710283fc5983d8a1209d9696a4eae9db8d7ac94b402981fe2fe2e39ad"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5cfa124eda500ba4b0d3afc3e91ea27ed4754e727c7f025f293a22f512bcd4c9"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6b28d303b9d57c17a5164eb1fd2d5119bb6ff4413d5894e74873280483eeb5"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b5a5bbe29c10c5bfd63893747a1bf6f8049df607638c786252cb9243b86b6706"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f0fda83e113bb0fb27dc003685f32a5dcb99c9c4f41f4fa0838ac35265c23b5c"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-win32.whl", hash = "sha256:957f8d85d5e834397ef78a6109550aeb0d27a53b5032f7a57f2451e1adc37e98"}, - {file = "sqlalchemy-2.0.40-cp39-cp39-win_amd64.whl", hash = "sha256:1ffdf9c91428e59744f8e6f98190516f8e1d05eec90e936eb08b257332c5e870"}, - {file = "sqlalchemy-2.0.40-py3-none-any.whl", hash = "sha256:32587e2e1e359276957e6fe5dad089758bc042a971a8a09ae8ecf7a8fe23d07a"}, - {file = "sqlalchemy-2.0.40.tar.gz", hash = "sha256:d827099289c64589418ebbcaead0145cd19f4e3e8a93919a0100247af245fa00"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7001dc9d5f6bb4deb756d5928eaefe1930f6f4179da3924cbd95ee0e9f4dce89"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1a89ce07ad2d4b8cfc30bd5889ec40613e028ed80ef47da7d9dd2ce969ad30e0"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10853a53a4a00417a00913d270dddda75815fcb80675874285f41051c094d7dd"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fac0fa4e4f55f118fd87177dacb1c6522fe39c28d498d259014020fec9164c29"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3713e21ea67bca727eecd4a24bf68bcd414c403faae4989442be60994301ded0"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-win32.whl", hash = "sha256:d404dc897ce10e565d647795861762aa2d06ca3f4a728c5e9a835096c7059018"}, + {file = "sqlalchemy-2.0.48-cp310-cp310-win_amd64.whl", hash = "sha256:841a94c66577661c1f088ac958cd767d7c9bf507698f45afffe7a4017049de76"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617"}, + {file = "sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99"}, + {file = "sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131"}, + {file = "sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485"}, + {file = "sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6"}, + {file = "sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f"}, + {file = "sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f8649a14caa5f8a243628b1d61cf530ad9ae4578814ba726816adb1121fc493e"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6bb85c546591569558571aa1b06aba711b26ae62f111e15e56136d69920e1616"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6b764fb312bd35e47797ad2e63f0d323792837a6ac785a4ca967019357d2bc7"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7c998f2ace8bf76b453b75dbcca500d4f4b9dd3908c13e89b86289b37784848b"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:d64177f443594c8697369c10e4bbcac70ef558e0f7921a1de7e4a3d1734bcf67"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-win32.whl", hash = "sha256:01f6bbd4308b23240cf7d3ef117557c8fd097ec9549d5d8a52977544e35b40ad"}, + {file = "sqlalchemy-2.0.48-cp38-cp38-win_amd64.whl", hash = "sha256:858e433f12b0e5b3ed2f8da917433b634f4937d0e8793e5cb33c54a1a01df565"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4599a95f9430ae0de82b52ff0d27304fe898c17cb5f4099f7438a51b9998ac77"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f27f9da0a7d22b9f981108fd4b62f8b5743423388915a563e651c20d06c1f457"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d8fcccbbc0c13c13702c471da398b8cd72ba740dca5859f148ae8e0e8e0d3e7e"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a5b429eb84339f9f05e06083f119ad814e6d85e27ecbdf9c551dfdbb128eaf8a"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:bcb8ebbf2e2c36cfe01a94f2438012c6a9d494cf80f129d9753bcdf33bfc35a6"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-win32.whl", hash = "sha256:e214d546c8ecb5fc22d6e6011746082abf13a9cf46eefb45769c7b31407c97b5"}, + {file = "sqlalchemy-2.0.48-cp39-cp39-win_amd64.whl", hash = "sha256:b8fc3454b4f3bd0a368001d0e968852dad45a873f8b4babd41bc302ec851a099"}, + {file = "sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096"}, + {file = "sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7"}, ] [package.dependencies] -greenlet = {version = ">=1", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = ">=1", markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""} typing-extensions = ">=4.6.0" [package.extras] @@ -2503,90 +2647,105 @@ structlog = "^24.1.0" type = "git" url = "https://github.com/bcgov/sbc-connect-common.git" reference = "main" -resolved_reference = "7f1cc0ea4a374310ac558ff435fa6b7ea7bb2f8b" +resolved_reference = "2a3d5a2d5b5ff3c905626f3193db5ed0af6d4952" subdirectory = "python/structured-logging" [[package]] name = "tomli" -version = "2.2.1" +version = "2.4.0" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, - {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, - {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, - {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, - {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, - {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, - {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, - {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, - {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, - {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, - {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, - {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, - {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, - {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, - {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, - {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, - {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, - {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, -] - -[[package]] -name = "types-python-dateutil" -version = "2.9.0.20241206" -description = "Typing stubs for python-dateutil" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, - {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867"}, + {file = "tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95"}, + {file = "tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d"}, + {file = "tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576"}, + {file = "tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a"}, + {file = "tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa"}, + {file = "tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1"}, + {file = "tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a"}, + {file = "tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b"}, + {file = "tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51"}, + {file = "tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729"}, + {file = "tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da"}, + {file = "tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0"}, + {file = "tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4"}, + {file = "tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c"}, + {file = "tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f"}, + {file = "tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86"}, + {file = "tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87"}, + {file = "tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6"}, + {file = "tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66"}, + {file = "tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702"}, + {file = "tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8"}, + {file = "tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776"}, + {file = "tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475"}, + {file = "tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9"}, + {file = "tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df"}, + {file = "tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f"}, + {file = "tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b"}, + {file = "tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087"}, + {file = "tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd"}, + {file = "tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4"}, + {file = "tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a"}, + {file = "tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c"}, ] [[package]] name = "typing-extensions" -version = "4.13.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +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.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, - {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, + {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 = "typing-inspection" -version = "0.4.0" +version = "0.4.2" description = "Runtime typing introspection tools" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"}, - {file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"}, + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, ] [package.dependencies] typing-extensions = ">=4.12.0" +[[package]] +name = "tzdata" +version = "2025.3" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1"}, + {file = "tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7"}, +] + [[package]] name = "uri-template" version = "1.3.0" @@ -2604,62 +2763,62 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "2.4.0" +version = "2.6.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, - {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, + {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) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +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 = "webcolors" -version = "24.11.1" +version = "25.10.0" description = "A library for working with the color formats defined by HTML and CSS." optional = false -python-versions = ">=3.9" +python-versions = ">=3.10" groups = ["main"] files = [ - {file = "webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9"}, - {file = "webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6"}, + {file = "webcolors-25.10.0-py3-none-any.whl", hash = "sha256:032c727334856fc0b968f63daa252a1ac93d33db2f5267756623c210e57a4f1d"}, + {file = "webcolors-25.10.0.tar.gz", hash = "sha256:62abae86504f66d0f6364c2a8520de4a0c47b80c03fc3a5f1815fedbef7c19bf"}, ] [[package]] name = "werkzeug" -version = "3.1.3" +version = "3.1.6" description = "The comprehensive WSGI web application library." 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.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131"}, + {file = "werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25"}, ] [package.dependencies] -MarkupSafe = ">=2.1.1" +markupsafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] [[package]] name = "zimports" -version = "0.6.1" +version = "0.6.3" description = "Yet another import fixing tool" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" groups = ["main"] files = [ - {file = "zimports-0.6.1-py3-none-any.whl", hash = "sha256:d2483cfcae4b0783a2573b93fd186a5694e5b8ddf5bf110317a72dd9e72dfbfe"}, - {file = "zimports-0.6.1.tar.gz", hash = "sha256:8c801a653cd3aaea7b8347c42237bd32705bcb0be5d1987a16d8df56cc31baab"}, + {file = "zimports-0.6.3-py3-none-any.whl", hash = "sha256:33adb19d62a2206c9256082752cd4d3b0695e5e9f9cb8184558573b0992ae3fe"}, + {file = "zimports-0.6.3.tar.gz", hash = "sha256:0091c43de53f6976be05d5a9ccf9455bc5730f5d6f8a0f9544bc9eb6db0f4bb6"}, ] [package.dependencies] diff --git a/queue_services/business-bn/pyproject.toml b/queue_services/business-bn/pyproject.toml index c87e8b6a64..ddfec22f9a 100644 --- a/queue_services/business-bn/pyproject.toml +++ b/queue_services/business-bn/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "business-bn" -version = "0.1.1" +version = "0.1.2" description = "" authors = ["Patrick Wang "] readme = "README.md" diff --git a/queue_services/business-bn/src/business_bn/bn_processors/__init__.py b/queue_services/business-bn/src/business_bn/bn_processors/__init__.py index da5b303eda..ca48f80af7 100644 --- a/queue_services/business-bn/src/business_bn/bn_processors/__init__.py +++ b/queue_services/business-bn/src/business_bn/bn_processors/__init__.py @@ -107,7 +107,12 @@ def build_input_xml(template_name, data): """ template_loader = jinja2.FileSystemLoader(searchpath=current_app.config.get("TEMPLATE_PATH")) - template_env = jinja2.Environment(loader=template_loader, autoescape=True) + template_env = jinja2.Environment( + loader=template_loader, + autoescape=True, + trim_blocks=True, + lstrip_blocks=True + ) template = template_env.get_template(f"{template_name}.xml") return template.render(data) From dae4828907f9a91f0a87c599a13d2e724a656d07 Mon Sep 17 00:00:00 2001 From: Argus Chiu Date: Thu, 12 Mar 2026 13:52:24 -0700 Subject: [PATCH 17/46] 32484 Separate auth flows implementation for tombstone migration (#4130) --- data-tool/flows/auth/__init__.py | 0 data-tool/flows/auth/auth_affiliation_flow.py | 282 +++++++++++++ data-tool/flows/auth/auth_contact_flow.py | 235 +++++++++++ data-tool/flows/auth/auth_create_flow.py | 260 ++++++++++++ data-tool/flows/auth/auth_delete_flow.py | 229 +++++++++++ data-tool/flows/auth/auth_invite_flow.py | 242 ++++++++++++ data-tool/flows/auth/auth_models.py | 37 ++ data-tool/flows/auth/auth_queries.py | 317 +++++++++++++++ data-tool/flows/auth/auth_tasks.py | 369 ++++++++++++++++++ data-tool/flows/common/auth_service.py | 224 +++++++---- data-tool/flows/config.py | 77 +++- .../scripts/colin_corps_extract_postgres_ddl | 49 +++ 12 files changed, 2229 insertions(+), 92 deletions(-) create mode 100644 data-tool/flows/auth/__init__.py create mode 100644 data-tool/flows/auth/auth_affiliation_flow.py create mode 100644 data-tool/flows/auth/auth_contact_flow.py create mode 100644 data-tool/flows/auth/auth_create_flow.py create mode 100644 data-tool/flows/auth/auth_delete_flow.py create mode 100644 data-tool/flows/auth/auth_invite_flow.py create mode 100644 data-tool/flows/auth/auth_models.py create mode 100644 data-tool/flows/auth/auth_queries.py create mode 100644 data-tool/flows/auth/auth_tasks.py diff --git a/data-tool/flows/auth/__init__.py b/data-tool/flows/auth/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data-tool/flows/auth/auth_affiliation_flow.py b/data-tool/flows/auth/auth_affiliation_flow.py new file mode 100644 index 0000000000..56a84f2148 --- /dev/null +++ b/data-tool/flows/auth/auth_affiliation_flow.py @@ -0,0 +1,282 @@ +import math +import os +from typing import Dict, List + +from prefect import flow +from prefect.context import get_run_context +from prefect.futures import wait +from prefect.states import Failed +from prefect.task_runners import ConcurrentTaskRunner +from sqlalchemy import text + +from common.extract_tracking_service import ExtractTrackingService, ProcessingStatuses +from common.init_utils import colin_extract_init, get_config +from common.query_utils import convert_result_set_to_dict + +from .auth_models import AuthCreatePlan, AuthDeletePlan, AuthSelectionMode +from .auth_queries import ( + get_auth_business_profiles_query, + get_auth_reservable_corps_query, + get_auth_reservable_count_query, +) +from .auth_tasks import get_auth_token, parse_accounts_csv, perform_auth_create_for_corp, perform_auth_delete_for_corp + +FLOW_NAME = 'auth-affiliation-flow' + + +def _get_max_workers() -> int: + try: + v = int(os.getenv('AUTH_MAX_WORKERS', '50')) + return v if v > 0 else 50 + except Exception: + return 50 + + +def _parse_selection_mode(config) -> AuthSelectionMode: + raw = (getattr(config, 'AUTH_SELECTION_MODE', 'MIGRATION_FILTER') or 'MIGRATION_FILTER').strip().upper() + try: + return AuthSelectionMode(raw) + except Exception as e: + raise ValueError(f'Unknown AUTH_SELECTION_MODE: {raw}') from e + + +def _fetch_profiles(colin_engine, corp_nums: List[str], suffix: str) -> Dict[str, dict]: + if not corp_nums: + return {} + sql = get_auth_business_profiles_query(corp_nums, suffix or '') + with colin_engine.connect() as conn: + rs = conn.execute(text(sql)) + rows = convert_result_set_to_dict(rs) + return {r['identifier']: r for r in rows} + + +@flow( + name='Auth-Affiliation-Flow', + log_prints=True, + persist_result=False, + task_runner=ConcurrentTaskRunner(max_workers=_get_max_workers()) +) +def auth_affiliation_flow(): + """ + Create OR delete affiliations (mutually exclusive for this run). + + - Create mode: uses AuthCreatePlan(create_affiliations=True) + - Delete mode: uses AuthDeletePlan(delete_affiliations=True) + + Selection excludes any corp already tracked in auth_processing for (corp_num, FLOW_NAME, environment). + """ + config = get_config() + colin_engine = colin_extract_init(config) + selection_mode = _parse_selection_mode(config) + + do_create = bool(getattr(config, 'AUTH_CREATE_AFFILIATIONS', False)) + do_delete = bool(getattr(config, 'AUTH_DELETE_AFFILIATIONS', False)) + + if do_create and do_delete: + raise ValueError('Invalid config: cannot both AUTH_CREATE_AFFILIATIONS and AUTH_DELETE_AFFILIATIONS in one run') + if not do_create and not do_delete: + raise ValueError('Nothing to do: set either AUTH_CREATE_AFFILIATIONS or AUTH_DELETE_AFFILIATIONS') + + create_plan = None + delete_plan = None + if do_create: + create_plan = AuthCreatePlan( + create_entity=bool(getattr(config, 'AUTH_CREATE_ENTITY', True)), + upsert_contact=False, + create_affiliations=True, + send_unaffiliated_invite=False, + fail_if_missing_email=False, + dry_run=bool(getattr(config, 'AUTH_DRY_RUN', False)), + ) + plan_desc = create_plan + else: + delete_plan = AuthDeletePlan( + delete_affiliations=True, + delete_entity=False, + delete_invites=False, + dry_run=bool(getattr(config, 'AUTH_DRY_RUN', False)), + ) + plan_desc = delete_plan + + # Count reservable + count_sql = get_auth_reservable_count_query( + flow_name=FLOW_NAME, + config=config, + selection_mode=selection_mode + ) + with colin_engine.connect() as conn: + total_reservable = int(conn.execute(text(count_sql)).scalar() or 0) + + if total_reservable <= 0: + print('No reservable corps found for this run.') + return + + if getattr(config, 'AUTH_BATCHES', 0) <= 0: + raise ValueError('AUTH_BATCHES must be explicitly set to a positive integer') + if getattr(config, 'AUTH_BATCH_SIZE', 0) <= 0: + raise ValueError('AUTH_BATCH_SIZE must be explicitly set to a positive integer') + + batch_size = config.AUTH_BATCH_SIZE + max_corps = min(total_reservable, config.AUTH_BATCHES * config.AUTH_BATCH_SIZE) + + flow_run_id = get_run_context().flow_run.id + + tracking = ExtractTrackingService( + config.DATA_LOAD_ENV, + colin_engine, + FLOW_NAME, + table_name='auth_processing', + statement_timeout_ms=getattr(config, 'RESERVE_STATEMENT_TIMEOUT_MS', None) + ) + + extra_insert_cols = ['account_ids'] + + base_query = get_auth_reservable_corps_query( + flow_name=FLOW_NAME, + config=config, + batch_size=max_corps, + selection_mode=selection_mode, + include_account_ids=True, + include_contact_email=False + ) + + reserved = tracking.reserve_for_flow( + base_query=base_query, + flow_run_id=flow_run_id, + extra_insert_cols=extra_insert_cols, + fallback_account_ids=config.AFFILIATE_ENTITY_ACCOUNT_IDS_CSV + ) + + if reserved <= 0: + print('No corps reserved (cohort may be exhausted or already reserved).') + return + + batches = min(math.ceil(reserved / batch_size), config.AUTH_BATCHES) + + print(f'👷 Auth affiliation mode: {"CREATE" if do_create else "DELETE"}') + print(f'👷 Plan: {plan_desc}') + print(f'👷 Reservable={total_reservable}, Reserved={reserved}, Batches={batches}, BatchSize={batch_size}') + print(f'👷 SelectionMode={selection_mode.value}') + + cnt = 0 + total_failed = 0 + total_completed = 0 + + while cnt < batches: + claimed = tracking.claim_batch( + flow_run_id, + batch_size, + extra_return_cols=extra_insert_cols, + as_dict=True + ) + if not claimed: + print('No more corps available to claim') + break + + corp_nums = [r['corp_num'] for r in claimed] + corp_accounts = {r['corp_num']: (r.get('account_ids') or None) for r in claimed} + + profiles = _fetch_profiles(colin_engine, corp_nums, getattr(config, 'CORP_NAME_SUFFIX', '') or '') if do_create else {} + + try: + token = get_auth_token(config) + except Exception as e: + err = f'Failed to obtain auth token: {repr(e)}' + print(f'❌ {err}') + for corp_num in corp_nums: + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error=err, + entity_action='FAILED' if (do_create and create_plan and create_plan.create_entity) else 'NOT_RUN', + contact_action='NOT_RUN', + affiliation_action='FAILED', + invite_action='NOT_RUN', + action_detail='token_error' + ) + return Failed(message=err) + + futures = [] + for corp_num in corp_nums: + accounts = parse_accounts_csv(corp_accounts.get(corp_num)) + + if do_create: + profile = profiles.get(corp_num) + if not profile: + total_failed += 1 + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error='Missing business profile for corp in COLIN extract', + entity_action='FAILED' if (create_plan and create_plan.create_entity) else 'NOT_RUN', + contact_action='NOT_RUN', + affiliation_action='FAILED', + invite_action='NOT_RUN', + action_detail='profile_missing' + ) + continue + + futures.append( + perform_auth_create_for_corp.submit( + config, + corp_num, + profile, + accounts, + create_plan, + token + ) + ) + else: + futures.append( + perform_auth_delete_for_corp.submit( + config, + corp_num, + accounts, + delete_plan, + token + ) + ) + + wait(futures) + + for f in futures: + res = f.result() + actions = [ + res.get('entity_action'), + res.get('contact_action'), + res.get('affiliation_action'), + res.get('invite_action'), + ] + failed = any(a == 'FAILED' for a in actions if a) + status = ProcessingStatuses.FAILED if failed else ProcessingStatuses.COMPLETED + + tracking.update_corp_status( + flow_run_id, + res['corp_num'], + status, + error=res.get('error'), + entity_action=res.get('entity_action'), + contact_action=res.get('contact_action'), + affiliation_action=res.get('affiliation_action'), + invite_action=res.get('invite_action'), + action_detail=res.get('action_detail') + ) + + if status == ProcessingStatuses.FAILED: + total_failed += 1 + else: + total_completed += 1 + + cnt += 1 + print(f'🌟 Complete round {cnt}/{batches}. Completed={total_completed}, Failed={total_failed}') + + if total_failed > 0: + return Failed(message=f'{total_failed} corps failed in {FLOW_NAME}.') + + print(f'🌰 {FLOW_NAME} complete. Completed={total_completed}, Failed={total_failed}') + + +if __name__ == '__main__': + auth_affiliation_flow() diff --git a/data-tool/flows/auth/auth_contact_flow.py b/data-tool/flows/auth/auth_contact_flow.py new file mode 100644 index 0000000000..2f118d9a2e --- /dev/null +++ b/data-tool/flows/auth/auth_contact_flow.py @@ -0,0 +1,235 @@ +import math +import os +from typing import Dict, List + +from prefect import flow +from prefect.context import get_run_context +from prefect.futures import wait +from prefect.states import Failed +from prefect.task_runners import ConcurrentTaskRunner +from sqlalchemy import text + +from common.extract_tracking_service import ExtractTrackingService, ProcessingStatuses +from common.init_utils import colin_extract_init, get_config +from common.query_utils import convert_result_set_to_dict + +from .auth_models import AuthCreatePlan, AuthSelectionMode +from .auth_queries import ( + get_auth_business_profiles_query, + get_auth_reservable_corps_query, + get_auth_reservable_count_query, +) +from .auth_tasks import get_auth_token, perform_auth_create_for_corp + +FLOW_NAME = 'auth-contact-flow' + + +def _get_max_workers() -> int: + try: + v = int(os.getenv('AUTH_MAX_WORKERS', '50')) + return v if v > 0 else 50 + except Exception: + return 50 + + +def _parse_selection_mode(config) -> AuthSelectionMode: + raw = (getattr(config, 'AUTH_SELECTION_MODE', 'MIGRATION_FILTER') or 'MIGRATION_FILTER').strip().upper() + try: + return AuthSelectionMode(raw) + except Exception as e: + raise ValueError(f'Unknown AUTH_SELECTION_MODE: {raw}') from e + + +def _fetch_profiles(colin_engine, corp_nums: List[str], suffix: str) -> Dict[str, dict]: + if not corp_nums: + return {} + sql = get_auth_business_profiles_query(corp_nums, suffix or '') + with colin_engine.connect() as conn: + rs = conn.execute(text(sql)) + rows = convert_result_set_to_dict(rs) + return {r['identifier']: r for r in rows} + + +@flow( + name='Auth-Contact-Flow', + log_prints=True, + persist_result=False, + task_runner=ConcurrentTaskRunner(max_workers=_get_max_workers()) +) +def auth_contact_flow(): + """ + Upsert entity contact email (no entity creation, no affiliations, no invite). + + Selection excludes any corp already tracked in auth_processing for (corp_num, FLOW_NAME, environment). + """ + config = get_config() + colin_engine = colin_extract_init(config) + selection_mode = _parse_selection_mode(config) + + plan = AuthCreatePlan( + create_entity=False, + upsert_contact=True, + create_affiliations=False, + send_unaffiliated_invite=False, + fail_if_missing_email=bool(getattr(config, 'AUTH_FAIL_IF_MISSING_EMAIL', False)), + dry_run=bool(getattr(config, 'AUTH_DRY_RUN', False)), + ) + + count_sql = get_auth_reservable_count_query( + flow_name=FLOW_NAME, + config=config, + selection_mode=selection_mode + ) + with colin_engine.connect() as conn: + total_reservable = int(conn.execute(text(count_sql)).scalar() or 0) + + if total_reservable <= 0: + print('No reservable corps found for this run.') + return + + if getattr(config, 'AUTH_BATCHES', 0) <= 0: + raise ValueError('AUTH_BATCHES must be explicitly set to a positive integer') + if getattr(config, 'AUTH_BATCH_SIZE', 0) <= 0: + raise ValueError('AUTH_BATCH_SIZE must be explicitly set to a positive integer') + + batch_size = config.AUTH_BATCH_SIZE + max_corps = min(total_reservable, config.AUTH_BATCHES * config.AUTH_BATCH_SIZE) + + flow_run_id = get_run_context().flow_run.id + + tracking = ExtractTrackingService( + config.DATA_LOAD_ENV, + colin_engine, + FLOW_NAME, + table_name='auth_processing', + statement_timeout_ms=getattr(config, 'RESERVE_STATEMENT_TIMEOUT_MS', None) + ) + + extra_insert_cols = ['contact_email'] + + base_query = get_auth_reservable_corps_query( + flow_name=FLOW_NAME, + config=config, + batch_size=max_corps, + selection_mode=selection_mode, + include_account_ids=False, + include_contact_email=True + ) + + reserved = tracking.reserve_for_flow( + base_query=base_query, + flow_run_id=flow_run_id, + extra_insert_cols=extra_insert_cols + ) + + if reserved <= 0: + print('No corps reserved (cohort may be exhausted or already reserved).') + return + + batches = min(math.ceil(reserved / batch_size), config.AUTH_BATCHES) + + print(f'👷 Auth contact plan: {plan}') + print(f'👷 Reservable={total_reservable}, Reserved={reserved}, Batches={batches}, BatchSize={batch_size}') + print(f'👷 SelectionMode={selection_mode.value}, DryRun={plan.dry_run}') + + cnt = 0 + total_failed = 0 + total_completed = 0 + + while cnt < batches: + claimed = tracking.claim_batch( + flow_run_id, + batch_size, + extra_return_cols=extra_insert_cols, + as_dict=True + ) + if not claimed: + print('No more corps available to claim') + break + + corp_nums = [r['corp_num'] for r in claimed] + profiles = _fetch_profiles(colin_engine, corp_nums, getattr(config, 'CORP_NAME_SUFFIX', '') or '') + + try: + token = get_auth_token(config) + except Exception as e: + err = f'Failed to obtain auth token: {repr(e)}' + print(f'❌ {err}') + for corp_num in corp_nums: + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error=err, + entity_action='NOT_RUN', + contact_action='FAILED', + affiliation_action='NOT_RUN', + invite_action='NOT_RUN', + action_detail='token_error' + ) + return Failed(message=err) + + futures = [] + for corp_num in corp_nums: + profile = profiles.get(corp_num) + if not profile: + total_failed += 1 + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error='Missing business profile for corp in COLIN extract', + entity_action='NOT_RUN', + contact_action='FAILED', + affiliation_action='NOT_RUN', + invite_action='NOT_RUN', + action_detail='profile_missing' + ) + continue + + futures.append( + perform_auth_create_for_corp.submit( + config, + corp_num, + profile, + [], + plan, + token + ) + ) + + wait(futures) + + for f in futures: + res = f.result() + failed = res.get('contact_action') == 'FAILED' + status = ProcessingStatuses.FAILED if failed else ProcessingStatuses.COMPLETED + + tracking.update_corp_status( + flow_run_id, + res['corp_num'], + status, + error=res.get('error'), + entity_action=res.get('entity_action'), + contact_action=res.get('contact_action'), + affiliation_action=res.get('affiliation_action'), + invite_action=res.get('invite_action'), + action_detail=res.get('action_detail') + ) + + if status == ProcessingStatuses.FAILED: + total_failed += 1 + else: + total_completed += 1 + + cnt += 1 + print(f'🌟 Complete round {cnt}/{batches}. Completed={total_completed}, Failed={total_failed}') + + if total_failed > 0: + return Failed(message=f'{total_failed} corps failed in {FLOW_NAME}.') + + print(f'🌰 {FLOW_NAME} complete. Completed={total_completed}, Failed={total_failed}') + + +if __name__ == '__main__': + auth_contact_flow() diff --git a/data-tool/flows/auth/auth_create_flow.py b/data-tool/flows/auth/auth_create_flow.py new file mode 100644 index 0000000000..6bcfe41e1b --- /dev/null +++ b/data-tool/flows/auth/auth_create_flow.py @@ -0,0 +1,260 @@ +import math +import os +from typing import Dict, List + +from prefect import flow +from prefect.context import get_run_context +from prefect.futures import wait +from prefect.states import Failed +from prefect.task_runners import ConcurrentTaskRunner +from sqlalchemy import text + +from common.extract_tracking_service import ExtractTrackingService, ProcessingStatuses +from common.init_utils import colin_extract_init, get_config +from common.query_utils import convert_result_set_to_dict + +from .auth_models import AuthCreatePlan, AuthSelectionMode +from .auth_queries import ( + get_auth_business_profiles_query, + get_auth_reservable_corps_query, + get_auth_reservable_count_query, +) +from .auth_tasks import get_auth_token, parse_accounts_csv, perform_auth_create_for_corp + +FLOW_NAME = 'auth-create-flow' + + +def _get_max_workers() -> int: + try: + v = int(os.getenv('AUTH_MAX_WORKERS', '50')) + return v if v > 0 else 50 + except Exception: + return 50 + + +def _parse_selection_mode(config) -> AuthSelectionMode: + raw = (getattr(config, 'AUTH_SELECTION_MODE', 'MIGRATION_FILTER') or 'MIGRATION_FILTER').strip().upper() + try: + return AuthSelectionMode(raw) + except Exception as e: + raise ValueError(f'Unknown AUTH_SELECTION_MODE: {raw}') from e + + +def _fetch_profiles(colin_engine, corp_nums: List[str], suffix: str) -> Dict[str, dict]: + if not corp_nums: + return {} + sql = get_auth_business_profiles_query(corp_nums, suffix or '') + with colin_engine.connect() as conn: + rs = conn.execute(text(sql)) + rows = convert_result_set_to_dict(rs) + return {r['identifier']: r for r in rows} + + +@flow( + name='Auth-Create-Flow', + log_prints=True, + persist_result=False, + task_runner=ConcurrentTaskRunner(max_workers=_get_max_workers()) +) +def auth_create_flow(): + """ + Create/ensure entities, optionally upsert contact, optionally create affiliations, + optionally send unaffiliated invite (mutually exclusive with affiliations). + + Selection excludes any corp already tracked in auth_processing for (corp_num, FLOW_NAME, environment). + """ + config = get_config() + colin_engine = colin_extract_init(config) + + selection_mode = _parse_selection_mode(config) + + plan = AuthCreatePlan( + create_entity=bool(getattr(config, 'AUTH_CREATE_ENTITY', True)), + upsert_contact=bool(getattr(config, 'AUTH_UPSERT_CONTACT', False)), + create_affiliations=bool(getattr(config, 'AUTH_CREATE_AFFILIATIONS', False)), + send_unaffiliated_invite=bool(getattr(config, 'AUTH_SEND_UNAFFILIATED_EMAIL', False)), + fail_if_missing_email=bool(getattr(config, 'AUTH_FAIL_IF_MISSING_EMAIL', False)), + dry_run=bool(getattr(config, 'AUTH_DRY_RUN', False)), + ) + + if plan.create_affiliations and plan.send_unaffiliated_invite: + raise ValueError('Invalid plan: cannot both create affiliations and send unaffiliated invite') + + # Count reservable + count_sql = get_auth_reservable_count_query( + flow_name=FLOW_NAME, + config=config, + selection_mode=selection_mode + ) + with colin_engine.connect() as conn: + total_reservable = int(conn.execute(text(count_sql)).scalar() or 0) + + if total_reservable <= 0: + print('No reservable corps found for this run.') + return + + # Throughput config + if getattr(config, 'AUTH_BATCHES', 0) <= 0: + raise ValueError('AUTH_BATCHES must be explicitly set to a positive integer') + if getattr(config, 'AUTH_BATCH_SIZE', 0) <= 0: + raise ValueError('AUTH_BATCH_SIZE must be explicitly set to a positive integer') + + batch_size = config.AUTH_BATCH_SIZE + max_corps = min(total_reservable, config.AUTH_BATCHES * config.AUTH_BATCH_SIZE) + + flow_run_id = get_run_context().flow_run.id + + tracking = ExtractTrackingService( + config.DATA_LOAD_ENV, + colin_engine, + FLOW_NAME, + table_name='auth_processing', + statement_timeout_ms=getattr(config, 'RESERVE_STATEMENT_TIMEOUT_MS', None) + ) + + include_account_ids = bool(plan.create_affiliations) + include_contact_email = bool(plan.upsert_contact or plan.send_unaffiliated_invite or plan.fail_if_missing_email) + + extra_insert_cols: List[str] = [] + if include_account_ids: + extra_insert_cols.append('account_ids') + if include_contact_email: + extra_insert_cols.append('contact_email') + + base_query = get_auth_reservable_corps_query( + flow_name=FLOW_NAME, + config=config, + batch_size=max_corps, + selection_mode=selection_mode, + include_account_ids=include_account_ids, + include_contact_email=include_contact_email + ) + + fallback_accounts = config.AFFILIATE_ENTITY_ACCOUNT_IDS_CSV if include_account_ids else None + reserved = tracking.reserve_for_flow( + base_query=base_query, + flow_run_id=flow_run_id, + extra_insert_cols=extra_insert_cols or None, + fallback_account_ids=fallback_accounts + ) + + if reserved <= 0: + print('No corps reserved (cohort may be exhausted or already reserved).') + return + + batches = min(math.ceil(reserved / batch_size), config.AUTH_BATCHES) + + print(f'👷 Auth create plan: {plan}') + print(f'👷 Reservable={total_reservable}, Reserved={reserved}, Batches={batches}, BatchSize={batch_size}') + print(f'👷 SelectionMode={selection_mode.value}, DryRun={plan.dry_run}') + + cnt = 0 + total_failed = 0 + total_completed = 0 + + while cnt < batches: + claimed = tracking.claim_batch( + flow_run_id, + batch_size, + extra_return_cols=extra_insert_cols or None, + as_dict=True + ) + if not claimed: + print('No more corps available to claim') + break + + corp_nums = [r['corp_num'] for r in claimed] + corp_accounts = {r['corp_num']: (r.get('account_ids') or None) for r in claimed} if include_account_ids else {} + + profiles = _fetch_profiles(colin_engine, corp_nums, getattr(config, 'CORP_NAME_SUFFIX', '') or '') + + try: + token = get_auth_token(config) + except Exception as e: + err = f'Failed to obtain auth token: {repr(e)}' + print(f'❌ {err}') + for corp_num in corp_nums: + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error=err, + entity_action='FAILED' if plan.create_entity else 'NOT_RUN', + contact_action='FAILED' if plan.upsert_contact else 'NOT_RUN', + affiliation_action='FAILED' if plan.create_affiliations else 'NOT_RUN', + invite_action='FAILED' if plan.send_unaffiliated_invite else 'NOT_RUN', + action_detail='token_error' + ) + return Failed(message=err) + + futures = [] + for corp_num in corp_nums: + profile = profiles.get(corp_num) + if not profile: + total_failed += 1 + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error='Missing business profile for corp in COLIN extract', + entity_action='FAILED' if plan.create_entity else 'NOT_RUN', + contact_action='FAILED' if plan.upsert_contact else 'NOT_RUN', + affiliation_action='FAILED' if plan.create_affiliations else 'NOT_RUN', + invite_action='FAILED' if plan.send_unaffiliated_invite else 'NOT_RUN', + action_detail='profile_missing' + ) + continue + + accounts = parse_accounts_csv(corp_accounts.get(corp_num)) if include_account_ids else [] + futures.append( + perform_auth_create_for_corp.submit( + config, + corp_num, + profile, + accounts, + plan, + token + ) + ) + + wait(futures) + + for f in futures: + res = f.result() + actions = [ + res.get('entity_action'), + res.get('contact_action'), + res.get('affiliation_action'), + res.get('invite_action'), + ] + failed = any(a == 'FAILED' for a in actions if a) + status = ProcessingStatuses.FAILED if failed else ProcessingStatuses.COMPLETED + + tracking.update_corp_status( + flow_run_id, + res['corp_num'], + status, + error=res.get('error'), + entity_action=res.get('entity_action'), + contact_action=res.get('contact_action'), + affiliation_action=res.get('affiliation_action'), + invite_action=res.get('invite_action'), + action_detail=res.get('action_detail') + ) + + if status == ProcessingStatuses.FAILED: + total_failed += 1 + else: + total_completed += 1 + + cnt += 1 + print(f'🌟 Complete round {cnt}/{batches}. Completed={total_completed}, Failed={total_failed}') + + if total_failed > 0: + return Failed(message=f'{total_failed} corps failed in {FLOW_NAME}.') + + print(f'🌰 {FLOW_NAME} complete. Completed={total_completed}, Failed={total_failed}') + + +if __name__ == '__main__': + auth_create_flow() diff --git a/data-tool/flows/auth/auth_delete_flow.py b/data-tool/flows/auth/auth_delete_flow.py new file mode 100644 index 0000000000..3632cf5307 --- /dev/null +++ b/data-tool/flows/auth/auth_delete_flow.py @@ -0,0 +1,229 @@ +import math +import os +from typing import List + +from prefect import flow +from prefect.context import get_run_context +from prefect.futures import wait +from prefect.states import Failed +from prefect.task_runners import ConcurrentTaskRunner +from sqlalchemy import text + +from common.extract_tracking_service import ExtractTrackingService, ProcessingStatuses +from common.init_utils import colin_extract_init, get_config + +from .auth_models import AuthDeletePlan, AuthSelectionMode +from .auth_queries import ( + get_auth_reservable_corps_query, + get_auth_reservable_count_query, +) +from .auth_tasks import get_auth_token, parse_accounts_csv, perform_auth_delete_for_corp + +FLOW_NAME = 'auth-delete-flow' + + +def _get_max_workers() -> int: + try: + v = int(os.getenv('AUTH_MAX_WORKERS', '50')) + return v if v > 0 else 50 + except Exception: + return 50 + + +def _parse_selection_mode(config) -> AuthSelectionMode: + raw = (getattr(config, 'AUTH_SELECTION_MODE', 'MIGRATION_FILTER') or 'MIGRATION_FILTER').strip().upper() + try: + return AuthSelectionMode(raw) + except Exception as e: + raise ValueError(f'Unknown AUTH_SELECTION_MODE: {raw}') from e + + +@flow( + name='Auth-Delete-Flow', + log_prints=True, + persist_result=False, + task_runner=ConcurrentTaskRunner(max_workers=_get_max_workers()) +) +def auth_delete_flow(): + """ + Delete affiliations (optional) and delete entity (optional). + + SAFETY: If AUTH_REQUIRE_CONFIRMATION is True, AUTH_CONFIRMATION_TOKEN must be set (non-empty), + or the flow will fail fast before reserving/claiming. + + Selection excludes any corp already tracked in auth_processing for (corp_num, FLOW_NAME, environment). + """ + config = get_config() + colin_engine = colin_extract_init(config) + + # Safety gate + if bool(getattr(config, 'AUTH_REQUIRE_CONFIRMATION', False)): + if not (getattr(config, 'AUTH_CONFIRMATION_TOKEN', '') or '').strip(): + raise ValueError('AUTH_REQUIRE_CONFIRMATION is True but AUTH_CONFIRMATION_TOKEN is not set.') + print('🛑 Delete confirmation token is present (value not displayed). Proceeding.') + + selection_mode = _parse_selection_mode(config) + + plan = AuthDeletePlan( + delete_affiliations=bool(getattr(config, 'AUTH_DELETE_AFFILIATIONS', False)), + delete_entity=bool(getattr(config, 'AUTH_DELETE_ENTITY', False)), + delete_invites=bool(getattr(config, 'AUTH_DELETE_INVITES', False)), + dry_run=bool(getattr(config, 'AUTH_DRY_RUN', False)), + ) + + # Count reservable + count_sql = get_auth_reservable_count_query( + flow_name=FLOW_NAME, + config=config, + selection_mode=selection_mode + ) + with colin_engine.connect() as conn: + total_reservable = int(conn.execute(text(count_sql)).scalar() or 0) + + if total_reservable <= 0: + print('No reservable corps found for this run.') + return + + # Throughput config + if getattr(config, 'AUTH_BATCHES', 0) <= 0: + raise ValueError('AUTH_BATCHES must be explicitly set to a positive integer') + if getattr(config, 'AUTH_BATCH_SIZE', 0) <= 0: + raise ValueError('AUTH_BATCH_SIZE must be explicitly set to a positive integer') + + batch_size = config.AUTH_BATCH_SIZE + max_corps = min(total_reservable, config.AUTH_BATCHES * config.AUTH_BATCH_SIZE) + + flow_run_id = get_run_context().flow_run.id + + tracking = ExtractTrackingService( + config.DATA_LOAD_ENV, + colin_engine, + FLOW_NAME, + table_name='auth_processing', + statement_timeout_ms=getattr(config, 'RESERVE_STATEMENT_TIMEOUT_MS', None) + ) + + include_account_ids = bool(plan.delete_affiliations) + + extra_insert_cols: List[str] = [] + if include_account_ids: + extra_insert_cols.append('account_ids') + + base_query = get_auth_reservable_corps_query( + flow_name=FLOW_NAME, + config=config, + batch_size=max_corps, + selection_mode=selection_mode, + include_account_ids=include_account_ids, + include_contact_email=False + ) + + fallback_accounts = config.AFFILIATE_ENTITY_ACCOUNT_IDS_CSV if include_account_ids else None + reserved = tracking.reserve_for_flow( + base_query=base_query, + flow_run_id=flow_run_id, + extra_insert_cols=extra_insert_cols or None, + fallback_account_ids=fallback_accounts + ) + + if reserved <= 0: + print('No corps reserved (cohort may be exhausted or already reserved).') + return + + batches = min(math.ceil(reserved / batch_size), config.AUTH_BATCHES) + + print(f'👷 Auth delete plan: {plan}') + print(f'👷 Reservable={total_reservable}, Reserved={reserved}, Batches={batches}, BatchSize={batch_size}') + print(f'👷 SelectionMode={selection_mode.value}, DryRun={plan.dry_run}') + + cnt = 0 + total_failed = 0 + total_completed = 0 + + while cnt < batches: + claimed = tracking.claim_batch( + flow_run_id, + batch_size, + extra_return_cols=extra_insert_cols or None, + as_dict=True + ) + if not claimed: + print('No more corps available to claim') + break + + corp_nums = [r['corp_num'] for r in claimed] + corp_accounts = {r['corp_num']: (r.get('account_ids') or None) for r in claimed} if include_account_ids else {} + + try: + token = get_auth_token(config) + except Exception as e: + err = f'Failed to obtain auth token: {repr(e)}' + print(f'❌ {err}') + for corp_num in corp_nums: + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error=err, + entity_action='FAILED' if plan.delete_entity else 'NOT_RUN', + contact_action='NOT_RUN', + affiliation_action='FAILED' if plan.delete_affiliations else 'NOT_RUN', + invite_action='FAILED' if plan.delete_invites else 'NOT_RUN', + action_detail='token_error' + ) + return Failed(message=err) + + futures = [] + for corp_num in corp_nums: + accounts = parse_accounts_csv(corp_accounts.get(corp_num)) if include_account_ids else [] + futures.append( + perform_auth_delete_for_corp.submit( + config, + corp_num, + accounts, + plan, + token + ) + ) + + wait(futures) + + for f in futures: + res = f.result() + actions = [ + res.get('entity_action'), + res.get('contact_action'), + res.get('affiliation_action'), + res.get('invite_action'), + ] + failed = any(a == 'FAILED' for a in actions if a) + status = ProcessingStatuses.FAILED if failed else ProcessingStatuses.COMPLETED + + tracking.update_corp_status( + flow_run_id, + res['corp_num'], + status, + error=res.get('error'), + entity_action=res.get('entity_action'), + contact_action=res.get('contact_action'), + affiliation_action=res.get('affiliation_action'), + invite_action=res.get('invite_action'), + action_detail=res.get('action_detail') + ) + + if status == ProcessingStatuses.FAILED: + total_failed += 1 + else: + total_completed += 1 + + cnt += 1 + print(f'🌟 Complete round {cnt}/{batches}. Completed={total_completed}, Failed={total_failed}') + + if total_failed > 0: + return Failed(message=f'{total_failed} corps failed in {FLOW_NAME}.') + + print(f'🌰 {FLOW_NAME} complete. Completed={total_completed}, Failed={total_failed}') + + +if __name__ == '__main__': + auth_delete_flow() diff --git a/data-tool/flows/auth/auth_invite_flow.py b/data-tool/flows/auth/auth_invite_flow.py new file mode 100644 index 0000000000..8d1f9e4156 --- /dev/null +++ b/data-tool/flows/auth/auth_invite_flow.py @@ -0,0 +1,242 @@ +import math +import os +from typing import Dict, List + +from prefect import flow +from prefect.context import get_run_context +from prefect.futures import wait +from prefect.states import Failed +from prefect.task_runners import ConcurrentTaskRunner +from sqlalchemy import text + +from common.extract_tracking_service import ExtractTrackingService, ProcessingStatuses +from common.init_utils import colin_extract_init, get_config +from common.query_utils import convert_result_set_to_dict + +from .auth_models import AuthCreatePlan, AuthSelectionMode +from .auth_queries import ( + get_auth_business_profiles_query, + get_auth_reservable_corps_query, + get_auth_reservable_count_query, +) +from .auth_tasks import get_auth_token, perform_auth_create_for_corp + +FLOW_NAME = 'auth-invite-flow' + + +def _get_max_workers() -> int: + try: + v = int(os.getenv('AUTH_MAX_WORKERS', '50')) + return v if v > 0 else 50 + except Exception: + return 50 + + +def _parse_selection_mode(config) -> AuthSelectionMode: + raw = (getattr(config, 'AUTH_SELECTION_MODE', 'MIGRATION_FILTER') or 'MIGRATION_FILTER').strip().upper() + try: + return AuthSelectionMode(raw) + except Exception as e: + raise ValueError(f'Unknown AUTH_SELECTION_MODE: {raw}') from e + + +def _fetch_profiles(colin_engine, corp_nums: List[str], suffix: str) -> Dict[str, dict]: + if not corp_nums: + return {} + sql = get_auth_business_profiles_query(corp_nums, suffix or '') + with colin_engine.connect() as conn: + rs = conn.execute(text(sql)) + rows = convert_result_set_to_dict(rs) + return {r['identifier']: r for r in rows} + + +@flow( + name='Auth-Invite-Flow', + log_prints=True, + persist_result=False, + task_runner=ConcurrentTaskRunner(max_workers=_get_max_workers()) +) +def auth_invite_flow(): + """ + Send unaffiliated invite for an entity. + + Optionally upserts contact email first (AUTH_UPSERT_CONTACT). + (Affiliations are mutually exclusive with invites; this flow does invites only.) + + Selection excludes any corp already tracked in auth_processing for (corp_num, FLOW_NAME, environment). + """ + config = get_config() + colin_engine = colin_extract_init(config) + selection_mode = _parse_selection_mode(config) + + plan = AuthCreatePlan( + create_entity=False, + upsert_contact=bool(getattr(config, 'AUTH_UPSERT_CONTACT', False)), + create_affiliations=False, + send_unaffiliated_invite=True, + fail_if_missing_email=bool(getattr(config, 'AUTH_FAIL_IF_MISSING_EMAIL', False)), + dry_run=bool(getattr(config, 'AUTH_DRY_RUN', False)), + ) + + count_sql = get_auth_reservable_count_query( + flow_name=FLOW_NAME, + config=config, + selection_mode=selection_mode + ) + with colin_engine.connect() as conn: + total_reservable = int(conn.execute(text(count_sql)).scalar() or 0) + + if total_reservable <= 0: + print('No reservable corps found for this run.') + return + + if getattr(config, 'AUTH_BATCHES', 0) <= 0: + raise ValueError('AUTH_BATCHES must be explicitly set to a positive integer') + if getattr(config, 'AUTH_BATCH_SIZE', 0) <= 0: + raise ValueError('AUTH_BATCH_SIZE must be explicitly set to a positive integer') + + batch_size = config.AUTH_BATCH_SIZE + max_corps = min(total_reservable, config.AUTH_BATCHES * config.AUTH_BATCH_SIZE) + + flow_run_id = get_run_context().flow_run.id + + tracking = ExtractTrackingService( + config.DATA_LOAD_ENV, + colin_engine, + FLOW_NAME, + table_name='auth_processing', + statement_timeout_ms=getattr(config, 'RESERVE_STATEMENT_TIMEOUT_MS', None) + ) + + extra_insert_cols = ['contact_email'] + + base_query = get_auth_reservable_corps_query( + flow_name=FLOW_NAME, + config=config, + batch_size=max_corps, + selection_mode=selection_mode, + include_account_ids=False, + include_contact_email=True + ) + + reserved = tracking.reserve_for_flow( + base_query=base_query, + flow_run_id=flow_run_id, + extra_insert_cols=extra_insert_cols + ) + + if reserved <= 0: + print('No corps reserved (cohort may be exhausted or already reserved).') + return + + batches = min(math.ceil(reserved / batch_size), config.AUTH_BATCHES) + + print(f'👷 Auth invite plan: {plan}') + print(f'👷 Reservable={total_reservable}, Reserved={reserved}, Batches={batches}, BatchSize={batch_size}') + print(f'👷 SelectionMode={selection_mode.value}, DryRun={plan.dry_run}') + + cnt = 0 + total_failed = 0 + total_completed = 0 + + while cnt < batches: + claimed = tracking.claim_batch( + flow_run_id, + batch_size, + extra_return_cols=extra_insert_cols, + as_dict=True + ) + if not claimed: + print('No more corps available to claim') + break + + corp_nums = [r['corp_num'] for r in claimed] + profiles = _fetch_profiles(colin_engine, corp_nums, getattr(config, 'CORP_NAME_SUFFIX', '') or '') + + try: + token = get_auth_token(config) + except Exception as e: + err = f'Failed to obtain auth token: {repr(e)}' + print(f'❌ {err}') + for corp_num in corp_nums: + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error=err, + entity_action='NOT_RUN', + contact_action='FAILED' if plan.upsert_contact else 'NOT_RUN', + affiliation_action='NOT_RUN', + invite_action='FAILED', + action_detail='token_error' + ) + return Failed(message=err) + + futures = [] + for corp_num in corp_nums: + profile = profiles.get(corp_num) + if not profile: + total_failed += 1 + tracking.update_corp_status( + flow_run_id, + corp_num, + ProcessingStatuses.FAILED, + error='Missing business profile for corp in COLIN extract', + entity_action='NOT_RUN', + contact_action='FAILED' if plan.upsert_contact else 'NOT_RUN', + affiliation_action='NOT_RUN', + invite_action='FAILED', + action_detail='profile_missing' + ) + continue + + futures.append( + perform_auth_create_for_corp.submit( + config, + corp_num, + profile, + [], + plan, + token + ) + ) + + wait(futures) + + for f in futures: + res = f.result() + actions = [ + res.get('contact_action'), + res.get('invite_action'), + ] + failed = any(a == 'FAILED' for a in actions if a) + status = ProcessingStatuses.FAILED if failed else ProcessingStatuses.COMPLETED + + tracking.update_corp_status( + flow_run_id, + res['corp_num'], + status, + error=res.get('error'), + entity_action=res.get('entity_action'), + contact_action=res.get('contact_action'), + affiliation_action=res.get('affiliation_action'), + invite_action=res.get('invite_action'), + action_detail=res.get('action_detail') + ) + + if status == ProcessingStatuses.FAILED: + total_failed += 1 + else: + total_completed += 1 + + cnt += 1 + print(f'🌟 Complete round {cnt}/{batches}. Completed={total_completed}, Failed={total_failed}') + + if total_failed > 0: + return Failed(message=f'{total_failed} corps failed in {FLOW_NAME}.') + + print(f'🌰 {FLOW_NAME} complete. Completed={total_completed}, Failed={total_failed}') + + +if __name__ == '__main__': + auth_invite_flow() diff --git a/data-tool/flows/auth/auth_models.py b/data-tool/flows/auth/auth_models.py new file mode 100644 index 0000000000..673d6afdff --- /dev/null +++ b/data-tool/flows/auth/auth_models.py @@ -0,0 +1,37 @@ +from dataclasses import dataclass +from enum import Enum + + +class AuthSelectionMode(str, Enum): + """How an auth flow selects candidate corps to process.""" + MANUAL = "MANUAL" + MIGRATION_FILTER = "MIGRATION_FILTER" + CORP_PROCESSING = "CORP_PROCESSING" + + +class AuthComponentStatus(str, Enum): + """Per-component outcome stored in auth_processing.""" + SUCCESS = "SUCCESS" + SKIPPED = "SKIPPED" + FAILED = "FAILED" + NOT_RUN = "NOT_RUN" + + +@dataclass(frozen=True) +class AuthCreatePlan: + """Immutable plan for create-like auth flows.""" + create_entity: bool = True + upsert_contact: bool = False + create_affiliations: bool = False + send_unaffiliated_invite: bool = False + fail_if_missing_email: bool = False + dry_run: bool = False + + +@dataclass(frozen=True) +class AuthDeletePlan: + """Immutable plan for delete-like auth flows.""" + delete_affiliations: bool = False + delete_entity: bool = False + delete_invites: bool = False # only if supported by API + dry_run: bool = False diff --git a/data-tool/flows/auth/auth_queries.py b/data-tool/flows/auth/auth_queries.py new file mode 100644 index 0000000000..b6d81f4d0e --- /dev/null +++ b/data-tool/flows/auth/auth_queries.py @@ -0,0 +1,317 @@ +from __future__ import annotations + +from typing import List, Sequence + +from .auth_models import AuthSelectionMode + +# Match the existing tombstone corp type cohort by default. +CORP_TYPE_FILTER = "('BC', 'C', 'ULC', 'CUL', 'CC', 'CCC', 'QA', 'QB', 'QC', 'QD', 'QE')" + + +def _escape_sql_literal(val: str) -> str: + """Escape a value for safe embedding in a SQL single-quoted literal.""" + return (val or "").replace("'", "''") + + +def _quote_list(values: Sequence[str]) -> str: + """Quote/escape a list of values for SQL IN (...) lists.""" + return ", ".join([f"'{_escape_sql_literal(v)}'" for v in values]) + + +def _parse_corp_nums_csv(csv_val: str | None) -> List[str]: + if not csv_val: + return [] + parts: List[str] = [] + for tok in str(csv_val).split(','): + t = tok.strip().upper() + if t: + parts.append(t) + # Deduplicate while preserving order + seen = set() + out: List[str] = [] + for t in parts: + if t not in seen: + seen.add(t) + out.append(t) + return out + + +def get_auth_reservable_corps_query( + *, + flow_name: str, + config, + batch_size: int, + selection_mode: AuthSelectionMode, + include_account_ids: bool = False, + include_contact_email: bool = False, +) -> str: + """ + Build SQL to select corps eligible to be reserved for an auth flow. + + Contract: Must SELECT at least: + - corp_num + - corp_type_cd + - mig_batch_id + + Optionally selects: + - account_ids (CSV string) + - contact_email + + IMPORTANT: Always excludes corps already present in auth_processing for the same + (corp_num, flow_name, environment). + """ + environment = getattr(config, 'DATA_LOAD_ENV', '') + mig_group_ids = getattr(config, 'MIG_GROUP_IDS', None) + mig_batch_ids = getattr(config, 'MIG_BATCH_IDS', None) + source_flow = getattr(config, 'AUTH_SOURCE_FLOW_NAME', 'tombstone-flow') + + # Optional select columns + account_map_cte = "" + account_join = "" + account_select = "" + if include_account_ids: + if selection_mode == AuthSelectionMode.MIGRATION_FILTER: + # account_map is expensive; only include when requested. + account_map_cte = f""" + WITH account_map AS ( + SELECT mca.corp_num, + array_to_string(array_agg(DISTINCT mca.account_id ORDER BY mca.account_id), ',') AS account_ids + FROM mig_corp_account mca + JOIN mig_batch b2 ON b2.id = mca.mig_batch_id + WHERE mca.target_environment = '{_escape_sql_literal(environment)}' + {f" AND b2.id IN ({mig_batch_ids})" if mig_batch_ids else ""} + {f" AND b2.mig_group_id IN ({mig_group_ids})" if mig_group_ids else ""} + GROUP BY mca.corp_num + ) + """ + account_join = "LEFT JOIN account_map am ON am.corp_num = c.corp_num" + account_select = ", COALESCE(am.account_ids, NULL::varchar(100)) AS account_ids" + elif selection_mode == AuthSelectionMode.CORP_PROCESSING: + account_select = ", cp.account_ids AS account_ids" + else: + # MANUAL + account_select = ", NULL::varchar(100) AS account_ids" + + contact_select = ", c.admin_email AS contact_email" if include_contact_email else "" + + ap_join = f""" + LEFT JOIN auth_processing ap + ON ap.corp_num = c.corp_num + AND ap.flow_name = '{_escape_sql_literal(flow_name)}' + AND ap.environment = '{_escape_sql_literal(environment)}' + """ + + # MIG filters for MIGRATION_FILTER mode + mig_extra_where = "" + if selection_mode == AuthSelectionMode.MIGRATION_FILTER: + if mig_batch_ids: + mig_extra_where += f" AND b.id IN ({mig_batch_ids})" + if mig_group_ids: + mig_extra_where += f" AND g.id IN ({mig_group_ids})" + + if selection_mode == AuthSelectionMode.MIGRATION_FILTER: + query = f""" + {account_map_cte} + SELECT + c.corp_num, + c.corp_type_cd, + b.id AS mig_batch_id + {account_select} + {contact_select} + FROM mig_corp_batch mcb + JOIN mig_batch b ON b.id = mcb.mig_batch_id + JOIN mig_group g ON g.id = b.mig_group_id + JOIN corporation c ON c.corp_num = mcb.corp_num + JOIN corp_state cs + ON cs.corp_num = c.corp_num + AND cs.end_event_id IS NULL + {account_join} + {ap_join} + WHERE 1=1 + {mig_extra_where} + AND c.corp_type_cd IN {CORP_TYPE_FILTER} + AND ap.corp_num IS NULL + LIMIT {int(batch_size)} + """ + return query + + if selection_mode == AuthSelectionMode.CORP_PROCESSING: + query = f""" + SELECT + cp.corp_num, + c.corp_type_cd, + cp.mig_batch_id AS mig_batch_id + {account_select} + {contact_select} + FROM corp_processing cp + JOIN corporation c ON c.corp_num = cp.corp_num + JOIN corp_state cs + ON cs.corp_num = c.corp_num + AND cs.end_event_id IS NULL + {ap_join} + WHERE 1=1 + AND cp.flow_name = '{_escape_sql_literal(source_flow)}' + AND cp.environment = '{_escape_sql_literal(environment)}' + AND cp.processed_status IN ('COMPLETED', 'PARTIAL') + AND c.corp_type_cd IN {CORP_TYPE_FILTER} + AND ap.corp_num IS NULL + LIMIT {int(batch_size)} + """ + return query + + # MANUAL + corp_nums = _parse_corp_nums_csv(getattr(config, 'AUTH_CORP_NUMS', '')) + corp_filter = f"AND c.corp_num IN ({_quote_list(corp_nums)})" if corp_nums else "AND 1=0" + + query = f""" + SELECT + c.corp_num, + c.corp_type_cd, + NULL::integer AS mig_batch_id + {account_select} + {contact_select} + FROM corporation c + JOIN corp_state cs + ON cs.corp_num = c.corp_num + AND cs.end_event_id IS NULL + {ap_join} + WHERE 1=1 + {corp_filter} + AND c.corp_type_cd IN {CORP_TYPE_FILTER} + AND ap.corp_num IS NULL + LIMIT {int(batch_size)} + """ + return query + + +def get_auth_reservable_count_query( + *, + flow_name: str, + config, + selection_mode: AuthSelectionMode, +) -> str: + """ + Count corps eligible to be reserved for this auth flow. + + Must match get_auth_reservable_corps_query(...) filters, including: + - selection_mode logic + - MIG filters (when enabled) + - corp type filter + - corp_state current row (end_event_id IS NULL) + - exclusion of existing auth_processing rows for (corp_num, flow_name, environment) + """ + environment = getattr(config, 'DATA_LOAD_ENV', '') + mig_group_ids = getattr(config, 'MIG_GROUP_IDS', None) + mig_batch_ids = getattr(config, 'MIG_BATCH_IDS', None) + source_flow = getattr(config, 'AUTH_SOURCE_FLOW_NAME', 'tombstone-flow') + + ap_join = f""" + LEFT JOIN auth_processing ap + ON ap.corp_num = c.corp_num + AND ap.flow_name = '{_escape_sql_literal(flow_name)}' + AND ap.environment = '{_escape_sql_literal(environment)}' + """ + + mig_extra_where = "" + if selection_mode == AuthSelectionMode.MIGRATION_FILTER: + if mig_batch_ids: + mig_extra_where += f" AND b.id IN ({mig_batch_ids})" + if mig_group_ids: + mig_extra_where += f" AND g.id IN ({mig_group_ids})" + + if selection_mode == AuthSelectionMode.MIGRATION_FILTER: + return f""" + SELECT count(*) + FROM mig_corp_batch mcb + JOIN mig_batch b ON b.id = mcb.mig_batch_id + JOIN mig_group g ON g.id = b.mig_group_id + JOIN corporation c ON c.corp_num = mcb.corp_num + JOIN corp_state cs + ON cs.corp_num = c.corp_num + AND cs.end_event_id IS NULL + {ap_join} + WHERE 1=1 + {mig_extra_where} + AND c.corp_type_cd IN {CORP_TYPE_FILTER} + AND ap.corp_num IS NULL + """ + + if selection_mode == AuthSelectionMode.CORP_PROCESSING: + return f""" + SELECT count(*) + FROM corp_processing cp + JOIN corporation c ON c.corp_num = cp.corp_num + JOIN corp_state cs + ON cs.corp_num = c.corp_num + AND cs.end_event_id IS NULL + {ap_join} + WHERE 1=1 + AND cp.flow_name = '{_escape_sql_literal(source_flow)}' + AND cp.environment = '{_escape_sql_literal(environment)}' + AND cp.processed_status IN ('COMPLETED', 'PARTIAL') + AND c.corp_type_cd IN {CORP_TYPE_FILTER} + AND ap.corp_num IS NULL + """ + + corp_nums = _parse_corp_nums_csv(getattr(config, 'AUTH_CORP_NUMS', '')) + corp_filter = f"AND c.corp_num IN ({_quote_list(corp_nums)})" if corp_nums else "AND 1=0" + return f""" + SELECT count(*) + FROM corporation c + JOIN corp_state cs + ON cs.corp_num = c.corp_num + AND cs.end_event_id IS NULL + {ap_join} + WHERE 1=1 + {corp_filter} + AND c.corp_type_cd IN {CORP_TYPE_FILTER} + AND ap.corp_num IS NULL + """ + + +def get_auth_business_profiles_query(corp_nums: List[str], suffix: str) -> str: + """ + Batch-friendly business profile lookup for Auth API actions. + + Returns one row per corp_num with: + - identifier (corp_num) + - legal_name (current CO/NB corp_name + suffix) + - legal_type (corp_type_cd) + - admin_email (corporation.admin_email) + - pass_code (corporation.corp_password) + + NOTE: Callers must never log or persist pass_code. + """ + if not corp_nums: + return """ + SELECT + NULL::varchar(10) AS identifier, + NULL::varchar(150) AS legal_name, + NULL::varchar(3) AS legal_type, + NULL::varchar(254) AS admin_email, + NULL::varchar(300) AS pass_code + WHERE 1=0; + """ + + corp_nums_up = [c.strip().upper() for c in corp_nums if c and c.strip()] + corp_nums_sql = _quote_list(corp_nums_up) + suffix_sql = _escape_sql_literal(suffix or "") + + return f""" + SELECT DISTINCT ON (c.corp_num) + c.corp_num AS identifier, + (COALESCE(NULLIF(trim(cn.corp_name), ''), c.corp_num) || '{suffix_sql}') AS legal_name, + c.corp_type_cd AS legal_type, + c.admin_email AS admin_email, + c.corp_password AS pass_code + FROM corporation c + LEFT JOIN corp_name cn + ON cn.corp_num = c.corp_num + AND cn.end_event_id IS NULL + AND cn.corp_name_typ_cd IN ('CO', 'NB') + WHERE c.corp_num IN ({corp_nums_sql}) + ORDER BY + c.corp_num, + CASE cn.corp_name_typ_cd WHEN 'CO' THEN 0 WHEN 'NB' THEN 1 ELSE 2 END, + cn.start_event_id DESC NULLS LAST; + """ diff --git a/data-tool/flows/auth/auth_tasks.py b/data-tool/flows/auth/auth_tasks.py new file mode 100644 index 0000000000..3592cfd2f8 --- /dev/null +++ b/data-tool/flows/auth/auth_tasks.py @@ -0,0 +1,369 @@ +from __future__ import annotations + +from http import HTTPStatus +from typing import Any, Dict, List + +from prefect import task +from prefect.cache_policies import NO_CACHE + +from common.auth_service import AuthService +from .auth_models import AuthComponentStatus, AuthCreatePlan, AuthDeletePlan + + +def parse_accounts_csv(csv_val: str | None) -> List[int]: + """Parse a CSV list of account IDs into a sorted, de-duped list of ints.""" + if not csv_val: + return [] + out: List[int] = [] + for tok in str(csv_val).split(','): + t = tok.strip() + if t.isdigit(): + out.append(int(t)) + return sorted(set(out)) + + +def parse_corp_nums_csv(csv_val: str | None) -> List[str]: + """Parse a CSV list of corp numbers into a de-duped list (preserving order).""" + if not csv_val: + return [] + parts: List[str] = [] + for tok in str(csv_val).replace('\n', ',').split(','): + t = tok.strip().upper() + if t: + parts.append(t) + seen = set() + out: List[str] = [] + for t in parts: + if t not in seen: + seen.add(t) + out.append(t) + return out + + +def _status_code(val: Any) -> int: + try: + return int(val) + except Exception: + return 0 + + +def _truncate(val: str, max_len: int) -> str: + if val is None: + return '' + if len(val) <= max_len: + return val + return val[: max_len - 3] + '...' + + +@task(cache_policy=NO_CACHE) +def get_auth_token(config) -> str: + """Get one bearer token to reuse for an entire claimed batch.""" + token = AuthService.get_bearer_token(config) + if not token: + raise Exception("Unable to obtain auth token") + return token + + +@task(cache_policy=NO_CACHE) +def perform_auth_create_for_corp( + config, + corp_num: str, + profile: Dict[str, Any], + account_ids: List[int], + plan: AuthCreatePlan, + token: str +) -> Dict[str, Any]: + """ + Execute auth create-like operations for a single corp based on plan. + + Returns a dict suitable for persisting to auth_processing: + - entity_action/contact_action/affiliation_action/invite_action + - action_detail (short) + - error (short, safe) + """ + entity_action = AuthComponentStatus.NOT_RUN + contact_action = AuthComponentStatus.NOT_RUN + affiliation_action = AuthComponentStatus.NOT_RUN + invite_action = AuthComponentStatus.NOT_RUN + + detail_parts: List[str] = [] + errors: List[str] = [] + + try: + identifier = (profile or {}).get('identifier') or corp_num + legal_name = (profile or {}).get('legal_name') or identifier + legal_type = (profile or {}).get('legal_type') or (profile or {}).get('corp_type_cd') or '' + + # NEVER log or persist pass_code (secret). + pass_code = (profile or {}).get('pass_code') or '' + if getattr(config, 'USE_CUSTOM_PASSCODE', False) and getattr(config, 'CUSTOM_PASSCODE', ''): + pass_code = getattr(config, 'CUSTOM_PASSCODE') + + # Determine contact email (safe to store) + email = (profile or {}).get('admin_email') or (profile or {}).get('contact_email') or '' + if getattr(config, 'USE_CUSTOM_CONTACT_EMAIL', False) and getattr(config, 'CUSTOM_CONTACT_EMAIL', ''): + email = getattr(config, 'CUSTOM_CONTACT_EMAIL') + + # Mutual exclusion: affiliations vs invite + if plan.create_affiliations and plan.send_unaffiliated_invite: + affiliation_action = AuthComponentStatus.FAILED + invite_action = AuthComponentStatus.FAILED + errors.append('plan_conflict_affiliations_vs_invite') + detail_parts.append('plan:invalid(affiliations+invite)') + return { + 'corp_num': corp_num, + 'entity_action': entity_action.value, + 'contact_action': contact_action.value, + 'affiliation_action': affiliation_action.value, + 'invite_action': invite_action.value, + 'action_detail': _truncate('; '.join(detail_parts), 2000), + 'error': _truncate('; '.join(errors), 1000) or None + } + + # 1) Create entity + if plan.create_entity: + if plan.dry_run: + entity_action = AuthComponentStatus.SKIPPED + detail_parts.append('entity:DRY_RUN') + else: + status = AuthService.create_entity( + config=config, + business_registration=identifier, + business_name=legal_name, + corp_type_code=legal_type, + pass_code=pass_code, + token=token + ) + code = _status_code(status) + detail_parts.append(f'entity:{code}') + if code == int(HTTPStatus.OK): + entity_action = AuthComponentStatus.SUCCESS + else: + entity_action = AuthComponentStatus.FAILED + errors.append(f'create_entity:{code}') + + # 2) Upsert contact + if plan.upsert_contact: + if not email: + detail_parts.append('contact:missing_email') + if plan.fail_if_missing_email: + contact_action = AuthComponentStatus.FAILED + errors.append('missing_email_for_contact') + else: + contact_action = AuthComponentStatus.SKIPPED + elif plan.dry_run: + contact_action = AuthComponentStatus.SKIPPED + detail_parts.append('contact:DRY_RUN') + else: + status = AuthService.update_contact_email( + config=config, + identifier=identifier, + email=email, + token=token + ) + code = _status_code(status) + detail_parts.append(f'contact:{code}') + if code == int(HTTPStatus.OK): + contact_action = AuthComponentStatus.SUCCESS + else: + contact_action = AuthComponentStatus.FAILED + errors.append(f'upsert_contact:{code}') + + # 3) Create affiliations + if plan.create_affiliations: + if not account_ids: + affiliation_action = AuthComponentStatus.SKIPPED + detail_parts.append('affiliations:no_accounts') + elif plan.dry_run: + affiliation_action = AuthComponentStatus.SKIPPED + detail_parts.append(f'affiliations:DRY_RUN({len(account_ids)})') + else: + ok = 0 + fail = 0 + for acct in sorted(set(account_ids)): + status = AuthService.create_affiliation( + config=config, + account=acct, + business_registration=identifier, + business_name=legal_name, + corp_type_code=legal_type, + pass_code=pass_code, + details={'identifier': identifier}, + token=token + ) + code = _status_code(status) + if code == int(HTTPStatus.OK): + ok += 1 + else: + fail += 1 + detail_parts.append(f'affiliations:ok{ok} fail{fail}') + if fail > 0: + affiliation_action = AuthComponentStatus.FAILED + errors.append(f'create_affiliations_failed:{fail}') + else: + affiliation_action = AuthComponentStatus.SUCCESS + + # 4) Send unaffiliated invite + if plan.send_unaffiliated_invite: + if not email: + detail_parts.append('invite:missing_email') + if plan.fail_if_missing_email: + invite_action = AuthComponentStatus.FAILED + errors.append('missing_email_for_invite') + else: + invite_action = AuthComponentStatus.SKIPPED + elif plan.dry_run: + invite_action = AuthComponentStatus.SKIPPED + detail_parts.append('invite:DRY_RUN') + else: + status = AuthService.send_unaffiliated_email( + config=config, + identifier=identifier, + email=email, + token=token + ) + code = _status_code(status) + detail_parts.append(f'invite:{code}') + if code == int(HTTPStatus.OK): + invite_action = AuthComponentStatus.SUCCESS + else: + invite_action = AuthComponentStatus.FAILED + errors.append(f'send_invite:{code}') + + except Exception as e: + detail_parts.append('exception') + errors.append(_truncate(repr(e), 900)) + if plan.create_entity and entity_action == AuthComponentStatus.NOT_RUN: + entity_action = AuthComponentStatus.FAILED + if plan.upsert_contact and contact_action == AuthComponentStatus.NOT_RUN: + contact_action = AuthComponentStatus.FAILED + if plan.create_affiliations and affiliation_action == AuthComponentStatus.NOT_RUN: + affiliation_action = AuthComponentStatus.FAILED + if plan.send_unaffiliated_invite and invite_action == AuthComponentStatus.NOT_RUN: + invite_action = AuthComponentStatus.FAILED + + action_detail = _truncate('; '.join(detail_parts), 2000) + error = _truncate('; '.join(errors), 1000) if errors else None + + return { + 'corp_num': corp_num, + 'entity_action': entity_action.value, + 'contact_action': contact_action.value, + 'affiliation_action': affiliation_action.value, + 'invite_action': invite_action.value, + 'action_detail': action_detail, + 'error': error + } + + +@task(cache_policy=NO_CACHE) +def perform_auth_delete_for_corp( + config, + corp_num: str, + account_ids: List[int], + plan: AuthDeletePlan, + token: str +) -> Dict[str, Any]: + """ + Execute auth delete-like operations for a single corp based on plan. + + IMPORTANT: Uses explicit delete primitives (no combined delete_affiliation()). + + Returns a dict suitable for persisting to auth_processing. + """ + identifier = corp_num + + entity_action = AuthComponentStatus.NOT_RUN + contact_action = AuthComponentStatus.NOT_RUN + affiliation_action = AuthComponentStatus.NOT_RUN + invite_action = AuthComponentStatus.NOT_RUN + + detail_parts: List[str] = [] + errors: List[str] = [] + + try: + # 1) Delete affiliations (best-effort across accounts) + if plan.delete_affiliations: + if not account_ids: + affiliation_action = AuthComponentStatus.SKIPPED + detail_parts.append('del_affiliations:no_accounts') + elif plan.dry_run: + affiliation_action = AuthComponentStatus.SKIPPED + detail_parts.append(f'del_affiliations:DRY_RUN({len(account_ids)})') + else: + ok = 0 + not_found = 0 + fail = 0 + for acct in sorted(set(account_ids)): + status = AuthService.delete_affiliation_only( + config=config, + account=acct, + identifier=identifier, + token=token + ) + code = _status_code(status) + if code in (int(HTTPStatus.OK), int(HTTPStatus.NO_CONTENT)): + ok += 1 + elif code == int(HTTPStatus.NOT_FOUND): + not_found += 1 + else: + fail += 1 + + detail_parts.append(f'del_affiliations:ok{ok} nf{not_found} fail{fail}') + if fail > 0: + affiliation_action = AuthComponentStatus.FAILED + errors.append(f'delete_affiliations_failed:{fail}') + elif ok > 0: + affiliation_action = AuthComponentStatus.SUCCESS + else: + affiliation_action = AuthComponentStatus.SKIPPED + + # 2) Delete invites (NOT supported by current client/API) + if plan.delete_invites: + invite_action = AuthComponentStatus.FAILED + detail_parts.append('del_invites:UNSUPPORTED') + errors.append('delete_invites_not_supported') + + # 3) Delete entity (after affiliations) + if plan.delete_entity: + if plan.dry_run: + entity_action = AuthComponentStatus.SKIPPED + detail_parts.append('del_entity:DRY_RUN') + else: + status = AuthService.delete_entity( + config=config, + identifier=identifier, + token=token + ) + code = _status_code(status) + detail_parts.append(f'del_entity:{code}') + if code in (int(HTTPStatus.OK), int(HTTPStatus.NO_CONTENT)): + entity_action = AuthComponentStatus.SUCCESS + elif code == int(HTTPStatus.NOT_FOUND): + entity_action = AuthComponentStatus.SKIPPED + else: + entity_action = AuthComponentStatus.FAILED + errors.append(f'delete_entity:{code}') + + except Exception as e: + detail_parts.append('exception') + errors.append(_truncate(repr(e), 900)) + if plan.delete_affiliations and affiliation_action == AuthComponentStatus.NOT_RUN: + affiliation_action = AuthComponentStatus.FAILED + if plan.delete_invites and invite_action == AuthComponentStatus.NOT_RUN: + invite_action = AuthComponentStatus.FAILED + if plan.delete_entity and entity_action == AuthComponentStatus.NOT_RUN: + entity_action = AuthComponentStatus.FAILED + + action_detail = _truncate('; '.join(detail_parts), 2000) + error = _truncate('; '.join(errors), 1000) if errors else None + + return { + 'corp_num': corp_num, + 'entity_action': entity_action.value, + 'contact_action': contact_action.value, + 'affiliation_action': affiliation_action.value, + 'invite_action': invite_action.value, + 'action_detail': action_detail, + 'error': error + } diff --git a/data-tool/flows/common/auth_service.py b/data-tool/flows/common/auth_service.py index f7a468f7e5..4480982541 100644 --- a/data-tool/flows/common/auth_service.py +++ b/data-tool/flows/common/auth_service.py @@ -1,26 +1,23 @@ import json from http import HTTPStatus -from typing import Dict +from typing import Optional import requests class AuthService: - """Wrapper to call Authentication Services. - """ + """Wrapper to call Authentication Services.""" BEARER: str = 'Bearer ' CONTENT_TYPE_JSON = {'Content-Type': 'application/json'} @classmethod - def get_time_out(cls, config): - try: - timeout = int(config.ACCOUNT_SVC_TIMEOUT, 20) - except Exception: - timeout = 20 + def get_time_out(cls, config) -> int: + """Return request timeout (seconds).""" + return getattr(config, 'ACCOUNT_SVC_TIMEOUT', None) or 20 @classmethod - def get_bearer_token(cls, config): + def get_bearer_token(cls, config) -> Optional[str]: """Get a valid Bearer token for the service to use.""" token_url = config.ACCOUNT_SVC_AUTH_URL client_id = config.ACCOUNT_SVC_CLIENT_ID @@ -29,36 +26,45 @@ def get_bearer_token(cls, config): data = 'grant_type=client_credentials' # get service account token - res = requests.post(url=token_url, - data=data, - headers={'content-type': 'application/x-www-form-urlencoded'}, - auth=(client_id, client_secret), - timeout=cls.get_time_out(config)) + res = requests.post( + url=token_url, + data=data, + headers={'content-type': 'application/x-www-form-urlencoded'}, + auth=(client_id, client_secret), + timeout=cls.get_time_out(config) + ) try: return res.json().get('access_token') except Exception: return None + @classmethod + def _resolve_token(cls, config, token: str | None) -> Optional[str]: + """Use provided token, or fetch a new one.""" + return token or cls.get_bearer_token(config) # pylint: disable=too-many-arguments, too-many-locals disable=invalid-name; @classmethod - def create_affiliation(cls, - config, - account: int, - business_registration: str, - business_name: str = None, - corp_type_code: str = 'TMP', - corp_sub_type_code: str = None, - pass_code: str = '', - details: dict = None): + def create_affiliation( + cls, + config, + account: int, + business_registration: str, + business_name: str = None, + corp_type_code: str = 'TMP', + corp_sub_type_code: str = None, + pass_code: str = '', + details: dict = None, + *, + token: str | None = None + ) -> HTTPStatus: """Affiliate a business to an account.""" auth_url = config.AUTH_SVC_URL account_svc_entity_url = f'{auth_url}/entities' account_svc_affiliate_url = f'{auth_url}/orgs/{account}/affiliations' - token = cls.get_bearer_token(config) - + token = cls._resolve_token(config, token) if not token: return HTTPStatus.UNAUTHORIZED @@ -90,6 +96,7 @@ def create_affiliation(cls, affiliate_data['passCode'] = pass_code if details: affiliate_data['entityDetails'] = details + affiliate = requests.post( url=account_svc_affiliate_url, headers={**cls.CONTENT_TYPE_JSON, @@ -98,30 +105,48 @@ def create_affiliation(cls, timeout=cls.get_time_out(config) ) - if ( - affiliate.status_code != HTTPStatus.CREATED - or entity_record.status_code not in (HTTPStatus.ACCEPTED, HTTPStatus.CREATED) - ): + # Be tolerant of idempotent responses (existing entity/affiliation). + entity_ok = entity_record.status_code in ( + HTTPStatus.ACCEPTED, + HTTPStatus.CREATED, + HTTPStatus.OK, + HTTPStatus.CONFLICT + ) + if entity_record.status_code == HTTPStatus.BAD_REQUEST and 'DATA_ALREADY_EXISTS' in entity_record.text: + entity_ok = True + + affiliate_ok = affiliate.status_code in ( + HTTPStatus.CREATED, + HTTPStatus.OK, + HTTPStatus.CONFLICT + ) + if affiliate.status_code == HTTPStatus.BAD_REQUEST and 'DATA_ALREADY_EXISTS' in affiliate.text: + affiliate_ok = True + + if not (entity_ok and affiliate_ok): return HTTPStatus.BAD_REQUEST + return HTTPStatus.OK @classmethod - def create_entity(cls, - config, - business_registration: str, - business_name: str, - corp_type_code: str, - pass_code: str = ''): - """Update an entity.""" + def create_entity( + cls, + config, + business_registration: str, + business_name: str, + corp_type_code: str, + pass_code: str = '', + *, + token: str | None = None + ) -> HTTPStatus: + """Create an entity.""" auth_url = config.AUTH_SVC_URL account_svc_entity_url = f'{auth_url}/entities' - token = cls.get_bearer_token(config) - + token = cls._resolve_token(config, token) if not token: return HTTPStatus.UNAUTHORIZED - # Create an entity record entity_data = { 'businessIdentifier': business_registration, 'corpTypeCode': corp_type_code, @@ -139,28 +164,31 @@ def create_entity(cls, timeout=cls.get_time_out(config) ) - if entity_record.status_code not in (HTTPStatus.ACCEPTED, HTTPStatus.CREATED): - return HTTPStatus.BAD_REQUEST - return HTTPStatus.OK - + if entity_record.status_code in (HTTPStatus.ACCEPTED, HTTPStatus.CREATED, HTTPStatus.OK, HTTPStatus.CONFLICT): + return HTTPStatus.OK + if entity_record.status_code == HTTPStatus.BAD_REQUEST and 'DATA_ALREADY_EXISTS' in entity_record.text: + return HTTPStatus.OK + return HTTPStatus.BAD_REQUEST @classmethod - def update_entity(cls, - config, - business_registration: str, - business_name: str, - corp_type_code: str, - state: str = None): + def update_entity( + cls, + config, + business_registration: str, + business_name: str, + corp_type_code: str, + state: str = None, + *, + token: str | None = None + ) -> HTTPStatus: """Update an entity.""" auth_url = config.AUTH_SVC_URL account_svc_entity_url = f'{auth_url}/entities' - token = cls.get_bearer_token(config) - + token = cls._resolve_token(config, token) if not token: return HTTPStatus.UNAUTHORIZED - # Create an entity record entity_data = { 'businessIdentifier': business_registration, 'corpTypeCode': corp_type_code, @@ -182,24 +210,22 @@ def update_entity(cls, return HTTPStatus.OK @classmethod - def delete_affiliation(cls, config, account: int, business_registration: str) -> Dict: - """Affiliate a business to an account. - - """ + def delete_affiliation(cls, config, account: int, business_registration: str, *, token: str | None = None) -> HTTPStatus: + """Legacy combined delete: deletes BOTH affiliation and entity (avoid in delete flows).""" auth_url = config.AUTH_SVC_URL account_svc_entity_url = f'{auth_url}/entities' account_svc_affiliate_url = f'{auth_url}/orgs/{account}/affiliations' - token = cls.get_bearer_token(config) + token = cls._resolve_token(config, token) + if not token: + return HTTPStatus.UNAUTHORIZED - # Delete an account:business affiliation affiliate = requests.delete( url=account_svc_affiliate_url + '/' + business_registration, headers={**cls.CONTENT_TYPE_JSON, 'Authorization': cls.BEARER + token}, timeout=cls.get_time_out(config) ) - # Delete an entity record entity_record = requests.delete( url=account_svc_entity_url + '/' + business_registration, headers={**cls.CONTENT_TYPE_JSON, @@ -207,19 +233,65 @@ def delete_affiliation(cls, config, account: int, business_registration: str) -> timeout=cls.get_time_out(config) ) - if affiliate.status_code != HTTPStatus.OK \ - or entity_record.status_code not in (HTTPStatus.OK, HTTPStatus.NO_CONTENT): + affiliate_ok = affiliate.status_code in (HTTPStatus.OK, HTTPStatus.NO_CONTENT, HTTPStatus.NOT_FOUND) + entity_ok = entity_record.status_code in (HTTPStatus.OK, HTTPStatus.NO_CONTENT, HTTPStatus.NOT_FOUND) + + if not (affiliate_ok and entity_ok): return HTTPStatus.BAD_REQUEST return HTTPStatus.OK @classmethod - def update_contact_email(cls, config, identifier: str, email: str) -> Dict: + def delete_entity(cls, config, identifier: str, *, token: str | None = None) -> HTTPStatus: + """Delete an entity ONLY.""" + auth_url = config.AUTH_SVC_URL + account_svc_entity_url = f'{auth_url}/entities' + + token = cls._resolve_token(config, token) + if not token: + return HTTPStatus.UNAUTHORIZED + + rv = requests.delete( + url=f'{account_svc_entity_url}/{identifier}', + headers={**cls.CONTENT_TYPE_JSON, + 'Authorization': cls.BEARER + token}, + timeout=cls.get_time_out(config) + ) + try: + return HTTPStatus(rv.status_code) + except Exception: + return HTTPStatus.BAD_REQUEST + + @classmethod + def delete_affiliation_only(cls, config, account: int, identifier: str, *, token: str | None = None) -> HTTPStatus: + """Delete an affiliation ONLY (does not delete the entity).""" + auth_url = config.AUTH_SVC_URL + account_svc_affiliate_url = f'{auth_url}/orgs/{account}/affiliations' + + token = cls._resolve_token(config, token) + if not token: + return HTTPStatus.UNAUTHORIZED + + rv = requests.delete( + url=f'{account_svc_affiliate_url}/{identifier}', + headers={**cls.CONTENT_TYPE_JSON, + 'Authorization': cls.BEARER + token}, + timeout=cls.get_time_out(config) + ) + try: + return HTTPStatus(rv.status_code) + except Exception: + return HTTPStatus.BAD_REQUEST + + @classmethod + def update_contact_email(cls, config, identifier: str, email: str, *, token: str | None = None) -> HTTPStatus: """Update contact email of the business.""" - token = cls.get_bearer_token(config) + token = cls._resolve_token(config, token) + if not token: + return HTTPStatus.UNAUTHORIZED + auth_url = config.AUTH_SVC_URL account_svc_entity_url = f'{auth_url}/entities' - # Create an entity record data = { 'email': email, 'phone': '', @@ -247,18 +319,25 @@ def update_contact_email(cls, config, identifier: str, email: str) -> Dict: timeout=cls.get_time_out(config) ) - if rv.status_code in (HTTPStatus.OK, HTTPStatus.CREATED): + if rv.status_code in (HTTPStatus.OK, HTTPStatus.CREATED, HTTPStatus.ACCEPTED): return HTTPStatus.OK - return rv.status_code + try: + return HTTPStatus(rv.status_code) + except Exception: + return HTTPStatus.BAD_REQUEST @classmethod - def send_unaffiliated_email(cls, config, identifier: str, email: str) -> Dict: - """Send unaffiliated email to the business.""" - token = cls.get_bearer_token(config) + def send_unaffiliated_email(cls, config, identifier: str, email: str, *, token: str | None = None) -> HTTPStatus: + """Send unaffiliated email/invite to the business (if supported by API).""" + token = cls._resolve_token(config, token) + if not token: + return HTTPStatus.UNAUTHORIZED + auth_url = config.AUTH_SVC_URL account_svc_affiliation_invitation_url = f'{auth_url}/affiliationInvitations' print(f'👷 Sending unaffiliated email to {email} for {identifier}...') + data = {} rv = requests.post( url=f'{account_svc_affiliation_invitation_url}/unaffiliated/{identifier}', @@ -269,8 +348,11 @@ def send_unaffiliated_email(cls, config, identifier: str, email: str) -> Dict: data=json.dumps(data), timeout=cls.get_time_out(config) ) - + if rv.status_code in (HTTPStatus.OK, HTTPStatus.CREATED): return HTTPStatus.OK - return rv.status_code + try: + return HTTPStatus(rv.status_code) + except Exception: + return HTTPStatus.BAD_REQUEST diff --git a/data-tool/flows/config.py b/data-tool/flows/config.py index e90243e433..4f0a6fdf46 100644 --- a/data-tool/flows/config.py +++ b/data-tool/flows/config.py @@ -29,6 +29,20 @@ load_dotenv(find_dotenv()) +def _get_int(name: str, default: int = 0) -> int: + """Safe int env parsing that avoids None.isnumeric() crashes.""" + val = os.getenv(name) + return int(val) if (val and val.isnumeric()) else default + + +def _get_bool(name: str, default: bool = False) -> bool: + """Safe bool env parsing (case-insensitive).""" + val = os.getenv(name) + if val is None: + return default + return val.strip().lower() == 'true' + + def get_named_config(config_name: str = 'production'): """Return the configuration object based on the name. @@ -134,23 +148,20 @@ class _Config(): # pylint: disable=too-few-public-methods ACCOUNT_SVC_AFFILIATE_URL = os.getenv('ACCOUNT_SVC_AFFILIATE_URL') ACCOUNT_SVC_CLIENT_ID = os.getenv('ACCOUNT_SVC_CLIENT_ID') ACCOUNT_SVC_CLIENT_SECRET = os.getenv('ACCOUNT_SVC_CLIENT_SECRET') - ACCOUNT_SVC_TIMEOUT = os.getenv('ACCOUNT_SVC_TIMEOUT') - ACCOUNT_SVC_TIMEOUT = int(ACCOUNT_SVC_TIMEOUT) if ACCOUNT_SVC_TIMEOUT.isnumeric() else None + ACCOUNT_SVC_TIMEOUT = _get_int('ACCOUNT_SVC_TIMEOUT', 0) + ACCOUNT_SVC_TIMEOUT = int(ACCOUNT_SVC_TIMEOUT) if ACCOUNT_SVC_TIMEOUT > 0 else None # batch delete flow - DELETE_BATCHES = os.getenv('DELETE_BATCHES') - DELETE_BATCHES = int(DELETE_BATCHES) if DELETE_BATCHES.isnumeric() else 0 - DELETE_BATCH_SIZE = os.getenv('DELETE_BATCH_SIZE') - DELETE_BATCH_SIZE = int(DELETE_BATCH_SIZE) if DELETE_BATCH_SIZE.isnumeric() else 0 + DELETE_BATCHES = _get_int('DELETE_BATCHES', 0) + DELETE_BATCH_SIZE = _get_int('DELETE_BATCH_SIZE', 0) - DELETE_AUTH_RECORDS = os.getenv('DELETE_AUTH_RECORDS').lower() == 'true' - DELETE_CORP_PROCESSING_RECORDS = os.getenv('DELETE_CORP_PROCESSING_RECORDS').lower() == 'true' + # Fix footgun: env vars may be unset + DELETE_AUTH_RECORDS = _get_bool('DELETE_AUTH_RECORDS', False) + DELETE_CORP_PROCESSING_RECORDS = _get_bool('DELETE_CORP_PROCESSING_RECORDS', False) # tombstone flow - TOMBSTONE_BATCHES = os.getenv('TOMBSTONE_BATCHES') - TOMBSTONE_BATCHES = int(TOMBSTONE_BATCHES) if TOMBSTONE_BATCHES.isnumeric() else 0 - TOMBSTONE_BATCH_SIZE = os.getenv('TOMBSTONE_BATCH_SIZE') - TOMBSTONE_BATCH_SIZE = int(TOMBSTONE_BATCH_SIZE) if TOMBSTONE_BATCH_SIZE.isnumeric() else 0 + TOMBSTONE_BATCHES = _get_int('TOMBSTONE_BATCHES', 0) + TOMBSTONE_BATCH_SIZE = _get_int('TOMBSTONE_BATCH_SIZE', 0) # reservation (reserve_for_flow) query statement timeout (Postgres statement_timeout, in ms). # When set, long-running reservation queries fail fast instead of tying up a worker indefinitely. @@ -158,17 +169,14 @@ class _Config(): # pylint: disable=too-few-public-methods RESERVE_STATEMENT_TIMEOUT_MS = int(RESERVE_STATEMENT_TIMEOUT_MS) if RESERVE_STATEMENT_TIMEOUT_MS.isnumeric() else None # verify flow - VERIFY_BATCHES = os.getenv('VERIFY_BATCHES') - VERIFY_BATCHES = int(VERIFY_BATCHES) if VERIFY_BATCHES.isnumeric() else 0 - VERIFY_BATCH_SIZE = os.getenv('VERIFY_BATCH_SIZE') - VERIFY_BATCH_SIZE = int(VERIFY_BATCH_SIZE) if VERIFY_BATCH_SIZE.isnumeric() else 0 + VERIFY_BATCHES = _get_int('VERIFY_BATCHES', 0) + VERIFY_BATCH_SIZE = _get_int('VERIFY_BATCH_SIZE', 0) VERIFY_SUMMARY_PATH = os.getenv('VERIFY_SUMMARY_PATH') # freeze flow - FREEZE_BATCHES = os.getenv('FREEZE_BATCHES') - FREEZE_BATCHES = int(FREEZE_BATCHES) if FREEZE_BATCHES.isnumeric() else 0 - FREEZE_BATCH_SIZE = os.getenv('FREEZE_BATCH_SIZE') - FREEZE_BATCH_SIZE = int(FREEZE_BATCH_SIZE) if FREEZE_BATCH_SIZE.isnumeric() else 0 + FREEZE_BATCHES = _get_int('FREEZE_BATCHES', 0) + FREEZE_BATCH_SIZE = _get_int('FREEZE_BATCH_SIZE', 0) + # ORACLE COLIN DB DB_USER_COLIN_ORACLE = os.getenv('DATABASE_USERNAME_COLIN_ORACLE', '') DB_PASSWORD_COLIN_ORACLE = os.getenv('DATABASE_PASSWORD_COLIN_ORACLE', '') @@ -189,6 +197,34 @@ class _Config(): # pylint: disable=too-few-public-methods MIG_GROUP_IDS = os.getenv('MIG_GROUP_IDS') MIG_BATCH_IDS = os.getenv('MIG_BATCH_IDS') + # ------------------------------------------------------------------------------------------ + # Auth-only flows (auth_processing tracking) + # ------------------------------------------------------------------------------------------ + # Selection + AUTH_SELECTION_MODE = os.getenv('AUTH_SELECTION_MODE', 'MIGRATION_FILTER') + AUTH_CORP_NUMS = os.getenv('AUTH_CORP_NUMS', '') + AUTH_SOURCE_FLOW_NAME = os.getenv('AUTH_SOURCE_FLOW_NAME', 'tombstone-flow') + + # Throughput + AUTH_BATCHES = _get_int('AUTH_BATCHES', 0) + AUTH_BATCH_SIZE = _get_int('AUTH_BATCH_SIZE', 0) + AUTH_MAX_WORKERS = _get_int('AUTH_MAX_WORKERS', 50) or 50 + + # Create plan + AUTH_CREATE_ENTITY = _get_bool('AUTH_CREATE_ENTITY', True) + AUTH_UPSERT_CONTACT = _get_bool('AUTH_UPSERT_CONTACT', False) + AUTH_CREATE_AFFILIATIONS = _get_bool('AUTH_CREATE_AFFILIATIONS', False) + AUTH_SEND_UNAFFILIATED_EMAIL = _get_bool('AUTH_SEND_UNAFFILIATED_EMAIL', False) + AUTH_FAIL_IF_MISSING_EMAIL = _get_bool('AUTH_FAIL_IF_MISSING_EMAIL', False) + AUTH_DRY_RUN = _get_bool('AUTH_DRY_RUN', False) + + # Delete plan + AUTH_DELETE_AFFILIATIONS = _get_bool('AUTH_DELETE_AFFILIATIONS', False) + AUTH_DELETE_ENTITY = _get_bool('AUTH_DELETE_ENTITY', False) + AUTH_DELETE_INVITES = _get_bool('AUTH_DELETE_INVITES', False) # unsupported unless API confirmed + AUTH_REQUIRE_CONFIRMATION = _get_bool('AUTH_REQUIRE_CONFIRMATION', False) + AUTH_CONFIRMATION_TOKEN = os.getenv('AUTH_CONFIRMATION_TOKEN', '') + TESTING = False DEBUG = False @@ -210,7 +246,6 @@ class TestConfig(_Config): # pylint: disable=too-few-public-methods TESTING = True - class ProdConfig(_Config): # pylint: disable=too-few-public-methods """Production environment configuration.""" diff --git a/data-tool/scripts/colin_corps_extract_postgres_ddl b/data-tool/scripts/colin_corps_extract_postgres_ddl index 31f5412d74..512ef219db 100644 --- a/data-tool/scripts/colin_corps_extract_postgres_ddl +++ b/data-tool/scripts/colin_corps_extract_postgres_ddl @@ -2,6 +2,10 @@ create sequence affiliation_processing_id_seq; alter sequence affiliation_processing_id_seq owner to postgres; +create sequence if not exists auth_processing_id_seq; + +alter sequence auth_processing_id_seq owner to postgres; + create sequence corp_processing_id_seq; alter sequence corp_processing_id_seq owner to postgres; @@ -792,6 +796,51 @@ create index if not exists idx_corp_processing_last_processed_event_id create index if not exists idx_corp_processing_flow_name on corp_processing (flow_name); +create table if not exists auth_processing +( + id integer default nextval('auth_processing_id_seq'::regclass) not null + constraint pk_auth_processing primary key, + + corp_num varchar(10) not null + constraint fk_auth_processing_corporation references corporation (corp_num), + corp_type_cd varchar(3), + mig_batch_id integer + constraint fk_auth_processing_batch references mig_batch, + + flow_name varchar(100) not null, + environment varchar(25) not null, + flow_run_id uuid, + processed_status varchar(25) not null, + + create_date timestamp with time zone default current_timestamp not null, + last_modified timestamp with time zone default current_timestamp not null, + claimed_at timestamp with time zone, + last_error varchar(1000), + + -- safe inputs (no secrets) + account_ids varchar(100), + contact_email varchar(254), + + -- outcomes per component (queryable) + entity_action varchar(25), + contact_action varchar(25), + affiliation_action varchar(25), + invite_action varchar(25), + + -- short structured summary, never secrets + action_detail varchar(2000), + + constraint unq_auth_processing unique (corp_num, flow_name, environment) +); + +alter table auth_processing owner to postgres; + +create index if not exists idx_auth_processing_claim_batch + on auth_processing (environment, flow_name, flow_run_id, processed_status, claimed_at); + +create index if not exists idx_auth_processing_flow_env_status + on auth_processing (flow_name, environment, processed_status, corp_num); + create table if not exists corp_restriction ( corp_num varchar(10) not null From 2878d887f50f6720745848ac095f0c64e1063597 Mon Sep 17 00:00:00 2001 From: meawong Date: Thu, 12 Mar 2026 14:23:20 -0700 Subject: [PATCH 18/46] 31793 - Add Canadian Postal Code Validation (#4145) * 31783 - Update common postal code validation and unit tests * 31793 - Update regex pattern and unit tests * 31783 - Update unit tests * 31783 - Update assert statements in test_validate_incorporation_address_whitespace * 31798 - Update COD tests * 31798 - lint fix * 31783 - Update regex patterns and tests * 31783 - Update validation error message + unit tests to reflect new message --- .../filings/validations/common_validations.py | 14 +++- .../test_validation_basic.py | 10 ++- .../validations/test_common_validations.py | 76 ++++++++++++++++++- .../test_incorporation_application.py | 12 ++- 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/legal-api/src/legal_api/services/filings/validations/common_validations.py b/legal-api/src/legal_api/services/filings/validations/common_validations.py index 69f86bf786..6d255dbc88 100644 --- a/legal-api/src/legal_api/services/filings/validations/common_validations.py +++ b/legal-api/src/legal_api/services/filings/validations/common_validations.py @@ -52,6 +52,11 @@ "postalCode", ) +CANADIAN_POSTAL_CODE_REGEX = re.compile( + r"^[ABCEGHJ-NPRSTVXY]\d[ABCEGHJ-NPRSTV-Z] ?\d[ABCEGHJ-NPRSTV-Z]\d$", + re.IGNORECASE +) + PARTY_NAME_MAX_LENGTH = 30 # Share structure constants @@ -789,7 +794,7 @@ def _validate_postal_code( address: dict, address_path: str ) -> dict: - """Validate that postal code is optional for specified country.""" + """Validate postal code presence and format for the given country.""" country = address["addressCountry"] postal_code = address.get("postalCode") try: @@ -798,6 +803,13 @@ def _validate_postal_code( not postal_code: return {"error": _("Postal code is required."), "path": f"{address_path}/postalCode"} + + # Canadian postal code format validation + if country == "CA" and postal_code and not CANADIAN_POSTAL_CODE_REGEX.match(postal_code): + return { + "error": _("Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'."), + "path": f"{address_path}/postalCode" + } except LookupError: # Different ISO-2 country validations are done at filing level, # this can be refactored into a common validator in the future diff --git a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_basic.py b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_basic.py index e78fe2cbad..9f3274725c 100644 --- a/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_basic.py +++ b/legal-api/tests/unit/services/filings/validations/change_of_director/test_validation_basic.py @@ -212,7 +212,9 @@ def test_validate_cod_basic(session, test_name, now, {"streetAddress": "456 B St", "addressCity": "Victoria", "addressCountry": "CA", "postalCode": "V8W1C2"}, HTTPStatus.BAD_REQUEST, [ {'error': 'postalCode cannot start or end with whitespace.', - 'path': '/filing/changeOfDirectors/directors/0/deliveryAddress/postalCode'} + 'path': '/filing/changeOfDirectors/directors/0/deliveryAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", + 'path': '/filing/changeOfDirectors/directors/0/deliveryAddress/postalCode'}, ] ), ( @@ -248,6 +250,8 @@ def test_validate_cod_basic(session, test_name, now, {"streetAddress": "456 B St", "addressCity": "Victoria", "addressCountry": "CA", "postalCode": " V8W1C2 "}, HTTPStatus.BAD_REQUEST, [ {'error': 'postalCode cannot start or end with whitespace.', + 'path': '/filing/changeOfDirectors/directors/0/mailingAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", 'path': '/filing/changeOfDirectors/directors/0/mailingAddress/postalCode'} ] ), @@ -258,7 +262,11 @@ def test_validate_cod_basic(session, test_name, now, HTTPStatus.BAD_REQUEST, [ {'error': 'postalCode cannot start or end with whitespace.', 'path': '/filing/changeOfDirectors/directors/0/deliveryAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", + 'path': '/filing/changeOfDirectors/directors/0/deliveryAddress/postalCode'}, {'error': 'postalCode cannot start or end with whitespace.', + 'path': '/filing/changeOfDirectors/directors/0/mailingAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", 'path': '/filing/changeOfDirectors/directors/0/mailingAddress/postalCode'} ] ), diff --git a/legal-api/tests/unit/services/filings/validations/test_common_validations.py b/legal-api/tests/unit/services/filings/validations/test_common_validations.py index abb05b1c3a..7dd8538180 100644 --- a/legal-api/tests/unit/services/filings/validations/test_common_validations.py +++ b/legal-api/tests/unit/services/filings/validations/test_common_validations.py @@ -256,7 +256,81 @@ def test_validate_offices_addresses(session, filing_type, filing_data, office_ty error_fields = {e['path'].split('/')[-1] for e in err4} assert error_fields == set(WHITESPACE_VALIDATED_ADDRESS_FIELDS) - + +@pytest.mark.parametrize('filing_type, filing_data, office_type', [ + ('amaglamationApplication', AMALGAMATION_APPLICATION, 'registeredOffice'), + ('changeOfAddress', CHANGE_OF_ADDRESS, 'registeredOffice'), + ('changeOfLiquidators', CHANGE_OF_LIQUIDATORS, 'liquidationRecordsOffice'), + ('changeOfRegistration', CHANGE_OF_REGISTRATION, 'businessOffice'), + ('continuationIn', CONTINUATION_IN, 'registeredOffice'), + ('conversion', FIRMS_CONVERSION, 'businessOffice'), + ('correction', CORRECTION, 'registeredOffice'), + ('incorporationApplication', INCORPORATION, 'registeredOffice'), + ('registration', REGISTRATION, 'businessOffice'), + ('restoration', RESTORATION, 'registeredOffice') +]) +@pytest.mark.parametrize('postal_code, expected_valid', [ + # Valid cases + ('V6B 1A1', True), # with space + ('V6B1A1', True), # without space + ('v6b 1a1', True), # lowercase + ('v6B1a1', True), # mixed case + ('K1N 3H9', True), # different province + + # Invalid cases + ('V6B\t1A1', False), # tab + ('V6B\n1A1', False), # newline + ('12345', False), # US zip code + ('V6B 1A1', False), # double space + ('V6B', False), # too short + ('V6B 1A1X', False), # too long + ('VV6 1A1', False), # wrong character positions + + # Invalid cases with disallowed first letters (D, F, I, O, Q, U, W) + ('D6B 1A1', False), + ('F6B 1A1', False), + ('I6B 1A1', False), + ('O6B 1A1', False), + ('Q6B 1A1', False), + ('U6B 1A1', False), + ('W6B 1A1', False), + +]) +def test_validate_offices_addresses_canadian_postal_code(session, filing_type, filing_data, office_type, postal_code, expected_valid): + """Test Canadian postal code format validation for office addresses.""" + filing = copy.deepcopy(FILING_HEADER) + filing['filing'][filing_type] = copy.deepcopy(filing_data) + + address = { + 'streetAddress': '123 Main St', + 'addressCity': 'Vancouver', + 'addressCountry': 'CA', + 'postalCode': postal_code, + 'addressRegion': 'BC' + } + filing['filing'][filing_type]['offices'][office_type]['deliveryAddress'] = address + + errs = validate_offices_addresses(filing, filing_type) + + if expected_valid: + assert errs == [] + else: + assert errs + assert errs[0]['error'] == "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'." + assert 'postalCode' in errs[0]['path'] + + +def test_validate_offices_addresses_non_ca_postal_code_not_validated(session): + """Test that non-Canadian addresses are not subject to Canadian postal code format validation.""" + filing = copy.deepcopy(FILING_HEADER) + filing_type = 'incorporationApplication' + filing['filing'][filing_type] = copy.deepcopy(INCORPORATION) + + # A US zip code on a US address should not trigger Canadian format validation + filing['filing'][filing_type]['offices']['registeredOffice']['deliveryAddress'] = VALID_ADDRESS_EX_CA + errs = validate_offices_addresses(filing, filing_type) + assert errs == [] + @pytest.mark.parametrize('filing_type, filing_data, party_key', [ ('amaglamationApplication', AMALGAMATION_APPLICATION, 'parties'), diff --git a/legal-api/tests/unit/services/filings/validations/test_incorporation_application.py b/legal-api/tests/unit/services/filings/validations/test_incorporation_application.py index ab2faab707..7aaf4e71d7 100644 --- a/legal-api/tests/unit/services/filings/validations/test_incorporation_application.py +++ b/legal-api/tests/unit/services/filings/validations/test_incorporation_application.py @@ -333,7 +333,9 @@ def test_validate_incorporation_addresses_basic(session, mocker, test_name, lega {"streetAddress": "456 B St", "addressCity": "Victoria", "addressCountry": "CA", "addressRegion": "BC", "postalCode": "V8W1C2"}, HTTPStatus.BAD_REQUEST, [ {'error': 'postalCode cannot start or end with whitespace.', - 'path': '/filing/incorporationApplication/offices/registeredOffice/deliveryAddress/postalCode'} + 'path': '/filing/incorporationApplication/offices/registeredOffice/deliveryAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", + 'path': '/filing/incorporationApplication/offices/registeredOffice/deliveryAddress/postalCode'}, ] ), ( @@ -369,7 +371,9 @@ def test_validate_incorporation_addresses_basic(session, mocker, test_name, lega {"streetAddress": "456 B St", "addressCity": "Victoria", "addressCountry": "CA", "addressRegion": "BC", "postalCode": " V8W1C2 "}, HTTPStatus.BAD_REQUEST, [ {'error': 'postalCode cannot start or end with whitespace.', - 'path': '/filing/incorporationApplication/offices/registeredOffice/mailingAddress/postalCode'} + 'path': '/filing/incorporationApplication/offices/registeredOffice/mailingAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", + 'path': '/filing/incorporationApplication/offices/registeredOffice/mailingAddress/postalCode'}, ] ), ( @@ -379,7 +383,11 @@ def test_validate_incorporation_addresses_basic(session, mocker, test_name, lega HTTPStatus.BAD_REQUEST, [ {'error': 'postalCode cannot start or end with whitespace.', 'path': '/filing/incorporationApplication/offices/registeredOffice/deliveryAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", + 'path': '/filing/incorporationApplication/offices/registeredOffice/deliveryAddress/postalCode'}, {'error': 'postalCode cannot start or end with whitespace.', + 'path': '/filing/incorporationApplication/offices/registeredOffice/mailingAddress/postalCode'}, + {'error': "Postal code must follow Canadian format, e.g. 'A1A 1A1' or 'A1A1A1'.", 'path': '/filing/incorporationApplication/offices/registeredOffice/mailingAddress/postalCode'} ] ), From e762c60385c672a167427326033d6f37f87d39cd Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Fri, 13 Mar 2026 08:53:29 -0700 Subject: [PATCH 19/46] Business API - DRS API integration. Signed-off-by: Doug Lovett --- legal-api/pyproject.toml | 2 +- .../common/businessDetails.html | 2 + legal-api/src/legal_api/core/filing.py | 20 +- .../src/legal_api/reports/document_service.py | 339 +++++++++++++++++- legal-api/src/legal_api/reports/report.py | 175 +++++---- .../business_filings/business_documents.py | 54 ++- legal-api/tests/conftest.py | 19 +- .../tests/unit/core/test_filing_ledger.py | 22 +- .../unit/reports/test_document_service.py | 288 +++++++++++++++ .../test_filing_documents.py | 62 +++- .../test_filings_ledger.py | 201 ++++++++--- 11 files changed, 1028 insertions(+), 156 deletions(-) diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index 6a0c91de97..8f30f852c9 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "legal-api" -version = "3.0.0" +version = "3.0.1" description = "" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/legal-api/report-templates/template-parts/common/businessDetails.html b/legal-api/report-templates/template-parts/common/businessDetails.html index 288d85ddc6..c6c5404f8b 100644 --- a/legal-api/report-templates/template-parts/common/businessDetails.html +++ b/legal-api/report-templates/template-parts/common/businessDetails.html @@ -362,11 +362,13 @@ +{# {% if reportType == 'summary' %}
Information current as of {{report_date_time}}
{% else %}
Document retrieved on {{report_date_time}}
{% endif %} +#}
diff --git a/legal-api/src/legal_api/core/filing.py b/legal-api/src/legal_api/core/filing.py index 3c450e43cc..82395efe5f 100644 --- a/legal-api/src/legal_api/core/filing.py +++ b/legal-api/src/legal_api/core/filing.py @@ -26,6 +26,7 @@ from legal_api.core.meta import FilingMeta from legal_api.models import Business, Document, DocumentType, UserRoles from legal_api.models import Filing as FilingStorage +from legal_api.reports.document_service import DocumentService from legal_api.services import VersionedBusinessDetailsService from legal_api.services.authz import has_roles, is_competent_authority @@ -102,7 +103,6 @@ class FilingTypes(str, Enum): SPECIALRESOLUTION = "specialResolution" TRANSITION = "transition" TRANSPARENCY_REGISTER = "transparencyRegister" - TOMBSTONE = "lear_tombstone" class FilingTypesCompact(str, Enum): """Render enum for filing types with sub-types.""" @@ -402,6 +402,9 @@ def ledger(business_id: int, # noqa: PLR0913 query = query.order_by(desc(FilingStorage.filing_date)) + drs_service: DocumentService = DocumentService() + drs_docs = drs_service.get_documents_by_business_id(business.identifier) if business else [] + ledger = [] for filing in query.all(): @@ -443,6 +446,10 @@ def ledger(business_id: int, # noqa: PLR0913 if filing.court_order_file_number or filing.order_details: Filing._add_ledger_order(filing, ledger_filing) + filing2: Filing = Filing() + filing2._storage = filing # pylint: disable=protected-access + filing_docs = Filing.get_document_list(business, filing2, jwt) + ledger_filing["documents"] = drs_service.update_filing_documents(drs_docs, filing_docs, filing) ledger.append(ledger_filing) return ledger @@ -480,7 +487,7 @@ def _add_ledger_order(filing: FilingStorage, ledger_filing: dict) -> dict: ledger_filing["data"]["order"] = court_order_data @staticmethod - def get_document_list(business, # noqa: PLR0912 + def get_document_list(business, # noqa: PLR0912, PLR0915 filing, jwt: JwtManager) -> dict | None: """Return a list of documents for a particular filing.""" @@ -512,7 +519,6 @@ def get_document_list(business, # noqa: PLR0912 identifier = withdrawn_filing.storage.temp_reg doc_url = url_for("API2.get_documents", identifier=identifier, filing_id=filing.id, legal_filing_name=None) - documents = {"documents": {}} # for paper_only filings return and empty documents list if filing.storage and filing.storage.paper_only: @@ -531,6 +537,8 @@ def get_document_list(business, # noqa: PLR0912 Document.type == DocumentType.COURT_ORDER.value).one_or_none()): documents["documents"]["uploadedCourtOrder"] = f"{base_url}{doc_url}/uploadedCourtOrder" documents["documents"]["receipt"] = f"{base_url}{doc_url}/receipt" + elif filing.storage and filing.storage.source == filing.storage.Source.COLIN.value: + documents["documents"]["receipt"] = f"{base_url}{doc_url}/receipt" no_outputs_except_receipt = filing.filing_type in [ Filing.FilingTypes.CHANGEOFLIQUIDATORS.value, @@ -568,10 +576,13 @@ def get_document_list(business, # noqa: PLR0912 del documents["documents"]["receipt"] return documents + legal_filings = filing.storage.meta_data.get("legalFilings") if filing.storage.meta_data else None + if not legal_filings and filing.storage and filing.storage.source == filing.storage.Source.COLIN.value: + legal_filings = [filing.filing_type] if ( filing.status in (Filing.Status.COMPLETED, Filing.Status.CORRECTED) and filing.storage.meta_data and - (legal_filings := filing.storage.meta_data.get("legalFilings")) + legal_filings and not (no_outputs_except_receipt or no_outputs_except_receipt_dissolution) ): legal_filings_copy = copy.deepcopy(legal_filings) @@ -612,7 +623,6 @@ def get_document_list(business, # noqa: PLR0912 adds = [FilingMeta.get_all_outputs(business.legal_type, doc) for doc in legal_filings] additional = {item for sublist in adds for item in sublist} - FilingMeta.alter_outputs(filing.storage, business, additional) for doc in additional: documents["documents"][doc] = f"{base_url}{doc_url}/{doc}" diff --git a/legal-api/src/legal_api/reports/document_service.py b/legal-api/src/legal_api/reports/document_service.py index ffdb6ddcc4..cdc314335c 100644 --- a/legal-api/src/legal_api/reports/document_service.py +++ b/legal-api/src/legal_api/reports/document_service.py @@ -18,8 +18,54 @@ from flask import current_app, jsonify from legal_api.exceptions import BusinessException -from legal_api.models import Business, Document +from legal_api.models import Business, Document, Filing from legal_api.services import AccountService +from legal_api.utils.base import BaseEnum + +BUSINESS_DOCS_PATH: str = "{url}/application-reports/history/{product}/{business_id}?includeDocuments=true" +GET_REPORT_PATH: str = "{url}/application-reports/{product}/{drsId}" +GET_REPORT_CERTIFIED_PATH: str = "{url}/application-reports/{product}/{drsId}?certifiedCopy=true" +POST_REPORT_PATH: str = "{url}/application-reports/{product}/{bus_id}/{filing_id}/{report_type}" +POST_REPORT_PARAMS: str = "?consumerFilingDate={filing_date}&consumerFilename={filename}" +GET_DOCUMENT_PATH: str = "{url}/searches/{document_class}?documentServiceId={drs_id}" +FILING_DOCS_PATH: str = "{url}/application-reports/events/{product}/{business_id}/{filing_id}?includeDocuments=true" +DRS_REPORT_PARAMS: str = "?reportType={report_type}&drsId={drs_id}" +DRS_DOCUMENT_PARAMS: str = "?documentClass={document_class}&drsId={drs_id}" +# Used for audit trail and db queries. +BUSINESS_API_ACCOUNT_ID: str = "business-api" +FILING_DOCUMENTS = { + "certifiedRules": { + "documentType": "COOP_RULES", + "documentClass": "COOP" + }, + "certifiedMemorandum": { + "documentType": "COOP_MEMORANDUM", + "documentClass": "COOP" + }, + "affidavit": { + "documentType": "CORP_AFFIDAVIT", # "COSD", + "documentClass": "CORP" + }, + "uploadedCourtOrder": { + "documentType": "CRT", + "documentClass": "CORP" + } +} +STATIC_DOCUMENTS = { + "Unlimited Liability Corporation Information": { + "documentType": "DIRECTOR_AFFIDAVIT", + "documentClass": "CORP" + } +} + + +class ReportTypes(BaseEnum): + """Render an Enum of the document service report types.""" + + CERT = "CERT" + FILING = "FILING" + NOA = "NOA" + RECEIPT = "RECEIPT" class DocumentService: @@ -107,9 +153,12 @@ def create_document( "Account-Id": account_id, "Authorization": "Bearer " + token } - post_url = (f"{self.url}/application-reports/" + filename: str = f"{business_identifier}_{filing_identifier}_{report_type}.pdf" + url: str = self.url.replace("/documents", "") + post_url = (f"{url}/application-reports/" f"{self.product_code}/{business_identifier}/" - f"{filing_identifier}/{report_type}") + f"{filing_identifier}/{report_type}?consumerFiliename={filename}") + # Include filing date if available. response = requests.post(url=post_url, headers=headers, data=binary_or_url) content = self.get_content(response) if response.status_code != HTTPStatus.CREATED: @@ -117,7 +166,7 @@ def create_document( self.create_document_record( Business.find_by_identifier(business_identifier).id, filing_identifier, report_type, content["identifier"], - f"{business_identifier}_{filing_identifier}_{report_type}.pdf" + filename ) return content, response.status_code @@ -145,14 +194,15 @@ def get_document( "Content-Type": "application/pdf", "Authorization": "Bearer " + token } + url: str = self.url.replace("/documents", "") get_url = "" if file_key is not None: - get_url = f"{self.url}/application-reports/{self.product_code}/{file_key}" + get_url = f"{url}/application-reports/{self.product_code}/{file_key}" else: document = self.has_document(business_identifier, filing_identifier, report_type) if document is False: raise BusinessException("Document not found", HTTPStatus.NOT_FOUND) - get_url = f"{self.url}/application-reports/{self.product_code}/{document.file_key}" + get_url = f"{url}/application-reports/{self.product_code}/{document.file_key}" if get_url != "": response = requests.get(url=get_url, headers=headers) @@ -161,3 +211,280 @@ def get_document( return jsonify(message=str(content)), response.status_code return content, response.status_code return jsonify(message="Document not found"), HTTPStatus.NOT_FOUND + + def get_documents_by_business_id(self, business_identifier: str) -> list: + """ + Get all available document information from the DRS by business identifier. + + business_identifier: The business identifier. + return: The list of available filing reports and documents for the business, or [] if there is a problem. + """ + try: + if not business_identifier: + return [] + token = AccountService.get_bearer_token() + headers = { + "x-ApiKey": self.api_key, + "Account-Id": BUSINESS_API_ACCOUNT_ID, + "Content-Type": "application/json", + "Authorization": "Bearer " + token + } + url: str = self.url.replace("/documents", "") + get_url = BUSINESS_DOCS_PATH.format(url=url, product=self.product_code, business_id=business_identifier) + response = requests.get(url=get_url, headers=headers) + content = self.get_content(response) + if response.status_code not in (HTTPStatus.OK, HTTPStatus.NOT_FOUND): + current_app.logger.error(f"DRS call {get_url} failed status={response.status_code}: {content}") + return content if response.status_code == HTTPStatus.OK else [] + except Exception: + return [] + + def get_documents_by_filing_id(self, business_identifier: str, filing_identifier: str) -> list: + """ + Get all available document information from the DRS by filing identifier. + + business_identifier: The business identifier. + filing_identifier: The filing identifier. + return: The list of available filing reports and documents for the filing, or [] if there is a problem or none. + """ + try: + if not business_identifier or not filing_identifier: + return [] + token = AccountService.get_bearer_token() + headers = { + "x-ApiKey": self.api_key, + "Account-Id": BUSINESS_API_ACCOUNT_ID, + "Content-Type": "application/json", + "Authorization": "Bearer " + token + } + url: str = self.url.replace("/documents", "") + get_url = FILING_DOCS_PATH.format( + url=url, + product=self.product_code, + business_id=business_identifier, + filing_id=filing_identifier + ) + response = requests.get(url=get_url, headers=headers) + content = self.get_content(response) + if response.status_code not in (HTTPStatus.OK, HTTPStatus.NOT_FOUND): + current_app.logger.error(f"DRS call {get_url} failed status={response.status_code}: {content}") + return content if response.status_code == HTTPStatus.OK else [] + except Exception: + return [] + + def update_document_list(self, drs_docs: list, document_list: list) -> list: # noqa: PLR0912 + """ + Update document_list urls with DRS information if a filing document maps to a DRS output or document. + + drs_docs: The DRS reports/documents for the filing identifier. + document_list: The business list of reports/documents for the filing. + include_docs: Include client documents linked to the filing if they exist. + return: The updated list of filing reports and documents for the filing. + """ + if not drs_docs or not document_list or not document_list.get("documents"): + return document_list + doc_list = document_list.get("documents") + for doc in drs_docs: + if doc.get("reportType") and doc.get("reportType") == "RECEIPT" and doc_list.get("receipt"): + query_params: str = DRS_REPORT_PARAMS.format(report_type="RECEIPT", drs_id=doc.get("identifier")) + doc_list["receipt"] = doc_list["receipt"] + query_params + elif doc.get("reportType") and doc.get("reportType") == "NOA" and doc_list.get("noticeOfArticles"): + query_params: str = DRS_REPORT_PARAMS.format(report_type="NOA", drs_id=doc.get("identifier")) + doc_list["noticeOfArticles"] = doc_list["noticeOfArticles"] + query_params + elif doc.get("reportType") and doc.get("reportType") == "FILING" and doc_list.get("legalFilings"): + query_params: str = DRS_REPORT_PARAMS.format(report_type="FILING", drs_id=doc.get("identifier")) + filing = doc_list["legalFilings"][0] + filing_key = next(iter(filing.keys())) + filing[filing_key] = filing[filing_key] + query_params + elif doc.get("reportType") and doc.get("reportType") == "CERT": + query_params: str = DRS_REPORT_PARAMS.format(report_type="CERT", drs_id=doc.get("identifier")) + for key in doc_list: + if str(key).find("certificate") > -1: + doc_list[key] = doc_list[key] + query_params + break + elif doc.get("documentClass"): + query_params: str = DRS_DOCUMENT_PARAMS.format( + document_class=doc.get("documentClass"), + drs_id=doc.get("identifier") + ) + for key in doc_list: + if key == "staticDocuments": + for static_doc in doc_list.get("staticDocuments"): + name: str = static_doc.get("name") + if (name and name == doc.get("name")) or ( + name and STATIC_DOCUMENTS.get(name) and + doc.get("documentClass") == STATIC_DOCUMENTS[name].get("documentClass") and + doc.get("documentType") == STATIC_DOCUMENTS[name].get("documentType") + ): + static_doc["url"] = static_doc["url"] + query_params + break + elif ( + FILING_DOCUMENTS.get(key) and + doc.get("documentClass") == FILING_DOCUMENTS[key].get("documentClass") and + doc.get("documentType") == FILING_DOCUMENTS[key].get("documentType") + ): + doc_list[key] = doc_list[key] + query_params + break + return document_list + + def update_filing_documents(self, drs_docs: list, filing_docs: list, filing: Filing) -> list: # noqa: PLR0912 + """ + Get outputs and documents for a ledger filing, adding DRS information if available by mapping on the filing ID. + + drs_docs: The DRS reports/documents for the business identifier. + filing_docs: The filing list of reports/documents. + return: The updated list of filing reports and documents for the filing. + """ + doc_list = filing_docs.get("documents") if filing_docs else None + if not filing_docs or not doc_list: + return [] + if not drs_docs or not filing or filing.paper_only: + return doc_list + drs_filing_id: int = filing.id + # If DRS reports/documents exist add the DRS download info to the document URLs. + if filing and filing.source == filing.Source.COLIN.value: + meta_data = filing.meta_data + if meta_data and meta_data.get("colinFilingInfo") and meta_data["colinFilingInfo"].get("eventId"): + drs_filing_id = meta_data["colinFilingInfo"].get("eventId") + + for doc in drs_docs: + if doc.get("eventIdentifier", 0) == drs_filing_id: + if doc.get("reportType") and doc.get("reportType") == "RECEIPT" and doc_list.get("receipt"): + query_params: str = DRS_REPORT_PARAMS.format(report_type="RECEIPT", drs_id=doc.get("identifier")) + doc_list["receipt"] = doc_list["receipt"] + query_params + elif doc.get("reportType") and doc.get("reportType") == "NOA" and doc_list.get("noticeOfArticles"): + query_params: str = DRS_REPORT_PARAMS.format(report_type="NOA", drs_id=doc.get("identifier")) + doc_list["noticeOfArticles"] = doc_list["noticeOfArticles"] + query_params + elif doc.get("reportType") and doc.get("reportType") == "FILING" and doc_list.get("legalFilings"): + query_params: str = DRS_REPORT_PARAMS.format(report_type="FILING", drs_id=doc.get("identifier")) + legal_filing = doc_list["legalFilings"][0] + filing_key = next(iter(legal_filing.keys())) + legal_filing[filing_key] = legal_filing[filing_key] + query_params + elif doc.get("reportType") and doc.get("reportType") == "CERT": + query_params: str = DRS_REPORT_PARAMS.format(report_type="CERT", drs_id=doc.get("identifier")) + for key in doc_list: + if str(key).find("certificate") > -1: + doc_list[key] = doc_list[key] + query_params + break + elif doc.get("documentClass"): + query_params: str = DRS_DOCUMENT_PARAMS.format( + document_class=doc.get("documentClass"), + drs_id=doc.get("identifier") + ) + for key in doc_list: + if key == "staticDocuments": + for static_doc in doc_list.get("staticDocuments"): + name: str = static_doc.get("name") + if (name and name == doc.get("name")) or ( + name and STATIC_DOCUMENTS.get(name) and + doc.get("documentClass") == STATIC_DOCUMENTS[name].get("documentClass") and + doc.get("documentType") == STATIC_DOCUMENTS[name].get("documentType") + ): + static_doc["url"] = static_doc["url"] + query_params + break + elif ( + FILING_DOCUMENTS.get(key) and + doc.get("documentClass") == FILING_DOCUMENTS[key].get("documentClass") and + doc.get("documentType") == FILING_DOCUMENTS[key].get("documentType") + ): + doc_list[key] = doc_list[key] + query_params + break + return doc_list + + def get_filing_report(self, drs_id: str, report_type: str): + """ + Get a filing report document from the document service by unique DRS identifier. + + drs_id: The unique DRS identifier for the requested document. + report_type: The report type: request a certified copy for NOA and FILING report types. + return: The document binary data. + """ + token = AccountService.get_bearer_token() + headers = { + "x-apikey": self.api_key, + "Account-Id": BUSINESS_API_ACCOUNT_ID, + "Accept": "application/pdf", + "Authorization": "Bearer " + token + } + url: str = self.url.replace("/documents", "") + get_url = GET_REPORT_PATH + if report_type in (ReportTypes.FILING.value, ReportTypes.NOA.value): + get_url = GET_REPORT_CERTIFIED_PATH + get_url = get_url.format(url=url, product=self.product_code, drsId=drs_id) + response = requests.get(url=get_url, headers=headers) + if response.status_code not in (HTTPStatus.OK, HTTPStatus.NOT_FOUND): + current_app.logger.error(f"DRS call {get_url} failed status={response.status_code}: {response.content}") + return response + + def get_filing_document(self, drs_id: str, doc_class: str): + """ + Get a filing document from the document service by unique DRS identifier. + + drs_id: The unique DRS identifier for the requested document. + document_class: The DRS document class for the business to filter on. + return: The document binary data. + """ + token = AccountService.get_bearer_token() + headers = { + "x-apikey": self.api_key, + "Account-Id": BUSINESS_API_ACCOUNT_ID, + "Accept": "application/pdf", + "Authorization": "Bearer " + token + } + url: str = self.url.replace("/documents", "") + get_url = GET_DOCUMENT_PATH.format(url=url, document_class=doc_class, drs_id=drs_id) + response = requests.get(url=get_url, headers=headers) + if response.status_code not in (HTTPStatus.OK, HTTPStatus.NOT_FOUND): + current_app.logger.error(f"DRS call {get_url} failed status={response.status_code}: {response.content}") + return response + + def create_filing_report( + self, + business_identifier: str, + filing: Filing, + report_meta: dict, + report_response + ): + """ + Create a DRS application report record with a report service report. + + business_identifier: Required - associates the report with the business. + filing: Required - contains the filing ID and filing date. + report_type: Required - the DRS report type. + report_response: Required - contains the report binary data + return: The DRS API response. + """ + if not business_identifier or not filing or not report_meta or not report_meta.get("reportType"): + return report_response + token = AccountService.get_bearer_token() + headers = { + "x-apikey": self.api_key, + "Account-Id": BUSINESS_API_ACCOUNT_ID, + "Content-Type": "application/pdf", + "Authorization": "Bearer " + token + } + url: str = self.url.replace("/documents", "") + report_type: str = report_meta.get("reportType") + filename: str = report_meta.get("fileName") + ".pdf" + filing_date: str = filing.effective_date.isoformat()[:10] + post_url = POST_REPORT_PATH.format( + url=url, + product=self.product_code, + bus_id=business_identifier, + filing_id=filing.id, + report_type=report_type + ) + post_url += POST_REPORT_PARAMS.format(filing_date=filing_date, filename=filename) + response = requests.post(url=post_url, headers=headers, data=report_response.content) + if response.status_code not in (HTTPStatus.OK, HTTPStatus.CREATED): + current_app.logger.error(f"DRS call {post_url} failed status={response.status_code}: {response.content}") + return report_response + if report_type == ReportTypes.CERT.value: + return report_response + # Get the certified copy of the NOA and FILING report types. + response_json = json.loads(response.content) + if response_json and response_json.get("identifier"): + response2 = self.get_filing_report(response_json.get("identifier"), report_type) + if response2.status_code == HTTPStatus.OK: + return response2 + return report_response diff --git a/legal-api/src/legal_api/reports/report.py b/legal-api/src/legal_api/reports/report.py index 6aa09098ab..9e1d8eafb9 100644 --- a/legal-api/src/legal_api/reports/report.py +++ b/legal-api/src/legal_api/reports/report.py @@ -36,7 +36,7 @@ PartyRole, ) from legal_api.models.business import ASSOCIATION_TYPE_DESC -from legal_api.reports.document_service import DocumentService +from legal_api.reports.document_service import DocumentService, ReportTypes from legal_api.reports.registrar_meta import RegistrarInfo from legal_api.services import VersionedBusinessDetailsService, flags from legal_api.utils.auth import jwt @@ -78,21 +78,20 @@ def _get_static_report(self): ) def _get_report(self): - if flags.is_on("enable-document-records"): - account_id = request.headers.get("Account-Id", None) - if account_id is not None and self._business is not None: - document, status = self._document_service.get_document( - self._business.identifier, - self._filing.id, - self._report_key, - account_id + account_id = request.headers.get("Account-Id", None) + if account_id is not None and self._business is not None: + document, status = self._document_service.get_document( + self._business.identifier, + self._filing.id, + self._report_key, + account_id + ) + if status == HTTPStatus.OK: + return current_app.response_class( + response=document, + status=status, + mimetype="application/pdf" ) - if status == HTTPStatus.OK: - return current_app.response_class( - response=document, - status=status, - mimetype="application/pdf" - ) if self._filing.business_id: self._business = Business.find_by_internal_id(self._filing.business_id) @@ -114,32 +113,15 @@ def _get_report(self): if response.status_code != HTTPStatus.OK: return jsonify(message=str(response.content)), response.status_code - if flags.is_on("enable-document-records"): - create_document = account_id is not None - create_filing_types = [ - "incorporationApplication", - "continuationIn", - "amalgamation", - "registration" - ] - if self._filing.filing_type in create_filing_types: - create_document = create_document and self._business and self._business.tax_id - else: - create_document = create_document and \ - self._filing.status in (Filing.Status.COMPLETED, Filing.Status.CORRECTED) - - if create_document: - self._document_service.create_document( - self._business.identifier, - self._filing.id, - self._report_key, - account_id, - response.content - ) - + response_drs = self._document_service.create_filing_report( + self._business.identifier, + self._filing, + ReportMeta.reports.get(self._report_key), + response + ) return current_app.response_class( - response=response.content, - status=response.status_code, + response=response_drs.content, + status=response_drs.status_code, mimetype="application/pdf" ) @@ -1291,6 +1273,10 @@ def _format_office_data(self, filing, prev_completed_filing: Filing): def _format_party_data(self, filing, prev_completed_filing: Filing): filing["parties"] = filing.get("correction").get("parties", []) + if relationships := filing.get("correction").get("relationships"): + # map relationships to parties for pdf templates + filing["parties"].extend([self._map_relationship_to_party(relationship) for relationship in relationships]) + if filing.get("parties"): self._format_directors(filing["parties"]) filing["partyChange"] = False @@ -1491,6 +1477,24 @@ def _get_environment(): return "TEST" return "" + @staticmethod + def _map_relationship_to_party(relationship): + # FUTURE: update pdf templates to expect relationships schema and remove this + organization_name = relationship["entity"].get("businessName") + return { + "officer": { + "id": relationship["entity"].get("identifier"), + "firstName": relationship["entity"].get("givenName"), + "middleName": relationship["entity"].get("middleInitial"), + "lastName": relationship["entity"].get("familyName"), + "organizationName": organization_name, + "partyType": "organization" if organization_name else "person" + }, + "deliveryAddress": relationship.get("deliveryAddress"), + "mailingAddress": relationship.get("mailingAddress"), + "roles": relationship.get("roles", []) + } + class ReportMeta: # pylint: disable=too-few-public-methods """Helper class to maintain the report meta information.""" @@ -1498,35 +1502,43 @@ class ReportMeta: # pylint: disable=too-few-public-methods reports = { "amalgamationApplication": { "filingDescription": "Amalgamation Application", - "fileName": "amalgamationApplication" + "fileName": "amalgamationApplication", + "reportType": ReportTypes.FILING.value }, "certificateOfAmalgamation": { "filingDescription": "Certificate Of Amalgamation", - "fileName": "certificateOfAmalgamation" + "fileName": "certificateOfAmalgamation", + "reportType": ReportTypes.CERT.value }, "certificateOfIncorporation": { "filingDescription": "Certificate of Incorporation", - "fileName": "certificateOfIncorporation" + "fileName": "certificateOfIncorporation", + "reportType": ReportTypes.CERT.value }, "incorporationApplication": { "filingDescription": "Incorporation Application", - "fileName": "incorporationApplication" + "fileName": "incorporationApplication", + "reportType": ReportTypes.FILING.value }, "noticeOfArticles": { "filingDescription": "Notice of Articles", - "fileName": "noticeOfArticles" + "fileName": "noticeOfArticles", + "reportType": ReportTypes.NOA.value }, "alterationNotice": { "filingDescription": "Alteration Notice", - "fileName": "alterationNotice" + "fileName": "alterationNotice", + "reportType": ReportTypes.FILING.value }, "transition": { "filingDescription": "Transition Application", - "fileName": "transitionApplication" + "fileName": "transitionApplication", + "reportType": ReportTypes.FILING.value }, "changeOfAddress": { "hasDifferentTemplates": True, "filingDescription": "Change of Address", + "reportType": ReportTypes.FILING.value, "default": { "fileName": "bcAddressChange" }, @@ -1537,6 +1549,7 @@ class ReportMeta: # pylint: disable=too-few-public-methods "changeOfDirectors": { "hasDifferentTemplates": True, "filingDescription": "Change of Directors", + "reportType": ReportTypes.FILING.value, "default": { "fileName": "bcDirectorChange" }, @@ -1547,6 +1560,7 @@ class ReportMeta: # pylint: disable=too-few-public-methods "annualReport": { "hasDifferentTemplates": True, "filingDescription": "Annual Report", + "reportType": ReportTypes.FILING.value, "default": { "fileName": "bcAnnualReport" }, @@ -1556,55 +1570,68 @@ class ReportMeta: # pylint: disable=too-few-public-methods }, "changeOfName": { "filingDescription": "Change of Name", - "fileName": "changeOfName" + "fileName": "changeOfName", + "reportType": ReportTypes.FILING.value }, "specialResolution": { "filingDescription": "Special Resolution", - "fileName": "specialResolution" + "fileName": "specialResolution", + "reportType": ReportTypes.FILING.value }, "specialResolutionApplication": { "filingDescription": "Special Resolution Application", - "fileName": "specialResolutionApplication" + "fileName": "specialResolutionApplication", + "reportType": ReportTypes.FILING.value }, "voluntaryDissolution": { "filingDescription": "Voluntary Dissolution", - "fileName": "voluntaryDissolution" + "fileName": "voluntaryDissolution", + "reportType": ReportTypes.FILING.value }, "certificateOfNameChange": { "filingDescription": "Certificate of Name Change", - "fileName": "certificateOfNameChange" + "fileName": "certificateOfNameChange", + "reportType": ReportTypes.CERT.value }, "certificateOfNameCorrection": { "filingDescription": "Certificate of Name Correction", - "fileName": "certificateOfNameChange" + "fileName": "certificateOfNameChange", + "reportType": ReportTypes.CERT.value }, "certificateOfDissolution": { "filingDescription": "Certificate of Dissolution", - "fileName": "certificateOfDissolution" + "fileName": "certificateOfDissolution", + "reportType": ReportTypes.CERT.value }, "dissolution": { "filingDescription": "Dissolution Application", - "fileName": "dissolution" + "fileName": "dissolution", + "reportType": ReportTypes.FILING.value }, "registration": { "filingDescription": "Statement of Registration", - "fileName": "registration" + "fileName": "registration", + "reportType": ReportTypes.FILING.value }, "amendedRegistrationStatement": { "filingDescription": "Amended Registration Statement", - "fileName": "amendedRegistrationStatement" + "fileName": "amendedRegistrationStatement", + "reportType": ReportTypes.FILING.value }, "correctedRegistrationStatement": { "filingDescription": "Corrected Registration Statement", - "fileName": "amendedRegistrationStatement" + "fileName": "amendedRegistrationStatement", + "reportType": ReportTypes.FILING.value }, "changeOfRegistration": { "filingDescription": "Change of Registration", - "fileName": "changeOfRegistration" + "fileName": "changeOfRegistration", + "reportType": ReportTypes.FILING.value }, "correction": { "hasDifferentTemplates": True, "filingDescription": "Correction", + "reportType": ReportTypes.FILING.value, "default": { "fileName": "correction" }, @@ -1617,31 +1644,38 @@ class ReportMeta: # pylint: disable=too-few-public-methods }, "certificateOfRestoration": { "filingDescription": "Certificate of Restoration", - "fileName": "certificateOfRestoration" + "fileName": "certificateOfRestoration", + "reportType": ReportTypes.FILING.value }, "restoration": { "filingDescription": "Restoration Application", - "fileName": "restoration" + "fileName": "restoration", + "reportType": ReportTypes.FILING.value }, "letterOfConsent": { "filingDescription": "Letter Of Consent", - "fileName": "letterOfConsent" + "fileName": "letterOfConsent", + "reportType": ReportTypes.FILING.value }, "letterOfConsentAmalgamationOut": { "filingDescription": "Letter Of Consent", - "fileName": "letterOfConsentAmalgamationOut" + "fileName": "letterOfConsentAmalgamationOut", + "reportType": ReportTypes.FILING.value }, "letterOfAgmExtension": { "filingDescription": "Letter Of AGM Extension", - "fileName": "letterOfAgmExtension" + "fileName": "letterOfAgmExtension", + "reportType": ReportTypes.FILING.value }, "letterOfAgmLocationChange": { "filingDescription": "Letter Of AGM Location Change", - "fileName": "letterOfAgmLocationChange" + "fileName": "letterOfAgmLocationChange", + "reportType": ReportTypes.FILING.value }, "continuationIn": { "filingDescription": "Continuation Application", - "fileName": "continuationApplication" + "fileName": "continuationApplication", + "reportType": ReportTypes.FILING.value }, "certificateOfContinuation": { "filingDescription": "Certificate of Continuation", @@ -1649,15 +1683,18 @@ class ReportMeta: # pylint: disable=too-few-public-methods }, "noticeOfWithdrawal": { "filingDescription": "Notice of Withdrawal", - "fileName": "noticeOfWithdrawal" + "fileName": "noticeOfWithdrawal", + "reportType": ReportTypes.FILING.value }, "appointReceiver": { "filingDescription": "Appoint Receiver", - "fileName": "appointReceiver" + "fileName": "appointReceiver", + "reportType": ReportTypes.FILING.value }, "ceaseReceiver": { "filingDescription": "Cease Receiver", - "fileName": "ceaseReceiver" + "fileName": "ceaseReceiver", + "reportType": ReportTypes.FILING.value } } diff --git a/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py b/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py index e89306619d..c1343c4564 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py +++ b/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py @@ -27,6 +27,7 @@ from legal_api.models import Business, Document from legal_api.models import Filing as FilingModel from legal_api.reports import get_pdf +from legal_api.reports.document_service import DocumentService from legal_api.resources.v2.business.bp import bp from legal_api.services import MinioService, authorized from legal_api.utils.auth import jwt @@ -37,6 +38,11 @@ DOCUMENTS_BASE_ROUTE: Final = "//filings//documents" +PARAM_REPORT_TYPE: str = "reportType" +PARAM_DOC_CLASS = "documentClass" +PARAM_DRS_ID = "drsId" +CONTENT_JSON = {"Content-Type": "application/json"} +CONTENT_PDF = {"Content-Type": "application/pdf"} @cors_preflight("GET, POST") @@ -55,7 +61,6 @@ def get_documents(identifier: str, # noqa: PLR0911, PLR0912 return jsonify( message=get_error_message(ErrorCode.NOT_AUTHORIZED, identifier=identifier) ), HTTPStatus.UNAUTHORIZED - if identifier.startswith("T"): filing_model = FilingModel.get_temp_reg_filing(identifier) business = Business.find_by_internal_id(filing_model.business_id) @@ -95,6 +100,9 @@ def get_documents(identifier: str, # noqa: PLR0911, PLR0912 file_name=file_name, filing_id=filing_id, identifier=identifier) ), HTTPStatus.NOT_FOUND + if drs_params := _get_drs_params(): + return _get_drs_documents(drs_params) + if legal_filing_name: if legal_filing_name.lower().startswith("receipt"): return _get_receipt(business, filing, jwt.get_token_auth_header()) @@ -112,6 +120,32 @@ def get_documents(identifier: str, # noqa: PLR0911, PLR0912 return {}, HTTPStatus.NOT_FOUND +def _get_drs_params() -> dict: + """Extract DRS parameters from the request.""" + params: dict = {} + if request.args.get(PARAM_REPORT_TYPE): + params["reportType"] = request.args.get(PARAM_REPORT_TYPE) + if request.args.get(PARAM_DRS_ID): + params["drsId"] = request.args.get(PARAM_DRS_ID) + if request.args.get(PARAM_DOC_CLASS): + params["documentClass"] = request.args.get(PARAM_DOC_CLASS) + return params + + +def _get_drs_documents(drs_params: dict) -> dict: + """Return an indvidual DRS document as binary data, or a DRS JSON error.""" + doc_service: DocumentService = DocumentService() + drs_id: str = drs_params.get(PARAM_DRS_ID) + response = None + if drs_params.get(PARAM_REPORT_TYPE): + response = doc_service.get_filing_report(drs_id, drs_params.get(PARAM_REPORT_TYPE)) + else: + response = doc_service.get_filing_document(drs_id, drs_params.get(PARAM_DOC_CLASS)) + + content_type: str = CONTENT_PDF if response.status_code == HTTPStatus.OK else CONTENT_JSON + return response.content, response.status_code, content_type + + def _is_document_available(business, filing, file_name): """Check if the document is available.""" document_list = Filing.get_document_list(business, filing, jwt) @@ -128,11 +162,23 @@ def _is_document_available(business, filing, file_name): return file_name in documents -def _get_document_list(business, filing): +def _get_document_list(business: Business, filing: Filing): """Get list of document outputs.""" - if not (document_list := Filing.get_document_list(business, filing, jwt)): + document_list = Filing.get_document_list(business, filing, jwt) + if not (document_list): return {}, HTTPStatus.NOT_FOUND - + storage: FilingModel = filing.storage + # If DRS reports/documents exist add the DRS download info to the document URLs. + if storage and not storage.paper_only: + drs_filing_id: int = storage.id + if storage.source and storage.source == storage.Source.COLIN.value and storage.meta_data: + meta_data = storage.meta_data + if meta_data and meta_data.get("colinFilingInfo") and meta_data["colinFilingInfo"].get("eventId"): + drs_filing_id = meta_data["colinFilingInfo"].get("eventId") + identifier = business.identifier if business else storage.temp_reg + doc_service: DocumentService = DocumentService() + drs_docs: list = doc_service.get_documents_by_filing_id(identifier, drs_filing_id) + document_list = doc_service.update_document_list(drs_docs, document_list) return jsonify(document_list), HTTPStatus.OK diff --git a/legal-api/tests/conftest.py b/legal-api/tests/conftest.py index 841884e684..d13c6185bb 100644 --- a/legal-api/tests/conftest.py +++ b/legal-api/tests/conftest.py @@ -313,7 +313,7 @@ def _mock_get_side_effect(request, context): DOCUMENT_API_URL = 'http://document-api.com' DOCUMENT_API_VERSION = '/api/v1' -DOCUMENT_SVC_URL = f'{DOCUMENT_API_URL + DOCUMENT_API_VERSION}/documents' +DOCUMENT_SVC_URL = f'{DOCUMENT_API_URL + DOCUMENT_API_VERSION}' DOCUMENT_PRODUCT_CODE = 'BUSINESS' @pytest.fixture() @@ -331,4 +331,21 @@ def mock_doc_service(): mock.get(re.compile(f"{get_url}.*"), status_code=HTTPStatus.OK, text=json.dumps(mock_response)) + get_url2 = f'{DOCUMENT_SVC_URL}/application-reports/history/{DOCUMENT_PRODUCT_CODE}/' + mock.get(re.compile(f"{get_url2}.*"), + status_code=HTTPStatus.OK, + text=json.dumps(mock_response)) yield mock + + +@pytest.fixture() +def mock_drs_service(): + mock_response = [] + with requests_mock.Mocker(real_http=True) as m: + get_url = f'{DOCUMENT_SVC_URL}/application-reports/{DOCUMENT_PRODUCT_CODE}/' + get_url2 = f'{DOCUMENT_SVC_URL}/application-reports/history/{DOCUMENT_PRODUCT_CODE}/' + get_url3 = f'{DOCUMENT_SVC_URL}/application-reports/events/{DOCUMENT_PRODUCT_CODE}/' + m.register_uri('GET', re.compile(f"{get_url}.*"), json=mock_response, status_code=HTTPStatus.OK) + m.register_uri('GET', re.compile(f"{get_url2}.*"), json=mock_response, status_code=HTTPStatus.OK) + m.register_uri('GET', re.compile(f"{get_url3}.*"), json=mock_response, status_code=HTTPStatus.OK) + yield m diff --git a/legal-api/tests/unit/core/test_filing_ledger.py b/legal-api/tests/unit/core/test_filing_ledger.py index 06cdebb81a..fadb95caef 100644 --- a/legal-api/tests/unit/core/test_filing_ledger.py +++ b/legal-api/tests/unit/core/test_filing_ledger.py @@ -17,14 +17,16 @@ import datedelta import pytest +from flask import current_app from registry_schemas.example_data import FILING_TEMPLATE from legal_api.core import Filing as CoreFiling from legal_api.models import Business, Comment, Filing, UserRoles from legal_api.models.user import UserRoles +from legal_api.services.authz import STAFF_ROLE from legal_api.utils.datetime import datetime from tests.unit.models import factory_business, factory_completed_filing, factory_user -from tests.unit.services.utils import helper_create_jwt +from tests.unit.services.utils import helper_create_jwt, create_header def load_ledger(business, founding_date): @@ -65,16 +67,24 @@ def load_ledger(business, founding_date): return i -def test_simple_ledger_search(session): +def test_simple_ledger_search(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns values for all the expected keys.""" # setup identifier = 'BC1234567' founding_date = datetime.utcnow() - datedelta.datedelta(months=len(Filing.FILINGS.keys())) business = factory_business(identifier=identifier, founding_date=founding_date, last_ar_date=None, entity_type=Business.LegalTypes.BCOMP.value) num_of_files = load_ledger(business, founding_date) + token = helper_create_jwt(jwt, roles=[STAFF_ROLE], username='testuser') + headers = {'Authorization': 'Bearer ' + token, 'Account-Id': 1} - # test - ledger = CoreFiling.ledger(business.id) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] + + # test + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + ledger = CoreFiling.ledger(business.id, jwt) # Did we get the full set assert len(ledger) == num_of_files @@ -83,7 +93,7 @@ def test_simple_ledger_search(session): alteration = next((f for f in ledger if f.get('name') == 'alteration'), None) assert alteration - assert 18 == len(alteration.keys()) + assert 18 <= len(alteration.keys()) assert 'availableOnPaperOnly' in alteration assert 'effectiveDate' in alteration assert 'filingId' in alteration @@ -100,7 +110,7 @@ def test_simple_ledger_search(session): # assert alteration['filingLink'] -def test_common_ledger_items(session): +def test_common_ledger_items(session,): """Assert that common ledger items works as expected.""" identifier = 'BC1234567' founding_date = datetime.utcnow() - datedelta.datedelta(months=len(Filing.FILINGS.keys())) diff --git a/legal-api/tests/unit/reports/test_document_service.py b/legal-api/tests/unit/reports/test_document_service.py index c8f982134a..7038a78e3f 100644 --- a/legal-api/tests/unit/reports/test_document_service.py +++ b/legal-api/tests/unit/reports/test_document_service.py @@ -17,12 +17,300 @@ from datetime import datetime from http import HTTPStatus import datedelta +import json +import pytest + +from legal_api.models import Filing from legal_api.reports.document_service import DocumentService from tests.unit.models import factory_business, factory_completed_filing from registry_schemas.example_data import FILING_TEMPLATE +META_COLIN = { + "colinFilingInfo": { + "eventId": 7678798, + "eventType": "FILE", + "filingType": "NOCDR" + } +} +META_MODERN = {} +DOCS_MODERN = { + "documents": { + "certificateOfIncorporation": "https://test.com/BC0888490/filings/2405954/documents/certificateOfIncorporation", + "legalFilings": [ + {"incorporationApplication": "https://test.com/BC0888490/filings/2405954/documents/incorporationApplication"} + ], + "noticeOfArticles": "https://test.com/BC0888490/filings/2405954/documents/noticeOfArticles", + "receipt": "https://test.com/BC0888490/filings/2405954/documents/receipt" + } +} +DOCS_COLIN = { + "documents": { + "certificateOfIncorporation": "https://test.com/BC0791952/filings/515091/documents/certificateOfIncorporation", + "legalFilings": [ + {"incorporationApplication": "https://test.com/BC0791952/filings/515091/documents/incorporationApplication"} + ], + "noticeOfArticles": "https://test.com/BC0791952/filings/515091/documents/noticeOfArticles", + "receipt": "https://test.com/BC0791952/filings/515091/documents/receipt" + } +} +DOCS_STATIC1 = { + "documents": { + "certificateOfIncorporation": "https://test.com/CP1044798/filings/233791/documents/certificateOfIncorporation", + "certifiedMemorandum": "https://test.com/CP1044798/filings/233791/documents/certifiedMemorandum", + "certifiedRules": "https://test.com/CP1044798/filings/233791/documents/certifiedRules", + "legalFilings": [ + {"incorporationApplication": "https://test.com/CP1044798/filings/233791/documents/incorporationApplication"} + ], + "receipt": "https://test.com/CP1044798/filings/233791/documents/receipt" + } +} +DOCS_STATIC2 = { + "documents": { + "certificateOfContinuation": "https://test.com/C9900863/filings/155753/documents/certificateOfContinuation", + "legalFilings": [ + {"continuationIn": "https://test.com/C9900863/filings/155753/documents/continuationIn"} + ], + "noticeOfArticles": "https://test.com/C9900863/filings/155753/documents/noticeOfArticles", + "receipt": "https://test.com/C9900863/filings/155753/documents/receipt", + "staticDocuments": [ + { + "name": "Unlimited Liability Corporation Information", + "url": "https://test.com/C9900863/filings/155753/documents/static/DS0000100741" + }, + { + "name": "20250107-Authorization1.pdf", + "url": "https://test.com/C9900863/filings/155753/documents/static/DS0000100740" + } + ] + } +} +DRS_NONE = [] +DRS_MODERN = [ + { + "dateCreated": "2026-03-11T15:45:51+00:00", + "datePublished": "2026-03-03T20:00:00+00:00", + "entityIdentifier": "BC0888490", + "eventIdentifier": 2405954, + "identifier": "DSR0000100951", + "name": "certificateOfIncorporation.pdf", + "productCode": "BUSINESS", + "reportType": "CERT", + "url": "" + }, + { + "dateCreated": "2026-03-11T15:53:19+00:00", + "datePublished": "2026-03-03T20:00:00+00:00", + "entityIdentifier": "BC0888490", + "eventIdentifier": 2405954, + "identifier": "DSR0000100952", + "name": "noticeOfArticles.pdf", + "productCode": "BUSINESS", + "reportType": "NOA", + "url": "" + }, + { + "dateCreated": "2026-03-11T16:09:51+00:00", + "datePublished": "2026-03-03T20:00:00+00:00", + "entityIdentifier": "BC0888490", + "eventIdentifier": 2405954, + "identifier": "DSR0000100954", + "name": "incorporationApplication.pdf", + "productCode": "BUSINESS", + "reportType": "FILING", + "url": "" + } +] +DRS_COLIN = [ + { + "dateCreated": "2026-02-24T23:15:35+00:00", + "datePublished": "2007-05-23T18:40:59+00:00", + "entityIdentifier": "BC0791952", + "eventIdentifier": 7678798, + "identifier": "DSR0000100896", + "name": "BC0791952-ICORP-RECEIPT.pdf", + "productCode": "BUSINESS", + "reportType": "RECEIPT", + "url": "" + }, + { + "dateCreated": "2026-02-24T23:15:37+00:00", + "datePublished": "2007-05-23T18:40:59+00:00", + "entityIdentifier": "BC0791952", + "eventIdentifier": 7678798, + "identifier": "DSR0000100897", + "name": "BC0791952-ICORP-FILING.pdf", + "productCode": "BUSINESS", + "reportType": "FILING", + "url": "" + }, + { + "dateCreated": "2026-02-24T23:15:41+00:00", + "datePublished": "2007-05-23T18:40:59+00:00", + "entityIdentifier": "BC0791952", + "eventIdentifier": 7678798, + "identifier": "DSR0000100898", + "name": "BC0791952-ICORP-NOA.pdf", + "productCode": "BUSINESS", + "reportType": "NOA", + "url": "" + }, + { + "dateCreated": "2026-02-24T23:15:45+00:00", + "datePublished": "2007-05-23T18:40:59+00:00", + "entityIdentifier": "BC0791952", + "eventIdentifier": 7678798, + "identifier": "DSR0000100899", + "name": "BC0791952-ICORP-CERT.pdf", + "productCode": "BUSINESS", + "reportType": "CERT", + "url": "" + } +] +DRS_STATIC1 = [ + { + "consumerDocumentId": "0100000525", + "dateCreated": "2026-03-11T19:19:32+00:00", + "datePublished": "2026-01-22T00:00:00+00:00", + "documentClass": "COOP", + "documentType": "COOP_MEMORANDUM", + "documentTypeDescription": "Cooperative Memorandum", + "entityIdentifier": "CP1044798", + "eventIdentifier": 233791, + "identifier": "DS0000101630", + "name": "", + "url": "" + }, + { + "consumerDocumentId": "0100000526", + "dateCreated": "2026-03-11T19:19:37+00:00", + "datePublished": "2026-01-22T00:00:00+00:00", + "documentClass": "COOP", + "documentType": "COOP_RULES", + "documentTypeDescription": "Cooperative Rules", + "entityIdentifier": "CP1044798", + "eventIdentifier": 233791, + "identifier": "DS0000101631", + "name": "", + "url": "" + } +] +DRS_STATIC2 = [ + { + "consumerDocumentId": "0100000193", + "dateCreated": "2025-01-07T16:11:59+00:00", + "datePublished": "2025-01-07T20:00:00+00:00", + "documentClass": "CORP", + "documentType": "CNTA", + "documentTypeDescription": "Continuation in Authorization", + "entityIdentifier": "C9900863", + "eventIdentifier": 155753, + "identifier": "DS0000100740", + "name": "20250107-Authorization1.pdf", + "url": "" + }, + { + "consumerDocumentId": "0100000193", + "dateCreated": "2025-01-07T16:12:05+00:00", + "datePublished": "2025-01-07T20:00:00+00:00", + "documentClass": "CORP", + "documentType": "DIRECTOR_AFFIDAVIT", + "documentTypeDescription": "Director Affidavit", + "entityIdentifier": "C9900863", + "eventIdentifier": 155753, + "identifier": "DS0000100741", + "name": "20250107-Affidavit.pdf", + "url": "" + } +] + +# testdata pattern is ({description}, {doc_data}, {drs_data}, {receipt}, {filing}, {noa}, {cert}, {static}) +TEST_FILING_UPDATE_DATA = [ + ("No docs", DOCS_MODERN, DRS_NONE, None, None, None, None, None), + ("Modern docs", DOCS_MODERN, DRS_MODERN, None, "reportType=FILING&drsId=DSR0000100954", "reportType=NOA&drsId=DSR0000100952", "reportType=CERT&drsId=DSR0000100951", None), + ("Colin docs", DOCS_COLIN, DRS_COLIN, "reportType=RECEIPT&drsId=DSR0000100896", "reportType=FILING&drsId=DSR0000100897", "reportType=NOA&drsId=DSR0000100898", "reportType=CERT&drsId=DSR0000100899", None), + ("Static 1", DOCS_STATIC1, DRS_STATIC1, None, None, None, None, "documentClass=COOP&drsId=DS0000101630"), + ("Static 2", DOCS_STATIC2, DRS_STATIC2, None, None, None, None, "documentClass=CORP&drsId=DS0000100741"), +] +# testdata pattern is ({description}, {doc_data}, {drs_data}, {receipt}, {filing}, {noa}, {cert}, {static}, {meta}, {filing_id}) +TEST_BUSINESS_UPDATE_DATA = [ + ("No docs", DOCS_MODERN, DRS_NONE, None, None, None, None, None, META_MODERN, 2405954), + ("Modern docs", DOCS_MODERN, DRS_MODERN, None, "reportType=FILING&drsId=DSR0000100954", "reportType=NOA&drsId=DSR0000100952", "reportType=CERT&drsId=DSR0000100951", None, META_MODERN, 2405954), + ("Colin docs", DOCS_COLIN, DRS_COLIN, "reportType=RECEIPT&drsId=DSR0000100896", "reportType=FILING&drsId=DSR0000100897", "reportType=NOA&drsId=DSR0000100898", "reportType=CERT&drsId=DSR0000100899", None, META_COLIN, 0), + ("Static 1", DOCS_STATIC1, DRS_STATIC1, None, None, None, None, "documentClass=COOP&drsId=DS0000101630", META_MODERN, 233791), + ("Static 2", DOCS_STATIC2, DRS_STATIC2, None, None, None, None, "documentClass=CORP&drsId=DS0000100741", META_MODERN, 155753), +] + + +@pytest.mark.parametrize("desc,doc_data,drs_data,receipt,filing,noa,cert,static", TEST_FILING_UPDATE_DATA) +def test_update_filing_docs(session, desc, doc_data, drs_data, receipt, filing, noa, cert,static): + """Assert that updating filing output url's with DRS info works as expected.""" + doc_service: DocumentService = DocumentService() + filing_docs = copy.deepcopy(doc_data) + results = doc_service.update_document_list(drs_data, filing_docs) + assert results + text_results: str = json.dumps(results) + if not receipt: + assert text_results.find("reportType=RECEIPT") < 1 + else: + assert text_results.find(receipt) > 0 + if not filing: + assert text_results.find("reportType=FILING") < 1 + else: + assert text_results.find(filing) > 0 + if not noa: + assert text_results.find("reportType=NOA") < 1 + else: + assert text_results.find(noa) > 0 + if not cert: + assert text_results.find("reportType=CERT") < 1 + else: + assert text_results.find(cert) > 0 + if not static: + assert text_results.find("documentClass=") < 1 + else: + assert text_results.find(static) > 0 + + +@pytest.mark.parametrize("desc,doc_data,drs_data,receipt,filing,noa,cert,static,meta,filing_id", TEST_BUSINESS_UPDATE_DATA) +def test_update_ledger_docs(session, desc, doc_data, drs_data, receipt, filing, noa, cert,static, meta, filing_id): + """Assert that updating business ledger filing output url's with DRS info works as expected.""" + doc_service: DocumentService = DocumentService() + filing_docs = copy.deepcopy(doc_data) + filing1: Filing = Filing() + filing1.id = filing_id + filing1._meta_data = meta # pylint: disable=protected-access + filing1.paper_only = False + if filing_id == 0: + filing1.source = filing1.Source.COLIN.value + else: + filing1.source = filing1.Source.LEAR.value + results = doc_service.update_filing_documents(drs_data, filing_docs, filing1) + assert results + text_results: str = json.dumps(results) + if not receipt: + assert text_results.find("reportType=RECEIPT") < 1 + else: + assert text_results.find(receipt) > 0 + if not filing: + assert text_results.find("reportType=FILING") < 1 + else: + assert text_results.find(filing) > 0 + if not noa: + assert text_results.find("reportType=NOA") < 1 + else: + assert text_results.find(noa) > 0 + if not cert: + assert text_results.find("reportType=CERT") < 1 + else: + assert text_results.find(cert) > 0 + if not static: + assert text_results.find("documentClass=") < 1 + else: + assert text_results.find(static) > 0 + + def test_create_document(session, mock_doc_service, mocker): mocker.patch('legal_api.services.AccountService.get_bearer_token', return_value='') founding_date = datetime.utcnow() diff --git a/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py b/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py index 8b002f28ba..2a1a5b5b7d 100644 --- a/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py +++ b/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py @@ -1464,7 +1464,7 @@ def test_unpaid_filing(session, client, jwt): HTTPStatus.OK, '2024-09-26' ) ]) -def test_document_list_for_various_filing_states(session, mocker, client, jwt, +def test_document_list_for_various_filing_states(app, session, mocker, client, jwt, monkeypatch, mock_drs_service, requests_mock, test_name, identifier, entity_type, @@ -1516,9 +1516,21 @@ def test_document_list_for_various_filing_states(session, mocker, client, jwt, 'url': f'{base_url}/api/v2/businesses/{identifier}/filings/1/documents/static/{file_key}' }) - mocker.patch('legal_api.core.filing.has_roles', return_value=True) - rv = client.get(f'/api/v2/businesses/{business.identifier}/filings/{filing.id}/documents', - headers=create_header(jwt, [STAFF_ROLE], business.identifier)) + account_id: str = '1' + headers=create_header(jwt, [STAFF_ROLE], identifier, account_id=account_id) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] + + # test + with app.test_request_context(): + # mocker.patch('legal_api.core.filing.has_roles', return_value=True) + monkeypatch.setattr('flask.request.headers.get', mock_auth) + account_products_mock = [] + requests_mock.get(f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true", + json=account_products_mock, + status_code=HTTPStatus.OK) + rv = client.get(f'/api/v2/businesses/{business.identifier}/filings/{filing.id}/documents', + headers=headers) # remove the filing ID rv_data = json.loads(re.sub("/\d+/", "/", rv.data.decode("utf-8")).replace("\n", "")) @@ -1625,7 +1637,7 @@ def filer_action(filing_name, filing_json, meta_data, business): {'documents': {}}, HTTPStatus.OK ), ]) -def test_temp_document_list_for_various_filing_states(mocker, session, client, jwt, +def test_temp_document_list_for_various_filing_states(app, mocker, session, client, jwt, monkeypatch, mock_drs_service, requests_mock, test_name, temp_identifier, identifier, @@ -1656,10 +1668,21 @@ def test_temp_document_list_for_various_filing_states(mocker, session, client, j filing._payment_completion_date = '2017-10-01' filing.temp_reg = temp_identifier filing.save() - - mocker.patch('legal_api.core.filing.has_roles', return_value=True) - rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', - headers=create_header(jwt, [STAFF_ROLE], temp_identifier)) + account_id: str = '1' + headers=create_header(jwt, [STAFF_ROLE], temp_identifier, account_id=account_id) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] + + # test + with app.test_request_context(): + mocker.patch('legal_api.core.filing.has_roles', return_value=True) + monkeypatch.setattr('flask.request.headers.get', mock_auth) + account_products_mock = [] + requests_mock.get(f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true", + json=account_products_mock, + status_code=HTTPStatus.OK) + rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', + headers=headers) # remove the filing ID rv_data = json.loads(re.sub("/\d+/", "/", rv.data.decode("utf-8")).replace("\n", "")) @@ -1795,7 +1818,7 @@ def test_get_receipt_no_receipt_ca(session, client, jwt, requests_mock): HTTPStatus.OK ) ]) -def test_temp_document_list_for_now(mocker, session, client, jwt, +def test_temp_document_list_for_now(app, mocker, session, client, jwt, monkeypatch, mock_drs_service, requests_mock, test_name, temp_identifier, entity_type, @@ -1830,10 +1853,21 @@ def test_temp_document_list_for_now(mocker, session, client, jwt, filing.temp_reg = None filing.withdrawn_filing_id = withdrawn_filing.id filing.save() - - mocker.patch('legal_api.core.filing.has_roles', return_value=True) - rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', - headers=create_header(jwt, [STAFF_ROLE], temp_identifier)) + account_id: str = '1' + headers=create_header(jwt, [STAFF_ROLE], temp_identifier, account_id=account_id) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] + + # test + with app.test_request_context(): + mocker.patch('legal_api.core.filing.has_roles', return_value=True) + monkeypatch.setattr('flask.request.headers.get', mock_auth) + account_products_mock = [] + requests_mock.get(f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true", + json=account_products_mock, + status_code=HTTPStatus.OK) + rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', + headers=headers) # remove the filing ID rv_data = json.loads(re.sub("/\d+/", "/", rv.data.decode("utf-8")).replace("\n", "")) diff --git a/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py b/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py index 27e5a8eb56..72f46e4f70 100644 --- a/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py +++ b/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py @@ -59,7 +59,7 @@ from tests.unit.services.utils import create_header REGISTER_CORRECTION_APPLICATION = 'Register Correction Application' -def test_get_all_business_filings_only_one_in_ledger(session, client, jwt): +def test_get_all_business_filings_only_one_in_ledger(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the business info can be received in a valid JSONSchema format.""" import copy identifier = 'CP7654321' @@ -71,15 +71,21 @@ def test_get_all_business_filings_only_one_in_ledger(session, client, jwt): ar['filing']['header']['colinIds'] = [] print('test_get_all_business_filings - filing:', filings) + headers=create_header(jwt, [STAFF_ROLE], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [STAFF_ROLE], identifier)) + # test + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) assert rv.status_code == HTTPStatus.OK assert len(rv.json.get('filings')) == 0 # The endpoint will return only completed filings -def test_get_all_business_filings_multi_in_ledger(session, client, jwt): +def test_get_all_business_filings_multi_in_ledger(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the business info can be received in a valid JSONSchema format.""" import copy from tests import add_years @@ -95,25 +101,36 @@ def test_get_all_business_filings_multi_in_ledger(session, client, jwt): ar['filing']['annualReport']['annualGeneralMeetingDate'] = \ datetime.date(add_years(datetime(2001, 8, 5, 7, 7, 58, 272362), i)).isoformat() factory_filing(b, ar) + headers=create_header(jwt, [STAFF_ROLE], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [STAFF_ROLE], identifier)) + # test + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) assert rv.status_code == HTTPStatus.OK assert len(rv.json.get('filings')) == 0 -def test_ledger_search(session, client, jwt): +def test_ledger_search(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns values for all the expected keys.""" # setup identifier = 'BC1234567' founding_date = datetime.utcnow() - datedelta.datedelta(months=len(FILINGS.keys())) business = factory_business(identifier=identifier, founding_date=founding_date, last_ar_date=None, entity_type=Business.LegalTypes.BCOMP.value) num_of_files = load_ledger(business, founding_date) + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) ledger = rv.json @@ -124,7 +141,7 @@ def test_ledger_search(session, client, jwt): alteration = next((f for f in ledger['filings'] if f.get('name') == 'alteration'), None) assert alteration - assert 18 == len(alteration.keys()) + assert 18 <= len(alteration.keys()) assert 'availableOnPaperOnly' in alteration assert 'effectiveDate' in alteration assert 'filingId' in alteration @@ -159,7 +176,7 @@ def ledger_element_setup_filing(business, filing_name, filing_date, filing_dict= return f -def test_ledger_comment_count(session, client, jwt): +def test_ledger_comment_count(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'BC1234567' @@ -170,10 +187,16 @@ def test_ledger_comment_count(session, client, jwt): comment.comment = f'this comment {c}' filing_storage.comments.append(comment) filing_storage.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'][0]['commentsCount'] == number_of_comments @@ -191,7 +214,8 @@ def test_ledger_comment_count(session, client, jwt): ('filing-status-Withdrawn', Filing.Status.WITHDRAWN.value, 1), ]) -def test_get_all_business_filings_permitted_statuses(session, client, jwt, test_name, filing_status, expected): +def test_get_all_business_filings_permitted_statuses(app, session, client, jwt, test_name, filing_status, expected, + monkeypatch, mock_drs_service, mocker): """Assert that the ledger only shows filings with permitted statuses.""" # setup identifier = 'BC1234567' @@ -209,10 +233,16 @@ def test_get_all_business_filings_permitted_statuses(session, client, jwt, test_ filing_storage._status = filing_status filing_storage.skip_status_listener = True filing_storage.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert len(rv.json.get('filings')) == expected @@ -233,7 +263,8 @@ def test_get_all_business_filings_permitted_statuses(session, client, jwt, test_ ['fileNumber', 'orderDetails']), ]) -def test_ledger_court_order(session, client, jwt, test_name, file_number, order_date, effect_of_order, order_details, expected): +def test_ledger_court_order(app, session, client, jwt, test_name, file_number, order_date, effect_of_order, order_details, expected, + monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns court_order values.""" # setup identifier = 'BC1234567' @@ -245,10 +276,16 @@ def test_ledger_court_order(session, client, jwt, test_name, file_number, order_ filing_storage.order_details = order_details filing_storage.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'][0] @@ -260,7 +297,7 @@ def test_ledger_court_order(session, client, jwt, test_name, file_number, order_ assert not filing_json.get('data') -def test_ledger_display_name_annual_report(session, client, jwt): +def test_ledger_display_name_annual_report(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'BC1234567' @@ -275,10 +312,16 @@ def test_ledger_display_name_annual_report(session, client, jwt): business, filing_storage = ledger_element_setup_help(identifier, 'annualReport') filing_storage._meta_data = meta_data filing_storage.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'][0] @@ -287,7 +330,7 @@ def test_ledger_display_name_annual_report(session, client, jwt): assert filing_json['displayName'] == f'Annual Report ({date.fromisoformat(today).year})' -def test_ledger_display_unknown_name(session, client, jwt): +def test_ledger_display_unknown_name(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'BC1234567' @@ -296,10 +339,16 @@ def test_ledger_display_unknown_name(session, client, jwt): business, filing_storage = ledger_element_setup_help(identifier, 'someAncientNamedReport') filing_storage._meta_data = meta_data filing_storage.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'][0] @@ -308,7 +357,7 @@ def test_ledger_display_unknown_name(session, client, jwt): assert filing_json['displayName'] == 'Some Ancient Named Report' -def test_ledger_display_alteration_report(session, client, jwt): +def test_ledger_display_alteration_report(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'BC1234567' @@ -322,10 +371,16 @@ def test_ledger_display_alteration_report(session, client, jwt): business, filing_storage = ledger_element_setup_help(identifier, 'alteration') filing_storage._meta_data = meta_data filing_storage.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'][0] @@ -340,7 +395,8 @@ def test_ledger_display_alteration_report(session, client, jwt): ('limitedRestorationExtension', 'Limited Restoration Extension Application'), ('limitedRestorationToFull', 'Conversion to Full Restoration Application'), ]) -def test_ledger_display_restoration(session, client, jwt, restoration_type, expected_display_name): +def test_ledger_display_restoration(app, session, client, jwt, restoration_type, expected_display_name, + monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct names of the four restoration types.""" # setup identifier = 'BC1234567' @@ -360,10 +416,16 @@ def test_ledger_display_restoration(session, client, jwt, restoration_type, expe filing['filing']['restoration']['type'] = restoration_type factory_completed_filing(business, filing, filing_date=filing_date) + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'] @@ -378,7 +440,8 @@ def test_ledger_display_restoration(session, client, jwt, restoration_type, expe ('CC', Business.LegalTypes.BC_CCC.value, 'BC Community Contribution Company Incorporation Application'), ('BC', Business.LegalTypes.COMP.value, 'BC Limited Company Incorporation Application'), ]) -def test_ledger_display_incorporation(session, client, jwt, test_name, entity_type, expected_display_name): +def test_ledger_display_incorporation(app, session, client, jwt, test_name, entity_type, expected_display_name, + monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'BC1234567' @@ -407,17 +470,23 @@ def test_ledger_display_incorporation(session, client, jwt, test_name, entity_ty 'legalName': business_name} } f._meta_data = {**{'applicationDate': today}, **ia_meta} + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'] assert rv.json['filings'][0]['displayName'] == expected_display_name -def test_ledger_display_corrected_incorporation(session, client, jwt): +def test_ledger_display_corrected_incorporation(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'BC1234567' @@ -425,10 +494,16 @@ def test_ledger_display_corrected_incorporation(session, client, jwt): correction = ledger_element_setup_filing(business, 'correction', filing_date=business.founding_date + datedelta.datedelta(months=3)) original.parent_filing_id = correction.id original.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'] @@ -441,7 +516,7 @@ def test_ledger_display_corrected_incorporation(session, client, jwt): assert False -def test_ledger_display_corrected_annual_report(session, client, jwt): +def test_ledger_display_corrected_annual_report(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'BC1234567' @@ -460,10 +535,16 @@ def test_ledger_display_corrected_annual_report(session, client, jwt): correction_meta = {'legalFilings': ['annualReport', 'correction']} correction._meta_data = {**{'applicationDate': today}, **correction_meta} correction.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'] @@ -490,7 +571,8 @@ def test_ledger_display_corrected_annual_report(session, client, jwt): ('unknown-public', None, UserRoles.public_user, 'some-user', '', '', 'some-user'), ] ) -def test_ledger_redaction(session, client, jwt, test_name, submitter_role, jwt_role, username, firstname, lastname, expected): +def test_ledger_redaction(app, session, client, jwt, test_name, submitter_role, jwt_role, username, firstname, lastname, expected, + monkeypatch, mock_drs_service, mocker): """Assert that the core filing is saved to the backing store.""" from legal_api.core.filing import Filing as CoreFiling try: @@ -520,9 +602,16 @@ def test_ledger_redaction(session, client, jwt, test_name, submitter_role, jwt_r new_filing.submitter_roles = submitter_role setattr(new_filing, 'skip_status_listener', True) # skip status listener new_filing.save() - - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [jwt_role], identifier)) + headers=create_header(jwt, [jwt_role], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] + + # test + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + # app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) except Exception as err: print(err) @@ -530,7 +619,7 @@ def test_ledger_redaction(session, client, jwt, test_name, submitter_role, jwt_r assert rv.json['filings'][0]['submitter'] == expected -def test_ledger_display_special_resolution_correction(session, client, jwt): +def test_ledger_display_special_resolution_correction(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'CP1234567' @@ -567,10 +656,16 @@ def test_ledger_display_special_resolution_correction(session, client, jwt): correction_2_meta = {'legalFilings': ['correction']} correction_2._meta_data = {**{'applicationDate': today}, **correction_2_meta} correction_2.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'] @@ -583,7 +678,7 @@ def test_ledger_display_special_resolution_correction(session, client, jwt): assert False -def test_ledger_display_non_special_resolution_correction_name(session, client, jwt): +def test_ledger_display_non_special_resolution_correction_name(app, session, client, jwt, monkeypatch, mock_drs_service, mocker): """Assert that the ledger returns the correct number of comments.""" # setup identifier = 'CP1234567' @@ -604,10 +699,16 @@ def test_ledger_display_non_special_resolution_correction_name(session, client, correction_meta = {'legalFilings': ['correction']} correction._meta_data = {**{'applicationDate': today}, **correction_meta} correction.save() + headers=create_header(jwt, [UserRoles.system], identifier) + def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library methods + return headers[one] # test - rv = client.get(f'/api/v2/businesses/{identifier}/filings', - headers=create_header(jwt, [UserRoles.system], identifier)) + with app.test_request_context(): + monkeypatch.setattr('flask.request.headers.get', mock_auth) + app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} + rv = client.get(f'/api/v2/businesses/{identifier}/filings', + headers=headers) # validate assert rv.json['filings'] From cd7ebab1c899da77ed6f1977dac23375094e5f30 Mon Sep 17 00:00:00 2001 From: Kial Date: Fri, 13 Mar 2026 12:42:23 -0400 Subject: [PATCH 20/46] =?UTF-8?q?Business=20API=20-=20correction=20updates?= =?UTF-8?q?=20to=20support=20relationships=20schema=20and=E2=80=A6=20(#414?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Business API - correction updates to support relationships schema and liquidators/receivers, liquidator tweaks Signed-off-by: Kial Jinnah * chore: cleanup Signed-off-by: Kial Jinnah * chore: cleanup Signed-off-by: Kial Jinnah * Business Model - update registry schemas Signed-off-by: Kial Jinnah * Add relationship handling in correction reports Signed-off-by: Kial Jinnah * add lear_only column on filings model Signed-off-by: Kial Jinnah * chore: ruff fix Signed-off-by: Kial Jinnah * Update test and remove unnecessary code based off lear_only col Signed-off-by: Kial Jinnah --------- Signed-off-by: Kial Jinnah --- .../versions/b5ded56cab5b_filing_lear_only.py | 28 +++ legal-api/poetry.lock | 16 +- legal-api/pyproject.toml | 2 +- legal-api/src/legal_api/models/filing.py | 3 + legal-api/src/legal_api/reports/report.py | 22 +++ .../resources/v2/business/colin_sync.py | 107 ++++++++---- .../validations/change_of_liquidators.py | 14 +- .../validations/change_of_receivers.py | 2 +- .../filings/validations/common_validations.py | 164 ++++++++++++++---- .../filings/validations/correction.py | 16 ++ .../filings/validations/transition.py | 2 +- legal-api/tests/unit/models/__init__.py | 13 ++ .../unit/resources/v2/test_colin_sync.py | 90 +++++++++- .../filings/validations/test_correction.py | 40 ++++- .../business-registry-model/poetry.lock | 8 +- .../business-registry-model/pyproject.toml | 2 +- .../src/business_model/models/filing.py | 2 + .../versions/b5ded56cab5b_filing_lear_only.py | 28 +++ 18 files changed, 461 insertions(+), 98 deletions(-) create mode 100644 legal-api/migrations/versions/b5ded56cab5b_filing_lear_only.py create mode 100644 python/common/business-registry-model/src/business_model_migrations/versions/b5ded56cab5b_filing_lear_only.py diff --git a/legal-api/migrations/versions/b5ded56cab5b_filing_lear_only.py b/legal-api/migrations/versions/b5ded56cab5b_filing_lear_only.py new file mode 100644 index 0000000000..1e6f2f5cd6 --- /dev/null +++ b/legal-api/migrations/versions/b5ded56cab5b_filing_lear_only.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: b5ded56cab5b +Revises: 2467e09986f2 +Create Date: 2026-03-13 09:27:35.973908 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b5ded56cab5b' +down_revision = '2467e09986f2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('filings', sa.Column('lear_only', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('filings', 'lear_only') + # ### end Alembic commands ### diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 200c993859..9714cf7012 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "alembic" @@ -785,11 +785,11 @@ files = [ ] [package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.25.0,<3.0.dev0" [package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] +grpc = ["grpcio (>=1.38.0,<2.0.dev0)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-datastore" @@ -1237,7 +1237,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} @@ -2231,8 +2231,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.63" -resolved_reference = "0e584cc385d1cd00722f96032af346b48f875d72" +reference = "2.18.64" +resolved_reference = "18b577402737130667e275d78a64e7a1ab3c9751" [[package]] name = "reportlab" @@ -3134,4 +3134,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9.22,<3.10" -content-hash = "1cb0b50b3e444405eec28df9a200178315601fa43e355662e85a1b1db1a4c03e" +content-hash = "9b6d5880cd7ec64a44c915635951a73309acfa6f88b1c32c2e1e888a8ca783ac" diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index 6a0c91de97..170ad13c6f 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "blinker (==1.4)", "pyjwt (==2.8.0)", - "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.63#egg=registry_schemas", + "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.64#egg=registry_schemas", "sql-versioning @ git+https://github.com/bcgov/lear.git@main#subdirectory=python/common/sql-versioning", "gcp-queue @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/gcp-queue", "structured-logging @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/structured-logging" diff --git a/legal-api/src/legal_api/models/filing.py b/legal-api/src/legal_api/models/filing.py index f51533537d..eb1e5f6f3e 100644 --- a/legal-api/src/legal_api/models/filing.py +++ b/legal-api/src/legal_api/models/filing.py @@ -773,6 +773,7 @@ class Source(Enum): "court_order_file_number", "deletion_locked", "hide_in_ledger", + "lear_only", "effective_date", "order_details", "paper_only", @@ -805,6 +806,7 @@ class Source(Enum): _source = db.Column("source", db.String(15), default=Source.LEAR.value) paper_only = db.Column("paper_only", db.Boolean, unique=False, default=False) colin_only = db.Column("colin_only", db.Boolean, unique=False, default=False) + lear_only = db.Column("lear_only", db.Boolean, unique=False, default=False) payment_account = db.Column("payment_account", db.String(30)) effective_date = db.Column("effective_date", db.DateTime(timezone=True), default=datetime.utcnow) submitter_roles = db.Column("submitter_roles", db.String(200)) @@ -1343,6 +1345,7 @@ def get_completed_filings_for_colin(limit=20, offset=0): ~Business.legal_type.in_(excluded_businesses), ~Filing._filing_type.in_(excluded_filings), ~and_(Filing._filing_type == "dissolution", Filing._filing_sub_type == "delay"), + ~Filing.lear_only, Filing.colin_event_ids == None, # pylint: disable=singleton-comparison # noqa: E711; Filing._status == Filing.Status.COMPLETED.value, Filing._source == Filing.Source.LEAR.value, diff --git a/legal-api/src/legal_api/reports/report.py b/legal-api/src/legal_api/reports/report.py index 6aa09098ab..5b6783bfff 100644 --- a/legal-api/src/legal_api/reports/report.py +++ b/legal-api/src/legal_api/reports/report.py @@ -1291,6 +1291,10 @@ def _format_office_data(self, filing, prev_completed_filing: Filing): def _format_party_data(self, filing, prev_completed_filing: Filing): filing["parties"] = filing.get("correction").get("parties", []) + if relationships := filing.get("correction").get("relationships"): + # map relationships to parties for pdf templates + filing["parties"].extend([self._map_relationship_to_party(relationship) for relationship in relationships]) + if filing.get("parties"): self._format_directors(filing["parties"]) filing["partyChange"] = False @@ -1490,6 +1494,24 @@ def _get_environment(): if namespace.endswith("test"): return "TEST" return "" + + @staticmethod + def _map_relationship_to_party(relationship): + # FUTURE: update pdf templates to expect relationships schema and remove this + organization_name = relationship["entity"].get("businessName") + return { + "officer": { + "id": relationship["entity"].get("identifier"), + "firstName": relationship["entity"].get("givenName"), + "middleName": relationship["entity"].get("middleInitial"), + "lastName": relationship["entity"].get("familyName"), + "organizationName": organization_name, + "partyType": "organization" if organization_name else "person" + }, + "deliveryAddress": relationship.get("deliveryAddress"), + "mailingAddress": relationship.get("mailingAddress"), + "roles": relationship.get("roles", []) + } class ReportMeta: # pylint: disable=too-few-public-methods diff --git a/legal-api/src/legal_api/resources/v2/business/colin_sync.py b/legal-api/src/legal_api/resources/v2/business/colin_sync.py index c33c306b3e..5621674e86 100644 --- a/legal-api/src/legal_api/resources/v2/business/colin_sync.py +++ b/legal-api/src/legal_api/resources/v2/business/colin_sync.py @@ -52,7 +52,7 @@ @bp.route("/internal/filings", methods=["GET"]) @cross_origin(origin="*") @jwt.has_one_of_roles([UserRoles.colin]) -def get_completed_filings_for_colin(): # noqa: PLR0912 +def get_completed_filings_for_colin(): """Get filings by status formatted in json.""" filings = [] @@ -62,41 +62,17 @@ def get_completed_filings_for_colin(): # noqa: PLR0912 for filing in pending_filings: business = Business.find_by_internal_id(filing.business_id) - filing_json = copy.deepcopy(filing.filing_json) - filing_json["filingId"] = filing.id - filing_json["filing"]["header"]["source"] = Filing.Source.LEAR.value - filing_json["filing"]["header"]["date"] = (filing.payment_completion_date or filing.filing_date).isoformat() - filing_json["filing"]["header"]["learEffectiveDate"] = filing.effective_date.isoformat() - filing_json["filing"]["header"]["isFutureEffective"] = filing.is_future_effective - filing_json["filing"]["header"]["hideInLedger"] = filing.hide_in_ledger - - if filing.submitter_roles: - filing_json["filing"]["header"]["isStaff"] = ( - UserRoles.staff in filing.submitter_roles or UserRoles.system in filing.submitter_roles - ) - if filing.filing_submitter: - filing_json["filing"]["header"]["filedBy"] = { - "userName": filing.filing_submitter.username, - "firstName": filing.filing_submitter.firstname, - "lastName": filing.filing_submitter.lastname, - "email": filing.filing_submitter.email - } - - if not filing_json["filing"].get("business"): - if filing.transaction_id: - business_revision = VersionedBusinessDetailsService.get_business_revision_obj(filing, business.id) - filing_json["filing"]["business"] = VersionedBusinessDetailsService.business_revision_json( - business_revision, business.json()) - else: - # should never happen unless its a test data created directly in db. - # found some filing in DEV, adding this check to avoid exception - filing_json["filing"]["business"] = business.json() - elif not filing_json["filing"]["business"].get("legalName"): - filing_json["filing"]["business"]["legalName"] = business.legal_name + filing_json = _get_initial_filing_json(filing, business) if filing.filing_type == "correction" and business.legal_type != Business.LegalTypes.COOP.value: try: set_correction_flags(filing_json, filing) + inner_filing_json = filing_json["filing"].get(filing.filing_type, {}) + if inner_filing_json.get("relationships"): + # set directors and completing party from the db when filing_json is using relationships schema - ignore other parties + # NOTE: in the case where only non director parties were changed then set_correction_flags will not set partyChanged and parties will not be processed by the colin-api + # if no other colin relevant corrections were made then the lear_only flag will be set during the filer processing and it will not be picked up by 'get_completed_filings_for_colin' + _set_relationship_parties(business, filing, inner_filing_json) except Exception as ex: current_app.logger.error(f"correction: filingId={filing.id}, error: {ex!s}") # to skip this filing and block subsequent filing from syncing in update-colin-filings @@ -127,6 +103,43 @@ def get_completed_filings_for_colin(): # noqa: PLR0912 return jsonify({"filings": filings}), HTTPStatus.OK +def _get_initial_filing_json(filing: Filing, business: Business): + """Return the initial filing json with common colin sync values set.""" + filing_json = copy.deepcopy(filing.filing_json) + filing_json["filingId"] = filing.id + filing_json["filing"]["header"]["source"] = Filing.Source.LEAR.value + filing_json["filing"]["header"]["date"] = (filing.payment_completion_date or filing.filing_date).isoformat() + filing_json["filing"]["header"]["learEffectiveDate"] = filing.effective_date.isoformat() + filing_json["filing"]["header"]["isFutureEffective"] = filing.is_future_effective + filing_json["filing"]["header"]["hideInLedger"] = filing.hide_in_ledger + + if filing.submitter_roles: + filing_json["filing"]["header"]["isStaff"] = ( + UserRoles.staff in filing.submitter_roles or UserRoles.system in filing.submitter_roles + ) + if filing.filing_submitter: + filing_json["filing"]["header"]["filedBy"] = { + "userName": filing.filing_submitter.username, + "firstName": filing.filing_submitter.firstname, + "lastName": filing.filing_submitter.lastname, + "email": filing.filing_submitter.email + } + + if not filing_json["filing"].get("business"): + if filing.transaction_id: + business_revision = VersionedBusinessDetailsService.get_business_revision_obj(filing, business.id) + filing_json["filing"]["business"] = VersionedBusinessDetailsService.business_revision_json( + business_revision, business.json()) + else: + # should never happen unless its a test data created directly in db. + # found some filing in DEV, adding this check to avoid exception + filing_json["filing"]["business"] = business.json() + elif not filing_json["filing"]["business"].get("legalName"): + filing_json["filing"]["business"]["legalName"] = business.legal_name + + return filing_json + + def set_correction_flags(filing_json, filing: Filing): """Set what section changed in this correction.""" if filing.meta_data.get("commentOnly", False): @@ -324,6 +337,36 @@ def _set_shares(primary_or_holding_business, amalgamation_filing, transaction_id amalgamation_filing["shareStructure"]["resolutionDates"] = business_dates +def _map_entity_to_officer(entity: dict[str, str]): + return { + "firstName": entity.get("givenName"), + "lastName": entity.get("familyName"), + "middleInitial": entity.get("middleInitial"), + "organizationName": entity.get("businessName"), + "id": entity.get("identifier") + } + + +def _set_relationship_parties(business: Business, filing: Filing, filing_json: dict): + """Override filing_json with parties set from the db. Only Directors and Completing Party will be added.""" + parties: list = VersionedBusinessDetailsService.get_party_role_revision(filing, + business.id, + role=PartyRole.RoleTypes.DIRECTOR.value) + + # copy completing party from filing json + for party_info in filing_json.get("relationships"): + if comp_party_role := next((x for x in party_info.get("roles") + if x["roleType"].lower() == "completing party"), None): + mapped_party_info = { + "officer": _map_entity_to_officer(party_info["entity"]), + "roles": [comp_party_role] + } + parties.append(mapped_party_info) + break + + filing_json["parties"] = parties + + @bp.route("/internal/filings/", methods=["PATCH"]) @cross_origin(origin="*") @jwt.has_one_of_roles([UserRoles.colin]) diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py index 6b74dc91fe..452842731b 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_liquidators.py @@ -52,14 +52,22 @@ def validate(business: Business, filing_json: dict) -> Optional[Error]: business, filing_json, filing_type, - PartyRole.RoleTypes.LIQUIDATOR, + [PartyRole.RoleTypes.LIQUIDATOR], filing_sub_type in ["appointLiquidator", "intentToLiquidate"], filing_sub_type in ["ceaseLiquidator", "changeAddressLiquidator"] )) if filing_json["filing"][filing_type].get("offices"): - allowed_offices = ["liquidationRecordsOffice"] if filing_sub_type in ["intentToLiquidate", "changeAddressLiquidator"] else [] - required_offices = ["liquidationRecordsOffice"] if filing_sub_type in ["intentToLiquidate"] else [] + allowed_offices = [] + required_offices = [] + if filing_sub_type in ["intentToLiquidate", "changeAddressLiquidator"]: + allowed_offices = ["liquidationRecordsOffice"] + if filing_sub_type == "intentToLiquidate": + required_offices = ["liquidationRecordsOffice"] + elif filing_sub_type == "appointLiquidator" and not business.in_liquidation: + allowed_offices = ["liquidationRecordsOffice"] + required_offices = ["liquidationRecordsOffice"] + msg.extend(validate_offices(filing_json, filing_type, allowed_offices, required_offices, False)) if msg: diff --git a/legal-api/src/legal_api/services/filings/validations/change_of_receivers.py b/legal-api/src/legal_api/services/filings/validations/change_of_receivers.py index f17599dc04..f482a575fd 100644 --- a/legal-api/src/legal_api/services/filings/validations/change_of_receivers.py +++ b/legal-api/src/legal_api/services/filings/validations/change_of_receivers.py @@ -51,7 +51,7 @@ def validate(business: Business, filing_json: dict) -> Optional[Error]: business, filing_json, filing_type, - PartyRole.RoleTypes.RECEIVER, + [PartyRole.RoleTypes.RECEIVER], filing_sub_type in ["amendReceiver", "appointReceiver"], filing_sub_type in ["amendReceiver", "ceaseReceiver", "changeAddressReceiver"] )) diff --git a/legal-api/src/legal_api/services/filings/validations/common_validations.py b/legal-api/src/legal_api/services/filings/validations/common_validations.py index 6d255dbc88..ba848e23d3 100644 --- a/legal-api/src/legal_api/services/filings/validations/common_validations.py +++ b/legal-api/src/legal_api/services/filings/validations/common_validations.py @@ -543,9 +543,10 @@ def validate_relationships( # noqa: PLR0913 business: Business, filing_json: dict, filing_type: str, - role_type: PartyRole.RoleTypes, + role_types: list[PartyRole.RoleTypes], allow_new: bool, - allow_edits: bool + allow_edits: bool, + role_types_for_colin_sync: Optional[list[PartyRole.RoleTypes]] = None ) -> list: """Validate the relationships information.""" msg = [] @@ -553,75 +554,176 @@ def validate_relationships( # noqa: PLR0913 party_path = f"/filing/{filing_type}/relationships" # get relevant parties for the business - party_roles: list[PartyRole] = PartyRole.get_party_roles(business.id, - datetime.now(tz=timezone.utc).date(), - role_type.value) + today = datetime.now(tz=timezone.utc).date() + party_roles: list[PartyRole] = [] + for role_type in role_types: + if roles := PartyRole.get_party_roles(business.id, today, role_type.value): + party_roles.extend(roles) + party_ids = [str(party_role.party_id) for party_role in party_roles] # Check if party is a valid party of the given role for index, relationship in enumerate(relationships): + path = f"{party_path}/{index}" identifier = relationship.get("entity", {}).get("identifier") if identifier and not allow_edits: - msg.append({"error": "Relationship edits are not allowed in this filing.", "path": f"{party_path}/{index}/entity"}) + msg.append({"error": "Relationship edits are not allowed in this filing.", "path": f"{path}/entity"}) elif identifier and identifier not in party_ids: - msg.append({"error": "Relationship with this identifier is not valid for this filing.", "path": f"{party_path}/{index}/entity/identifier"}) + msg.append({"error": "Relationship with this identifier is not valid for this filing.", "path": f"{path}/entity/identifier"}) elif not identifier and not allow_new: - msg.append({"error": "New Relationships are not allowed in this filing.", "path": f"{party_path}/{index}/entity"}) + msg.append({"error": "New Relationships are not allowed in this filing.", "path": f"{path}/entity"}) - msg.extend(validate_relationship_entity_name(relationship, party_path, index)) + msg.extend(validate_relationship_entity_name(relationship, path)) + msg.extend(validate_relationship_roles(relationship["roles"], role_types, path)) + + # Below is for colin sync checking only (i.e. any relationship with Director roles) + converted_sync_roles = [role.value.lower().replace(" ", "_") for role in role_types_for_colin_sync or []] + if any(role for role in relationship["roles"] if role["roleType"].lower() in converted_sync_roles): + validate_relationship_entity_colin_sync(relationship, business.legal_type, f"{path}/entity") msg.extend(validate_parties_addresses(filing_json, filing_type, "relationships")) - return msg -def validate_relationship_entity_name(party: dict, party_path: str, index: int) -> list: - """Validate relationship entity name.""" - msg = [] - - entity = party["entity"] - organization_name = entity.get("businessName", None) - family_name = entity.get("familyName", None) +def _get_relationship_entity_values(party: dict) -> tuple[str, str, str, str, str, str]: + """Return the expected mapped entity values.""" + entity: dict[str, str] = party["entity"] + organization_name = entity.get("businessName") or "" + given_name = entity.get("givenName") or "" + family_name = entity.get("familyName") or "" + middle_initial = entity.get("middleInitial") or "" party_type = "person" if family_name else "organization" party_roles = [x.get("roleType") for x in party["roles"]] party_roles_str = ", ".join(party_roles) + return organization_name, given_name, family_name, middle_initial, party_type, party_roles_str + +def validate_relationship_entity_name(party: dict, path: str) -> list: + """Validate relationship entity name.""" + msg = [] + organization_name, given_name, family_name, middle_initial, party_type, party_roles_str = _get_relationship_entity_values(party) + entity = party["entity"] if party_type == "person": # Only familyName is required if not family_name or not family_name.strip(): - msg.append({"error": f"{party_roles_str} familyName is required", "path": f"{party_path}/{index}/entity/familyName"}) + msg.append({"error": f"{party_roles_str} familyName is required", "path": f"{path}/entity/familyName"}) if organization_name: err_msg = f"{party_roles_str} businessName should not be set for a person relationship entity" - msg.append({"error": err_msg, "path": f"{party_path}/{index}/entity/businessName"}) + msg.append({"error": err_msg, "path": f"{path}/entity/businessName"}) if entity.get("businessIdentifier"): err_msg = f"{party_roles_str} businessIdentifier should not be set for a person relationship entity" - msg.append({"error": err_msg, "path": f"{party_path}/{index}/entity/businessIdentifier"}) + msg.append({"error": err_msg, "path": f"{path}/entity/businessIdentifier"}) elif party_type == "organization": if not organization_name or not organization_name.strip(): - msg.append({"error": f"{party_roles_str} businessName is required", "path": f"{party_path}/{index}/entity/businessName"}) + msg.append({"error": f"{party_roles_str} businessName is required", "path": f"{path}/entity/businessName"}) - if entity.get("givenName") not in (None, ""): + if given_name not in (None, ""): err_msg = f"{party_roles_str} givenName should not be set for an organization relationship entity" - msg.append({"error": err_msg, "path": f"{party_path}/{index}/entity/givenName"}) + msg.append({"error": err_msg, "path": f"{path}/entity/givenName"}) - if entity.get("middleInitial") not in (None, ""): + if middle_initial not in (None, ""): err_msg = f"{party_roles_str} middleInitial should not be set for an organization relationship entity" - msg.append({"error": err_msg, "path": f"{party_path}/{index}/entity/middleInitial"}) - - if entity.get("additionalName") not in (None, ""): - err_msg = f"{party_roles_str} additionalName should not be set for an organization relationship entity" - msg.append({"error": err_msg, "path": f"{party_path}/{index}/entity/additionalName"}) + msg.append({"error": err_msg, "path": f"{path}/entity/middleInitial"}) if entity.get("alternateName") not in (None, ""): err_msg = f"{party_roles_str} alternateName should not be set for an organization relationship entity" - msg.append({"error": err_msg, "path": f"{party_path}/{index}/entity/alternateName"}) + msg.append({"error": err_msg, "path": f"{path}/entity/alternateName"}) if entity.get("fullName") not in (None, ""): err_msg = f"{party_roles_str} fullName should not be set for an organization relationship entity" - msg.append({"error": err_msg, "path": f"{party_path}/{index}/entity/fullName"}) + msg.append({"error": err_msg, "path": f"{path}/entity/fullName"}) + + return msg + + +def _validate_relationship_entity_person_colin_sync(relationship: dict, legal_type: str, entity_path: str, custom_max_length: int, family_name_max_length: int): + """Validate the relationship entity name for a person for the COLIN sync.""" + msg = [] + _, given_name, family_name, middle_initial, _, party_roles_str = _get_relationship_entity_values(relationship) + stripped_given_name = given_name.strip() + if (legal_type in Business.CORPS) and (not stripped_given_name): + msg.append({"error": f"{party_roles_str} giveName is required", "path": f"{entity_path}/givenName"}) + elif given_name != stripped_given_name: + msg.append({ + "error": f"{party_roles_str} giveName cannot start or end with whitespace", + "path": f"{entity_path}/givenName" + }) + elif len(given_name) > custom_max_length: + err_msg = f"{party_roles_str} given name cannot be longer than {custom_max_length} characters" + msg.append({"error": err_msg, "path": f"{entity_path}/givenName"}) + + stripped_middle_initial = middle_initial.strip() + if middle_initial is not None and stripped_middle_initial: + if middle_initial != stripped_middle_initial: + msg.append({"error": f"{party_roles_str} middleInitial cannot start or end with whitespace", + "path": f"{entity_path}/middleInitial"}) + elif len(middle_initial) > custom_max_length: + err_msg = f"{party_roles_str} middleInitial cannot be longer than {custom_max_length} characters" + msg.append({"error": err_msg, "path": f"{entity_path}/middleInitial"}) + + stripped_family_name = family_name.strip() + if (legal_type in Business.CORPS) and (not stripped_family_name): + msg.append({"error": f"{party_roles_str} familyName is required", "path": f"{entity_path}/familyName"}) + elif family_name != stripped_family_name: + msg.append({ + "error": f"{party_roles_str} familyName cannot start or end with whitespace", + "path": f"{entity_path}/familyName" + }) + elif len(family_name) > family_name_max_length: + err_msg = f"{party_roles_str} family name cannot be longer than {family_name_max_length} characters" + msg.append({"error": err_msg, "path": f"{entity_path}/familyName"}) + + return msg + + +def _validate_relationship_entity_org_colin_sync(relationship: dict, entity_path: str, max_length: int): + """Validate the relationship entity name for an organization for the COLIN sync.""" + msg = [] + organization_name, _, _, _, _, party_roles_str = _get_relationship_entity_values(relationship) + stripped = organization_name.strip() + if organization_name != stripped: + err_msg = f"{party_roles_str} businessName cannot start or end with whitespace" + msg.append({"error": err_msg, "path": f"{entity_path}/businessName"}) + elif len(organization_name) > max_length: + err_msg = f"{party_roles_str} businessName cannot be longer than {max_length} characters" + msg.append({"error": err_msg, "path": f"{entity_path}/businessName"}) + + return msg + + +def validate_relationship_entity_colin_sync(relationship: dict, legal_type: str, path: str) -> list: + """Validate the relationship entity name for COLIN sync.""" + # FUTURE: This validation should be removed when COLIN sync is no longer required. + msg = [] + + custom_allowed_max_length = 20 + family_name_max_length = PARTY_NAME_MAX_LENGTH + + _, _, _, _, party_type, _ = _get_relationship_entity_values(relationship) + + if party_type == "person": + msg.extend(_validate_relationship_entity_person_colin_sync(relationship, legal_type, path, custom_allowed_max_length, family_name_max_length)) + + elif party_type == "organization": + msg.extend(_validate_relationship_entity_org_colin_sync(relationship, path, family_name_max_length)) + + return msg + + +def validate_relationship_roles(roles: list[dict[str, str]], + allowed_roles: list[PartyRole.RoleTypes], + path: str) -> list: + """Validate relationship roles.""" + msg = [] + converted_allowed_roles = [role.value.lower().replace(" ", "_") for role in allowed_roles] + for index, role in enumerate(roles): + if role.get("roleType").lower() not in converted_allowed_roles: + err_msg = "Invalid role type for this filing." + msg.append({"error": err_msg, "path": f"{path}/{index}/roleType"}) + # FUTURE: appointment/cessation date checks (currently set to filing effective date by filer) return msg diff --git a/legal-api/src/legal_api/services/filings/validations/correction.py b/legal-api/src/legal_api/services/filings/validations/correction.py index 951a35b43d..9da1cc83f1 100644 --- a/legal-api/src/legal_api/services/filings/validations/correction.py +++ b/legal-api/src/legal_api/services/filings/validations/correction.py @@ -30,6 +30,7 @@ validate_parties_addresses, validate_parties_names, validate_pdf, + validate_relationships, validate_resolution_date_in_share_structure, validate_share_structure, ) @@ -79,6 +80,21 @@ def validate(business: Business, filing: dict) -> Error: if not is_comment_only_correction: if filing.get("filing", {}).get("correction", {}).get("parties", None): msg.extend(validate_parties_addresses(filing, filing_type)) + if filing.get("filing", {}).get("correction", {}).get("relationships", None): + msg.extend(validate_relationships( + business, + filing, + filing_type, + [ + PartyRole.RoleTypes.DIRECTOR, + PartyRole.RoleTypes.LIQUIDATOR, + PartyRole.RoleTypes.RECEIVER, + PartyRole.RoleTypes.COMPLETING_PARTY + ], + True, + True, + [PartyRole.RoleTypes.DIRECTOR, PartyRole.RoleTypes.COMPLETING_PARTY] + )) if filing.get("filing", {}).get("correction", {}).get("offices", None): msg.extend(validate_offices_addresses(filing, filing_type)) # validations for firms diff --git a/legal-api/src/legal_api/services/filings/validations/transition.py b/legal-api/src/legal_api/services/filings/validations/transition.py index 6ca9cc08e0..6bcda8f52d 100644 --- a/legal-api/src/legal_api/services/filings/validations/transition.py +++ b/legal-api/src/legal_api/services/filings/validations/transition.py @@ -53,7 +53,7 @@ def validate(business: Business, filing_json: dict) -> Optional[Error]: msg.extend(validate_relationships(business, filing_json, filing_type, - PartyRole.RoleTypes.DIRECTOR, + [PartyRole.RoleTypes.DIRECTOR], False, True)) diff --git a/legal-api/tests/unit/models/__init__.py b/legal-api/tests/unit/models/__init__.py index 5b32830f09..c74aecae60 100644 --- a/legal-api/tests/unit/models/__init__.py +++ b/legal-api/tests/unit/models/__init__.py @@ -172,6 +172,18 @@ def factory_business(identifier, return business +def factory_address(street: str, address_type: str): + """Return an Address.""" + return Address( + city='Test City', + street=street, + postal_code='T3S3T3', + country='TA', + region='BC', + address_type=address_type + ) + + def factory_business_mailing_address(business): """Create a business entity.""" address = Address( @@ -260,6 +272,7 @@ def factory_completed_filing(business, filing.payment_token = payment_token filing.effective_date = filing_date filing.payment_completion_date = filing_date + filing._meta_data = {} if colin_id: colin_event = ColinEventId() colin_event.colin_event_id = colin_id diff --git a/legal-api/tests/unit/resources/v2/test_colin_sync.py b/legal-api/tests/unit/resources/v2/test_colin_sync.py index fa3f291846..a3ec4efbaf 100644 --- a/legal-api/tests/unit/resources/v2/test_colin_sync.py +++ b/legal-api/tests/unit/resources/v2/test_colin_sync.py @@ -14,31 +14,36 @@ """Tests to assure the colin sync end-point.""" import copy +from datetime import datetime from http import HTTPStatus import pytest from registry_schemas.example_data import ( ANNUAL_REPORT, CORRECTION_AR, - CORRECTION_INCORPORATION, - INCORPORATION_FILING_TEMPLATE, + CORRECTION_COL, + CORRECTION_COR ) -from legal_api.models import Business, Filing +from legal_api.models import Business, Filing, PartyRole +from legal_api.models.colin_event_id import ColinEventId from legal_api.services.authz import COLIN_SVC_ROLE from tests.unit.services.utils import create_header from tests.unit.models import ( + factory_address, factory_business, factory_business_mailing_address, factory_completed_filing, + factory_error_filing, + factory_party_role, factory_filing, + factory_pending_filing ) +from tests.unit.models import db def test_get_internal_filings(session, client, jwt): """Assert that the internal filings get endpoint returns all completed filings without colin ids.""" - from legal_api.models.colin_event_id import ColinEventId - from tests.unit.models import factory_error_filing, factory_pending_filing # setup identifier = 'CP7654321' b = factory_business(identifier) @@ -82,7 +87,6 @@ def test_get_internal_filings(session, client, jwt): def test_patch_internal_filings(session, client, jwt): """Assert that the internal filings patch endpoint updates the colin_event_id.""" - from legal_api.models.colin_event_id import ColinEventId # setup identifier = 'CP7654321' b = factory_business(identifier) @@ -106,7 +110,6 @@ def test_patch_internal_filings(session, client, jwt): def test_get_colin_id(session, client, jwt): """Assert the internal/filings/colin_id get endpoint returns properly.""" - from legal_api.models.colin_event_id import ColinEventId # setup identifier = 'CP7654321' b = factory_business(identifier) @@ -129,7 +132,6 @@ def test_get_colin_id(session, client, jwt): def test_get_colin_last_update(session, client, jwt): """Assert the get endpoint for ColinLastUpdate returns last updated colin id.""" - from tests.unit.models import db # setup colin_id = 1234 db.session.execute( @@ -153,3 +155,75 @@ def test_post_colin_last_update(session, client, jwt): ) assert rv.status_code == HTTPStatus.CREATED assert rv.json == {'maxId': colin_id} + + +def test_get_completed_filings_for_colin_corps_correction(session, client, jwt): + """Assert that corps corrections are returned as expected.""" + # setup + identifier = 'BC7654321' + b = factory_business(identifier=identifier, entity_type=Business.LegalTypes.COMP.value) + factory_business_mailing_address(b) + + correction_cod = copy.deepcopy(CORRECTION_COL) + correction_cod['filing']['correction']['correctedFilingType'] = 'changeOfDirectors' + correction_cod['filing']['correction']['relationships'][0]['roles'][0]['roleType'] = 'Director' + + filing_col = factory_completed_filing(b, CORRECTION_COL) + # Filer will set this for corrections on liquidators only + filing_col.lear_only = True + filing_col.save() + filing_cor = factory_completed_filing(b, CORRECTION_COR) + # Filer will set this for corrections on receivers only + filing_cor.lear_only = True + filing_cor.save() + filing_cod = factory_completed_filing(b, correction_cod) + # Need to apply the relationships to the db + for filing in [filing_col, filing_cor, filing_cod]: + for relationship in filing.filing_json['filing']['correction']['relationships']: + mailing_address = factory_address(relationship['mailingAddress']['streetAddress'], 'mailing') + delivery_address = factory_address(relationship['deliveryAddress']['streetAddress'], 'delivery') + officer = { + 'firstName': relationship['entity']['givenName'], + 'lastName': relationship['entity']['familyName'], + 'middleInitial': relationship['entity'].get('middleInitial'), + 'partyType': 'person', + 'organizationName': '' + } + role_type = PartyRole.RoleTypes.DIRECTOR + if relationship['roles'][0]['roleType'].lower() == 'receiver': + role_type = PartyRole.RoleTypes.RECEIVER + elif relationship['roles'][0]['roleType'].lower() == 'liquidator': + role_type = PartyRole.RoleTypes.LIQUIDATOR + + party_role = factory_party_role( + delivery_address, + mailing_address, + officer, + filing.effective_date, + None, + role_type + ) + b.party_roles.append(party_role) + b.save() + + assert filing_col.status == Filing.Status.COMPLETED.value + assert filing_cor.status == Filing.Status.COMPLETED.value + assert filing_cod.status == Filing.Status.COMPLETED.value + + # test endpoint returned filing1 only (completed, no corrections, with no colin id set) + rv = client.get('/api/v2/businesses/internal/filings', + headers=create_header(jwt, [COLIN_SVC_ROLE])) + assert rv.status_code == HTTPStatus.OK + filings = rv.json.get('filings') + # Should only return the filing with directors + assert len(filings) == 1 + # Should have been mapped to expected filing json + assert filings[0]['filingId'] == filing_cod.id + assert filings[0]['filing']['correction']['correctedFilingType'] == 'changeOfDirectors' + assert filings[0]['filing']['correction']['correctedFilingType'] == 'changeOfDirectors' + assert filings[0]['filing']['correction']['partyChanged'] == True + assert len(filings[0]['filing']['correction']['parties']) == 1 + assert filings[0]['filing']['correction']['parties'][0]['officer']['firstName'] == correction_cod['filing']['correction']['relationships'][0]['entity']['givenName'] + assert filings[0]['filing']['correction']['parties'][0]['mailingAddress']['streetAddress'] == correction_cod['filing']['correction']['relationships'][0]['mailingAddress']['streetAddress'] + assert filings[0]['filing']['correction']['parties'][0]['deliveryAddress']['streetAddress'] == correction_cod['filing']['correction']['relationships'][0]['deliveryAddress']['streetAddress'] + assert filings[0]['filing']['correction']['parties'][0]['role'] == 'director' diff --git a/legal-api/tests/unit/services/filings/validations/test_correction.py b/legal-api/tests/unit/services/filings/validations/test_correction.py index 702ebd90a4..d16733aeec 100644 --- a/legal-api/tests/unit/services/filings/validations/test_correction.py +++ b/legal-api/tests/unit/services/filings/validations/test_correction.py @@ -13,22 +13,46 @@ # limitations under the License. """Test Correction validations.""" import copy +import pytest from http import HTTPStatus -from registry_schemas.example_data import ANNUAL_REPORT, CORRECTION_AR +from registry_schemas.example_data import ( + ANNUAL_REPORT, + CHANGE_OF_DIRECTORS, + CHANGE_OF_LIQUIDATORS, + CHANGE_OF_RECEIVERS, + CORRECTION_AR, + CORRECTION_COL, + CORRECTION_COR, + FILING_TEMPLATE) from legal_api.services.filings import validate -from tests.unit.models import factory_business, factory_completed_filing, factory_filing +from tests.unit.models import factory_business, factory_business_mailing_address, factory_completed_filing, factory_filing -def test_valid_correction(mocker, session): +CORRECTION_COD = copy.deepcopy(CORRECTION_COL) +CORRECTION_COD['filing']['correction']['correctedFilingType'] = 'changeOfDirectors' +CORRECTION_COD['filing']['correction']['relationships'][0]['roles'][0]['roleType'] = 'Director' + + +@pytest.mark.parametrize('test_name, legal_type, identifier, initial_filing, correction_filing', [ + ('AR', 'CP', 'CP1234567', ANNUAL_REPORT, CORRECTION_AR), + ('COD', 'BC', 'BC1234567', CHANGE_OF_DIRECTORS, CORRECTION_COD), + ('COL', 'BC', 'BC1234567', CHANGE_OF_LIQUIDATORS, CORRECTION_COL), + ('COR', 'BC', 'BC1234567', CHANGE_OF_RECEIVERS, CORRECTION_COR) +]) +def test_valid_correction(mocker, session, test_name, legal_type, identifier, initial_filing, correction_filing): """Test that a valid correction passes validation.""" # setup - identifier = 'CP1234567' - business = factory_business(identifier) - corrected_filing = factory_completed_filing(business, ANNUAL_REPORT) - - f = copy.deepcopy(CORRECTION_AR) + filing_template = copy.deepcopy(FILING_TEMPLATE) + initial_filing_type = correction_filing['filing']['correction']['correctedFilingType'] + filing_template['filing']['header']['name'] = initial_filing_type + filing_template['filing'][initial_filing_type] = initial_filing + business = factory_business(identifier, entity_type=legal_type) + factory_business_mailing_address(business) + corrected_filing = factory_completed_filing(business, filing_template) + + f = copy.deepcopy(correction_filing) f['filing']['header']['identifier'] = identifier f['filing']['correction']['correctedFilingId'] = corrected_filing.id diff --git a/python/common/business-registry-model/poetry.lock b/python/common/business-registry-model/poetry.lock index 338aa7a26c..d4bda154b5 100644 --- a/python/common/business-registry-model/poetry.lock +++ b/python/common/business-registry-model/poetry.lock @@ -1472,7 +1472,7 @@ rpds-py = ">=0.7.0" [[package]] name = "registry_schemas" -version = "2.18.56" +version = "2.18.64" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -1490,8 +1490,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.62" -resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" +reference = "2.18.64" +resolved_reference = "18b577402737130667e275d78a64e7a1ab3c9751" [[package]] name = "requests" @@ -2085,4 +2085,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.13,<3.14" -content-hash = "12f11946bf62a6bf3d1a21472a765fe85e24629b2811aa213527d1e88d6fb22e" +content-hash = "2252d662bcc7f5a43d0e61651e96b9e7a0efd5b71c61eb3f1cda5f630cff19da" diff --git a/python/common/business-registry-model/pyproject.toml b/python/common/business-registry-model/pyproject.toml index 498ee8f35b..424f9ef1a2 100644 --- a/python/common/business-registry-model/pyproject.toml +++ b/python/common/business-registry-model/pyproject.toml @@ -9,7 +9,7 @@ readme = "README.md" requires-python = ">=3.13,<3.14" dependencies = [ "sql-versioning @ git+https://github.com/bcgov/lear.git@main#subdirectory=python/common/sql-versioning-alt", - "registry-schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.62", + "registry-schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.64", "flask-migrate (>=4.1.0,<5.0.0)", "pg8000 (>=1.31.2,<2.0.0)", "pydantic (>=2.10.6,<3.0.0)", diff --git a/python/common/business-registry-model/src/business_model/models/filing.py b/python/common/business-registry-model/src/business_model/models/filing.py index 225e37fd30..27ff78a6dd 100644 --- a/python/common/business-registry-model/src/business_model/models/filing.py +++ b/python/common/business-registry-model/src/business_model/models/filing.py @@ -774,6 +774,7 @@ class Source(Enum): 'court_order_file_number', 'deletion_locked', 'hide_in_ledger', + 'lear_only', 'effective_date', 'order_details', 'paper_only', @@ -806,6 +807,7 @@ class Source(Enum): _source = db.Column('source', db.String(15), default=Source.LEAR.value) paper_only = db.Column('paper_only', db.Boolean, unique=False, default=False) colin_only = db.Column('colin_only', db.Boolean, unique=False, default=False) + lear_only = db.Column('lear_only', db.Boolean, unique=False, default=False) payment_account = db.Column('payment_account', db.String(30)) effective_date = db.Column('effective_date', db.DateTime(timezone=True), default=func.now()) submitter_roles = db.Column('submitter_roles', db.String(200)) diff --git a/python/common/business-registry-model/src/business_model_migrations/versions/b5ded56cab5b_filing_lear_only.py b/python/common/business-registry-model/src/business_model_migrations/versions/b5ded56cab5b_filing_lear_only.py new file mode 100644 index 0000000000..9e2ba0d9a3 --- /dev/null +++ b/python/common/business-registry-model/src/business_model_migrations/versions/b5ded56cab5b_filing_lear_only.py @@ -0,0 +1,28 @@ +"""empty message + +Revision ID: b5ded56cab5b +Revises: 2467e09986f2 +Create Date: 2026-03-13 09:27:35.973908 + +""" +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b5ded56cab5b' +down_revision = '2467e09986f2' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('filings', sa.Column('lear_only', sa.Boolean(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('filings', 'lear_only') + # ### end Alembic commands ### From a15847bc6725446d3bcedd5e20926288cb3e0d48 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Fri, 13 Mar 2026 10:32:23 -0700 Subject: [PATCH 21/46] Business API - DRS API integration - updates 1. Signed-off-by: Doug Lovett --- legal-api/src/legal_api/core/filing.py | 3 +- .../src/legal_api/reports/document_service.py | 55 +++++++++++-------- .../business_filings/business_documents.py | 9 +-- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/legal-api/src/legal_api/core/filing.py b/legal-api/src/legal_api/core/filing.py index 82395efe5f..2e662ed342 100644 --- a/legal-api/src/legal_api/core/filing.py +++ b/legal-api/src/legal_api/core/filing.py @@ -103,6 +103,7 @@ class FilingTypes(str, Enum): SPECIALRESOLUTION = "specialResolution" TRANSITION = "transition" TRANSPARENCY_REGISTER = "transparencyRegister" + TOMBSTONE = "lear_tombstone" class FilingTypesCompact(str, Enum): """Render enum for filing types with sub-types.""" @@ -487,7 +488,7 @@ def _add_ledger_order(filing: FilingStorage, ledger_filing: dict) -> dict: ledger_filing["data"]["order"] = court_order_data @staticmethod - def get_document_list(business, # noqa: PLR0912, PLR0915 + def get_document_list(business, # noqa: PLR0912, PLR0915, NOSONAR (S3776) filing, jwt: JwtManager) -> dict | None: """Return a list of documents for a particular filing.""" diff --git a/legal-api/src/legal_api/reports/document_service.py b/legal-api/src/legal_api/reports/document_service.py index cdc314335c..32a6593eac 100644 --- a/legal-api/src/legal_api/reports/document_service.py +++ b/legal-api/src/legal_api/reports/document_service.py @@ -57,6 +57,10 @@ "documentClass": "CORP" } } +APP_JSON = "application/json" +APP_PDF = "application/pdf" +DOC_PATH = "/documents" +BEARER = "Bearer " class ReportTypes(BaseEnum): @@ -148,13 +152,13 @@ def create_document( token = AccountService.get_bearer_token() headers = { - "Content-Type": "application/json", + "Content-Type": APP_JSON, "X-ApiKey": self.api_key, "Account-Id": account_id, - "Authorization": "Bearer " + token + "Authorization": BEARER + token } filename: str = f"{business_identifier}_{filing_identifier}_{report_type}.pdf" - url: str = self.url.replace("/documents", "") + url: str = self.url.replace(DOC_PATH, "") post_url = (f"{url}/application-reports/" f"{self.product_code}/{business_identifier}/" f"{filing_identifier}/{report_type}?consumerFiliename={filename}") @@ -191,10 +195,10 @@ def get_document( headers = { "X-ApiKey": self.api_key, "Account-Id": account_id, - "Content-Type": "application/pdf", - "Authorization": "Bearer " + token + "Content-Type": APP_PDF, + "Authorization": BEARER + token } - url: str = self.url.replace("/documents", "") + url: str = self.url.replace(DOC_PATH, "") get_url = "" if file_key is not None: get_url = f"{url}/application-reports/{self.product_code}/{file_key}" @@ -226,10 +230,10 @@ def get_documents_by_business_id(self, business_identifier: str) -> list: headers = { "x-ApiKey": self.api_key, "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Content-Type": "application/json", - "Authorization": "Bearer " + token + "Content-Type": APP_JSON, + "Authorization": BEARER + token } - url: str = self.url.replace("/documents", "") + url: str = self.url.replace(DOC_PATH, "") get_url = BUSINESS_DOCS_PATH.format(url=url, product=self.product_code, business_id=business_identifier) response = requests.get(url=get_url, headers=headers) content = self.get_content(response) @@ -254,10 +258,10 @@ def get_documents_by_filing_id(self, business_identifier: str, filing_identifier headers = { "x-ApiKey": self.api_key, "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Content-Type": "application/json", - "Authorization": "Bearer " + token + "Content-Type": APP_JSON, + "Authorization": BEARER + token } - url: str = self.url.replace("/documents", "") + url: str = self.url.replace(DOC_PATH, "") get_url = FILING_DOCS_PATH.format( url=url, product=self.product_code, @@ -272,7 +276,7 @@ def get_documents_by_filing_id(self, business_identifier: str, filing_identifier except Exception: return [] - def update_document_list(self, drs_docs: list, document_list: list) -> list: # noqa: PLR0912 + def update_document_list(self, drs_docs: list, document_list: list) -> list: # noqa: PLR0912, NOSONAR (S3776) """ Update document_list urls with DRS information if a filing document maps to a DRS output or document. @@ -327,7 +331,12 @@ def update_document_list(self, drs_docs: list, document_list: list) -> list: # break return document_list - def update_filing_documents(self, drs_docs: list, filing_docs: list, filing: Filing) -> list: # noqa: PLR0912 + def update_filing_documents( # noqa: PLR0912, NOSONAR (S3776) + self, + drs_docs: list, + filing_docs: list, + filing: Filing + ) -> list: """ Get outputs and documents for a ledger filing, adding DRS information if available by mapping on the filing ID. @@ -403,10 +412,10 @@ def get_filing_report(self, drs_id: str, report_type: str): headers = { "x-apikey": self.api_key, "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Accept": "application/pdf", - "Authorization": "Bearer " + token + "Accept": APP_PDF, + "Authorization": BEARER + token } - url: str = self.url.replace("/documents", "") + url: str = self.url.replace(DOC_PATH, "") get_url = GET_REPORT_PATH if report_type in (ReportTypes.FILING.value, ReportTypes.NOA.value): get_url = GET_REPORT_CERTIFIED_PATH @@ -428,10 +437,10 @@ def get_filing_document(self, drs_id: str, doc_class: str): headers = { "x-apikey": self.api_key, "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Accept": "application/pdf", - "Authorization": "Bearer " + token + "Accept": APP_PDF, + "Authorization": BEARER + token } - url: str = self.url.replace("/documents", "") + url: str = self.url.replace(DOC_PATH, "") get_url = GET_DOCUMENT_PATH.format(url=url, document_class=doc_class, drs_id=drs_id) response = requests.get(url=get_url, headers=headers) if response.status_code not in (HTTPStatus.OK, HTTPStatus.NOT_FOUND): @@ -460,10 +469,10 @@ def create_filing_report( headers = { "x-apikey": self.api_key, "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Content-Type": "application/pdf", - "Authorization": "Bearer " + token + "Content-Type": APP_PDF, + "Authorization": BEARER + token } - url: str = self.url.replace("/documents", "") + url: str = self.url.replace(DOC_PATH, "") report_type: str = report_meta.get("reportType") filename: str = report_meta.get("fileName") + ".pdf" filing_date: str = filing.effective_date.isoformat()[:10] diff --git a/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py b/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py index c1343c4564..8ba369352e 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py +++ b/legal-api/src/legal_api/resources/v2/business/business_filings/business_documents.py @@ -41,8 +41,9 @@ PARAM_REPORT_TYPE: str = "reportType" PARAM_DOC_CLASS = "documentClass" PARAM_DRS_ID = "drsId" +APP_PDF = "application/pdf" CONTENT_JSON = {"Content-Type": "application/json"} -CONTENT_PDF = {"Content-Type": "application/pdf"} +CONTENT_PDF = {"Content-Type": APP_PDF} @cors_preflight("GET, POST") @@ -92,7 +93,7 @@ def get_documents(identifier: str, # noqa: PLR0911, PLR0912 return {"documents": {}}, HTTPStatus.OK return _get_document_list(business, filing) - if "application/pdf" in request.accept_mimetypes: + if APP_PDF in request.accept_mimetypes: file_name = (legal_filing_name or file_key) if not _is_document_available(business, filing, file_name): return jsonify( @@ -114,7 +115,7 @@ def get_documents(identifier: str, # noqa: PLR0911, PLR0912 return current_app.response_class( response=response.data, status=response.status, - mimetype="application/pdf" + mimetype=APP_PDF ) return {}, HTTPStatus.NOT_FOUND @@ -132,7 +133,7 @@ def _get_drs_params() -> dict: return params -def _get_drs_documents(drs_params: dict) -> dict: +def _get_drs_documents(drs_params: dict): """Return an indvidual DRS document as binary data, or a DRS JSON error.""" doc_service: DocumentService = DocumentService() drs_id: str = drs_params.get(PARAM_DRS_ID) From 35e4c4271582145db5c819d0cb40011f63ff3f82 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Fri, 13 Mar 2026 10:47:11 -0700 Subject: [PATCH 22/46] Business API - DRS API integration - updates 2. Signed-off-by: Doug Lovett --- legal-api/src/legal_api/core/filing.py | 2 +- legal-api/src/legal_api/reports/document_service.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/legal-api/src/legal_api/core/filing.py b/legal-api/src/legal_api/core/filing.py index 2e662ed342..46d3fb2b5c 100644 --- a/legal-api/src/legal_api/core/filing.py +++ b/legal-api/src/legal_api/core/filing.py @@ -488,7 +488,7 @@ def _add_ledger_order(filing: FilingStorage, ledger_filing: dict) -> dict: ledger_filing["data"]["order"] = court_order_data @staticmethod - def get_document_list(business, # noqa: PLR0912, PLR0915, NOSONAR (S3776) + def get_document_list(business, # noqa: PLR0912, PLR0915 NOSONAR(S3776) filing, jwt: JwtManager) -> dict | None: """Return a list of documents for a particular filing.""" diff --git a/legal-api/src/legal_api/reports/document_service.py b/legal-api/src/legal_api/reports/document_service.py index 32a6593eac..14d9968e63 100644 --- a/legal-api/src/legal_api/reports/document_service.py +++ b/legal-api/src/legal_api/reports/document_service.py @@ -276,7 +276,7 @@ def get_documents_by_filing_id(self, business_identifier: str, filing_identifier except Exception: return [] - def update_document_list(self, drs_docs: list, document_list: list) -> list: # noqa: PLR0912, NOSONAR (S3776) + def update_document_list(self, drs_docs: list, document_list: list) -> list: # noqa: PLR0912 NOSONAR(S3776) """ Update document_list urls with DRS information if a filing document maps to a DRS output or document. @@ -331,7 +331,7 @@ def update_document_list(self, drs_docs: list, document_list: list) -> list: # break return document_list - def update_filing_documents( # noqa: PLR0912, NOSONAR (S3776) + def update_filing_documents( # noqa: PLR0912 NOSONAR(S3776) self, drs_docs: list, filing_docs: list, From cf3a4bc857ce7d820e026c71def30024f35017b4 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Fri, 13 Mar 2026 13:06:27 -0700 Subject: [PATCH 23/46] Updates from Kial's review. Signed-off-by: Doug Lovett --- .../common/businessDetails.html | 9 +- legal-api/src/legal_api/core/filing.py | 6 +- .../src/legal_api/reports/document_service.py | 162 +++++++----------- 3 files changed, 64 insertions(+), 113 deletions(-) diff --git a/legal-api/report-templates/template-parts/common/businessDetails.html b/legal-api/report-templates/template-parts/common/businessDetails.html index c6c5404f8b..f981e20481 100644 --- a/legal-api/report-templates/template-parts/common/businessDetails.html +++ b/legal-api/report-templates/template-parts/common/businessDetails.html @@ -361,14 +361,7 @@ {% endif %}
- -{# -{% if reportType == 'summary' %} -
Information current as of {{report_date_time}}
-{% else %} -
Document retrieved on {{report_date_time}}
-{% endif %} -#} +
diff --git a/legal-api/src/legal_api/core/filing.py b/legal-api/src/legal_api/core/filing.py index 46d3fb2b5c..440f831be1 100644 --- a/legal-api/src/legal_api/core/filing.py +++ b/legal-api/src/legal_api/core/filing.py @@ -447,9 +447,9 @@ def ledger(business_id: int, # noqa: PLR0913 if filing.court_order_file_number or filing.order_details: Filing._add_ledger_order(filing, ledger_filing) - filing2: Filing = Filing() - filing2._storage = filing # pylint: disable=protected-access - filing_docs = Filing.get_document_list(business, filing2, jwt) + core_filing: Filing = Filing() # Filing.get_document_list needs a core Filing. + core_filing._storage = filing # pylint: disable=protected-access + filing_docs = Filing.get_document_list(business, core_filing, jwt) ledger_filing["documents"] = drs_service.update_filing_documents(drs_docs, filing_docs, filing) ledger.append(ledger_filing) diff --git a/legal-api/src/legal_api/reports/document_service.py b/legal-api/src/legal_api/reports/document_service.py index 14d9968e63..63e07fe22b 100644 --- a/legal-api/src/legal_api/reports/document_service.py +++ b/legal-api/src/legal_api/reports/document_service.py @@ -149,14 +149,7 @@ def create_document( """ if self.has_document(business_identifier, filing_identifier, report_type): raise BusinessException("Document already exists", HTTPStatus.CONFLICT) - - token = AccountService.get_bearer_token() - headers = { - "Content-Type": APP_JSON, - "X-ApiKey": self.api_key, - "Account-Id": account_id, - "Authorization": BEARER + token - } + headers = self._get_request_headers(account_id) filename: str = f"{business_identifier}_{filing_identifier}_{report_type}.pdf" url: str = self.url.replace(DOC_PATH, "") post_url = (f"{url}/application-reports/" @@ -191,13 +184,7 @@ def get_document( account_id: The account id. return: The document url (or binary). """ - token = AccountService.get_bearer_token() - headers = { - "X-ApiKey": self.api_key, - "Account-Id": account_id, - "Content-Type": APP_PDF, - "Authorization": BEARER + token - } + headers = self._get_request_headers(account_id) url: str = self.url.replace(DOC_PATH, "") get_url = "" if file_key is not None: @@ -226,13 +213,7 @@ def get_documents_by_business_id(self, business_identifier: str) -> list: try: if not business_identifier: return [] - token = AccountService.get_bearer_token() - headers = { - "x-ApiKey": self.api_key, - "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Content-Type": APP_JSON, - "Authorization": BEARER + token - } + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) url: str = self.url.replace(DOC_PATH, "") get_url = BUSINESS_DOCS_PATH.format(url=url, product=self.product_code, business_id=business_identifier) response = requests.get(url=get_url, headers=headers) @@ -254,13 +235,7 @@ def get_documents_by_filing_id(self, business_identifier: str, filing_identifier try: if not business_identifier or not filing_identifier: return [] - token = AccountService.get_bearer_token() - headers = { - "x-ApiKey": self.api_key, - "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Content-Type": APP_JSON, - "Authorization": BEARER + token - } + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) url: str = self.url.replace(DOC_PATH, "") get_url = FILING_DOCS_PATH.format( url=url, @@ -276,13 +251,12 @@ def get_documents_by_filing_id(self, business_identifier: str, filing_identifier except Exception: return [] - def update_document_list(self, drs_docs: list, document_list: list) -> list: # noqa: PLR0912 NOSONAR(S3776) + def update_document_list(self, drs_docs: list, document_list: list) -> list: """ Update document_list urls with DRS information if a filing document maps to a DRS output or document. drs_docs: The DRS reports/documents for the filing identifier. document_list: The business list of reports/documents for the filing. - include_docs: Include client documents linked to the filing if they exist. return: The updated list of filing reports and documents for the filing. """ if not drs_docs or not document_list or not document_list.get("documents"): @@ -307,36 +281,10 @@ def update_document_list(self, drs_docs: list, document_list: list) -> list: # doc_list[key] = doc_list[key] + query_params break elif doc.get("documentClass"): - query_params: str = DRS_DOCUMENT_PARAMS.format( - document_class=doc.get("documentClass"), - drs_id=doc.get("identifier") - ) - for key in doc_list: - if key == "staticDocuments": - for static_doc in doc_list.get("staticDocuments"): - name: str = static_doc.get("name") - if (name and name == doc.get("name")) or ( - name and STATIC_DOCUMENTS.get(name) and - doc.get("documentClass") == STATIC_DOCUMENTS[name].get("documentClass") and - doc.get("documentType") == STATIC_DOCUMENTS[name].get("documentType") - ): - static_doc["url"] = static_doc["url"] + query_params - break - elif ( - FILING_DOCUMENTS.get(key) and - doc.get("documentClass") == FILING_DOCUMENTS[key].get("documentClass") and - doc.get("documentType") == FILING_DOCUMENTS[key].get("documentType") - ): - doc_list[key] = doc_list[key] + query_params - break + doc_list = self._update_static_document(doc, doc_list) return document_list - def update_filing_documents( # noqa: PLR0912 NOSONAR(S3776) - self, - drs_docs: list, - filing_docs: list, - filing: Filing - ) -> list: + def update_filing_documents(self, drs_docs: list, filing_docs: list, filing: Filing) -> list: # noqa: PLR0912 """ Get outputs and documents for a ledger filing, adding DRS information if available by mapping on the filing ID. @@ -376,28 +324,7 @@ def update_filing_documents( # noqa: PLR0912 NOSONAR(S3776) doc_list[key] = doc_list[key] + query_params break elif doc.get("documentClass"): - query_params: str = DRS_DOCUMENT_PARAMS.format( - document_class=doc.get("documentClass"), - drs_id=doc.get("identifier") - ) - for key in doc_list: - if key == "staticDocuments": - for static_doc in doc_list.get("staticDocuments"): - name: str = static_doc.get("name") - if (name and name == doc.get("name")) or ( - name and STATIC_DOCUMENTS.get(name) and - doc.get("documentClass") == STATIC_DOCUMENTS[name].get("documentClass") and - doc.get("documentType") == STATIC_DOCUMENTS[name].get("documentType") - ): - static_doc["url"] = static_doc["url"] + query_params - break - elif ( - FILING_DOCUMENTS.get(key) and - doc.get("documentClass") == FILING_DOCUMENTS[key].get("documentClass") and - doc.get("documentType") == FILING_DOCUMENTS[key].get("documentType") - ): - doc_list[key] = doc_list[key] + query_params - break + doc_list = self._update_static_document(doc, doc_list) return doc_list def get_filing_report(self, drs_id: str, report_type: str): @@ -408,13 +335,7 @@ def get_filing_report(self, drs_id: str, report_type: str): report_type: The report type: request a certified copy for NOA and FILING report types. return: The document binary data. """ - token = AccountService.get_bearer_token() - headers = { - "x-apikey": self.api_key, - "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Accept": APP_PDF, - "Authorization": BEARER + token - } + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) url: str = self.url.replace(DOC_PATH, "") get_url = GET_REPORT_PATH if report_type in (ReportTypes.FILING.value, ReportTypes.NOA.value): @@ -433,13 +354,7 @@ def get_filing_document(self, drs_id: str, doc_class: str): document_class: The DRS document class for the business to filter on. return: The document binary data. """ - token = AccountService.get_bearer_token() - headers = { - "x-apikey": self.api_key, - "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Accept": APP_PDF, - "Authorization": BEARER + token - } + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) url: str = self.url.replace(DOC_PATH, "") get_url = GET_DOCUMENT_PATH.format(url=url, document_class=doc_class, drs_id=drs_id) response = requests.get(url=get_url, headers=headers) @@ -465,13 +380,7 @@ def create_filing_report( """ if not business_identifier or not filing or not report_meta or not report_meta.get("reportType"): return report_response - token = AccountService.get_bearer_token() - headers = { - "x-apikey": self.api_key, - "Account-Id": BUSINESS_API_ACCOUNT_ID, - "Content-Type": APP_PDF, - "Authorization": BEARER + token - } + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) url: str = self.url.replace(DOC_PATH, "") report_type: str = report_meta.get("reportType") filename: str = report_meta.get("fileName") + ".pdf" @@ -497,3 +406,52 @@ def create_filing_report( if response2.status_code == HTTPStatus.OK: return response2 return report_response + + def _update_static_document(self, doc: dict, doc_list: list) -> list: + """ + Update static document urls in the document_list if a DRS match is found. + + docs: The DRS document information to match on. + doc_list: The business list of reports/documents for the filing. + return: The updated list of static documents for the filing. + """ + query_params: str = DRS_DOCUMENT_PARAMS.format( + document_class=doc.get("documentClass"), + drs_id=doc.get("identifier") + ) + for key in doc_list: + if key == "staticDocuments": + for static_doc in doc_list.get("staticDocuments"): + name: str = static_doc.get("name") + if (name and name == doc.get("name")) or ( + name and STATIC_DOCUMENTS.get(name) and + doc.get("documentClass") == STATIC_DOCUMENTS[name].get("documentClass") and + doc.get("documentType") == STATIC_DOCUMENTS[name].get("documentType") + ): + static_doc["url"] = static_doc["url"] + query_params + break + elif ( + FILING_DOCUMENTS.get(key) and + doc.get("documentClass") == FILING_DOCUMENTS[key].get("documentClass") and + doc.get("documentType") == FILING_DOCUMENTS[key].get("documentType") + ): + doc_list[key] = doc_list[key] + query_params + break + return doc_list + + + def _get_request_headers(self, account_id: str) -> dict: + """ + Get request headers for the DRS api call. + + account_id: The account use if not the default. + return: The request headers. + """ + token = AccountService.get_bearer_token() + headers = { + "x-apikey": self.api_key, + "Account-Id": account_id if account_id else BUSINESS_API_ACCOUNT_ID, + "Content-Type": APP_PDF, + "Authorization": BEARER + token + } + return headers From a5faf618ba4f4bb71e1d90307799349de6e392f0 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Fri, 13 Mar 2026 18:31:46 -0700 Subject: [PATCH 24/46] Resolve test context unintended errors. Signed-off-by: Doug Lovett --- .../tests/unit/core/test_filing_ledger.py | 1 - .../test_filing_documents.py | 42 +++++++++---------- .../test_filings_ledger.py | 13 ------ 3 files changed, 21 insertions(+), 35 deletions(-) diff --git a/legal-api/tests/unit/core/test_filing_ledger.py b/legal-api/tests/unit/core/test_filing_ledger.py index fadb95caef..1a7cb29582 100644 --- a/legal-api/tests/unit/core/test_filing_ledger.py +++ b/legal-api/tests/unit/core/test_filing_ledger.py @@ -83,7 +83,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} ledger = CoreFiling.ledger(business.id, jwt) # Did we get the full set diff --git a/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py b/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py index 2a1a5b5b7d..73905583a7 100644 --- a/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py +++ b/legal-api/tests/unit/resources/v2/test_business_filings/test_filing_documents.py @@ -21,6 +21,7 @@ import re from datetime import datetime from http import HTTPStatus +import requests_mock import pytest from flask import current_app @@ -1464,7 +1465,7 @@ def test_unpaid_filing(session, client, jwt): HTTPStatus.OK, '2024-09-26' ) ]) -def test_document_list_for_various_filing_states(app, session, mocker, client, jwt, monkeypatch, mock_drs_service, requests_mock, +def test_document_list_for_various_filing_states(app, session, mocker, client, jwt, monkeypatch, mock_drs_service, test_name, identifier, entity_type, @@ -1523,14 +1524,14 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): - # mocker.patch('legal_api.core.filing.has_roles', return_value=True) monkeypatch.setattr('flask.request.headers.get', mock_auth) account_products_mock = [] - requests_mock.get(f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true", - json=account_products_mock, - status_code=HTTPStatus.OK) - rv = client.get(f'/api/v2/businesses/{business.identifier}/filings/{filing.id}/documents', - headers=headers) + with requests_mock.Mocker() as m: + mock_url = f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true" + m.get(mock_url, json=account_products_mock, status_code=HTTPStatus.OK) + rv = client.get(f'/api/v2/businesses/{business.identifier}/filings/{filing.id}/documents', + headers=headers) + m.reset_mock() # remove the filing ID rv_data = json.loads(re.sub("/\d+/", "/", rv.data.decode("utf-8")).replace("\n", "")) @@ -1637,7 +1638,7 @@ def filer_action(filing_name, filing_json, meta_data, business): {'documents': {}}, HTTPStatus.OK ), ]) -def test_temp_document_list_for_various_filing_states(app, mocker, session, client, jwt, monkeypatch, mock_drs_service, requests_mock, +def test_temp_document_list_for_various_filing_states(app, mocker, session, client, jwt, monkeypatch, mock_drs_service, test_name, temp_identifier, identifier, @@ -1675,14 +1676,14 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): - mocker.patch('legal_api.core.filing.has_roles', return_value=True) monkeypatch.setattr('flask.request.headers.get', mock_auth) account_products_mock = [] - requests_mock.get(f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true", - json=account_products_mock, - status_code=HTTPStatus.OK) - rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', - headers=headers) + with requests_mock.Mocker() as m: + mock_url = f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true" + m.get(mock_url, json=account_products_mock, status_code=HTTPStatus.OK) + rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', + headers=headers) + m.reset_mock() # remove the filing ID rv_data = json.loads(re.sub("/\d+/", "/", rv.data.decode("utf-8")).replace("\n", "")) @@ -1818,7 +1819,7 @@ def test_get_receipt_no_receipt_ca(session, client, jwt, requests_mock): HTTPStatus.OK ) ]) -def test_temp_document_list_for_now(app, mocker, session, client, jwt, monkeypatch, mock_drs_service, requests_mock, +def test_temp_document_list_for_now(app, mocker, session, client, jwt, monkeypatch, mock_drs_service, test_name, temp_identifier, entity_type, @@ -1860,14 +1861,13 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): - mocker.patch('legal_api.core.filing.has_roles', return_value=True) monkeypatch.setattr('flask.request.headers.get', mock_auth) account_products_mock = [] - requests_mock.get(f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true", - json=account_products_mock, - status_code=HTTPStatus.OK) - rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', - headers=headers) + with requests_mock.Mocker() as m: + mock_url = f"{app.config['AUTH_SVC_URL']}/orgs/{account_id}/products?include_hidden=true" + m.get(mock_url, json=account_products_mock, status_code=HTTPStatus.OK) + rv = client.get(f'/api/v2/businesses/{temp_identifier}/filings/{filing.id}/documents', headers=headers) + m.reset_mock() # remove the filing ID rv_data = json.loads(re.sub("/\d+/", "/", rv.data.decode("utf-8")).replace("\n", "")) diff --git a/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py b/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py index 72f46e4f70..a5481fef7d 100644 --- a/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py +++ b/legal-api/tests/unit/resources/v2/test_business_filings/test_filings_ledger.py @@ -194,7 +194,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -240,7 +239,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -283,7 +281,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -319,7 +316,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -346,7 +342,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -378,7 +373,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -423,7 +417,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -477,7 +470,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -501,7 +493,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -542,7 +533,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -609,7 +599,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - # app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) except Exception as err: @@ -663,7 +652,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) @@ -706,7 +694,6 @@ def mock_auth(one, two): # pylint: disable=unused-argument; mocks of library me # test with app.test_request_context(): monkeypatch.setattr('flask.request.headers.get', mock_auth) - app.app_ctx_globals_class.jwt_oidc_token_info = {'idp_userid': '123'} rv = client.get(f'/api/v2/businesses/{identifier}/filings', headers=headers) From f724ca8632b56dfaa4dcdab42b5f735c729d51a4 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Mon, 16 Mar 2026 10:44:45 -0700 Subject: [PATCH 25/46] Colin Data Migration active officer updates. Signed-off-by: Doug Lovett --- .../colin_business_officers.sql | 141 +++++++++++++++++- 1 file changed, 139 insertions(+), 2 deletions(-) diff --git a/data-tool/scripts/colin-migration-backfill/colin_business_officers.sql b/data-tool/scripts/colin-migration-backfill/colin_business_officers.sql index 7663523c5e..3fc7d801e0 100644 --- a/data-tool/scripts/colin-migration-backfill/colin_business_officers.sql +++ b/data-tool/scripts/colin-migration-backfill/colin_business_officers.sql @@ -649,7 +649,9 @@ declare where cp2.corp_num = cp.corp_num and cp2.party_typ_cd = cp.party_typ_cd and cp.end_event_id is not null - and cp2.start_event_id = cp.end_event_id) as edit_offices + and cp2.start_event_id = cp.end_event_id + and lower(cp2.last_name) = lower(cp.last_name) + and lower(cp2.first_name) = lower(cp.first_name)) as edit_offices from colin_extract.corp_party cp where cp.corp_num = v_corp_num and cp.party_typ_cd = 'OFF' @@ -701,6 +703,9 @@ begin rec_hist_party.cessation_dt := rec_hist_party.party_end_date; end if; party_role_id := nextval('party_roles_id_seq'); + insert into party_roles_version + values(party_role_id, to_officer_role(officer_role), rec_hist_party.appointment_dt, rec_hist_party.cessation_dt, + p_business_id, party_id, p_trans_id, null, 0, null, cast('OFFICER' as partyclasstype)); insert into party_roles values(party_role_id, to_officer_role(officer_role), rec_hist_party.appointment_dt, rec_hist_party.cessation_dt, p_business_id, party_id, null, cast('OFFICER' as partyclasstype)); @@ -738,11 +743,33 @@ begin rec_hist_offices.cessation_dt := rec_hist_offices.party_end_date; end if; party_role_id := nextval('party_roles_id_seq'); + insert into party_roles_version + values(party_role_id, to_officer_role(officer_role), rec_hist_offices.appointment_dt, rec_hist_offices.cessation_dt, + p_business_id, party_id, p_trans_id, null, 0, null, cast('OFFICER' as partyclasstype)); insert into party_roles values(party_role_id, to_officer_role(officer_role), rec_hist_offices.appointment_dt, rec_hist_offices.cessation_dt, p_business_id, party_id, null, cast('OFFICER' as partyclasstype)); end if; - end loop; + end loop; + -- Removing + elsif rec_hist_offices.edit_offices is null and rec_hist_offices.end_event_id is not null and rec_hist_offices.end_event_id > 0 then + for i in 1 .. array_length(string_to_array(rec_hist_offices.offices, ','), 1) + loop + officer_role := SPLIT_PART(rec_hist_offices.offices, ',', i); + if rec_hist_offices.appointment_dt is null then + rec_hist_offices.appointment_dt := rec_hist_party.party_date; + end if; + if rec_hist_offices.cessation_dt is null then + rec_hist_offices.cessation_dt := rec_hist_offices.party_end_date; + end if; + party_role_id := nextval('party_roles_id_seq'); + insert into party_roles_version + values(party_role_id, to_officer_role(officer_role), rec_hist_offices.appointment_dt, rec_hist_offices.cessation_dt, + p_business_id, party_id, p_trans_id, null, 0, null, cast('OFFICER' as partyclasstype)); + insert into party_roles + values(party_role_id, to_officer_role(officer_role), rec_hist_offices.appointment_dt, rec_hist_offices.cessation_dt, + p_business_id, party_id, null, cast('OFFICER' as partyclasstype)); + end loop; end if; end loop; close cur_hist_edit_offices; @@ -1039,3 +1066,113 @@ begin return update_counter; end; $$; + +-- Tombstone data patch to load current officers for a single company which has previously migrated tombstone data. +-- Identical to colin_tombstone_officers but with a range of colin_extract.corp_processing id's when running larger +-- migrations in batches. +-- The colin_extract.corp_processing table defines the set of colin corps previously migrated. +-- It must accurately represent the environment load. +-- The return value is the number of corps updated. If the number is unexpected, verify the +-- colin_extract.corp_processing and mig_corp_processing_history tables and rerun. +-- On success, mig_corp_processing_history.officer_tombstone_migrated is set to true for the corp. +-- Note: to reload a corp's active officers manually delete, including the mig_corp_processing_history table. +create or replace function public.colin_tombstone_officers_range(p_env character varying, p_cp_id_start integer, p_cp_id_end integer) returns integer + language plpgsql +as $$ +declare + cur_outstanding_cars cursor(v_env character varying, v_cp_id_start integer, v_cp_id_end integer) + for select distinct cp.id as corp_processing_id, b.id as business_id, b.identifier as corp_num, e.event_id as colin_event_id, + (select f2.transaction_id from filings f2 where f2.business_id = b.id and f2.status = 'TOMBSTONE') as transaction_id, + (select count(cp2.corp_party_id) + from colin_extract.corp_party cp2 + where cp2.corp_num = e.corp_num + and cp2.party_typ_cd = 'OFF' + and cp2.end_event_id is null) as exists_count, + (select count(cp2.corp_party_id) + from colin_extract.corp_party cp2 + where cp2.corp_num = e.corp_num + and cp2.party_typ_cd = 'OFF' + and cp2.last_name = ci.surname + and cp2.first_name = ci.firname) as match_count + from colin_extract.conv_ledger cl, colin_extract.event e, colin_extract.carindiv ci, colin_event_ids ce, + filings f, businesses b,colin_extract.corp_processing cp + where e.event_id = cl.event_id + and e.event_id = ce.colin_event_id + and cl.cars_docmnt_id = ci.documtid + and ce.filing_id = f.id + and f.business_id = b.id + and b.identifier = e.corp_num + and b.identifier = cp.corp_num + and cp.environment = v_env + and cp.id between v_cp_id_start and v_cp_id_end + and f.source = 'COLIN' + and f.status = 'COMPLETED' + and not exists (select mcph.corp_processing_id from mig_corp_processing_history mcph where mcph.corp_processing_id = cp.id) + and ci.offiflag = 'Y' + order by b.identifier, e.event_id; + cur_outstanding cursor(v_env character varying, v_cp_id_start integer, v_cp_id_end integer) + for select cp.id as corp_processing_id, b.id as business_id, b.identifier as corp_num, + (select count(p.start_event_id) + from colin_extract.corp_party p + where cp.corp_num = p.corp_num + and p.party_typ_cd = 'OFF') as officer_count, + f.transaction_id + from businesses b, filings f, colin_extract.corp_processing cp + where b.identifier = cp.corp_num + and b.id = f.business_id + and cp.processed_status = 'COMPLETED' + and f.source = 'COLIN' + and f.status = 'TOMBSTONE' + and not exists (select mcph.corp_processing_id from mig_corp_processing_history mcph where mcph.corp_processing_id = cp.id) + and not exists (select pr.id from party_roles pr where pr.business_id = b.id and pr.party_class_type = 'OFFICER') + and cp.environment = v_env + and cp.id between v_cp_id_start and v_cp_id_end + order by cp.id; + rec_cars record; + rec_officer record; + update_counter integer := 0; + previous_id integer := 0; +begin + open cur_outstanding_cars(p_env, p_cp_id_start, p_cp_id_end); + loop + fetch cur_outstanding_cars into rec_cars; + exit when not found; + if rec_cars.match_count = 0 then + update_counter := update_counter + 1; + perform colin_tombstone_cars_officer(cast(rec_cars.colin_event_id as integer), + cast(rec_cars.business_id as integer), + cast(rec_cars.transaction_id as integer), + cast(rec_cars.corp_num as character varying)); + end if; + if previous_id != rec_cars.corp_processing_id then + previous_id := rec_cars.corp_processing_id; + if rec_cars.exists_count = 0 then + insert into mig_corp_processing_history + values(rec_cars.corp_processing_id, rec_cars.transaction_id, false, false, true, null, now()); + end if; + end if; + end loop; + close cur_outstanding_cars; + previous_id := 0; + + open cur_outstanding(p_env, p_cp_id_start, p_cp_id_end); + loop + fetch cur_outstanding into rec_officer; + exit when not found; + if rec_officer.officer_count > 0 then + update_counter := update_counter + 1; + perform colin_tombstone_officer(rec_officer.business_id, + cast(rec_officer.transaction_id as integer), + rec_officer.corp_num); + end if; + if previous_id != rec_officer.corp_processing_id then + previous_id := rec_officer.corp_processing_id; + insert into mig_corp_processing_history + values(rec_officer.corp_processing_id, rec_officer.transaction_id, false, false, true, null, now()); + end if; + end loop; + close cur_outstanding; + + return update_counter; +end; +$$; From ccb8b9c6730b1b62583371903cccf26051d882fc Mon Sep 17 00:00:00 2001 From: Kial Date: Mon, 16 Mar 2026 16:48:25 -0400 Subject: [PATCH 26/46] 32669 correction relationships processing (#4151) * 32669 Business filer - update corrections to handle relationships Signed-off-by: Kial Jinnah * chore: ruff fix Signed-off-by: Kial Jinnah * chore: address sonar cube comment Signed-off-by: Kial Jinnah * address pr comment Signed-off-by: Kial Jinnah --------- Signed-off-by: Kial Jinnah --- queue_services/business-filer/poetry.lock | 38 ++--- queue_services/business-filer/pyproject.toml | 2 +- .../filing_components/correction.py | 48 ++++++ .../tests/unit/test_filer/test_correction.py | 139 ++++++++++++++++++ .../unit/test_filer/test_correction_bcia.py | 5 + 5 files changed, 212 insertions(+), 20 deletions(-) create mode 100644 queue_services/business-filer/tests/unit/test_filer/test_correction.py diff --git a/queue_services/business-filer/poetry.lock b/queue_services/business-filer/poetry.lock index f7fc95e170..8cf9962a31 100644 --- a/queue_services/business-filer/poetry.lock +++ b/queue_services/business-filer/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. [[package]] name = "alembic" @@ -113,7 +113,7 @@ files = [ [[package]] name = "business-model" -version = "3.3.22" +version = "3.3.23" description = "" optional = false python-versions = ">=3.13,<3.14" @@ -133,14 +133,14 @@ pg8000 = ">=1.31.2,<2.0.0" pycountry = ">=24.6.1,<25.0.0" pydantic = ">=2.10.6,<3.0.0" pytz = ">=2025.1,<2026.0" -registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.62"} +registry-schemas = {git = "https://github.com/bcgov/business-schemas.git", rev = "2.18.64"} sql-versioning = {git = "https://github.com/bcgov/lear.git", rev = "main", subdirectory = "python/common/sql-versioning-alt"} [package.source] type = "git" url = "https://github.com/bcgov/lear.git" reference = "main" -resolved_reference = "6ad7e1989f612f541c3468d459ccfff784bb6a29" +resolved_reference = "cd7ebab1c899da77ed6f1977dac23375094e5f30" subdirectory = "python/common/business-registry-model" [[package]] @@ -867,7 +867,7 @@ files = [ [package.dependencies] google-auth = ">=2.14.1,<3.0.0" googleapis-common-protos = ">=1.56.2,<2.0.0" -grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} +grpcio = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""} 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" @@ -875,7 +875,7 @@ 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) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] +grpc = ["grpcio (>=1.33.2,<2.0.dev0)", "grpcio (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -917,11 +917,11 @@ files = [ ] [package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" -google-auth = ">=1.25.0,<3.0dev" +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0.dev0" +google-auth = ">=1.25.0,<3.0.dev0" [package.extras] -grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] +grpc = ["grpcio (>=1.38.0,<2.0.dev0)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-datastore" @@ -936,11 +936,11 @@ files = [ ] [package.dependencies] -google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} -google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" -google-cloud-core = ">=1.4.0,<3.0.0dev" -proto-plus = {version = ">=1.22.2,<2.0.0dev", markers = "python_version >= \"3.11\""} -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" +google-api-core = {version = ">=1.34.0,<2.0.dev0 || >=2.11.dev0,<3.0.0.dev0", extras = ["grpc"]} +google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0.dev0" +google-cloud-core = ">=1.4.0,<3.0.0.dev0" +proto-plus = {version = ">=1.22.2,<2.0.0.dev0", markers = "python_version >= \"3.11\""} +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.0.dev0" [package.extras] libcst = ["libcst (>=0.2.5)"] @@ -1174,7 +1174,7 @@ files = [ [package.dependencies] googleapis-common-protos = ">=1.5.5" grpcio = ">=1.71.0" -protobuf = ">=5.26.1,<6.0dev" +protobuf = ">=5.26.1,<6.0.dev0" [[package]] name = "gunicorn" @@ -1358,7 +1358,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} @@ -2113,7 +2113,7 @@ rpds-py = ">=0.7.0" [[package]] name = "registry_schemas" -version = "2.18.62" +version = "2.18.64" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -2131,8 +2131,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.62" -resolved_reference = "1cea81cf14104fb7d4989169236eff13b3df16c4" +reference = "2.18.64" +resolved_reference = "18b577402737130667e275d78a64e7a1ab3c9751" [[package]] name = "requests" diff --git a/queue_services/business-filer/pyproject.toml b/queue_services/business-filer/pyproject.toml index 84049a43ef..c1ce19c260 100644 --- a/queue_services/business-filer/pyproject.toml +++ b/queue_services/business-filer/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "business-filer" -version = "3.0.11" +version = "3.0.12" description = "Business Registry Filer Service" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/queue_services/business-filer/src/business_filer/filing_processors/filing_components/correction.py b/queue_services/business-filer/src/business_filer/filing_processors/filing_components/correction.py index 7fbf14e8fc..468be1c2b5 100644 --- a/queue_services/business-filer/src/business_filer/filing_processors/filing_components/correction.py +++ b/queue_services/business-filer/src/business_filer/filing_processors/filing_components/correction.py @@ -53,6 +53,12 @@ shares, update_address, ) +from business_filer.filing_processors.filing_components.relationships import ( + cease_relationships, + create_relationships, + update_relationship_addresses, + update_relationship_entity_info, +) CEASE_ROLE_MAPPING = { **dict.fromkeys(Business.CORPS, PartyRole.RoleTypes.DIRECTOR.value), @@ -114,6 +120,22 @@ def correct_business_data(business: Business, # noqa: PLR0915 party_json = dpath.get(correction_filing, "/correction/parties") update_parties(business, party_json, correction_filing_rec) + # Update relationships (newer schema for parties) + with suppress(IndexError, KeyError, TypeError): + relationships = dpath.get(correction_filing, "/correction/relationships") + create_relationships(relationships, business, correction_filing_rec) + cease_relationships(relationships, + business, + [ + PartyRole.RoleTypes.DIRECTOR.value, + PartyRole.RoleTypes.LIQUIDATOR.value, + PartyRole.RoleTypes.RECEIVER.value + ], + filing_meta.application_date) + update_relationship_addresses(relationships, business) + update_relationship_entity_info(relationships, business) + _set_lear_only(correction_filing, correction_filing_rec, relationships, business) + # update court order, if any is present with suppress(IndexError, KeyError, TypeError): court_order_json = dpath.get(correction_filing, "/correction/courtOrder") @@ -276,3 +298,29 @@ def _update_addresses(offices_structure): address = Address.find_by_id(updated_address.get("id")) if address: update_address(address, updated_address) + + +def _set_lear_only(correction_filing: dict, filing_rec: Filing, relationships: list[dict], business: Business): + """Set lear_only if the only changes are to receivers and/or liquidators.""" + def _has_director_role(relationship: dict): + """Return True if the relationship contains a director role.""" + return any(role for role in relationship["roles"] if role["roleType"].lower() == "director") + + if ( + ( + not any(( + # below are the only changes the colin api supports for corrections + bool(dpath.get(correction_filing, "/correction/nameRequest", default=None)), + bool(dpath.get(correction_filing, "/correction/nameTranslations", default=None)), + bool(dpath.get(correction_filing, "/correction/offices", default=None)), + bool(dpath.get(correction_filing, "/correction/parties", default=None)), + bool(dpath.get(correction_filing, "/correction/shareStructure", default=None)), + bool(dpath.get(correction_filing, "/correction/resolution", default=None))) + )) and ( + relationships and + # colin-api only supports relationships changes to directors + not any(relationship for relationship in relationships if _has_director_role(relationship)) + ) + ): + filing_rec.lear_only = True + \ No newline at end of file diff --git a/queue_services/business-filer/tests/unit/test_filer/test_correction.py b/queue_services/business-filer/tests/unit/test_filer/test_correction.py new file mode 100644 index 0000000000..2440fa467a --- /dev/null +++ b/queue_services/business-filer/tests/unit/test_filer/test_correction.py @@ -0,0 +1,139 @@ +# Copyright © 2026 Province of British Columbia +# +# Licensed under the BSD 3 Clause License, (the "License"); +# you may not use this file except in compliance with the License. +# The template for the license can be found here +# https://opensource.org/license/bsd-3-clause/ +# +# Redistribution and use in source and binary forms, +# with or without modification, are permitted provided that the +# following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +"""The Test Suite to ensure that the worker is operating correctly for corrections.""" +import copy +import pytest +import random + +from business_model.models import Business, Filing, PartyRole +from registry_schemas.example_data import ( + CHANGE_OF_DIRECTORS, + CHANGE_OF_RECEIVERS, + CHANGE_OF_LIQUIDATORS, + CORRECTION_COL, + CORRECTION_COR, + FILING_TEMPLATE +) + +from business_filer.common.filing_message import FilingMessage +from business_filer.services.filer import process_filing +from tests.unit import create_business, create_filing + + +COD = copy.deepcopy(CHANGE_OF_DIRECTORS) +COD['directors'][0]['actions'] = ['appointed'] +COD['directors'][1]['actions'] = ['appointed'] + +CORRECTION_COD = copy.deepcopy(CORRECTION_COL) +CORRECTION_COD['filing']['correction']['correctedFilingType'] = 'changeOfDirectors' +CORRECTION_COD['filing']['correction']['relationships'][0]['roles'][0]['roleType'] = 'Director' + + +def _assert_common_data(business: Business, filing: Filing): + """Assert the expected common data was updated by the filing processing.""" + assert filing.transaction_id + assert filing.business_id == business.id + assert filing.status == Filing.Status.COMPLETED.value + + +def _get_filing(filing_type: str, data: dict, identifier = 'BC1234567', needs_template = True): + """Return the filing json, payment id and identifier.""" + payment_id = str(random.SystemRandom().getrandbits(0x58)) + if needs_template: + filing = copy.deepcopy(FILING_TEMPLATE) + filing['filing'][filing_type] = copy.deepcopy(data) + else: + filing = copy.deepcopy(data) + + filing['filing']['header']['name'] = filing_type + filing['filing']['business']['identifier'] = identifier + filing['filing']['business']['legalType'] = 'BC' + return filing, payment_id, identifier + + +@pytest.mark.parametrize('filing_name, original_data, correction_data, expected_lear_only', [ + ('changeOfDirectors', COD, CORRECTION_COD, False), + ('changeOfLiquidators', CHANGE_OF_LIQUIDATORS, CORRECTION_COL, True), + ('changeOfReceivers', CHANGE_OF_RECEIVERS, CORRECTION_COR, True), +]) +def test_process_correction_filing_with_relationships(app, session, mocker, filing_name, original_data, correction_data, expected_lear_only): + """Assert that correction filings can be applied to the model correctly.""" + # mock out the email sender and event publishing + mocker.patch('business_filer.services.publish_event.PublishEvent.publish_email_message', return_value=None) + mocker.patch('business_filer.services.publish_event.PublishEvent.publish_event', return_value=None) + mocker.patch('business_filer.filing_processors.filing_components.business_profile.update_business_profile', + return_value=None) + mocker.patch('business_filer.services.AccountService.update_entity', return_value=None) + + orig_filing, payment_id, identifier = _get_filing(filing_name, original_data) + business = create_business(identifier) + orig_filing_rec = create_filing(payment_id, orig_filing, business.id) + + # process original filing + filing_msg = FilingMessage(filing_identifier=orig_filing_rec.id) + process_filing(filing_msg) + orig_processed_filing: Filing = Filing.find_by_id(orig_filing_rec.id) + # sanity checks + _assert_common_data(business, orig_processed_filing) + party_roles = business.party_roles.all() + assert len(party_roles) == 2 + + # setup correction + correction_filing, corrected_payment_id, _ = _get_filing('correction', correction_data, business.identifier, False) + expected_given_name = 'corrected given name' + expected_mailing_street = 'corrected mailing street' + expected_delivery_street = 'corrected delivery street' + corrected_party_id = party_roles[0].party.id + correction_filing['filing']['correction']['correctedFilingId'] = orig_filing_rec.id + correction_filing['filing']['correction']['relationships'][0]['entity']['identifier'] = corrected_party_id + correction_filing['filing']['correction']['relationships'][0]['entity']['givenName'] = expected_given_name + correction_filing['filing']['correction']['relationships'][0]['mailingAddress']['streetAddress'] = expected_mailing_street + correction_filing['filing']['correction']['relationships'][0]['deliveryAddress']['streetAddress'] = expected_delivery_street + + correction_filing_rec = create_filing(corrected_payment_id, correction_filing, business.id) + + # process original filing + correction_filing_msg = FilingMessage(filing_identifier=correction_filing_rec.id) + process_filing(correction_filing_msg) + correction_processed_filing: Filing = Filing.find_by_id(correction_filing_rec.id) + # assert changes + _assert_common_data(business, correction_processed_filing) + assert correction_processed_filing.lear_only == expected_lear_only + party_roles: list[PartyRole] = business.party_roles.all() + assert len(party_roles) == 2 + for role in party_roles: + if role.party.id == corrected_party_id: + assert role.party.first_name == expected_given_name.upper() + assert role.party.mailing_address.street == expected_mailing_street + assert role.party.delivery_address.street == expected_delivery_street diff --git a/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py b/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py index 725515ad67..0a7de2f821 100644 --- a/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py +++ b/queue_services/business-filer/tests/unit/test_filer/test_correction_bcia.py @@ -697,6 +697,8 @@ def tests_filer_resolution_dates_change(app, session, mocker, test_name, legal_t # Check outcome business = Business.find_by_internal_id(business_id) + filing = Filing.find_by_id(filing_id) + assert filing.lear_only == False resolution_dates = [res.resolution_date for res in business.resolutions.all()] if 'add_resolution_dates' in test_name: @@ -839,6 +841,8 @@ def tests_filer_share_class_and_series_change(app, session, mocker, test_name, l # Check outcome business = Business.find_by_internal_id(business_id) + filing = Filing.find_by_id(filing_id) + assert filing.lear_only == False if 'add_share_class' in test_name: assert len(business.share_classes.all()) == 4 @@ -912,6 +916,7 @@ def test_comment_only_correction(app, session, mocker, test_name): process_filing(filing_msg) final_filing = Filing.find_by_id(filing_id) + assert final_filing.lear_only == False meta_data = final_filing.meta_data.get('correction', {}) assert meta_data.get('commentOnly') From 4563826b1444bc9aad9e24b7b4d49b637d695df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9verin=20Beauvais?= Date: Tue, 17 Mar 2026 12:12:57 -0700 Subject: [PATCH 27/46] 32833 bump up versions (#4171) * - set legal_api version = 2.171.0 - set business-registry-dissolution version = 0.1.1 - set business-registry-model version = 3.3.24 - set business-filer version = 3.0.13 * - revert business-registry-model version * - revert business-filer version --------- Co-authored-by: Severin Beauvais --- legal-api/src/legal_api/version.py | 2 +- python/common/business-registry-dissolution/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/legal-api/src/legal_api/version.py b/legal-api/src/legal_api/version.py index 3b53010500..d16bb9fa38 100644 --- a/legal-api/src/legal_api/version.py +++ b/legal-api/src/legal_api/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = "2.170.0" # pylint: disable=invalid-name +__version__ = "2.171.0" # pylint: disable=invalid-name diff --git a/python/common/business-registry-dissolution/pyproject.toml b/python/common/business-registry-dissolution/pyproject.toml index f5451c8ee6..2f51c46a59 100644 --- a/python/common/business-registry-dissolution/pyproject.toml +++ b/python/common/business-registry-dissolution/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "business-registry-dissolution" -version = "0.1.0" +version = "0.1.1" description = "" authors = ["BrandonSharratt "] readme = "README.md" From a2b381bce37943b1922f13133cfa3f396b07aa01 Mon Sep 17 00:00:00 2001 From: Kial Date: Wed, 18 Mar 2026 08:13:42 -0400 Subject: [PATCH 28/46] Business API: 32670,32568 - small updates (#4176) * Business API: 32670,32568 - small updates Signed-off-by: Kial Jinnah * chore: cleanup Signed-off-by: Kial Jinnah * tweak in_dissolution to be allowed for businesses that require a prta filing Signed-off-by: Kial Jinnah --------- Signed-off-by: Kial Jinnah --- legal-api/src/legal_api/models/business.py | 3 + legal-api/src/legal_api/models/party_role.py | 4 +- .../resources/v2/business/colin_sync.py | 1 + .../tests/unit/models/test_party_role.py | 2 +- .../resources/v2/test_business_parties.py | 79 +++++++++---------- .../unit/resources/v2/test_colin_sync.py | 14 ++-- .../business-registry-model/pyproject.toml | 2 +- .../src/business_model/models/business.py | 3 + .../src/business_model/models/party_role.py | 4 +- .../tests/models/test_business.py | 18 +++-- .../tests/models/test_party_role.py | 41 +++++----- 11 files changed, 90 insertions(+), 81 deletions(-) diff --git a/legal-api/src/legal_api/models/business.py b/legal-api/src/legal_api/models/business.py index 9f3470b42a..a5288f875c 100644 --- a/legal-api/src/legal_api/models/business.py +++ b/legal-api/src/legal_api/models/business.py @@ -570,6 +570,9 @@ def transition_needed_but_not_filed(self) -> bool: @property def in_dissolution(self): """Return true if in dissolution, otherwise false.""" + # businesses in liquidation are not eligible for dissolution due to overdue ARs + if self.in_liquidation and not self.transition_needed_but_not_filed(): + return False # check a business has a batch_processing entry that matches business_id and status is not COMPLETED find_in_batch_processing = db.session.query(BatchProcessing, Batch).\ filter(BatchProcessing.business_id == self.id).\ diff --git a/legal-api/src/legal_api/models/party_role.py b/legal-api/src/legal_api/models/party_role.py index f334e4adb3..8886d2a60e 100644 --- a/legal-api/src/legal_api/models/party_role.py +++ b/legal-api/src/legal_api/models/party_role.py @@ -168,9 +168,7 @@ def get_active_directors(business_id: int, end_date: datetime) -> list: def get_party_roles(business_id: int, end_date: datetime | None = None, role: str | None = None) -> list: """Return the parties that match the filter conditions.""" unsupported_roles = [ - PartyRole.RoleTypes.OFFICER.value, - PartyRole.RoleTypes.LIQUIDATOR.value, - PartyRole.RoleTypes.RECEIVER.value, + PartyRole.RoleTypes.OFFICER.value ] party_roles = db.session.query(PartyRole). \ diff --git a/legal-api/src/legal_api/resources/v2/business/colin_sync.py b/legal-api/src/legal_api/resources/v2/business/colin_sync.py index 5621674e86..b380d601f3 100644 --- a/legal-api/src/legal_api/resources/v2/business/colin_sync.py +++ b/legal-api/src/legal_api/resources/v2/business/colin_sync.py @@ -351,6 +351,7 @@ def _set_relationship_parties(business: Business, filing: Filing, filing_json: d """Override filing_json with parties set from the db. Only Directors and Completing Party will be added.""" parties: list = VersionedBusinessDetailsService.get_party_role_revision(filing, business.id, + True, role=PartyRole.RoleTypes.DIRECTOR.value) # copy completing party from filing json diff --git a/legal-api/tests/unit/models/test_party_role.py b/legal-api/tests/unit/models/test_party_role.py index e7a22ed26a..a6c9ee3155 100644 --- a/legal-api/tests/unit/models/test_party_role.py +++ b/legal-api/tests/unit/models/test_party_role.py @@ -348,7 +348,7 @@ def test_get_party_roles_unsupported_list(session): ) party_role_4.save() # Find by all party roles - unsupported_list = ['officer', 'receiver', 'liquidator'] + unsupported_list = ['officer'] party_roles = PartyRole.get_party_roles(business.id, datetime.datetime.now()) assert len(party_roles) == 4 - len(unsupported_list) diff --git a/legal-api/tests/unit/resources/v2/test_business_parties.py b/legal-api/tests/unit/resources/v2/test_business_parties.py index 3ec99d7ca4..244b659b5f 100644 --- a/legal-api/tests/unit/resources/v2/test_business_parties.py +++ b/legal-api/tests/unit/resources/v2/test_business_parties.py @@ -42,20 +42,22 @@ def test_get_business_parties_one_party_multiple_roles(app, session, client, jwt middle_initial='' ) officer.save() - party_role_1 = PartyRole( - role=PartyRole.RoleTypes.DIRECTOR.value, - appointment_date=datetime.datetime(2017, 5, 17), - cessation_date=None, - party_id=officer.id - ) - party_role_2 = PartyRole( - role=PartyRole.RoleTypes.CUSTODIAN.value, - appointment_date=datetime.datetime(2017, 5, 17), - cessation_date=None, - party_id=officer.id - ) - business.party_roles.append(party_role_1) - business.party_roles.append(party_role_2) + roles_to_test = [ + PartyRole.RoleTypes.DIRECTOR.value, + PartyRole.RoleTypes.CUSTODIAN.value, + PartyRole.RoleTypes.RECEIVER.value, + PartyRole.RoleTypes.LIQUIDATOR.value, + ] + for role_type in roles_to_test: + party_role = PartyRole( + role=role_type, + appointment_date=datetime.datetime(2017, 5, 17), + cessation_date=None, + party_id=officer.id, + business_id=business.id + ) + business.party_roles.append(party_role) + business.save() # mock response from auth to give view access (not needed if staff / system) @@ -69,7 +71,7 @@ def test_get_business_parties_one_party_multiple_roles(app, session, client, jwt assert rv.status_code == HTTPStatus.OK assert 'parties' in rv.json assert len(rv.json['parties']) == 1 - assert len(rv.json['parties'][0]['roles']) == 2 + assert len(rv.json['parties'][0]['roles']) == len(roles_to_test) def test_get_business_parties_multiple_parties(session, client, jwt): @@ -77,31 +79,26 @@ def test_get_business_parties_multiple_parties(session, client, jwt): # setup identifier = 'CP7654321' business = factory_business(identifier) - officer_1 = Party( - first_name='Connor', - last_name='Horton', - middle_initial='' - ) - party_role_1 = PartyRole( - role=PartyRole.RoleTypes.DIRECTOR.value, - appointment_date=datetime.datetime(2017, 5, 17), - cessation_date=None, - party=officer_1 - ) - officer_2 = Party( - first_name='Abraham', - last_name='Mason', - middle_initial='' - ) + roles_to_test = [ + PartyRole.RoleTypes.DIRECTOR.value, + PartyRole.RoleTypes.CUSTODIAN.value, + PartyRole.RoleTypes.RECEIVER.value, + PartyRole.RoleTypes.LIQUIDATOR.value, + ] + for index, role_type in enumerate(roles_to_test): + officer = Party( + first_name=f'Michael{index}', + last_name=f'Crane{index}', + middle_initial='' + ) + party_role = PartyRole( + role=role_type, + appointment_date=datetime.datetime(2017, 5, 17), + cessation_date=None, + party=officer + ) + business.party_roles.append(party_role) - party_role_2 = PartyRole( - role=PartyRole.RoleTypes.CUSTODIAN.value, - appointment_date=datetime.datetime(2017, 5, 17), - cessation_date=None, - party=officer_2 - ) - business.party_roles.append(party_role_1) - business.party_roles.append(party_role_2) business.save() # test @@ -111,9 +108,11 @@ def test_get_business_parties_multiple_parties(session, client, jwt): # check assert rv.status_code == HTTPStatus.OK assert 'parties' in rv.json - assert len(rv.json['parties']) == 2 + assert len(rv.json['parties']) == 4 assert len(rv.json['parties'][0]['roles']) == 1 assert len(rv.json['parties'][1]['roles']) == 1 + assert len(rv.json['parties'][2]['roles']) == 1 + assert len(rv.json['parties'][3]['roles']) == 1 def test_get_business_parties_by_role(session, client, jwt): diff --git a/legal-api/tests/unit/resources/v2/test_colin_sync.py b/legal-api/tests/unit/resources/v2/test_colin_sync.py index a3ec4efbaf..b5aeb282e1 100644 --- a/legal-api/tests/unit/resources/v2/test_colin_sync.py +++ b/legal-api/tests/unit/resources/v2/test_colin_sync.py @@ -222,8 +222,12 @@ def test_get_completed_filings_for_colin_corps_correction(session, client, jwt): assert filings[0]['filing']['correction']['correctedFilingType'] == 'changeOfDirectors' assert filings[0]['filing']['correction']['correctedFilingType'] == 'changeOfDirectors' assert filings[0]['filing']['correction']['partyChanged'] == True - assert len(filings[0]['filing']['correction']['parties']) == 1 - assert filings[0]['filing']['correction']['parties'][0]['officer']['firstName'] == correction_cod['filing']['correction']['relationships'][0]['entity']['givenName'] - assert filings[0]['filing']['correction']['parties'][0]['mailingAddress']['streetAddress'] == correction_cod['filing']['correction']['relationships'][0]['mailingAddress']['streetAddress'] - assert filings[0]['filing']['correction']['parties'][0]['deliveryAddress']['streetAddress'] == correction_cod['filing']['correction']['relationships'][0]['deliveryAddress']['streetAddress'] - assert filings[0]['filing']['correction']['parties'][0]['role'] == 'director' + parties = filings[0]['filing']['correction']['parties'] + assert len(parties) == 1 + director = parties[0] + assert director['officer']['firstName'] == correction_cod['filing']['correction']['relationships'][0]['entity']['givenName'] + assert director['mailingAddress']['streetAddress'] == correction_cod['filing']['correction']['relationships'][0]['mailingAddress']['streetAddress'] + assert director['deliveryAddress']['streetAddress'] == correction_cod['filing']['correction']['relationships'][0]['deliveryAddress']['streetAddress'] + assert len(director['roles']) == 1 + assert director['roles'][0]['roleType'] == 'Director' + assert director['roles'][0]['appointmentDate'] \ No newline at end of file diff --git a/python/common/business-registry-model/pyproject.toml b/python/common/business-registry-model/pyproject.toml index 424f9ef1a2..71e408e2db 100644 --- a/python/common/business-registry-model/pyproject.toml +++ b/python/common/business-registry-model/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "business-model" -version = "3.3.23" +version = "3.3.24" description = "" authors = [ {name = "thor",email = "1042854+thorwolpert@users.noreply.github.com"} diff --git a/python/common/business-registry-model/src/business_model/models/business.py b/python/common/business-registry-model/src/business_model/models/business.py index dff2af68fe..79ac33e3ea 100644 --- a/python/common/business-registry-model/src/business_model/models/business.py +++ b/python/common/business-registry-model/src/business_model/models/business.py @@ -597,6 +597,9 @@ def transition_needed_but_not_filed(self) -> bool: @property def in_dissolution(self): """Return true if in dissolution, otherwise false.""" + # businesses in liquidation are not eligible for dissolution due to overdue ARs + if self.in_liquidation and not self.transition_needed_but_not_filed(): + return False # check a business has a batch_processing entry that matches business_id and status is not COMPLETED find_in_batch_processing = db.session.query(BatchProcessing, Batch).\ filter(BatchProcessing.business_id == self.id).\ diff --git a/python/common/business-registry-model/src/business_model/models/party_role.py b/python/common/business-registry-model/src/business_model/models/party_role.py index d141fea0a0..de81d2592a 100644 --- a/python/common/business-registry-model/src/business_model/models/party_role.py +++ b/python/common/business-registry-model/src/business_model/models/party_role.py @@ -159,9 +159,7 @@ def get_active_directors(business_id: int, end_date: datetime) -> list: def get_party_roles(business_id: int, end_date: datetime = None, role: str = None) -> list: """Return the parties that match the filter conditions.""" unsupported_roles = [ - PartyRole.RoleTypes.OFFICER.value, - PartyRole.RoleTypes.LIQUIDATOR.value, - PartyRole.RoleTypes.RECEIVER.value, + PartyRole.RoleTypes.OFFICER.value ] party_roles = db.session.query(PartyRole). \ diff --git a/python/common/business-registry-model/tests/models/test_business.py b/python/common/business-registry-model/tests/models/test_business.py index 367c95a8a6..821778ff72 100644 --- a/python/common/business-registry-model/tests/models/test_business.py +++ b/python/common/business-registry-model/tests/models/test_business.py @@ -866,16 +866,17 @@ def test_firm_business_json(session, test_name, legal_type): @pytest.mark.parametrize( - 'test_name, is_testing_business_id, batch_status, batch_processing_status, expected', + 'test_name, is_testing_business_id, batch_status, batch_processing_status, in_liquidation, expected', [ - ('test_valid_in_dissolution', False, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.PROCESSING, True), - ('test_batch_is_completed', False, Batch.BatchStatus.COMPLETED, BatchProcessing.BatchProcessingStatus.PROCESSING, False), - ('test_batch_processing_completed', False, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.COMPLETED, False), - ('test_both_completed', False, Batch.BatchStatus.COMPLETED, BatchProcessing.BatchProcessingStatus.COMPLETED, False), - ('test_no_matched_business_id', True, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.PROCESSING, False), - ('test_batch_processing_withdrawn', False, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.WITHDRAWN, False) + ('test_valid_in_dissolution', False, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.PROCESSING, False, True), + ('test_batch_is_completed', False, Batch.BatchStatus.COMPLETED, BatchProcessing.BatchProcessingStatus.PROCESSING, False, False), + ('test_batch_processing_completed', False, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.COMPLETED, False, False), + ('test_both_completed', False, Batch.BatchStatus.COMPLETED, BatchProcessing.BatchProcessingStatus.COMPLETED, False, False), + ('test_no_matched_business_id', True, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.PROCESSING, False, False), + ('test_batch_processing_withdrawn', False, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.WITHDRAWN, False, False), + ('test_in_liquidation_with_processing', False, Batch.BatchStatus.PROCESSING, BatchProcessing.BatchProcessingStatus.PROCESSING, True, False), ]) -def test_in_dissolution(session, test_name, is_testing_business_id, batch_status, batch_processing_status, expected): +def test_in_dissolution(session, test_name, is_testing_business_id, batch_status, batch_processing_status, in_liquidation, expected): """Assert in_dissolution works as expected, did not test with different batch types, since at this moment we only have one option in the BatchType Enum""" if is_testing_business_id: business_identifier_in = 'BC1234567' @@ -899,6 +900,7 @@ def test_in_dissolution(session, test_name, is_testing_business_id, batch_status else: business_identifier = 'BC1234567' business = factory_business(business_identifier) + business.in_liquidation = in_liquidation business.save() batch = Batch( batch_type=Batch.BatchType.INVOLUNTARY_DISSOLUTION, diff --git a/python/common/business-registry-model/tests/models/test_party_role.py b/python/common/business-registry-model/tests/models/test_party_role.py index 928c3be9c3..9ebfedd5a6 100644 --- a/python/common/business-registry-model/tests/models/test_party_role.py +++ b/python/common/business-registry-model/tests/models/test_party_role.py @@ -234,29 +234,30 @@ def test_get_party_roles(session): member.save() # sanity check assert member.id - party_role_1 = PartyRole( - role=PartyRole.RoleTypes.DIRECTOR.value, - appointment_date=datetime.datetime(2017, 5, 17), - cessation_date=None, - party_id=member.id, - business_id=business.id - ) - party_role_1.save() - party_role_2 = PartyRole( - role=PartyRole.RoleTypes.CUSTODIAN.value, - appointment_date=datetime.datetime(2017, 5, 17), - cessation_date=None, - party_id=member.id, - business_id=business.id - ) - party_role_2.save() + roles_to_test = [ + PartyRole.RoleTypes.DIRECTOR.value, + PartyRole.RoleTypes.CUSTODIAN.value, + PartyRole.RoleTypes.RECEIVER.value, + PartyRole.RoleTypes.LIQUIDATOR.value, + ] + for role_type in roles_to_test: + party_role = PartyRole( + role=role_type, + appointment_date=datetime.datetime(2017, 5, 17), + cessation_date=None, + party_id=member.id, + business_id=business.id + ) + party_role.save() + # Find by all party roles party_roles = PartyRole.get_party_roles(business.id, datetime.datetime.now()) - assert len(party_roles) == 2 + assert len(party_roles) == len(roles_to_test) # Find by party role - party_roles = PartyRole.get_party_roles(business.id, datetime.datetime.now(), PartyRole.RoleTypes.CUSTODIAN.value) - assert len(party_roles) == 1 + for role_type in roles_to_test: + party_roles = PartyRole.get_party_roles(business.id, datetime.datetime.now(), role_type) + assert len(party_roles) == 1 def test_get_party_roles_by_party_id(session): @@ -388,7 +389,7 @@ def test_get_party_roles_unsupported_list(session): ) party_role_4.save() # Find by all party roles - unsupported_list = ['officer', 'receiver', 'liquidator'] + unsupported_list = ['officer'] party_roles = PartyRole.get_party_roles(business.id, datetime.datetime.now()) assert len(party_roles) == 4 - len(unsupported_list) From 9cfe911a62ec609ee6df291672695b27a6a86b13 Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Wed, 18 Mar 2026 09:28:24 -0700 Subject: [PATCH 29/46] 32823 certify_by is not required for corporations (#4180) --- legal-api/poetry.lock | 18 ++++----- legal-api/pyproject.toml | 2 +- .../business_filings/business_filings.py | 38 ++++++++----------- .../filings/validations/common_validations.py | 27 ++++++++++--- .../filings/validations/validation.py | 2 +- .../validations/test_common_validations.py | 29 +++++++++++--- 6 files changed, 71 insertions(+), 45 deletions(-) diff --git a/legal-api/poetry.lock b/legal-api/poetry.lock index 9714cf7012..9016152375 100644 --- a/legal-api/poetry.lock +++ b/legal-api/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "alembic" @@ -785,11 +785,11 @@ files = [ ] [package.dependencies] -google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0.dev0" -google-auth = ">=1.25.0,<3.0.dev0" +google-api-core = ">=1.31.6,<2.0.dev0 || >2.3.0,<3.0.0dev" +google-auth = ">=1.25.0,<3.0dev" [package.extras] -grpc = ["grpcio (>=1.38.0,<2.0.dev0)", "grpcio-status (>=1.38.0,<2.0.dev0)"] +grpc = ["grpcio (>=1.38.0,<2.0dev)", "grpcio-status (>=1.38.0,<2.0.dev0)"] [[package]] name = "google-cloud-datastore" @@ -1237,7 +1237,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format\""} idna = {version = "*", optional = true, markers = "extra == \"format\""} isoduration = {version = "*", optional = true, markers = "extra == \"format\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format\""} -jsonschema-specifications = ">=2023.3.6" +jsonschema-specifications = ">=2023.03.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format\""} rfc3987 = {version = "*", optional = true, markers = "extra == \"format\""} @@ -2213,7 +2213,7 @@ typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} [[package]] name = "registry_schemas" -version = "2.18.63" +version = "2.18.65" description = "A short description of the project" optional = false python-versions = ">=3.6" @@ -2231,8 +2231,8 @@ strict-rfc3339 = "*" [package.source] type = "git" url = "https://github.com/bcgov/business-schemas.git" -reference = "2.18.64" -resolved_reference = "18b577402737130667e275d78a64e7a1ab3c9751" +reference = "2.18.65" +resolved_reference = "56b5f387f42cb02d06fe33df5d3468216e65d099" [[package]] name = "reportlab" @@ -3134,4 +3134,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.9.22,<3.10" -content-hash = "9b6d5880cd7ec64a44c915635951a73309acfa6f88b1c32c2e1e888a8ca783ac" +content-hash = "112d1641a00fbe06e51d3bede55de47b59917baf015f4be91198bc9bd4d1fb79" diff --git a/legal-api/pyproject.toml b/legal-api/pyproject.toml index 0c7984a41e..66b69fa57e 100644 --- a/legal-api/pyproject.toml +++ b/legal-api/pyproject.toml @@ -52,7 +52,7 @@ dependencies = [ "blinker (==1.4)", "pyjwt (==2.8.0)", - "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.64#egg=registry_schemas", + "registry_schemas @ git+https://github.com/bcgov/business-schemas.git@2.18.65#egg=registry_schemas", "sql-versioning @ git+https://github.com/bcgov/lear.git@main#subdirectory=python/common/sql-versioning", "gcp-queue @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/gcp-queue", "structured-logging @ git+https://github.com/bcgov/sbc-connect-common.git@main#subdirectory=python/structured-logging" diff --git a/legal-api/src/legal_api/resources/v2/business/business_filings/business_filings.py b/legal-api/src/legal_api/resources/v2/business/business_filings/business_filings.py index c8880f02c6..5450745acc 100644 --- a/legal-api/src/legal_api/resources/v2/business/business_filings/business_filings.py +++ b/legal-api/src/legal_api/resources/v2/business/business_filings/business_filings.py @@ -135,7 +135,7 @@ def saving_filings(body: FilingModel, # noqa: PLR0911, PLR0912 if err_msg: return jsonify({"errors": [err_msg, ]}), err_code json_input = copy.deepcopy(request.get_json()) # used for validation - ListFilingResource.modify_filing_json(json_input, filing, for_validation=True) + ListFilingResource.modify_filing_json(json_input, filing) # check authorization response, response_code = ListFilingResource.check_authorization(identifier, json_input, business, filing) @@ -1166,29 +1166,23 @@ def details_for_invoice(business_identifier: str, corp_type: str): ] @staticmethod - def modify_filing_json(filing_json, filing: Filing = None, for_validation=False): + def modify_filing_json(filing_json, filing: Filing = None): """Modify filing json values if needed.""" filing_type = filing_json["filing"]["header"]["name"] - if filing_type == Filing.FILINGS["continuationIn"].get("name"): - if for_validation and (not filing or - filing.status in [Filing.Status.DRAFT.value, - Filing.Status.CHANGE_REQUESTED.value]): - # certifiedBy is a common header, which is required field as per schema - # its not required for authorization (and cannot make it optional in schema only for continuationIn) - filing_json["filing"]["header"]["certifiedBy"] = "submission for review" - - if filing and filing.status == Filing.Status.APPROVED.value: - filing_json["filing"][filing_type]["isApproved"] = True - - # Once approved, user cannot change continuation in authorization details - if expro_business := filing.filing_json["filing"][filing_type].get("business"): - filing_json["filing"][filing_type]["business"] = expro_business - filing_json["filing"][filing_type]["authorization"] = \ - filing.filing_json["filing"][filing_type]["authorization"] - filing_json["filing"][filing_type]["nameRequest"] = \ - filing.filing_json["filing"][filing_type]["nameRequest"] - filing_json["filing"][filing_type]["foreignJurisdiction"] = \ - filing.filing_json["filing"][filing_type]["foreignJurisdiction"] + if (filing_type == Filing.FILINGS["continuationIn"].get("name") and + filing and filing.status == Filing.Status.APPROVED.value + ): + filing_json["filing"][filing_type]["isApproved"] = True + + # Once approved, user cannot change continuation in authorization details + if expro_business := filing.filing_json["filing"][filing_type].get("business"): + filing_json["filing"][filing_type]["business"] = expro_business + filing_json["filing"][filing_type]["authorization"] = \ + filing.filing_json["filing"][filing_type]["authorization"] + filing_json["filing"][filing_type]["nameRequest"] = \ + filing.filing_json["filing"][filing_type]["nameRequest"] + filing_json["filing"][filing_type]["foreignJurisdiction"] = \ + filing.filing_json["filing"][filing_type]["foreignJurisdiction"] @staticmethod def submit_filing_for_review(business: Union[Business, RegistrationBootstrap], filing: Filing): diff --git a/legal-api/src/legal_api/services/filings/validations/common_validations.py b/legal-api/src/legal_api/services/filings/validations/common_validations.py index ba848e23d3..9db2a50c40 100644 --- a/legal-api/src/legal_api/services/filings/validations/common_validations.py +++ b/legal-api/src/legal_api/services/filings/validations/common_validations.py @@ -23,7 +23,7 @@ from flask_babel import _ from pypdf import PdfReader -from legal_api.core.filing import Filing +from legal_api.core.filing import Filing as CoreFiling from legal_api.errors import Error from legal_api.models import Address, Business, PartyRole from legal_api.models.configuration import EMAIL_PATTERN @@ -134,7 +134,7 @@ def validate_share_structure(incorporation_json, filing_type, legal_type) -> Err memoize_names = [] # For incorporation applications, at least one share class is required, for Alteration can not include if not changing - if filing_type == Filing.FilingTypes.INCORPORATIONAPPLICATION.value and len(share_classes) == 0: + if filing_type == CoreFiling.FilingTypes.INCORPORATIONAPPLICATION.value and len(share_classes) == 0: msg.append({ "error": "A company must have at least one Class of Shares.", "path": f"/filing/{filing_type}/shareStructure/shareClasses" @@ -1190,13 +1190,28 @@ def validate_certify_name(filing_json) -> bool: return True return True -def validate_certified_by(filing_json: dict) -> list: +def validate_certified_by(filing_json: dict, business: Business) -> list: """Validate certifiedBy field.""" msg = [] - certified_by = filing_json["filing"]["header"]["certifiedBy"] + certified_by = filing_json["filing"]["header"].get("certifiedBy") + filing_type = filing_json["filing"]["header"].get("name") - # Only validate if non-whitespace characters are present - if certified_by.strip() and certified_by != certified_by.strip(): + if isinstance(business, Business): + legal_type = business.legal_type + elif filing_type == CoreFiling.FilingTypes.NOTICEOFWITHDRAWAL: + legal_type = filing_json["filing"].get("business", None).get("legalType") + else: + legal_type = filing_json["filing"][filing_type]["nameRequest"].get("legalType") + + if legal_type in Business.CORPS: + return msg # certifiedBy is not required for corporations + + if not certified_by: + msg.append({ + "error": "Certified by field is required.", + "path": "/filing/header/certifiedBy" + }) + elif certified_by.strip() and certified_by != certified_by.strip(): msg.append({ "error": "Certified by field cannot start or end with whitespace.", "path": "/filing/header/certifiedBy" diff --git a/legal-api/src/legal_api/services/filings/validations/validation.py b/legal-api/src/legal_api/services/filings/validations/validation.py index f32317455b..3173eaf52b 100644 --- a/legal-api/src/legal_api/services/filings/validations/validation.py +++ b/legal-api/src/legal_api/services/filings/validations/validation.py @@ -69,7 +69,7 @@ def validate(business: Business, # noqa: PLR0915, PLR0912, PLR0911 if err: return err - cert_errs = validate_certified_by(filing_json) + cert_errs = validate_certified_by(filing_json, business) if cert_errs: return Error(HTTPStatus.BAD_REQUEST, cert_errs) diff --git a/legal-api/tests/unit/services/filings/validations/test_common_validations.py b/legal-api/tests/unit/services/filings/validations/test_common_validations.py index 7dd8538180..479acbab70 100644 --- a/legal-api/tests/unit/services/filings/validations/test_common_validations.py +++ b/legal-api/tests/unit/services/filings/validations/test_common_validations.py @@ -554,6 +554,8 @@ def mock_address_find_by_id(address_id): assert edited_result['address_changed'] == False assert edited_result['delivery_address_changed'] == True +@pytest.mark.parametrize('legal_type', Business.CORPS + [ + Business.LegalTypes.COOP, Business.LegalTypes.SOLE_PROP, Business.LegalTypes.PARTNERSHIP]) @pytest.mark.parametrize('input_value, expected_error', [ ('John Doe', False), (' \t ', False), @@ -561,19 +563,34 @@ def mock_address_find_by_id(address_id): (' John Doe', True), ('John Doe ', True), ]) -def test_validate_certified_by(input_value, expected_error): +def test_validate_certified_by(session, legal_type, input_value, expected_error): """Test that certified by field can be validated.""" filing = copy.deepcopy(FILING_HEADER) filing['filing']['header']['certifiedBy'] = input_value - errors = validate_certified_by(filing) + if legal_type == Business.LegalTypes.COOP: + identifier = 'CP1234567' + filing['filing']['incorporationApplication'] = INCORPORATION + elif legal_type in [Business.LegalTypes.SOLE_PROP, Business.LegalTypes.PARTNERSHIP]: + identifier = 'FM1234567' + filing['filing']['registration'] = REGISTRATION + else: + identifier = 'BC1234567' + filing['filing']['incorporationApplication'] = INCORPORATION + + business = factory_business(identifier=identifier, entity_type=legal_type) - if expected_error: + errors = validate_certified_by(filing, business) + + if legal_type in Business.CORPS: + assert errors == [] + elif expected_error: assert errors - assert errors[0]['error'] == 'Certified by field cannot start or end with whitespace.' + if input_value: + assert errors[0]['error'] == 'Certified by field cannot start or end with whitespace.' + else: + assert errors[0]['error'] == 'Certified by field is required.' assert errors[0]['path'] == '/filing/header/certifiedBy' - else: - assert errors == [] @pytest.mark.parametrize( ('party_type', 'organization_name','officer_override', 'expected_errors'), From 0750ad487d62b8f17b28c890109759d1a2eb881a Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Wed, 18 Mar 2026 09:39:07 -0700 Subject: [PATCH 30/46] Business API rename ledger docs property UI issue. Signed-off-by: Doug Lovett --- legal-api/src/legal_api/core/filing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legal-api/src/legal_api/core/filing.py b/legal-api/src/legal_api/core/filing.py index 440f831be1..67ec93048a 100644 --- a/legal-api/src/legal_api/core/filing.py +++ b/legal-api/src/legal_api/core/filing.py @@ -450,7 +450,7 @@ def ledger(business_id: int, # noqa: PLR0913 core_filing: Filing = Filing() # Filing.get_document_list needs a core Filing. core_filing._storage = filing # pylint: disable=protected-access filing_docs = Filing.get_document_list(business, core_filing, jwt) - ledger_filing["documents"] = drs_service.update_filing_documents(drs_docs, filing_docs, filing) + ledger_filing["drsDocuments"] = drs_service.update_filing_documents(drs_docs, filing_docs, filing) ledger.append(ledger_filing) return ledger From 4f350d07a25d98947077bf00d4127e79039cee7f Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Wed, 18 Mar 2026 14:12:14 -0700 Subject: [PATCH 31/46] DRS fix filename from report meta issue. Signed-off-by: Doug Lovett --- .../src/legal_api/reports/document_service.py | 40 ++++++++++++++++++- legal-api/src/legal_api/reports/report.py | 37 +++++------------ 2 files changed, 48 insertions(+), 29 deletions(-) diff --git a/legal-api/src/legal_api/reports/document_service.py b/legal-api/src/legal_api/reports/document_service.py index 63e07fe22b..eb1bfb53bd 100644 --- a/legal-api/src/legal_api/reports/document_service.py +++ b/legal-api/src/legal_api/reports/document_service.py @@ -346,6 +346,39 @@ def get_filing_report(self, drs_id: str, report_type: str): current_app.logger.error(f"DRS call {get_url} failed status={response.status_code}: {response.content}") return response + def get_filing_report_by_filing_id(self, business_identifier: str, filing_identifier: int, report_type: str) -> list: + """ + Try to get a filing report document from the DRS by filing identifier and report type. + + business_identifier: The business identifier. + filing_identifier: The filing identifier. + report_type: The report type. + return: The report binary data status is OK. + """ + if not business_identifier or not filing_identifier or not report_type: + return None, HTTPStatus.NOT_FOUND + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) + url: str = self.url.replace(DOC_PATH, "") + get_url = FILING_DOCS_PATH.format( + url=url, + product=self.product_code, + business_id=business_identifier, + filing_id=filing_identifier + ) + response = requests.get(url=get_url, headers=headers) + if response.status_code != HTTPStatus.OK: + return response + response_json = json.loads(response.content) + drs_id: str = None + for doc in response_json: + if doc.get("reportType") == report_type and doc.get("eventIdentifier") == filing_identifier: + drs_id = doc.get("identifier") + break + if drs_id: + response = self.get_filing_report(drs_id, report_type) + return response.content, response.status_code + return None, HTTPStatus.NOT_FOUND + def get_filing_document(self, drs_id: str, doc_class: str): """ Get a filing document from the document service by unique DRS identifier. @@ -383,7 +416,12 @@ def create_filing_report( headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) url: str = self.url.replace(DOC_PATH, "") report_type: str = report_meta.get("reportType") - filename: str = report_meta.get("fileName") + ".pdf" + filename: str = filing.filing_type + if report_meta.get("fileName"): + filename = report_meta.get("fileName") + elif report_meta.get("default") and report_meta["default"].get("fileName"): + filename: str = report_meta["default"].get("fileName") + filename += ".pdf" filing_date: str = filing.effective_date.isoformat()[:10] post_url = POST_REPORT_PATH.format( url=url, diff --git a/legal-api/src/legal_api/reports/report.py b/legal-api/src/legal_api/reports/report.py index 17cb8098a1..c535354146 100644 --- a/legal-api/src/legal_api/reports/report.py +++ b/legal-api/src/legal_api/reports/report.py @@ -21,7 +21,7 @@ import pycountry import requests from dateutil.relativedelta import relativedelta -from flask import current_app, jsonify, request +from flask import current_app, jsonify from legal_api.core.meta.filing import FILINGS, FilingMeta from legal_api.models import ( @@ -78,14 +78,16 @@ def _get_static_report(self): ) def _get_report(self): - account_id = request.headers.get("Account-Id", None) - if account_id is not None and self._business is not None: - document, status = self._document_service.get_document( + # Try to get report from DRS first: get to here if duplicate UI request before refreshing filing documents. + if self._filing.business_id: + self._business = Business.find_by_internal_id(self._filing.business_id) + if self._business: + Report._populate_business_info_to_filing(self._filing, self._business) + report_meta: dict = ReportMeta.reports.get(self._report_key) + document, status = self._document_service.get_filing_report_by_filing_id( self._business.identifier, self._filing.id, - self._report_key, - account_id - ) + report_meta.get("reportType")) if status == HTTPStatus.OK: return current_app.response_class( response=document, @@ -93,9 +95,6 @@ def _get_report(self): mimetype="application/pdf" ) - if self._filing.business_id: - self._business = Business.find_by_internal_id(self._filing.business_id) - Report._populate_business_info_to_filing(self._filing, self._business) if self._report_key == "alteration": self._report_key = "alterationNotice" headers = { @@ -1476,24 +1475,6 @@ def _get_environment(): if namespace.endswith("test"): return "TEST" return "" - - @staticmethod - def _map_relationship_to_party(relationship): - # FUTURE: update pdf templates to expect relationships schema and remove this - organization_name = relationship["entity"].get("businessName") - return { - "officer": { - "id": relationship["entity"].get("identifier"), - "firstName": relationship["entity"].get("givenName"), - "middleName": relationship["entity"].get("middleInitial"), - "lastName": relationship["entity"].get("familyName"), - "organizationName": organization_name, - "partyType": "organization" if organization_name else "person" - }, - "deliveryAddress": relationship.get("deliveryAddress"), - "mailingAddress": relationship.get("mailingAddress"), - "roles": relationship.get("roles", []) - } @staticmethod def _map_relationship_to_party(relationship): From 88bf129a21410ec08de486d9a235910c4f86ad60 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Wed, 18 Mar 2026 14:47:28 -0700 Subject: [PATCH 32/46] Remove correction report retrieval date. Signed-off-by: Doug Lovett --- .../template-parts/correction/businessDetails.html | 3 +-- legal-api/src/legal_api/reports/document_service.py | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/legal-api/report-templates/template-parts/correction/businessDetails.html b/legal-api/report-templates/template-parts/correction/businessDetails.html index 65bf87ed09..6e6e07c937 100644 --- a/legal-api/report-templates/template-parts/correction/businessDetails.html +++ b/legal-api/report-templates/template-parts/correction/businessDetails.html @@ -37,8 +37,7 @@
- -
Document retrieved on {{report_date_time}}
+
diff --git a/legal-api/src/legal_api/reports/document_service.py b/legal-api/src/legal_api/reports/document_service.py index eb1bfb53bd..1febfd8332 100644 --- a/legal-api/src/legal_api/reports/document_service.py +++ b/legal-api/src/legal_api/reports/document_service.py @@ -346,7 +346,7 @@ def get_filing_report(self, drs_id: str, report_type: str): current_app.logger.error(f"DRS call {get_url} failed status={response.status_code}: {response.content}") return response - def get_filing_report_by_filing_id(self, business_identifier: str, filing_identifier: int, report_type: str) -> list: + def get_filing_report_by_filing_id(self, business_identifier: str, filing_identifier: int, report_type: str): """ Try to get a filing report document from the DRS by filing identifier and report type. @@ -367,9 +367,9 @@ def get_filing_report_by_filing_id(self, business_identifier: str, filing_identi ) response = requests.get(url=get_url, headers=headers) if response.status_code != HTTPStatus.OK: - return response + return response.content, response.status_code response_json = json.loads(response.content) - drs_id: str = None + drs_id = None for doc in response_json: if doc.get("reportType") == report_type and doc.get("eventIdentifier") == filing_identifier: drs_id = doc.get("identifier") From 36d46b3114f9362892ab2c58c0ba3100914e77f5 Mon Sep 17 00:00:00 2001 From: Argus Chiu Date: Wed, 18 Mar 2026 15:50:36 -0700 Subject: [PATCH 33/46] 32883 Add misc data migration notebooks (#4184) --- data-tool/notebooks/colin_delta.ipynb | 176 +++++++++++++ .../onboarding_bi.ipynb | 249 ++++++++++++++++++ .../notebooks/generate_send_ar_update.ipynb | 183 +++++++++++++ data-tool/notebooks/generated/.gitignore | 3 + 4 files changed, 611 insertions(+) create mode 100644 data-tool/notebooks/colin_delta.ipynb create mode 100644 data-tool/notebooks/corps_onboarding_process_flow/onboarding_bi.ipynb create mode 100644 data-tool/notebooks/generate_send_ar_update.ipynb create mode 100644 data-tool/notebooks/generated/.gitignore diff --git a/data-tool/notebooks/colin_delta.ipynb b/data-tool/notebooks/colin_delta.ipynb new file mode 100644 index 0000000000..7f06299833 --- /dev/null +++ b/data-tool/notebooks/colin_delta.ipynb @@ -0,0 +1,176 @@ +{ + "cells": [ + { + "metadata": {}, + "cell_type": "markdown", + "source": "## Find new colin events - split corp nums into batches", + "id": "15b441661c9c5239" + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import re\n", + "from pathlib import Path\n", + "from math import ceil\n", + "\n", + "# ====== CONFIG ======\n", + "# In SQL, Oracle type constructors are limited to 999 parameters.\n", + "# So keep this <= 999 to avoid ORA-00939 in sys.odcivarchar2list(...).\n", + "BATCH_SIZE = 999\n", + "\n", + "# How many corp nums to print per *line* inside sys.odcivarchar2list(...)\n", + "# (this is just formatting; set 500-999 as you requested)\n", + "VALUES_PER_LINE = 999\n", + "\n", + "CUTOFF = \"TIMESTAMP '2026-03-09 23:00:00'\" # or \":cutoff_ts\"\n", + "OUTPUT_FILE = \"generated/check_events.sql\"\n", + "\n", + "IDENTIFIERS = r\"\"\"\n", + "'1111111', '2222222', '3333333'\n", + "-- paste your full list here exactly as provided (single quoted, comma-separated)\n", + "\"\"\"\n", + "# ====================\n", + "\n", + "def parse_ids(s: str) -> list[str]:\n", + " # Extract values inside single quotes; preserves leading zeros like '0766566'\n", + " ids = re.findall(r\"'([^']*)'\", s)\n", + "\n", + " # De-dupe while preserving order (optional; remove if you want duplicates preserved)\n", + " seen = set()\n", + " out: list[str] = []\n", + " for x in ids:\n", + " if x and x not in seen:\n", + " seen.add(x)\n", + " out.append(x)\n", + " return out\n", + "\n", + "def format_as_odcivarchar2list_args(items: list[str], per_line: int) -> str:\n", + " \"\"\"\n", + " Returns a multi-line string of quoted items, comma-separated, with per_line items per line.\n", + " Example (per_line=3):\n", + " 'a','b','c',\n", + " 'd','e'\n", + " \"\"\"\n", + " lines: list[str] = []\n", + " for i in range(0, len(items), per_line):\n", + " chunk = items[i:i + per_line]\n", + " lines.append(\" \" + \",\".join(f\"'{v}'\" for v in chunk))\n", + " return \",\\n\".join(lines)\n", + "\n", + "def make_batch_cte(cte_name: str, items: list[str]) -> str:\n", + " args_sql = format_as_odcivarchar2list_args(items, VALUES_PER_LINE)\n", + " lines = [\n", + " f\"{cte_name} AS (\",\n", + " \" SELECT column_value AS corp_num\",\n", + " \" FROM TABLE(sys.odcivarchar2list(\",\n", + " args_sql,\n", + " \" ))\",\n", + " \")\",\n", + " ]\n", + " return \"\\n\".join(lines)\n", + "\n", + "corp_nums = parse_ids(IDENTIFIERS)\n", + "if not corp_nums:\n", + " raise SystemExit(\"No corp nums found. Paste the quoted list into IDENTIFIERS.\")\n", + "\n", + "n = len(corp_nums)\n", + "num_batches = ceil(n / BATCH_SIZE)\n", + "\n", + "batches = [\n", + " corp_nums[i * BATCH_SIZE : (i + 1) * BATCH_SIZE]\n", + " for i in range(num_batches)\n", + "]\n", + "\n", + "# Build corp_list_001, corp_list_002, ...\n", + "cte_blocks = []\n", + "batch_names = []\n", + "for idx, batch in enumerate(batches, start=1):\n", + " name = f\"corp_list_{idx:03d}\"\n", + " batch_names.append(name)\n", + " cte_blocks.append(make_batch_cte(name, batch))\n", + "\n", + "cte_blocks_sql = \",\\n\".join(cte_blocks)\n", + "\n", + "# Build unified corp_list CTE\n", + "corp_union_lines = [\"corp_list AS (\"]\n", + "corp_union_lines.append(f\" SELECT corp_num FROM {batch_names[0]}\")\n", + "for name in batch_names[1:]:\n", + " corp_union_lines.append(f\" UNION ALL SELECT corp_num FROM {name}\")\n", + "corp_union_lines.append(\")\")\n", + "corp_list_sql = \"\\n\".join(corp_union_lines)\n", + "\n", + "sql = f\"\"\"\\\n", + "WITH\n", + "{cte_blocks_sql},\n", + "{corp_list_sql},\n", + "latest_event AS (\n", + " SELECT e.event_id,\n", + " e.corp_num,\n", + " e.event_typ_cd,\n", + " e.event_timestmp,\n", + " e.trigger_dts,\n", + " ROW_NUMBER() OVER (\n", + " PARTITION BY e.corp_num\n", + " ORDER BY e.event_timestmp DESC\n", + " ) AS rn\n", + " FROM event e\n", + " JOIN corp_list c\n", + " ON c.corp_num = e.corp_num\n", + " WHERE e.event_timestmp > {CUTOFF}\n", + ")\n", + "SELECT le.EVENT_ID,\n", + " le.corp_num,\n", + " le.event_typ_cd,\n", + " le.event_timestmp,\n", + " le.trigger_dts,\n", + " f.FILING_TYP_CD\n", + "FROM latest_event le\n", + "left join filing f on le.EVENT_ID = f.EVENT_ID\n", + "WHERE rn = 1\n", + "ORDER BY event_timestmp DESC;\n", + "\"\"\"\n", + "\n", + "Path(OUTPUT_FILE).write_text(sql, encoding=\"utf-8\")\n", + "\n", + "print(f\"Wrote {OUTPUT_FILE}\")\n", + "print(f\"Total corp nums: {n}\")\n", + "print(f\"Batch size (per odcivarchar2list): {BATCH_SIZE}\")\n", + "print(f\"Values per line (formatting only): {VALUES_PER_LINE}\")\n", + "print(f\"Batches: {num_batches} -> {[len(b) for b in batches]}\")\n" + ], + "id": "3fcd34deb8dd94b2", + "outputs": [], + "execution_count": null + }, + { + "metadata": {}, + "cell_type": "code", + "source": "", + "id": "5bd841959dda7600", + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/data-tool/notebooks/corps_onboarding_process_flow/onboarding_bi.ipynb b/data-tool/notebooks/corps_onboarding_process_flow/onboarding_bi.ipynb new file mode 100644 index 0000000000..41b20e17ca --- /dev/null +++ b/data-tool/notebooks/corps_onboarding_process_flow/onboarding_bi.ipynb @@ -0,0 +1,249 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": "# Notebook for generating onboarding related csvs for BI Reports" + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "%pip install pandas\n", + "%pip install sqlalchemy>=2.0\n", + "%pip install dotenv\n", + "%pip install psycopg2-binary" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Import Libraries and Load Configuration\n", + "\n", + "Import required libraries and load environment variables." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "import os\n", + "import pandas as pd\n", + "from sqlalchemy import create_engine, text\n", + "from sqlalchemy.exc import SQLAlchemyError, OperationalError\n", + "from dotenv import load_dotenv\n", + "\n", + "load_dotenv()\n", + "\n", + "print(\"Libraries imported and configuration loaded successfully.\")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Database Setup\n", + "\n", + "Configure database connections using environment variables." + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "DATABASE_CONFIG = {\n", + " 'colin_extract': {\n", + " 'username': os.getenv(\"DATABASE_COLIN_EXTRACT_USERNAME\"),\n", + " 'password': os.getenv(\"DATABASE_COLIN_EXTRACT_PASSWORD\"),\n", + " 'host': os.getenv(\"DATABASE_COLIN_EXTRACT_HOST\"),\n", + " 'port': os.getenv(\"DATABASE_COLIN_EXTRACT_PORT\"),\n", + " 'name': os.getenv(\"DATABASE_COLIN_EXTRACT_NAME\")\n", + " }\n", + "}\n", + "\n", + "\n", + "for db_key, db_config in DATABASE_CONFIG.items():\n", + " uri = f\"postgresql://{db_config['username']}:{db_config['password']}@{db_config['host']}:{db_config['port']}/{db_config['name']}\"\n", + " DATABASE_CONFIG[db_key] = {'uri': uri}\n", + "\n", + "print(\"Database configurations successfully.\")\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Create Database Engines\n", + "\n", + "Create and test database connections for all configured databases." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "engines = {}\n", + "\n", + "for db_key, config in DATABASE_CONFIG.items():\n", + " try:\n", + " engine = create_engine(config['uri'])\n", + "\n", + " # Test connection\n", + " with engine.connect() as conn:\n", + " conn.execute(text(\"SELECT 1\"))\n", + "\n", + " engines[db_key] = engine\n", + " print(f\"{db_key.upper()} database engine created and tested successfully.\")\n", + "\n", + " except OperationalError as e:\n", + " print(f\"{db_key.upper()} database connection failed: {e}\")\n", + " raise\n", + " except SQLAlchemyError as e:\n", + " print(f\"{db_key.upper()} database engine creation failed: {e}\")\n", + " raise\n", + " except Exception as e:\n", + " print(f\"{db_key.upper()} unexpected error: {e}\")\n", + " raise\n", + "\n", + "ENGINE_NAMES = {engine: key for key, engine in engines.items()}\n", + "\n", + "print(\"All database engines ready for use.\")\n" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Query active/historical legacy data\n", + "\n", + "Comment out `is_active` and `not is_active` as required to generate the df required" + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "legacy_corps_query = f\"\"\"\n", + "select\n", + " mig_group_display_name as mig_group_name,\n", + " mig_batch_display_name as mig_batch_name,\n", + " migrated,\n", + " mig_date,\n", + " email_domain,\n", + " admin_email,\n", + " email_used_count,\n", + " corp_num,\n", + " corp_name,\n", + " corp_type_cd,\n", + " is_active,\n", + " meets_main_criteria,\n", + " has_officers,\n", + " has_3rd_party,\n", + " recognition_dts,\n", + " last_ar_filed_dt,\n", + " director_count,\n", + " filing_cnt,\n", + " months_since_last_ar_filing,\n", + " ar_unfiled_over_1yr,\n", + " file_cnt_last_2yrs,\n", + " last_file_event_ts,\n", + " last_event_ts,\n", + " event_file_types,\n", + " failed_events,\n", + " vendor,\n", + " has_password,\n", + " send_ar_ind,\n", + " meets_share_criteria,\n", + " has_other_share_currency,\n", + " has_share_par_value_lt1_gt6dp,\n", + " has_share_par_value_gt1_gt2dp,\n", + " address_all_any_bad_count,\n", + " office_all_any_bad_count,\n", + " party_all_any_bad_count,\n", + " is_frozen,\n", + " has_bar_filing,\n", + " last_bar_fiscal_year,\n", + " last_bar_filing_date,\n", + " months_since_last_bar_filing,\n", + " bar_account_id,\n", + " bar_account_has_mailing_address,\n", + " group_name\n", + "from mv_legacy_corps_data\n", + "where 1 = 1\n", + "and is_active\n", + "-- and not is_active\n", + "\"\"\"\n", + "\n", + "try:\n", + " with engines['colin_extract'].connect() as conn:\n", + " legacy_corps_df = pd.read_sql(legacy_corps_query, conn)\n", + "\n", + " if legacy_corps_df.empty:\n", + " raise ValueError(\"COLIN Extract database query returned empty result\")\n", + "\n", + " print(f\"Fetched {len(legacy_corps_df)} rows from COLIN Extract database.\")\n", + "\n", + "except Exception as e:\n", + " print(f\"Error fetching data from COLIN Extract: {e}\")\n", + " raise\n", + "\n", + "# Display results\n", + "with pd.option_context('display.max_rows', 5):\n", + " display(legacy_corps_query)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generate CSV for BI report\n", + "\n", + "Comment/uncomment csv to generate as required." + ] + }, + { + "metadata": {}, + "cell_type": "code", + "source": [ + "\n", + "# legacy_corps_df.to_csv('../generated/legacy_corps_data.csv', index=False, na_rep=\"\")\n", + "legacy_corps_df.to_csv('../generated/legacy_corps_data_active.csv', index=False, na_rep=\"\")\n", + "# legacy_corps_df.to_csv('../generated/legacy_corps_data_historical.csv', index=False, na_rep=\"\")\n" + ], + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "3.8.17", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.17" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/data-tool/notebooks/generate_send_ar_update.ipynb b/data-tool/notebooks/generate_send_ar_update.ipynb new file mode 100644 index 0000000000..0795042cff --- /dev/null +++ b/data-tool/notebooks/generate_send_ar_update.ipynb @@ -0,0 +1,183 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Generate SEND_AR_IND Update SQL\n", + "\n", + "Produces Oracle SQL to set `SEND_AR_IND = 'N'` on the `CORPORATION` table for a provided list of `CORP_NUM` values.\n", + "Automatically chunks the list to stay under Oracle's **1000-item IN-clause limit**.\n", + "\n", + "The output SQL contains:\n", + "1. A **SELECT** (verification) query — run this first to confirm what will change.\n", + "2. An **UPDATE** query — uncomment to execute the actual update.\n", + "\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1 — Paste your corp nums below\n", + "\n", + "Paste them as a single comma-delimited string of **single-quoted** values, e.g.:\n", + "\n", + "```python\n", + "raw_input = \"'1111111','2222222','3333333'\"\n", + "```" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "raw_input = (\n", + " \"'1111111', '2222222', '3333333'\"\n", + ")" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2 — Parse & chunk" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "import re\n", + "from typing import List\n", + "\n", + "CHUNK_SIZE = 1000 # Oracle IN-clause limit\n", + "\n", + "# Extract all single-quoted values from the raw input\n", + "corp_nums: List[str] = re.findall(r\"'[^']+'\", raw_input)\n", + "\n", + "# Deduplicate while preserving order\n", + "seen = set()\n", + "unique_corp_nums: List[str] = []\n", + "for cn in corp_nums:\n", + " if cn not in seen:\n", + " seen.add(cn)\n", + " unique_corp_nums.append(cn)\n", + "\n", + "# Chunk into groups of CHUNK_SIZE\n", + "chunks: List[List[str]] = [\n", + " unique_corp_nums[i:i + CHUNK_SIZE]\n", + " for i in range(0, len(unique_corp_nums), CHUNK_SIZE)\n", + "]\n", + "\n", + "print(f'Total values supplied : {len(corp_nums)}')\n", + "print(f'Unique values : {len(unique_corp_nums)}')\n", + "print(f'Chunks (≤{CHUNK_SIZE} each) : {len(chunks)}')" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3 — Generate SQL" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "VALS_PER_LINE = 1000 # how many values per line inside IN()\n", + "\n", + "def build_in_clauses(chunks: List[List[str]], column: str = 'c.CORP_NUM') -> str:\n", + " \"\"\"Build an OR-chained set of IN clauses from chunks, packed horizontally.\"\"\"\n", + " parts = []\n", + " for chunk in chunks:\n", + " lines = []\n", + " for i in range(0, len(chunk), VALS_PER_LINE):\n", + " lines.append(','.join(chunk[i:i + VALS_PER_LINE]))\n", + " values = ',\\n '.join(lines)\n", + " parts.append(f'{column} IN (\\n {values}\\n )')\n", + " return '\\n OR '.join(parts)\n", + "\n", + "\n", + "in_clauses = build_in_clauses(chunks)\n", + "\n", + "sql = f\"\"\"-- =============================================================\n", + "-- SEND_AR_IND Update Script\n", + "-- Generated for {len(unique_corp_nums)} corp(s) in {len(chunks)} chunk(s)\n", + "-- =============================================================\n", + "\n", + "-- STEP 1: VERIFY — Run this SELECT first to confirm the rows.\n", + "SELECT c.CORP_NUM,\n", + " c.CORP_TYP_CD,\n", + " c.SEND_AR_IND\n", + " FROM CORPORATION c\n", + " WHERE (\n", + " {in_clauses}\n", + ")\n", + " AND c.SEND_AR_IND <> 'N'\n", + " ORDER BY c.CORP_NUM;\n", + "\n", + "\n", + "-- STEP 2: UPDATE — Uncomment the block below once you've verified.\n", + "/*\n", + "UPDATE CORPORATION c\n", + " SET c.SEND_AR_IND = 'N'\n", + " WHERE (\n", + " {in_clauses}\n", + ")\n", + " AND c.SEND_AR_IND <> 'N';\n", + "\n", + "-- Check affected row count\n", + "-- SELECT SQL%ROWCOUNT AS rows_updated FROM DUAL;\n", + "\n", + "COMMIT;\n", + "*/\n", + "\"\"\"\n", + "\n", + "print(sql)" + ], + "outputs": [], + "execution_count": null + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4 — Save to file (optional)" + ] + }, + { + "cell_type": "code", + "metadata": {}, + "source": [ + "output_path = 'generated/send_ar_ind_update.sql'\n", + "\n", + "with open(output_path, 'w') as f:\n", + " f.write(sql)\n", + "\n", + "print(f'SQL written to {output_path}')" + ], + "outputs": [], + "execution_count": null + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/data-tool/notebooks/generated/.gitignore b/data-tool/notebooks/generated/.gitignore new file mode 100644 index 0000000000..0dac4faa5f --- /dev/null +++ b/data-tool/notebooks/generated/.gitignore @@ -0,0 +1,3 @@ +# Keep this folder in git, but not the generated artifacts. +* +!.gitignore From 04b930a85b6369e1505a446fbf74c691d041a603 Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Thu, 19 Mar 2026 09:33:21 -0700 Subject: [PATCH 34/46] 25893 colin sync - convert CBEN to CBN (#4185) --- colin-api/src/colin_api/models/business.py | 29 +++++++++++++++++----- colin-api/src/colin_api/resources/event.py | 2 ++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/colin-api/src/colin_api/models/business.py b/colin-api/src/colin_api/models/business.py index e0a3949264..698cb2ee28 100644 --- a/colin-api/src/colin_api/models/business.py +++ b/colin-api/src/colin_api/models/business.py @@ -42,7 +42,7 @@ class TypeCodes(Enum): BC_COMP = 'BC' ULC_COMP = 'ULC' CCC_COMP = 'CC' - BCOMP_CONTINUE_IN = 'CBEN' + BCOMP_CONTINUE_IN = 'CBEN' # In COLIN its CBN CONTINUE_IN = 'C' CCC_CONTINUE_IN = 'CCC' ULC_CONTINUE_IN = 'CUL' @@ -161,6 +161,22 @@ def as_slim_dict(self) -> Dict: } } + @classmethod + def map_legal_type_to_colin(cls, legal_type: str) -> str: + """Map legal type to COLIN legal type.""" + if legal_type == 'CBEN': + return 'CBN' + + return legal_type + + @classmethod + def map_corp_type_to_lear(cls, corp_type: str) -> str: + """Map corp type to LEAR legal type.""" + if corp_type == 'CBN': + return 'CBEN' + + return corp_type + @classmethod def get_colin_identifier(cls, lear_identifier, legal_type): """Convert identifier from lear to colin.""" @@ -334,7 +350,7 @@ def find_by_identifier(cls, # pylint: disable=too-many-statements business_obj.corp_num = business['corp_num'] business_obj.corp_state = business['corp_state'] business_obj.corp_state_class = business['corp_state_class'] - business_obj.corp_type = business['corp_typ_cd'] + business_obj.corp_type = cls.map_corp_type_to_lear(business['corp_typ_cd']) business_obj.email = business['admin_email'] business_obj.founding_date = convert_to_json_datetime(business['recognition_dts']) business_obj.good_standing = cls.is_in_good_standing(business, cursor) @@ -375,11 +391,12 @@ def create_corporation(cls, con, filing_info: Dict): # Expand query as NR data/ business info becomes more aparent cursor.execute( """ - insert into CORPORATION (CORP_NUM, CORP_TYP_CD, RECOGNITION_DTS) - values (:corp_num, :corp_type, TO_TIMESTAMP_TZ(:recognition_date,'YYYY-MM-DD"T"HH24:MI:SS.FFTZH:TZM')) + insert into CORPORATION (CORP_NUM, CORP_TYP_CD, RECOGNITION_DTS, SEND_AR_IND) + values + (:corp_num, :corp_type, TO_TIMESTAMP_TZ(:recognition_date,'YYYY-MM-DD"T"HH24:MI:SS.FFTZH:TZM'), 'N') """, corp_num=business.corp_num, - corp_type=business.corp_type, + corp_type=cls.map_legal_type_to_colin(business.corp_type), recognition_date=business.founding_date ) @@ -604,7 +621,7 @@ def update_corp_type(cls, cursor, corp_num: str, corp_type: str): WHERE corp_num = :corp_num """, corp_num=corp_num, - corp_type=corp_type + corp_type=cls.map_legal_type_to_colin(corp_type) ) except Exception as err: diff --git a/colin-api/src/colin_api/resources/event.py b/colin-api/src/colin_api/resources/event.py index 3f3ce120f6..e6820f4a9b 100644 --- a/colin-api/src/colin_api/resources/event.py +++ b/colin-api/src/colin_api/resources/event.py @@ -15,6 +15,7 @@ from flask import current_app, jsonify from flask_restx import Resource, cors +from colin_api.models import Business from colin_api.resources.business import API from colin_api.resources.db import DB from colin_api.utils.auth import COLIN_SVC_ROLE, jwt @@ -40,6 +41,7 @@ def get(corp_type, event_id): """ try: cursor = DB.connection.cursor() + corp_type = Business.map_legal_type_to_colin(corp_type) if event_id != 'earliest': querystring += 'and event.event_id > :max_event_id ' cursor.execute(querystring, max_event_id=event_id, corp_type=corp_type) From 339f219b1436406070480a2a7091d38ba504c192 Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Thu, 19 Mar 2026 10:08:13 -0700 Subject: [PATCH 35/46] 32833 bump up colin version (#4186) --- colin-api/src/colin_api/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colin-api/src/colin_api/version.py b/colin-api/src/colin_api/version.py index 7faccc7a3d..99c4cfaa98 100644 --- a/colin-api/src/colin_api/version.py +++ b/colin-api/src/colin_api/version.py @@ -22,4 +22,4 @@ Development release segment: .devN """ -__version__ = '2.165.0' # pylint: disable=invalid-name +__version__ = '2.171.0' # pylint: disable=invalid-name From 92d04e307c17e3c298abac036b57f762ac9c61d5 Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Thu, 19 Mar 2026 13:51:44 -0700 Subject: [PATCH 36/46] Business API fix temp filing retrieve outputs issue. Signed-off-by: Doug Lovett --- legal-api/src/legal_api/reports/report.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/legal-api/src/legal_api/reports/report.py b/legal-api/src/legal_api/reports/report.py index c535354146..707d2a1a47 100644 --- a/legal-api/src/legal_api/reports/report.py +++ b/legal-api/src/legal_api/reports/report.py @@ -81,11 +81,12 @@ def _get_report(self): # Try to get report from DRS first: get to here if duplicate UI request before refreshing filing documents. if self._filing.business_id: self._business = Business.find_by_internal_id(self._filing.business_id) - if self._business: Report._populate_business_info_to_filing(self._filing, self._business) + business_identifier = self._business.identifier if self._business else self._filing.temp_reg + if business_identifier: report_meta: dict = ReportMeta.reports.get(self._report_key) document, status = self._document_service.get_filing_report_by_filing_id( - self._business.identifier, + business_identifier, self._filing.id, report_meta.get("reportType")) if status == HTTPStatus.OK: @@ -113,7 +114,7 @@ def _get_report(self): return jsonify(message=str(response.content)), response.status_code response_drs = self._document_service.create_filing_report( - self._business.identifier, + business_identifier, self._filing, ReportMeta.reports.get(self._report_key), response From 07033cee15644637902f105f0a468d1ad354cb8f Mon Sep 17 00:00:00 2001 From: Argus Chiu Date: Thu, 19 Mar 2026 16:23:01 -0700 Subject: [PATCH 37/46] 32908 Fix colin subset extract issues (#4189) --- .../scripts/README_COLIN_Corps_Extract.md | 13 +- .../scripts/generate_cprd_subset_extract.py | 140 +++++++++++++++++- .../scripts/subset/subset_delete_chunk.sql | 79 +--------- .../subset/subset_disable_triggers.sql | 1 - .../scripts/subset/subset_enable_triggers.sql | 1 - .../subset_pg_cleanup_address_stage.sql | 4 + .../subset/subset_pg_fastload_begin.sql | 2 - .../subset_pg_prepare_address_stage.sql | 16 ++ .../subset_pg_purge_bcomps_excluded.sql | 3 + .../scripts/subset/subset_transfer_chunk.sql | 56 ++++++- 10 files changed, 234 insertions(+), 81 deletions(-) create mode 100644 data-tool/scripts/subset/subset_pg_cleanup_address_stage.sql create mode 100644 data-tool/scripts/subset/subset_pg_prepare_address_stage.sql diff --git a/data-tool/scripts/README_COLIN_Corps_Extract.md b/data-tool/scripts/README_COLIN_Corps_Extract.md index db8674d2b1..67333f5925 100644 --- a/data-tool/scripts/README_COLIN_Corps_Extract.md +++ b/data-tool/scripts/README_COLIN_Corps_Extract.md @@ -120,7 +120,7 @@ Generated scripts (gitignored by default): --chunk-size 500 \ --threads 4 \ --pg-fastload \ - --pg-disable-method replica_role \ + --pg-disable-method table_triggers \ --out /data-tool/scripts/_generated/subset_refresh.sql ``` @@ -132,13 +132,20 @@ Generated scripts (gitignored by default): --chunk-size 500 \ --threads 4 \ --pg-fastload \ - --pg-disable-method replica_role \ + --pg-disable-method table_triggers \ --out /data-tool/scripts/_generated/subset_load.sql ``` Optional performance flags: - Add `--pg-fastload` to enable Postgres session settings for faster bulk writes (templates `subset_pg_fastload_begin.sql` / `subset_pg_fastload_end.sql`). - - Use `--pg-disable-method replica_role` (default) or `session_replication_role` to suppress triggers during load. + - `--pg-disable-method` currently accepts only `table_triggers` and `replica_role`. + - The actual generator default is `table_triggers`, not `replica_role`. + - In refresh mode, preserved rows in `corp_processing`, `auth_processing`, `affiliation_processing`, and `colin_tracking` still reference `corporation` / `event`, so FK enforcement must stay suppressed across delete/reload. The generator now adds refresh-only trigger suppression for those preserved FK-owning tables when `--pg-disable-method table_triggers` is used. + - `table_triggers` changes table trigger state globally while the refresh runs, so use it against a quiesced/disposable extract DB and with a role that can disable the relevant triggers. + - If you use `replica_role`, remember it is session-local. If FK errors still occur, verify `current_setting('session_replication_role')` inside the nested delete/purge scripts being executed by DbSchemaCLI. + - `address` is treated as a shared/global table during subset refresh/load. The generator now prepares a helper staging table, transfers incoming Oracle addresses into it, and merges them into `public.address` by `addr_id` instead of deleting/reinserting address rows directly. The address extract also includes `notification_resend` references. + - The helper stage table is `public.subset_address_stage`. Do not overlap subset runs against the same target DB, and ensure the runtime role can create/drop that helper table. + - For diagnostics, add `--pg-debug-session-probes` (inline mode only). The generated SQL will print `pg_backend_pid()`, `current_user`, and `current_setting('session_replication_role')` in the master script and nested execute files so you can verify the master/nested `execute` session context during refresh. This does not directly instrument any separate DbSchemaCLI transfer writer sessions. 3. Run the generated main script with DbSchemaCLI: diff --git a/data-tool/scripts/generate_cprd_subset_extract.py b/data-tool/scripts/generate_cprd_subset_extract.py index a23cde3752..ff932e2977 100644 --- a/data-tool/scripts/generate_cprd_subset_extract.py +++ b/data-tool/scripts/generate_cprd_subset_extract.py @@ -77,6 +77,7 @@ class cfg_GenerationConfig: pg_fastload: bool pg_disable_method: cfg_PgDisableMethod + pg_debug_session_probes: bool oracle_in_strategy: cfg_OracleInStrategy or_of_in_max_ids: int @@ -106,6 +107,8 @@ class tmpl_TemplateSpec: @dataclass(frozen=True) class tmpl_TemplateBundle: + pg_prepare_address_stage: tmpl_TemplateSpec + pg_cleanup_address_stage: tmpl_TemplateSpec disable_triggers: tmpl_TemplateSpec enable_triggers: tmpl_TemplateSpec pg_boolean_casts: tmpl_TemplateSpec @@ -261,6 +264,18 @@ def _term(vals: Sequence[str]) -> str: return f"(\n{joined}\n)" +def sql_render_pg_session_probe(label: str) -> str: + quoted_label = sql_quote_literal(label) + return ( + "SELECT " + f"{quoted_label} AS probe_label,\n" + " pg_backend_pid() AS backend_pid,\n" + " current_user AS db_user,\n" + " current_setting('session_replication_role') AS session_replication_role,\n" + " clock_timestamp() AS observed_at;" + ) + + # ========================= # tmpl_* (template loading/validation/rendering) # ========================= @@ -268,6 +283,14 @@ def _term(vals: Sequence[str]) -> str: def tmpl_default_bundle(repo_root: Path) -> tmpl_TemplateBundle: subset_dir = repo_root / "data-tool" / "scripts" / "subset" + pg_prepare_address_stage = tmpl_TemplateSpec( + name="subset_pg_prepare_address_stage", + path=subset_dir / "subset_pg_prepare_address_stage.sql", + ) + pg_cleanup_address_stage = tmpl_TemplateSpec( + name="subset_pg_cleanup_address_stage", + path=subset_dir / "subset_pg_cleanup_address_stage.sql", + ) disable_triggers = tmpl_TemplateSpec( name="subset_disable_triggers", path=subset_dir / "subset_disable_triggers.sql", @@ -312,6 +335,8 @@ def tmpl_default_bundle(repo_root: Path) -> tmpl_TemplateBundle: ) return tmpl_TemplateBundle( + pg_prepare_address_stage=pg_prepare_address_stage, + pg_cleanup_address_stage=pg_cleanup_address_stage, disable_triggers=disable_triggers, enable_triggers=enable_triggers, pg_boolean_casts=pg_boolean_casts, @@ -408,6 +433,7 @@ def gen_build_chunk_sql( corp_ids_sql: str, target_predicate_sql: str, oracle_predicate_sql: str, + pg_debug_session_probes: bool, ) -> str: replacements = { TMPL_TOKEN_CORP_IDS: corp_ids_sql, @@ -423,6 +449,11 @@ def gen_build_chunk_sql( parts.append(f"-- oracle corp_num: {len(chunk.oracle_ids)}") parts.append("") + if pg_debug_session_probes: + parts.append("-- Debug probe: backend/session state for this nested execute file.") + parts.append(sql_render_pg_session_probe(f"chunk:{chunk.chunk_file.stem}")) + parts.append("") + if include_delete and mode == cfg_GenerationMode.REFRESH: rendered_delete = tmpl_render(delete_template_text, replacements=replacements) if TMPL_TOKEN_CORP_IDS in rendered_delete: @@ -454,6 +485,7 @@ def gen_write_chunk_files( delete_template_text: str, transfer_template_text: str, max_in_list: int, + pg_debug_session_probes: bool, ) -> List[Path]: out_paths: List[Path] = [] for ch in chunks: @@ -483,6 +515,7 @@ def gen_write_chunk_files( corp_ids_sql=corp_ids_sql, target_predicate_sql=target_predicate_sql, oracle_predicate_sql=oracle_predicate_sql, + pg_debug_session_probes=pg_debug_session_probes, ) gen_write_text(ch.chunk_file, chunk_sql) out_paths.append(ch.chunk_file) @@ -492,6 +525,11 @@ def gen_write_chunk_files( def _gen_emit_pg_disable_begin(lines: List[str], *, cfg: cfg_GenerationConfig, templates: tmpl_TemplateBundle) -> None: if cfg.pg_disable_method == cfg_PgDisableMethod.TABLE_TRIGGERS: lines.append(f"execute {templates.disable_triggers.path.as_posix()}") + if cfg.mode == cfg_GenerationMode.REFRESH: + lines.append("-- Refresh-only: preserved processing/tracking tables still reference corporation/event rows.") + lines.append("ALTER TABLE corp_processing DISABLE TRIGGER ALL;") + # lines.append("ALTER TABLE auth_processing DISABLE TRIGGER ALL;") + lines.append("ALTER TABLE colin_tracking DISABLE TRIGGER ALL;") lines.append("") return @@ -507,6 +545,11 @@ def _gen_emit_pg_disable_begin(lines: List[str], *, cfg: cfg_GenerationConfig, t def _gen_emit_pg_disable_end(lines: List[str], *, cfg: cfg_GenerationConfig, templates: tmpl_TemplateBundle) -> None: if cfg.pg_disable_method == cfg_PgDisableMethod.TABLE_TRIGGERS: lines.append(f"execute {templates.enable_triggers.path.as_posix()}") + if cfg.mode == cfg_GenerationMode.REFRESH: + lines.append("-- Refresh-only: restore preserved processing/tracking table triggers too.") + lines.append("ALTER TABLE corp_processing ENABLE TRIGGER ALL;") + # lines.append("ALTER TABLE auth_processing ENABLE TRIGGER ALL;") + lines.append("ALTER TABLE colin_tracking ENABLE TRIGGER ALL;") lines.append("") return @@ -519,12 +562,39 @@ def _gen_emit_pg_disable_end(lines: List[str], *, cfg: cfg_GenerationConfig, tem raise SystemExit(f"Unsupported pg_disable_method: {cfg.pg_disable_method}") +def _gen_emit_refresh_fk_note(lines: List[str], *, cfg: cfg_GenerationConfig) -> None: + if cfg.mode != cfg_GenerationMode.REFRESH: + return + + lines.append("-- Refresh mode preserves processing/tracking rows while deleting and reloading corporation/event rows.") + if cfg.pg_disable_method == cfg_PgDisableMethod.TABLE_TRIGGERS: + lines.append("-- table_triggers mode adds refresh-only trigger suppression for the preserved FK-owning tables too.") + else: + lines.append("-- replica_role mode relies on session_replication_role remaining active for nested execute/transfer work.") + lines.append("") + + +def gen_write_pg_debug_copy(*, out_path: Path, source_path: Path, probe_label: str) -> Path: + source_text = source_path.read_text(encoding="utf-8") + debug_lines = [ + f"-- generated debug copy: {out_path.name}", + "-- Debug probe: backend/session state for this nested execute file.", + sql_render_pg_session_probe(probe_label), + "", + source_text.rstrip(), + "", + ] + gen_write_text(out_path, "\n".join(debug_lines)) + return out_path + + def gen_build_master_script_inline( *, cfg: cfg_GenerationConfig, templates: tmpl_TemplateBundle, delete_chunk_files: Sequence[Path], transfer_chunk_files: Sequence[Path], + pg_purge_script_path: Path, ) -> str: lines: List[str] = [] lines.append("vset cli.settings.ignore_errors=false") @@ -534,6 +604,8 @@ def gen_build_master_script_inline( lines.append("vset format.timestamp=YYYY-MM-dd'T'hh:mm:ss'Z'") lines.append("") lines.append(f"connect {cfg.target_connection};") + lines.append("-- Prepare shared address staging table before learning schema") + lines.append(f"execute {templates.pg_prepare_address_stage.path.as_posix()}") lines.append(f"learn schema {cfg.target_schema};") lines.append("") @@ -550,6 +622,11 @@ def gen_build_master_script_inline( lines.append("") _gen_emit_pg_disable_begin(lines, cfg=cfg, templates=templates) + _gen_emit_refresh_fk_note(lines, cfg=cfg) + if cfg.pg_debug_session_probes: + lines.append("-- Debug probe: backend/session state in the master script after trigger/FK suppression.") + lines.append(sql_render_pg_session_probe("master:after_disable")) + lines.append("") if cfg.include_cars: lines.append("-- global cars* refresh (not corp-scoped; full dataset truncate + reload)") @@ -573,11 +650,15 @@ def gen_build_master_script_inline( lines.append("") lines.append("-- purge BCOMPS-excluded corps (computed in Postgres after load)") - lines.append(f"execute {templates.pg_purge_bcomps_excluded.path.as_posix()}") + lines.append(f"execute {pg_purge_script_path.as_posix()}") lines.append("") _gen_emit_pg_disable_end(lines, cfg=cfg, templates=templates) + lines.append("-- Cleanup shared address staging table") + lines.append(f"execute {templates.pg_cleanup_address_stage.path.as_posix()}") + lines.append("") + if cfg.pg_fastload: lines.append("-- Reset Postgres fast-load session settings") lines.append(f"execute {templates.pg_fastload_end.path.as_posix()}") @@ -612,6 +693,8 @@ def gen_build_master_script_vset( lines.append("vset format.timestamp=YYYY-MM-dd'T'hh:mm:ss'Z'") lines.append("") lines.append(f"connect {cfg.target_connection};") + lines.append("-- Prepare shared address staging table before learning schema") + lines.append(f"execute {templates.pg_prepare_address_stage.path.as_posix()}") lines.append(f"learn schema {cfg.target_schema};") lines.append("") @@ -628,6 +711,7 @@ def gen_build_master_script_vset( lines.append("") _gen_emit_pg_disable_begin(lines, cfg=cfg, templates=templates) + _gen_emit_refresh_fk_note(lines, cfg=cfg) if cfg.include_cars: lines.append("-- global cars* refresh (not corp-scoped; full dataset truncate + reload)") @@ -685,6 +769,10 @@ def _vset_predicates(target_values: Sequence[str], oracle_values: Sequence[str]) _gen_emit_pg_disable_end(lines, cfg=cfg, templates=templates) + lines.append("-- Cleanup shared address staging table") + lines.append(f"execute {templates.pg_cleanup_address_stage.path.as_posix()}") + lines.append("") + if cfg.pg_fastload: lines.append("-- Reset Postgres fast-load session settings") lines.append(f"execute {templates.pg_fastload_end.path.as_posix()}") @@ -781,6 +869,12 @@ def cli_parse_args(argv: List[str] | None = None) -> argparse.Namespace: "table_triggers=ALTER TABLE ... DISABLE/ENABLE TRIGGER ALL (default). " "replica_role=SET session_replication_role=replica|origin (requires superuser; disables triggers/FKs for session).", ) + parser.add_argument( + "--pg-debug-session-probes", + action="store_true", + help="Inline-mode only. Add diagnostic SELECT probes that print pg_backend_pid/current_user/" + "session_replication_role in the master script and nested execute files.", + ) parser.add_argument( "--target-connection", @@ -846,6 +940,7 @@ def cfg_build_config(args: argparse.Namespace) -> cfg_GenerationConfig: include_cars=bool(args.include_cars), pg_fastload=bool(args.pg_fastload), pg_disable_method=pg_disable_method, + pg_debug_session_probes=bool(args.pg_debug_session_probes), oracle_in_strategy=oracle_in_strategy, or_of_in_max_ids=or_of_in_max_ids, out_master=out_master, @@ -864,12 +959,17 @@ def _effective_oracle_strategy(cfg: cfg_GenerationConfig, total_ids: int) -> cfg def run(cfg: cfg_GenerationConfig) -> int: templates = tmpl_default_bundle(cfg.repo_root) + if cfg.pg_debug_session_probes and cfg.render_mode != cfg_RenderMode.INLINE: + raise SystemExit("--pg-debug-session-probes currently supports only --render-mode inline.") + # Load/validate templates we depend on (fail fast if template contract changes). delete_template_text = tmpl_load_text(templates.delete_chunk) transfer_template_text = tmpl_load_text(templates.transfer_chunk) # Ensure the execute-only templates exist too (even though we don't render them). for spec in ( + templates.pg_prepare_address_stage, + templates.pg_cleanup_address_stage, templates.disable_triggers, templates.enable_triggers, templates.pg_boolean_casts, @@ -906,6 +1006,7 @@ def run(cfg: cfg_GenerationConfig) -> int: delete_template_text=delete_template_text, transfer_template_text=transfer_template_text, max_in_list=cfg.chunk_size, + pg_debug_session_probes=cfg.pg_debug_session_probes, ) transfer_files = combined_files @@ -921,6 +1022,7 @@ def run(cfg: cfg_GenerationConfig) -> int: delete_template_text=delete_template_text, transfer_template_text=transfer_template_text, max_in_list=cfg.chunk_size, + pg_debug_session_probes=cfg.pg_debug_session_probes, ) oracle_ids_all = corp_to_oracle_ids(target_ids) @@ -939,6 +1041,15 @@ def run(cfg: cfg_GenerationConfig) -> int: delete_template_text=delete_template_text, transfer_template_text=transfer_template_text, max_in_list=cfg.chunk_size, + pg_debug_session_probes=cfg.pg_debug_session_probes, + ) + + pg_purge_script_path = templates.pg_purge_bcomps_excluded.path + if cfg.pg_debug_session_probes: + pg_purge_script_path = gen_write_pg_debug_copy( + out_path=cfg.out_chunks_dir / "pg_purge_bcomps_excluded_debug.sql", + source_path=templates.pg_purge_bcomps_excluded.path, + probe_label="execute:pg_purge_bcomps_excluded", ) master_text = gen_build_master_script_inline( @@ -946,6 +1057,7 @@ def run(cfg: cfg_GenerationConfig) -> int: templates=templates, delete_chunk_files=delete_files, transfer_chunk_files=transfer_files, + pg_purge_script_path=pg_purge_script_path, ) gen_write_text(cfg.out_master, master_text) @@ -980,6 +1092,19 @@ def run(cfg: cfg_GenerationConfig) -> int: print(" - cars* tables will NOT be refreshed (--no-cars was set).") print(f" - Postgres fast-load session settings: {'ENABLED' if cfg.pg_fastload else 'disabled'} (--pg-fastload)") print(f" - Postgres trigger suppression: {cfg.pg_disable_method.value} (--pg-disable-method)") + print(" - Address loads use shared staging table public.subset_address_stage and merge into public.address by addr_id.") + print(" - subset runs should not overlap on the same target DB, and the runtime role must be able to create/drop that helper table.") + if cfg.pg_debug_session_probes: + print(" - Postgres session probes: ENABLED (--pg-debug-session-probes)") + print(" - master + nested execute files will print pg_backend_pid/current_user/session_replication_role.") + print(" - probes confirm master/nested execute session context; DbSchemaCLI transfer writer sessions may still differ.") + if cfg.mode == cfg_GenerationMode.REFRESH: + print(" - refresh preserves processing/tracking rows while deleting/reloading corporation/event rows.") + if cfg.pg_disable_method == cfg_PgDisableMethod.TABLE_TRIGGERS: + print(" - table_triggers mode adds refresh-only trigger suppression for the preserved FK-owning tables.") + print(" - table_triggers changes table state globally while the refresh runs; use it against a quiesced extract DB with sufficient privileges.") + else: + print(" - replica_role is session-local; if FK errors still occur, probe current_setting('session_replication_role') inside nested execute files.") if cfg.pg_disable_method == cfg_PgDisableMethod.REPLICA_ROLE: print(" - NOTE: replica_role requires superuser and disables triggers/FKs for the session.") return 0 @@ -1018,6 +1143,19 @@ def run(cfg: cfg_GenerationConfig) -> int: print(" - Prefer --render-mode inline for faster runs (inline generates static SQL per chunk).") print(f" - Postgres fast-load session settings: {'ENABLED' if cfg.pg_fastload else 'disabled'} (--pg-fastload)") print(f" - Postgres trigger suppression: {cfg.pg_disable_method.value} (--pg-disable-method)") + print(" - Address loads use shared staging table public.subset_address_stage and merge into public.address by addr_id.") + print(" - subset runs should not overlap on the same target DB, and the runtime role must be able to create/drop that helper table.") + if cfg.pg_debug_session_probes: + print(" - Postgres session probes: ENABLED (--pg-debug-session-probes)") + print(" - master + nested execute files will print pg_backend_pid/current_user/session_replication_role.") + print(" - probes confirm master/nested execute session context; DbSchemaCLI transfer writer sessions may still differ.") + if cfg.mode == cfg_GenerationMode.REFRESH: + print(" - refresh preserves processing/tracking rows while deleting/reloading corporation/event rows.") + if cfg.pg_disable_method == cfg_PgDisableMethod.TABLE_TRIGGERS: + print(" - table_triggers mode adds refresh-only trigger suppression for the preserved FK-owning tables.") + print(" - table_triggers changes table state globally while the refresh runs; use it against a quiesced extract DB with sufficient privileges.") + else: + print(" - replica_role is session-local; if FK errors still occur, probe current_setting('session_replication_role') inside nested execute files.") if cfg.pg_disable_method == cfg_PgDisableMethod.REPLICA_ROLE: print(" - NOTE: replica_role requires superuser and disables triggers/FKs for the session.") return 0 diff --git a/data-tool/scripts/subset/subset_delete_chunk.sql b/data-tool/scripts/subset/subset_delete_chunk.sql index 2752fdc940..45ed54a76d 100644 --- a/data-tool/scripts/subset/subset_delete_chunk.sql +++ b/data-tool/scripts/subset/subset_delete_chunk.sql @@ -9,72 +9,14 @@ -- Note: This script intentionally does NOT delete internal migration/processing tables (mig_*, corp_processing, -- colin_tracking, affiliation_processing, etc). It only deletes the corp-scoped COLIN extract tables that are -- reloaded from Oracle. +-- IMPORTANT: +-- - Because preserved processing/tracking tables still reference corporation/event rows, refresh mode must keep +-- FK enforcement suppressed across this delete/reload window (for example via replica_role, or by disabling +-- triggers on the preserved referencing tables too). --- Capture address ids referenced by these corps BEFORE deleting corp rows. --- We use a TEMP table so we can safely delete addresses after deleting all referencing rows. -DROP TABLE IF EXISTS pg_temp.tmp_subset_refresh_addr_ids; -CREATE TEMP TABLE tmp_subset_refresh_addr_ids ( - addr_id numeric(10) PRIMARY KEY -); - -INSERT INTO tmp_subset_refresh_addr_ids (addr_id) -SELECT DISTINCT s.addr_id -FROM ( - SELECT cp.mailing_addr_id AS addr_id - FROM corp_party cp - WHERE cp.corp_num IN (&corp_ids_in) AND cp.mailing_addr_id IS NOT NULL - - UNION - SELECT cp.delivery_addr_id AS addr_id - FROM corp_party cp - WHERE cp.corp_num IN (&corp_ids_in) AND cp.delivery_addr_id IS NOT NULL - - UNION - SELECT o.mailing_addr_id AS addr_id - FROM office o - WHERE o.corp_num IN (&corp_ids_in) AND o.mailing_addr_id IS NOT NULL - - UNION - SELECT o.delivery_addr_id AS addr_id - FROM office o - WHERE o.corp_num IN (&corp_ids_in) AND o.delivery_addr_id IS NOT NULL - - UNION - SELECT cpl.mailing_addr_id AS addr_id - FROM completing_party cpl - JOIN event e ON e.event_id = cpl.event_id - WHERE e.corp_num IN (&corp_ids_in) AND cpl.mailing_addr_id IS NOT NULL - - UNION - SELECT sp.mailing_addr_id AS addr_id - FROM submitting_party sp - JOIN event e ON e.event_id = sp.event_id - WHERE e.corp_num IN (&corp_ids_in) AND sp.mailing_addr_id IS NOT NULL - - UNION - SELECT sp.notify_addr_id AS addr_id - FROM submitting_party sp - JOIN event e ON e.event_id = sp.event_id - WHERE e.corp_num IN (&corp_ids_in) AND sp.notify_addr_id IS NOT NULL - - UNION - SELECT n.mailing_addr_id AS addr_id - FROM notification n - JOIN event e ON e.event_id = n.event_id - WHERE e.corp_num IN (&corp_ids_in) AND n.mailing_addr_id IS NOT NULL - - UNION - SELECT nr.mailing_addr_id AS addr_id - FROM notification_resend nr - JOIN event e ON e.event_id = nr.event_id - WHERE e.corp_num IN (&corp_ids_in) AND nr.mailing_addr_id IS NOT NULL - - UNION - SELECT pn.mailing_addr_id AS addr_id - FROM party_notification pn - JOIN corp_party cp ON cp.corp_party_id = pn.party_id - WHERE cp.corp_num IN (&corp_ids_in) AND pn.mailing_addr_id IS NOT NULL -) s; +-- Address rows are treated as shared/global during subset refresh. +-- Do not delete them here: subset_transfer_chunk.sql stages incoming Oracle address rows and +-- merges them into public.address by addr_id. -- Delete child tables first (event-scoped). DELETE FROM notification_resend @@ -174,9 +116,4 @@ WHERE corp_num IN (&corp_ids_in); DELETE FROM corporation WHERE corp_num IN (&corp_ids_in); --- Delete addresses captured at the start (these will be reloaded from Oracle). -DELETE FROM address a -USING tmp_subset_refresh_addr_ids t -WHERE a.addr_id = t.addr_id; - -DROP TABLE IF EXISTS pg_temp.tmp_subset_refresh_addr_ids; +-- Address rows are refreshed via stage+merge in subset_transfer_chunk.sql. diff --git a/data-tool/scripts/subset/subset_disable_triggers.sql b/data-tool/scripts/subset/subset_disable_triggers.sql index a8401a0fb8..6089f524c8 100644 --- a/data-tool/scripts/subset/subset_disable_triggers.sql +++ b/data-tool/scripts/subset/subset_disable_triggers.sql @@ -8,7 +8,6 @@ ALTER TABLE event DISABLE TRIGGER ALL; ALTER TABLE filing DISABLE TRIGGER ALL; ALTER TABLE filing_user DISABLE TRIGGER ALL; ALTER TABLE office DISABLE TRIGGER ALL; -ALTER TABLE address DISABLE TRIGGER ALL; ALTER TABLE corp_comments DISABLE TRIGGER ALL; ALTER TABLE ledger_text DISABLE TRIGGER ALL; ALTER TABLE corp_party DISABLE TRIGGER ALL; diff --git a/data-tool/scripts/subset/subset_enable_triggers.sql b/data-tool/scripts/subset/subset_enable_triggers.sql index dbfadf4bd3..071691976d 100644 --- a/data-tool/scripts/subset/subset_enable_triggers.sql +++ b/data-tool/scripts/subset/subset_enable_triggers.sql @@ -8,7 +8,6 @@ ALTER TABLE event ENABLE TRIGGER ALL; ALTER TABLE filing ENABLE TRIGGER ALL; ALTER TABLE filing_user ENABLE TRIGGER ALL; ALTER TABLE office ENABLE TRIGGER ALL; -ALTER TABLE address ENABLE TRIGGER ALL; ALTER TABLE corp_comments ENABLE TRIGGER ALL; ALTER TABLE ledger_text ENABLE TRIGGER ALL; ALTER TABLE corp_party ENABLE TRIGGER ALL; diff --git a/data-tool/scripts/subset/subset_pg_cleanup_address_stage.sql b/data-tool/scripts/subset/subset_pg_cleanup_address_stage.sql new file mode 100644 index 0000000000..036fffabc6 --- /dev/null +++ b/data-tool/scripts/subset/subset_pg_cleanup_address_stage.sql @@ -0,0 +1,4 @@ +-- Cleanup the shared address staging table used by subset_transfer_chunk.sql. + +DROP TABLE IF EXISTS public.subset_address_stage; + diff --git a/data-tool/scripts/subset/subset_pg_fastload_begin.sql b/data-tool/scripts/subset/subset_pg_fastload_begin.sql index dec8880543..19f7fc1d89 100644 --- a/data-tool/scripts/subset/subset_pg_fastload_begin.sql +++ b/data-tool/scripts/subset/subset_pg_fastload_begin.sql @@ -4,8 +4,6 @@ -- disposable extract databases (where maximum durability is not required). -- -- IMPORTANT: --- - subset_delete_chunk.sql creates a TEMP table. Any temp-table related settings (e.g. temp_buffers) --- must be set before the first chunk delete runs. -- - Keep this file free of DO $$ blocks; some DbSchemaCLI builds split statements on semicolons -- and don't handle dollar-quoted bodies reliably. diff --git a/data-tool/scripts/subset/subset_pg_prepare_address_stage.sql b/data-tool/scripts/subset/subset_pg_prepare_address_stage.sql new file mode 100644 index 0000000000..5b82697659 --- /dev/null +++ b/data-tool/scripts/subset/subset_pg_prepare_address_stage.sql @@ -0,0 +1,16 @@ +-- Prepare the shared address staging table used by subset_transfer_chunk.sql. +-- This is a regular table (not TEMP) because DbSchemaCLI transfer work may use separate sessions. + +DROP TABLE IF EXISTS public.subset_address_stage; + +CREATE TABLE public.subset_address_stage ( + addr_id numeric(10), + province varchar(2), + country_typ_cd varchar(2), + postal_cd varchar(15), + addr_line_1 varchar(50), + addr_line_2 varchar(50), + addr_line_3 varchar(50), + city varchar(40) +); + diff --git a/data-tool/scripts/subset/subset_pg_purge_bcomps_excluded.sql b/data-tool/scripts/subset/subset_pg_purge_bcomps_excluded.sql index 29e05ebbe9..e79adf977e 100644 --- a/data-tool/scripts/subset/subset_pg_purge_bcomps_excluded.sql +++ b/data-tool/scripts/subset/subset_pg_purge_bcomps_excluded.sql @@ -7,6 +7,9 @@ -- - This script intentionally does NOT touch internal migration/processing tables (mig_*, corp_processing, -- colin_tracking, affiliation_processing, etc). It only purges the corp-scoped COLIN extract tables -- that are reloaded from Oracle. +-- - Because preserved processing/tracking tables still reference corporation/event rows, refresh mode must keep +-- FK enforcement suppressed across this purge window too (for example via replica_role, or by disabling +-- triggers on the preserved referencing tables too). -- - This script avoids DO $$ blocks for DbSchemaCLI compatibility. -- 1) Build keysets diff --git a/data-tool/scripts/subset/subset_transfer_chunk.sql b/data-tool/scripts/subset/subset_transfer_chunk.sql index 84797aadac..d97afdf497 100644 --- a/data-tool/scripts/subset/subset_transfer_chunk.sql +++ b/data-tool/scripts/subset/subset_transfer_chunk.sql @@ -262,8 +262,10 @@ join event e on e.corp_num = c.corp_num join filing_user u on u.event_id = e.event_id; --- address (must load before office and corp_party which reference address via FK) -transfer public.address from cprd using +-- address (shared/global table; stage then merge before loading dependents) +TRUNCATE TABLE public.subset_address_stage; + +transfer public.subset_address_stage from cprd using with corp_list as ( select /*+ materialize */ c.corp_num from corporation c @@ -319,6 +321,13 @@ from ( join notification x on x.event_id = e.event_id join address a on x.mailing_addr_id = a.addr_id + UNION ALL + select a.* + from corporation_cte c + join event e on e.corp_num = c.corp_num + join notification_resend x on x.event_id = e.event_id + join address a on x.mailing_addr_id = a.addr_id + UNION ALL select a.* from corporation_cte c @@ -334,6 +343,49 @@ from ( join address a on x.mailing_addr_id = a.addr_id ); +INSERT INTO public.address ( + addr_id, + province, + country_typ_cd, + postal_cd, + addr_line_1, + addr_line_2, + addr_line_3, + city +) +SELECT s.addr_id, + s.province, + s.country_typ_cd, + s.postal_cd, + s.addr_line_1, + s.addr_line_2, + s.addr_line_3, + s.city +FROM ( + SELECT DISTINCT ON (addr_id) + addr_id, + province, + country_typ_cd, + postal_cd, + addr_line_1, + addr_line_2, + addr_line_3, + city + FROM public.subset_address_stage + WHERE addr_id IS NOT NULL + ORDER BY addr_id +) s +ON CONFLICT (addr_id) DO UPDATE +SET province = EXCLUDED.province, + country_typ_cd = EXCLUDED.country_typ_cd, + postal_cd = EXCLUDED.postal_cd, + addr_line_1 = EXCLUDED.addr_line_1, + addr_line_2 = EXCLUDED.addr_line_2, + addr_line_3 = EXCLUDED.addr_line_3, + city = EXCLUDED.city; + +TRUNCATE TABLE public.subset_address_stage; + -- office transfer public.office from cprd using From 80d372e3e8fb58b4a58e1b9ec06d404acda41a4b Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Fri, 20 Mar 2026 11:09:51 -0700 Subject: [PATCH 38/46] Business API update report configuration for missing alteration. Signed-off-by: Doug Lovett --- legal-api/src/legal_api/reports/report.py | 24 +++++++-- .../unit/reports/test_document_service.py | 51 +++++++++++++++++++ 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/legal-api/src/legal_api/reports/report.py b/legal-api/src/legal_api/reports/report.py index 707d2a1a47..58f5c631cb 100644 --- a/legal-api/src/legal_api/reports/report.py +++ b/legal-api/src/legal_api/reports/report.py @@ -83,12 +83,15 @@ def _get_report(self): self._business = Business.find_by_internal_id(self._filing.business_id) Report._populate_business_info_to_filing(self._filing, self._business) business_identifier = self._business.identifier if self._business else self._filing.temp_reg + report_meta: dict = ReportMeta.reports.get(self._report_key) + if not report_meta: + report_meta = ReportMeta.reports.get("default") + report_type = report_meta.get("reportType") if business_identifier: - report_meta: dict = ReportMeta.reports.get(self._report_key) document, status = self._document_service.get_filing_report_by_filing_id( business_identifier, self._filing.id, - report_meta.get("reportType")) + report_type) if status == HTTPStatus.OK: return current_app.response_class( response=document, @@ -96,6 +99,7 @@ def _get_report(self): mimetype="application/pdf" ) + # Added "alteration" to ReportMeta, can these 2 lines be removed? if self._report_key == "alteration": self._report_key = "alterationNotice" headers = { @@ -116,7 +120,7 @@ def _get_report(self): response_drs = self._document_service.create_filing_report( business_identifier, self._filing, - ReportMeta.reports.get(self._report_key), + report_meta, response ) return current_app.response_class( @@ -1525,6 +1529,11 @@ class ReportMeta: # pylint: disable=too-few-public-methods "fileName": "noticeOfArticles", "reportType": ReportTypes.NOA.value }, + "alteration": { + "filingDescription": "Alteration Notice", + "fileName": "alterationNotice", + "reportType": ReportTypes.FILING.value + }, "alterationNotice": { "filingDescription": "Alteration Notice", "fileName": "alterationNotice", @@ -1645,7 +1654,7 @@ class ReportMeta: # pylint: disable=too-few-public-methods "certificateOfRestoration": { "filingDescription": "Certificate of Restoration", "fileName": "certificateOfRestoration", - "reportType": ReportTypes.FILING.value + "reportType": ReportTypes.CERT.value }, "restoration": { "filingDescription": "Restoration Application", @@ -1679,7 +1688,8 @@ class ReportMeta: # pylint: disable=too-few-public-methods }, "certificateOfContinuation": { "filingDescription": "Certificate of Continuation", - "fileName": "certificateOfContinuation" + "fileName": "certificateOfContinuation", + "reportType": ReportTypes.CERT.value }, "noticeOfWithdrawal": { "filingDescription": "Notice of Withdrawal", @@ -1695,6 +1705,10 @@ class ReportMeta: # pylint: disable=too-few-public-methods "filingDescription": "Cease Receiver", "fileName": "ceaseReceiver", "reportType": ReportTypes.FILING.value + }, + "default": { # Used as DRS fallback if no report key configuration found. + "fileName": "filing", + "reportType": ReportTypes.FILING.value } } diff --git a/legal-api/tests/unit/reports/test_document_service.py b/legal-api/tests/unit/reports/test_document_service.py index 7038a78e3f..022a1a9d47 100644 --- a/legal-api/tests/unit/reports/test_document_service.py +++ b/legal-api/tests/unit/reports/test_document_service.py @@ -23,6 +23,7 @@ from legal_api.models import Filing from legal_api.reports.document_service import DocumentService +from legal_api.reports.report import ReportMeta from tests.unit.models import factory_business, factory_completed_filing from registry_schemas.example_data import FILING_TEMPLATE @@ -241,6 +242,56 @@ ("Static 1", DOCS_STATIC1, DRS_STATIC1, None, None, None, None, "documentClass=COOP&drsId=DS0000101630", META_MODERN, 233791), ("Static 2", DOCS_STATIC2, DRS_STATIC2, None, None, None, None, "documentClass=CORP&drsId=DS0000100741", META_MODERN, 155753), ] +# testdata pattern is ({has_data}, {report_key}, {report_type}) +TEST_REPORT_META_DATA = [ + (False, "JUNK", None), + (True, "amalgamationApplication", "FILING"), + (True, "certificateOfAmalgamation", "CERT"), + (True, "certificateOfIncorporation", "CERT"), + (True, "incorporationApplication", "FILING"), + (True, "noticeOfArticles", "NOA"), + (True, "alteration", "FILING"), + (True, "alterationNotice", "FILING"), + (True, "transition", "FILING"), + (True, "changeOfAddress", "FILING"), + (True, "changeOfDirectors", "FILING"), + (True, "annualReport", "FILING"), + (True, "changeOfName", "FILING"), + (True, "specialResolution", "FILING"), + (True, "specialResolutionApplication", "FILING"), + (True, "voluntaryDissolution", "FILING"), + (True, "certificateOfNameChange", "CERT"), + (True, "certificateOfNameCorrection", "CERT"), + (True, "certificateOfDissolution", "CERT"), + (True, "dissolution", "FILING"), + (True, "registration", "FILING"), + (True, "amendedRegistrationStatement", "FILING"), + (True, "correctedRegistrationStatement", "FILING"), + (True, "changeOfRegistration", "FILING"), + (True, "correction", "FILING"), + (True, "certificateOfRestoration", "CERT"), + (True, "letterOfConsent", "FILING"), + (True, "letterOfConsentAmalgamationOut", "FILING"), + (True, "letterOfAgmExtension", "FILING"), + (True, "letterOfAgmLocationChange", "FILING"), + (True, "continuationIn", "FILING"), + (True, "certificateOfContinuation", "CERT"), + (True, "noticeOfWithdrawal", "FILING"), + (True, "appointReceiver", "FILING"), + (True, "ceaseReceiver", "FILING"), + (True, "default", "FILING"), +] + + +@pytest.mark.parametrize("has_data,report_key,report_type", TEST_REPORT_META_DATA) +def test_report_meta_type(session, has_data, report_key, report_type): + """Assert that DRS report type configuration is as expected.""" + report_meta = ReportMeta.reports.get(report_key) + if not has_data: + assert not report_meta + else: + assert report_meta + assert report_meta.get("reportType") == report_type @pytest.mark.parametrize("desc,doc_data,drs_data,receipt,filing,noa,cert,static", TEST_FILING_UPDATE_DATA) From 2495d7f292b5e39e62664ff4ae35ca1438aa5966 Mon Sep 17 00:00:00 2001 From: Argus Chiu Date: Fri, 20 Mar 2026 14:05:03 -0700 Subject: [PATCH 39/46] 32908 Add reset views script (#4191) --- data-tool/reset_colin_extract_views.sh | 232 +++++++++++++++++ .../scripts/README_COLIN_Corps_Extract.md | 65 +++++ ...colin_corps_extract_generate_view_drop.sql | 237 ++++++++++++++++++ 3 files changed, 534 insertions(+) create mode 100755 data-tool/reset_colin_extract_views.sh create mode 100644 data-tool/scripts/colin_corps_extract_generate_view_drop.sql diff --git a/data-tool/reset_colin_extract_views.sh b/data-tool/reset_colin_extract_views.sh new file mode 100755 index 0000000000..19bbaa0100 --- /dev/null +++ b/data-tool/reset_colin_extract_views.sh @@ -0,0 +1,232 @@ +#!/usr/bin/env bash +# reset_colin_extract_views.sh +# +# Plan / drop / reset the COLIN extract derived view layer. +# +# Modes: +# plan - print the generated drop SQL only (default) +# drop - execute the generated drop SQL only +# reset - execute the generated drop SQL, then reapply the views DDL +# +# Safety: +# - scopes drop planning to the allowlisted COLIN derived objects only +# - hard-fails on wrong object type or unexpected external view/MV dependents +# - never uses CASCADE +# - requires --yes for drop/reset modes +# +# Examples: +# ./data-tool/reset_colin_extract_views.sh \ +# --db colin-mig-corps-test-subset +# +# ./data-tool/reset_colin_extract_views.sh \ +# --mode reset --yes \ +# --db colin-mig-corps-test-subset \ +# --user some_user \ +# --psql-bin /opt/homebrew/opt/postgresql@15/bin/psql + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +GENERATOR_SQL="$ROOT_DIR/scripts/colin_corps_extract_generate_view_drop.sql" +VIEWS_DDL="$ROOT_DIR/scripts/colin_corps_extract_postgres_views_ddl" + +PSQL_BIN="${PSQL_BIN:-psql}" +PGHOST="${PGHOST:-localhost}" +PGPORT="${PGPORT:-5432}" +PGUSER="${PGUSER:-postgres}" +PGDATABASE="${PGDATABASE:-colin-mig-corps-test}" +PGSCHEMA="${PGSCHEMA:-public}" +MODE="plan" +ASSUME_YES="false" +ALLOW_EMPTY="false" + +usage() { + cat <<'USAGE' +Usage: reset_colin_extract_views.sh [options] + +Options: + --mode plan=print SQL only (default) + drop=execute generated drop SQL only + reset=drop and then reapply views DDL + --db, --dbname target database name (default: colin-mig-corps-test) + --host PostgreSQL host (default: localhost) + --port PostgreSQL port (default: 5432) + --user PostgreSQL user (default: postgres) + --schema target schema (default: public) + --psql-bin psql binary to use (default: psql or $PSQL_BIN) + --yes required for drop/reset execution + --allow-empty allow drop/reset when zero allowlisted objects exist + -h, --help show help + +Notes: + - Password handling is delegated to .pgpass or PGPASSWORD. + - plan mode is safe and does not require --yes. + - reset mode only resets the view/materialized-view layer; it does not rebuild + the whole extract database. + - reset mode currently supports only --schema public because the views DDL is + not schema-qualified. +USAGE + return 0 +} + +die() { + local message="$*" + printf >&2 'error: %s\n' "$message" + exit 1 +} + +require_command() { + local command_name="$1" + command -v "$command_name" >/dev/null 2>&1 || die "required command not found: $command_name" + return 0 +} + +psql_cmd=() +setup_psql_cmd() { + psql_cmd=( + "$PSQL_BIN" + -X + -v ON_ERROR_STOP=1 + -h "$PGHOST" + -p "$PGPORT" + -U "$PGUSER" + -d "$PGDATABASE" + ) + return 0 +} + +generate_drop_sql() { + "${psql_cmd[@]}" -qAt -v "schema_name=$PGSCHEMA" -f "$GENERATOR_SQL" + return 0 +} + +schema_exists() { + local result + result="$("${psql_cmd[@]}" -qAt -v "schema_name=$PGSCHEMA" <<'SQL' +SELECT 1 +FROM pg_namespace +WHERE nspname = :'schema_name' +LIMIT 1; +SQL +)" + if [[ "$result" == "1" ]]; then + return 0 + fi + return 1 +} + +run_reset_transaction() { + local search_path="public" + if [[ -n "${PGOPTIONS:-}" ]]; then + PGOPTIONS="${PGOPTIONS} -c search_path=${search_path}" "${psql_cmd[@]}" -1 -f "$plan_file" -f "$VIEWS_DDL" + return 0 + fi + + PGOPTIONS="-c search_path=${search_path}" "${psql_cmd[@]}" -1 -f "$plan_file" -f "$VIEWS_DDL" + return 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --mode) + MODE="${2:-}" + shift 2 + ;; + --db|--dbname) + PGDATABASE="${2:-}" + shift 2 + ;; + --host) + PGHOST="${2:-}" + shift 2 + ;; + --port) + PGPORT="${2:-}" + shift 2 + ;; + --user) + PGUSER="${2:-}" + shift 2 + ;; + --schema) + PGSCHEMA="${2:-}" + shift 2 + ;; + --psql-bin) + PSQL_BIN="${2:-}" + shift 2 + ;; + --yes) + ASSUME_YES="true" + shift + ;; + --allow-empty) + ALLOW_EMPTY="true" + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + die "unknown argument: $1" + ;; + esac +done + +case "$MODE" in + plan|drop|reset) + ;; + *) + die "invalid mode: $MODE" + ;; +esac + +require_command "$PSQL_BIN" +[[ -f "$GENERATOR_SQL" ]] || die "missing generator SQL: $GENERATOR_SQL" +[[ -f "$VIEWS_DDL" ]] || die "missing views DDL: $VIEWS_DDL" +setup_psql_cmd + +if [[ "$MODE" == "reset" && "$PGSCHEMA" != "public" ]]; then + die "reset mode currently supports only --schema public because the views DDL is not schema-qualified" +fi + +if [[ "$MODE" == "reset" ]] && ! schema_exists; then + die "schema '$PGSCHEMA' does not exist in database '$PGDATABASE'" +fi + +tmp_dir="${TMPDIR:-/tmp}" +tmp_dir="${tmp_dir%/}" +plan_db_slug="${PGDATABASE//[^A-Za-z0-9_.-]/_}" +plan_file="$(mktemp "${tmp_dir}/colin-view-drop-plan.${plan_db_slug}.XXXXXX")" +trap 'rm -f "$plan_file"' EXIT + +generate_drop_sql > "$plan_file" + +drop_count="$(grep -Ec '^[[:space:]]*DROP[[:space:]]+' "$plan_file" || true)" + +printf 'Generated drop plan for %s.%s using schema %s:\n' "$PGHOST" "$PGDATABASE" "$PGSCHEMA" +cat "$plan_file" + +if [[ "$MODE" == "plan" ]]; then + exit 0 +fi + +if [[ "$ASSUME_YES" != "true" ]]; then + die "--yes is required for mode '$MODE'" +fi + +if [[ "$drop_count" == "0" && "$ALLOW_EMPTY" != "true" ]]; then + die "zero allowlisted COLIN derived objects were found; refusing to run '$MODE' without --allow-empty" +fi + +if [[ "$MODE" == "drop" ]]; then + printf '\nExecuting generated drop plan...\n' + "${psql_cmd[@]}" -f "$plan_file" + printf 'Done. Dropped allowlisted COLIN derived objects only.\n' + exit 0 +fi + +printf '\nExecuting drop plan and reapplying views DDL in a single transaction...\n' +run_reset_transaction +printf 'Done. COLIN derived view layer has been regenerated.\n' diff --git a/data-tool/scripts/README_COLIN_Corps_Extract.md b/data-tool/scripts/README_COLIN_Corps_Extract.md index 67333f5925..77cec3ee42 100644 --- a/data-tool/scripts/README_COLIN_Corps_Extract.md +++ b/data-tool/scripts/README_COLIN_Corps_Extract.md @@ -68,6 +68,71 @@ Notes: --- +## Resetting the derived COLIN view/materialized-view layer + +Use this when the **derived view/MV definitions** in `colin_corps_extract_postgres_views_ddl` have changed and you need to drop + recreate just that layer in an existing extract DB. + +Files: +- Drop-plan generator: `data-tool/scripts/colin_corps_extract_generate_view_drop.sql` +- Wrapper: `data-tool/reset_colin_extract_views.sh` +- Reapply DDL: `data-tool/scripts/colin_corps_extract_postgres_views_ddl` + +Usage summary: +```text +./data-tool/reset_colin_extract_views.sh [options] + +Options: + --mode plan=print SQL only (default) + drop=execute generated drop SQL only + reset=drop and then reapply views DDL + --db, --dbname target database name (default: colin-mig-corps-test) + --host PostgreSQL host (default: localhost) + --port PostgreSQL port (default: 5432) + --user PostgreSQL user (default: postgres) + --schema target schema (default: public) + --psql-bin psql binary to use (default: psql or $PSQL_BIN) + --yes required for drop/reset execution + --allow-empty allow drop/reset when zero allowlisted objects exist + +Environment defaults: + PGDATABASE=colin-mig-corps-test + PGUSER=postgres + PGHOST=localhost + PGPORT=5432 + PGSCHEMA=public +``` + +Examples: +```bash +# Safe preview: print the generated dependency-aware drop plan only +./data-tool/reset_colin_extract_views.sh \ + --mode plan \ + --db colin-mig-corps-test-subset \ + --user postgres + +# Drop the allowlisted derived objects, then recreate them from the views DDL +./data-tool/reset_colin_extract_views.sh \ + --mode reset --yes \ + --db colin-mig-corps-test-subset \ + --user postgres + +# If the derived objects are already absent and you only want to recreate them, +# add --allow-empty explicitly. +./data-tool/reset_colin_extract_views.sh \ + --mode reset --yes --allow-empty \ + --db colin-mig-corps-test-subset \ + --user postgres +``` + +Safety rules: +- Only the COLIN-owned allowlisted views/materialized views are targeted. +- The generator **does not use `CASCADE`**. +- The generator **fails** if an unexpected non-allowlisted view/materialized view depends on a COLIN-owned object. +- `reset` mode currently supports only schema `public` because `colin_corps_extract_postgres_views_ddl` is not schema-qualified. +- For **data-only** changes, prefer `REFRESH MATERIALIZED VIEW` rather than drop/recreate. + +--- + ## Subset refresh / subset load (corp-id list; 10k+ supported) This workflow is for when you want to: diff --git a/data-tool/scripts/colin_corps_extract_generate_view_drop.sql b/data-tool/scripts/colin_corps_extract_generate_view_drop.sql new file mode 100644 index 0000000000..e5c2b00ed7 --- /dev/null +++ b/data-tool/scripts/colin_corps_extract_generate_view_drop.sql @@ -0,0 +1,237 @@ +-- Generate dependency-safe DROP statements for the COLIN extract derived view layer. +-- +-- Usage examples: +-- psql -X -qAt -v schema_name=public \ +-- -f data-tool/scripts/colin_corps_extract_generate_view_drop.sql +-- +-- The script: +-- - scopes itself to an allowlist of COLIN-owned views/materialized views +-- - raises an error if an allowlisted object exists with the wrong kind +-- - raises an error if any non-allowlisted view/materialized view depends on an allowlisted object +-- - emits DROP statements in dependency-safe reverse order +-- - emits comment lines for missing allowlisted objects +-- +-- Pipe the output back into psql to execute the drop plan, or use +-- data-tool/reset_colin_extract_views.sh for a higher-level wrapper. + +\if :{?schema_name} +\else +\set schema_name public +\endif + +SET colin_extract.reset_schema TO :'schema_name'; + +CREATE TEMP TABLE IF NOT EXISTS colin_extract_reset_allowlist ( + obj_name text PRIMARY KEY, + expected_relkind "char" NOT NULL CHECK (expected_relkind IN ('v', 'm')) +); + +TRUNCATE colin_extract_reset_allowlist; + +INSERT INTO colin_extract_reset_allowlist (obj_name, expected_relkind) +VALUES + ('v_addr_links', 'v'), + ('v_addr_issues', 'v'), + ('v_business_state', 'v'), + ('v_corp_issue_flags_long', 'v'), + ('mv_corps_with_officers', 'm'), + ('mv_corps_party_role_count', 'm'), + ('mv_admin_email_count', 'm'), + ('mv_admin_email_domain_count', 'm'), + ('mv_addr_quality_by_corp', 'm'), + ('mv_share_class_issue_flags', 'm'), + ('mv_legacy_corps_data', 'm'), + ('mv_corp_issue_flags', 'm'), + ('mv_issue_counts_by_corp_type', 'm'); + +DO $$ +DECLARE + v_schema text := current_setting('colin_extract.reset_schema'); + v_wrong_type_details text; + v_external_dep_details text; +BEGIN + WITH schema_ns AS ( + SELECT oid + FROM pg_namespace + WHERE nspname = v_schema + ), + existing AS ( + SELECT + a.obj_name, + a.expected_relkind, + c.oid, + c.relkind + FROM pg_temp.colin_extract_reset_allowlist a + LEFT JOIN schema_ns ns ON TRUE + LEFT JOIN pg_class c + ON c.relnamespace = ns.oid + AND c.relname = a.obj_name + ) + SELECT string_agg( + format('%I.%I exists as relkind=%s but expected=%s', + v_schema, + obj_name, + relkind, + expected_relkind), + E'\n' + ) + INTO v_wrong_type_details + FROM existing + WHERE oid IS NOT NULL + AND relkind <> expected_relkind; + + IF v_wrong_type_details IS NOT NULL THEN + RAISE EXCEPTION + USING MESSAGE = format( + 'Refusing to generate drop SQL because one or more allowlisted objects in schema %I have the wrong kind:%s%s', + v_schema, + E'\n', + v_wrong_type_details + ); + END IF; + + WITH src_objs AS ( + SELECT c.oid, n.nspname AS schema_name, c.relname AS obj_name + FROM pg_class c + JOIN pg_namespace n + ON n.oid = c.relnamespace + JOIN pg_temp.colin_extract_reset_allowlist a + ON a.obj_name = c.relname + WHERE n.nspname = v_schema + AND c.relkind IN ('v', 'm') + ), + external_dependents AS ( + SELECT DISTINCT + format('%I.%I depends on %I.%I', + dep_n.nspname, + dep_c.relname, + src.schema_name, + src.obj_name) AS detail + FROM src_objs src + JOIN pg_depend d + ON d.refobjid = src.oid + AND d.deptype = 'n' + JOIN pg_rewrite rw + ON rw.oid = d.objid + JOIN pg_class dep_c + ON dep_c.oid = rw.ev_class + AND dep_c.relkind IN ('v', 'm') + JOIN pg_namespace dep_n + ON dep_n.oid = dep_c.relnamespace + LEFT JOIN pg_temp.colin_extract_reset_allowlist dep_allow + ON dep_allow.obj_name = dep_c.relname + AND dep_n.nspname = v_schema + WHERE dep_allow.obj_name IS NULL + AND dep_n.nspname NOT IN ('pg_catalog', 'information_schema') + ) + SELECT string_agg(detail, E'\n') + INTO v_external_dep_details + FROM external_dependents; + + IF v_external_dep_details IS NOT NULL THEN + RAISE EXCEPTION + USING MESSAGE = format( + 'Refusing to generate drop SQL because unexpected external view/materialized-view dependents exist:%s%s', + E'\n', + v_external_dep_details + ); + END IF; +END $$; + +WITH params AS ( + SELECT :'schema_name'::text AS target_schema +), +existing AS ( + SELECT + p.target_schema, + a.obj_name, + a.expected_relkind, + c.oid + FROM params p + CROSS JOIN pg_temp.colin_extract_reset_allowlist a + LEFT JOIN pg_namespace n + ON n.nspname = p.target_schema + LEFT JOIN pg_class c + ON c.relnamespace = n.oid + AND c.relname = a.obj_name +) +SELECT format('-- Missing %s %I.%I; skipping drop and expecting recreate on reapply.', + CASE expected_relkind + WHEN 'm' THEN 'materialized view' + ELSE 'view' + END, + target_schema, + obj_name) +FROM existing +WHERE oid IS NULL +ORDER BY obj_name ASC; + +WITH RECURSIVE params AS ( + SELECT :'schema_name'::text AS target_schema +), +existing AS ( + SELECT + n.nspname AS schema_name, + c.relname AS obj_name, + c.oid, + c.relkind + FROM params p + JOIN pg_namespace n + ON n.nspname = p.target_schema + JOIN pg_class c + ON c.relnamespace = n.oid + JOIN pg_temp.colin_extract_reset_allowlist a + ON a.obj_name = c.relname + AND a.expected_relkind = c.relkind +), +edges AS ( + SELECT DISTINCT + dep_c.oid AS dependent_oid, + src_c.oid AS source_oid + FROM pg_depend d + JOIN pg_rewrite rw + ON rw.oid = d.objid + JOIN pg_class dep_c + ON dep_c.oid = rw.ev_class + AND dep_c.relkind IN ('v', 'm') + JOIN pg_class src_c + ON src_c.oid = d.refobjid + AND src_c.relkind IN ('v', 'm') + JOIN existing dep_e + ON dep_e.oid = dep_c.oid + JOIN existing src_e + ON src_e.oid = src_c.oid + WHERE d.deptype = 'n' + AND dep_c.oid <> src_c.oid +), +depths AS ( + SELECT e.oid, 0 AS depth + FROM existing e + + UNION + + SELECT ed.dependent_oid, depths.depth + 1 + FROM edges ed + JOIN depths + ON depths.oid = ed.source_oid +), +ordered AS ( + SELECT + e.schema_name, + e.obj_name, + e.relkind, + COALESCE(MAX(d.depth), 0) AS depth + FROM existing e + LEFT JOIN depths d + ON d.oid = e.oid + GROUP BY e.schema_name, e.obj_name, e.relkind +) +SELECT format('DROP %s IF EXISTS %I.%I;', + CASE relkind + WHEN 'm' THEN 'MATERIALIZED VIEW' + ELSE 'VIEW' + END, + schema_name, + obj_name) +FROM ordered +ORDER BY depth DESC, obj_name ASC; From 47d7fd33d1cdb291f8f9502998dc89050f804b0a Mon Sep 17 00:00:00 2001 From: Doug Lovett Date: Fri, 20 Mar 2026 17:46:10 -0700 Subject: [PATCH 40/46] Business API requests DRS binary data instead of download link. Signed-off-by: Doug Lovett --- legal-api/src/legal_api/reports/document_service.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/legal-api/src/legal_api/reports/document_service.py b/legal-api/src/legal_api/reports/document_service.py index 1febfd8332..7c44b49122 100644 --- a/legal-api/src/legal_api/reports/document_service.py +++ b/legal-api/src/legal_api/reports/document_service.py @@ -335,7 +335,7 @@ def get_filing_report(self, drs_id: str, report_type: str): report_type: The report type: request a certified copy for NOA and FILING report types. return: The document binary data. """ - headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID, APP_PDF) url: str = self.url.replace(DOC_PATH, "") get_url = GET_REPORT_PATH if report_type in (ReportTypes.FILING.value, ReportTypes.NOA.value): @@ -387,7 +387,7 @@ def get_filing_document(self, drs_id: str, doc_class: str): document_class: The DRS document class for the business to filter on. return: The document binary data. """ - headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID) + headers = self._get_request_headers(BUSINESS_API_ACCOUNT_ID, APP_PDF) url: str = self.url.replace(DOC_PATH, "") get_url = GET_DOCUMENT_PATH.format(url=url, document_class=doc_class, drs_id=drs_id) response = requests.get(url=get_url, headers=headers) @@ -478,7 +478,7 @@ def _update_static_document(self, doc: dict, doc_list: list) -> list: return doc_list - def _get_request_headers(self, account_id: str) -> dict: + def _get_request_headers(self, account_id: str, accept_mime_type = None) -> dict: """ Get request headers for the DRS api call. @@ -492,4 +492,6 @@ def _get_request_headers(self, account_id: str) -> dict: "Content-Type": APP_PDF, "Authorization": BEARER + token } + if accept_mime_type: + headers["Accept"] = accept_mime_type return headers From be798b2f40c551e9a724f8125ea023fad9f0a84a Mon Sep 17 00:00:00 2001 From: Argus Chiu Date: Mon, 23 Mar 2026 14:52:04 -0700 Subject: [PATCH 41/46] 32935 Add full extract support to colin delta notebook (#4195) --- data-tool/notebooks/colin_delta.ipynb | 141 ++++++++++++++++---------- 1 file changed, 88 insertions(+), 53 deletions(-) diff --git a/data-tool/notebooks/colin_delta.ipynb b/data-tool/notebooks/colin_delta.ipynb index 7f06299833..7f3c939256 100644 --- a/data-tool/notebooks/colin_delta.ipynb +++ b/data-tool/notebooks/colin_delta.ipynb @@ -15,6 +15,9 @@ "from math import ceil\n", "\n", "# ====== CONFIG ======\n", + "MODE = \"identifiers\"\n", + "# MODE = \"all_after_cutoff\"\n", + "\n", "# In SQL, Oracle type constructors are limited to 999 parameters.\n", "# So keep this <= 999 to avoid ORA-00939 in sys.odcivarchar2list(...).\n", "BATCH_SIZE = 999\n", @@ -23,7 +26,7 @@ "# (this is just formatting; set 500-999 as you requested)\n", "VALUES_PER_LINE = 999\n", "\n", - "CUTOFF = \"TIMESTAMP '2026-03-09 23:00:00'\" # or \":cutoff_ts\"\n", + "CUTOFF = \"TIMESTAMP '2026-03-23 06:00:00'\" # or \":cutoff_ts\"\n", "OUTPUT_FILE = \"generated/check_events.sql\"\n", "\n", "IDENTIFIERS = r\"\"\"\n", @@ -32,6 +35,11 @@ "\"\"\"\n", "# ====================\n", "\n", + "VALID_MODES = {\"identifiers\", \"all_after_cutoff\"}\n", + "# Match the supported full extract corp types from transfer_cprd_corps.sql\n", + "# and subset_transfer_chunk.sql.\n", + "FULL_EXTRACT_CORP_TYPE_FILTER = \"('BC', 'C', 'ULC', 'CUL', 'CC', 'CCC', 'QA', 'QB', 'QC', 'QD', 'QE')\"\n", + "\n", "def parse_ids(s: str) -> list[str]:\n", " # Extract values inside single quotes; preserves leading zeros like '0766566'\n", " ids = re.findall(r\"'([^']*)'\", s)\n", @@ -70,41 +78,67 @@ " ]\n", " return \"\\n\".join(lines)\n", "\n", - "corp_nums = parse_ids(IDENTIFIERS)\n", - "if not corp_nums:\n", - " raise SystemExit(\"No corp nums found. Paste the quoted list into IDENTIFIERS.\")\n", - "\n", - "n = len(corp_nums)\n", - "num_batches = ceil(n / BATCH_SIZE)\n", - "\n", - "batches = [\n", - " corp_nums[i * BATCH_SIZE : (i + 1) * BATCH_SIZE]\n", - " for i in range(num_batches)\n", - "]\n", - "\n", - "# Build corp_list_001, corp_list_002, ...\n", - "cte_blocks = []\n", - "batch_names = []\n", - "for idx, batch in enumerate(batches, start=1):\n", - " name = f\"corp_list_{idx:03d}\"\n", - " batch_names.append(name)\n", - " cte_blocks.append(make_batch_cte(name, batch))\n", - "\n", - "cte_blocks_sql = \",\\n\".join(cte_blocks)\n", - "\n", - "# Build unified corp_list CTE\n", - "corp_union_lines = [\"corp_list AS (\"]\n", - "corp_union_lines.append(f\" SELECT corp_num FROM {batch_names[0]}\")\n", - "for name in batch_names[1:]:\n", - " corp_union_lines.append(f\" UNION ALL SELECT corp_num FROM {name}\")\n", - "corp_union_lines.append(\")\")\n", - "corp_list_sql = \"\\n\".join(corp_union_lines)\n", + "def build_identifier_scope(items: list[str]) -> tuple[list[str], int, list[list[str]]]:\n", + " n = len(items)\n", + " num_batches = ceil(n / BATCH_SIZE)\n", + " batches = [\n", + " items[i * BATCH_SIZE : (i + 1) * BATCH_SIZE]\n", + " for i in range(num_batches)\n", + " ]\n", "\n", - "sql = f\"\"\"\\\n", - "WITH\n", - "{cte_blocks_sql},\n", - "{corp_list_sql},\n", - "latest_event AS (\n", + " cte_blocks = []\n", + " batch_names = []\n", + " for idx, batch in enumerate(batches, start=1):\n", + " name = f\"corp_list_{idx:03d}\"\n", + " batch_names.append(name)\n", + " cte_blocks.append(make_batch_cte(name, batch))\n", + "\n", + " corp_union_lines = [\"corp_list AS (\"]\n", + " corp_union_lines.append(f\" SELECT corp_num FROM {batch_names[0]}\")\n", + " for name in batch_names[1:]:\n", + " corp_union_lines.append(f\" UNION ALL SELECT corp_num FROM {name}\")\n", + " corp_union_lines.append(\")\")\n", + "\n", + " return [*cte_blocks, \"\\n\".join(corp_union_lines)], n, batches\n", + "\n", + "def build_scope(mode: str, identifiers: str) -> tuple[list[str], str, dict]:\n", + " if mode not in VALID_MODES:\n", + " raise SystemExit(f\"Unsupported MODE={mode!r}. Choose one of: {sorted(VALID_MODES)}\")\n", + "\n", + " if mode == \"identifiers\":\n", + " corp_nums = parse_ids(identifiers)\n", + " if not corp_nums:\n", + " raise SystemExit(\"No corp nums found. Paste the quoted list into IDENTIFIERS.\")\n", + "\n", + " scope_ctes, n, batches = build_identifier_scope(corp_nums)\n", + " join_sql = \" JOIN corp_list c\\n ON c.corp_num = e.corp_num\\n\"\n", + " return scope_ctes, join_sql, {\n", + " \"corp_count\": n,\n", + " \"batch_count\": len(batches),\n", + " \"batch_sizes\": [len(batch) for batch in batches],\n", + " \"uses_identifier_scope\": True,\n", + " }\n", + "\n", + " ignored_ids = parse_ids(identifiers)\n", + " if ignored_ids:\n", + " print(f\"MODE={mode}; ignoring {len(ignored_ids)} identifiers from IDENTIFIERS.\")\n", + "\n", + " join_sql = (\n", + " \" JOIN corporation c\\n\"\n", + " \" ON c.corp_num = e.corp_num\\n\"\n", + " f\" AND c.corp_typ_cd IN {FULL_EXTRACT_CORP_TYPE_FILTER}\\n\"\n", + " )\n", + "\n", + " return [], join_sql, {\n", + " \"corp_count\": 0,\n", + " \"batch_count\": 0,\n", + " \"batch_sizes\": [],\n", + " \"uses_identifier_scope\": False,\n", + " }\n", + "\n", + "scope_ctes, scope_join_sql, meta = build_scope(MODE, IDENTIFIERS)\n", + "\n", + "latest_event_sql = f\"\"\"latest_event AS (\n", " SELECT e.event_id,\n", " e.corp_num,\n", " e.event_typ_cd,\n", @@ -112,13 +146,18 @@ " e.trigger_dts,\n", " ROW_NUMBER() OVER (\n", " PARTITION BY e.corp_num\n", - " ORDER BY e.event_timestmp DESC\n", + " ORDER BY e.event_timestmp DESC, e.event_id DESC\n", " ) AS rn\n", " FROM event e\n", - " JOIN corp_list c\n", - " ON c.corp_num = e.corp_num\n", - " WHERE e.event_timestmp > {CUTOFF}\n", - ")\n", + "{scope_join_sql} WHERE e.event_timestmp > {CUTOFF}\n", + ")\"\"\"\n", + "\n", + "with_parts = [*scope_ctes, latest_event_sql]\n", + "with_clause = \",\\n\".join(with_parts)\n", + "\n", + "sql = f\"\"\"\\\n", + "WITH\n", + "{with_clause}\n", "SELECT le.EVENT_ID,\n", " le.corp_num,\n", " le.event_typ_cd,\n", @@ -128,28 +167,24 @@ "FROM latest_event le\n", "left join filing f on le.EVENT_ID = f.EVENT_ID\n", "WHERE rn = 1\n", - "ORDER BY event_timestmp DESC;\n", + "ORDER BY le.event_timestmp DESC, le.EVENT_ID DESC;\n", "\"\"\"\n", "\n", "Path(OUTPUT_FILE).write_text(sql, encoding=\"utf-8\")\n", "\n", "print(f\"Wrote {OUTPUT_FILE}\")\n", - "print(f\"Total corp nums: {n}\")\n", - "print(f\"Batch size (per odcivarchar2list): {BATCH_SIZE}\")\n", - "print(f\"Values per line (formatting only): {VALUES_PER_LINE}\")\n", - "print(f\"Batches: {num_batches} -> {[len(b) for b in batches]}\")\n" + "print(f\"Mode: {MODE}\")\n", + "if meta[\"uses_identifier_scope\"]:\n", + " print(f\"Total corp nums: {meta['corp_count']}\")\n", + " print(f\"Batch size (per odcivarchar2list): {BATCH_SIZE}\")\n", + " print(f\"Values per line (formatting only): {VALUES_PER_LINE}\")\n", + " print(f\"Batches: {meta['batch_count']} -> {meta['batch_sizes']}\")\n", + "else:\n", + " print(\"Identifier scope: disabled (querying all supported full-extract corp types after cutoff)\")\n" ], "id": "3fcd34deb8dd94b2", "outputs": [], "execution_count": null - }, - { - "metadata": {}, - "cell_type": "code", - "source": "", - "id": "5bd841959dda7600", - "outputs": [], - "execution_count": null } ], "metadata": { From f5450c8dbcf26c50d118e1723aca54b07882f625 Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Mon, 23 Mar 2026 15:05:21 -0700 Subject: [PATCH 42/46] 32879 prevent duplicate processing of same filing (#4192) --- .../business_filer/common/services/naics.py | 24 ++-- .../src/business_filer/resources/worker.py | 2 +- .../src/business_filer/services/filer.py | 6 +- .../test_filer/test_process_filing_core.py | 116 ++++++++++++++++++ 4 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 queue_services/business-filer/tests/unit/test_filer/test_process_filing_core.py diff --git a/queue_services/business-filer/src/business_filer/common/services/naics.py b/queue_services/business-filer/src/business_filer/common/services/naics.py index 427abbf598..1053e1c327 100644 --- a/queue_services/business-filer/src/business_filer/common/services/naics.py +++ b/queue_services/business-filer/src/business_filer/common/services/naics.py @@ -18,6 +18,8 @@ import requests from flask import current_app +from business_filer.exceptions import QueueException + from .account_service import AccountService @@ -27,15 +29,13 @@ class NaicsService: @staticmethod def find_by_code(naics_code: str): """Return NAICS Structure matching code.""" - try: - naics_url = current_app.config.get("NAICS_API_URL") - token = AccountService.get_bearer_token() - response = requests.get(naics_url + "/" + naics_code, headers={ - "Content-Type": "application/json", - "Authorization": "Bearer " + token - }) - response.raise_for_status() - return response.json() - except Exception as err: - current_app.logger.error(err) - return None + naics_url = current_app.config.get("NAICS_API_URL") + token = AccountService.get_bearer_token() + if not token: + raise QueueException("Failed to get token for naics call") + response = requests.get(naics_url + "/" + naics_code, headers={ + "Content-Type": "application/json", + "Authorization": "Bearer " + token + }) + response.raise_for_status() + return response.json() diff --git a/queue_services/business-filer/src/business_filer/resources/worker.py b/queue_services/business-filer/src/business_filer/resources/worker.py index 56291e2639..969a69eca4 100644 --- a/queue_services/business-filer/src/business_filer/resources/worker.py +++ b/queue_services/business-filer/src/business_filer/resources/worker.py @@ -95,7 +95,7 @@ def worker(): process_filing(filing_message) except Exception as err: # pylint: disable=broad-exception-caught current_app.logger.error(f"Error processing filing {filing_message}: {err}") - current_app.logger.debug(traceback.format_exc()) + current_app.logger.debug(f"{filing_message}: {traceback.format_exc()}") return {"error": f"Unable to process filing: {filing_message}"}, HTTPStatus.INTERNAL_SERVER_ERROR # Completed diff --git a/queue_services/business-filer/src/business_filer/services/filer.py b/queue_services/business-filer/src/business_filer/services/filer.py index 6a3a3d4b5d..3a16572a08 100644 --- a/queue_services/business-filer/src/business_filer/services/filer.py +++ b/queue_services/business-filer/src/business_filer/services/filer.py @@ -99,7 +99,11 @@ def get_filing_types(legal_filings: dict): def process_filing(filing_message: FilingMessage): # noqa: PLR0915, PLR0912 """Render the filings contained in the submission.""" - if not (filing_submission := Filing.find_by_id(filing_message.filing_identifier)): + if not (filing_submission := db.session.query(Filing) + .with_for_update(nowait=True) + .filter_by(id=filing_message.filing_identifier) + .first() + ): current_app.logger.error(f"No filing found for: {filing_message}") raise DefaultError(error_text=f"filing not found for {filing_message.filing_identifier}") diff --git a/queue_services/business-filer/tests/unit/test_filer/test_process_filing_core.py b/queue_services/business-filer/tests/unit/test_filer/test_process_filing_core.py new file mode 100644 index 0000000000..7b918a33e6 --- /dev/null +++ b/queue_services/business-filer/tests/unit/test_filer/test_process_filing_core.py @@ -0,0 +1,116 @@ +# 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. +"""The Test Suites to ensure that the core process_filing wrapper is operating correctly.""" +import pytest +import copy +from registry_schemas.example_data import ANNUAL_REPORT, REGISTRATION, FILING_HEADER + +from business_filer.common.filing_message import FilingMessage +from business_filer.exceptions import DefaultError +from business_filer.services.filer import process_filing +from business_model.models import Filing +from tests.unit import create_business, create_filing + + +def test_process_filing_not_found(app, session): + """Assert that a DefaultError is raised when filing is not found.""" + filing_msg = FilingMessage(filing_identifier=999999) + with pytest.raises(DefaultError) as excinfo: + process_filing(filing_msg) + assert "filing not found for 999999" in excinfo.value.error_text + + +def test_process_filing_duplicate(app, session, mocker): + """Assert that process_filing handles duplicate messages and the DB query succeeds without mocking.""" + # mock out the event publishing so we don't try to send emails + mocker.patch('business_filer.services.publish_event.PublishEvent.publish_email_message', return_value=None) + mocker.patch('business_filer.services.publish_event.PublishEvent.publish_event', return_value=None) + + business = create_business(identifier="CP1234567") + filing = create_filing("payment123", ANNUAL_REPORT, business.id) + filing_msg = FilingMessage(filing_identifier=filing.id) + + # First attempt: Processes successfully and commits (triggering DB updates) + process_filing(filing_msg) + + # Verify status changed to completed in DB + completed_filing = Filing.find_by_id(filing.id) + assert completed_filing.status == Filing.Status.COMPLETED.value + + # Second attempt (Duplicate queue message): + # This verifies that db.session.query(Filing).with_for_update() executes successfully against Postgres + # and properly halts execution by returning None, None when status is COMPLETED. + res1, res2 = process_filing(filing_msg) + + assert res1 is None + assert res2 is None + + +def test_process_filing_locked(app, session, mocker): + """Assert that process_filing propagates OperationalError when the row is locked by another transaction.""" + import time + import concurrent + from sqlalchemy.exc import OperationalError + + filing_json = copy.deepcopy(FILING_HEADER) + filing_json['filing']['header']['name'] = 'registration' + filing_json['filing']['registration'] = copy.deepcopy(REGISTRATION) + filing_json['filing']['registration']['startDate'] = '2026-03-19' + + filing = create_filing('123', filing_json) + filing_msg = FilingMessage(filing_identifier=filing.id) + + def mock_get_next_corp_num(legal_type): + time.sleep(3) # simulate delay + return "FM1234567" + + mocker.patch( + 'business_filer.filing_processors.filing_components.business_info.get_next_corp_num', + side_effect=mock_get_next_corp_num + ) + + from business_filer.common.services import NaicsService + naics_response = { + 'code': REGISTRATION['business']['naics']['naicsCode'], + 'naicsKey': 'a4667c26-d639-42fa-8af3-7ec73e392569' + } + mocker.patch.object(NaicsService, 'find_by_code', return_value=naics_response) + + mocker.patch('business_filer.filing_processors.filing_components.business_profile.update_business_profile', return_value=None) + mocker.patch('business_filer.filing_processors.filing_components.business_profile.update_affiliation', return_value=None) + + + def run_process(): + with app.app_context(): + try: + process_filing(filing_msg) + return "SUCCESS" + except Exception: + import traceback + return traceback.format_exc() + + with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor: + f1 = executor.submit(run_process) + f2 = executor.submit(run_process) + + res1 = f1.result() + res2 = f2.result() + + results = [res1, res2] + + success_count = sum(1 for r in results if r == "SUCCESS") + error_count = sum(1 for r in results if "OperationalError" in r) + + assert success_count == 1, f"Expected exactly one thread to succeed, got: {results}" + assert error_count == 1, f"Expected exactly one thread to hit lock timeout (OperationalError), got: {results}" From 11b9baaa40211b629d4d38ca1dea3fa60cadc17e Mon Sep 17 00:00:00 2001 From: meawong Date: Tue, 24 Mar 2026 11:10:36 -0700 Subject: [PATCH 43/46] 31834 - Remove Certified By Statement from Outputs for Corps (#4196) * 31834 - Update outputs that use the certified by statement * 31834 - Uncomment code * 31834 - Move isCorp check to certification.html and add unit tests --- .../template-parts/certification.html | 2 ++ legal-api/src/legal_api/reports/report.py | 11 ++++++ legal-api/tests/unit/reports/test_report.py | 35 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/legal-api/report-templates/template-parts/certification.html b/legal-api/report-templates/template-parts/certification.html index 4000310662..3b747b396c 100644 --- a/legal-api/report-templates/template-parts/certification.html +++ b/legal-api/report-templates/template-parts/certification.html @@ -1,3 +1,4 @@ +{% if not business.isCorp %}
Certification
@@ -12,3 +13,4 @@ {% endif %}
+{% endif %} diff --git a/legal-api/src/legal_api/reports/report.py b/legal-api/src/legal_api/reports/report.py index 58f5c631cb..00125e38b2 100644 --- a/legal-api/src/legal_api/reports/report.py +++ b/legal-api/src/legal_api/reports/report.py @@ -289,6 +289,7 @@ def _get_template_data(self): self._set_meta_info(filing) self._set_registrar_info(filing) self._set_completing_party(filing) + self._set_corp_flag(filing) filing["enable_new_ben_statements"] = flags.is_on("enable-new-ben-statements") filing["enable_sandbox"] = flags.is_on("enable-sandbox") @@ -359,6 +360,16 @@ def _handle_special_resolution_filing_data(self, filing): elif self._report_key == "specialResolutionApplication": self._format_special_resolution_application(filing, "alteration") + def _set_corp_flag(self, filing): + """Set a flag indicating whether the entity is a corporation.""" + if self._business: + legal_type = self._business.legal_type + else: + filing_json = self._filing.filing_json.get("filing", {}) + filing_type = self._filing.filing_type + legal_type = filing_json.get(filing_type, {}).get("nameRequest", {}).get("legalType") + filing["business"]["isCorp"] = legal_type in Business.CORPS + def _set_completing_party(self, filing): completing_party_role = PartyRole.get_party_roles_by_filing( self._filing.id, datetime.utcnow(), PartyRole.RoleTypes.COMPLETING_PARTY.value) diff --git a/legal-api/tests/unit/reports/test_report.py b/legal-api/tests/unit/reports/test_report.py index 0ed0f8cd89..11d6e9b882 100644 --- a/legal-api/tests/unit/reports/test_report.py +++ b/legal-api/tests/unit/reports/test_report.py @@ -415,3 +415,38 @@ def test_document_service_not_create_document(session, mock_doc_service, mocker) assert False except BusinessException as err: assert err.status_code == HTTPStatus.NOT_FOUND + +@pytest.mark.parametrize( + 'test_name, identifier, entity_type, expected_is_corp', + [ + # Corporation types - should be True + ('BC corp', 'BC1234567', 'BC', True), + ('BEN corp', 'BC1234567', 'BEN', True), + ('CC corp', 'BC1234567', 'CC', True), + ('ULC corp', 'BC1234567', 'ULC', True), + ('C continuation', 'C1234567', 'C', True), + ('CBEN continuation', 'C1234567', 'CBEN', True), + ('CCC continuation', 'C1234567', 'CCC', True), + ('CUL continuation', 'C1234567', 'CUL', True), + # Non-corporation types - should be False + ('CP cooperative', 'CP1234567', 'CP', False), + ('SP sole prop', 'FM1234567', 'SP', False), + ('GP partnership', 'FM1234567', 'GP', False), + ] +) +def test_set_corp_flag(session, test_name, identifier, entity_type, expected_is_corp): + """Assert _set_corp_flag correctly identifies corporation vs non-corporation types.""" + report = create_report( + identifier=identifier, + entity_type=entity_type, + report_type='annualReport', + filing_type='annualReport', + template=ANNUAL_REPORT + ) + report._populate_business_info_to_filing(report._filing, report._business) + + filing = report._filing.filing_json['filing'] + report._set_corp_flag(filing) + + assert filing['business']['isCorp'] == expected_is_corp, \ + f'{test_name}: expected isCorp={expected_is_corp} for legalType={entity_type}' From bf4dcd80f25da2ae54386b1eb7a3f2f2c6147fc0 Mon Sep 17 00:00:00 2001 From: Vysakh Menon Date: Tue, 24 Mar 2026 11:11:04 -0700 Subject: [PATCH 44/46] 31832 colin sync - use filed by instead of certify by for dissolution submitting party (#4197) --- colin-api/src/colin_api/models/filing.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/colin-api/src/colin_api/models/filing.py b/colin-api/src/colin_api/models/filing.py index 5d48055741..4aee7f2140 100644 --- a/colin-api/src/colin_api/models/filing.py +++ b/colin-api/src/colin_api/models/filing.py @@ -928,17 +928,24 @@ def _create_submitting_party(cls, cursor, filing, corp_num): mailing_addr_id = Address.create_new_address( cursor=cursor, address_info=mailing_address, corp_num=corp_num) + if filed_by := filing.header.get('filedBy'): + last_name = filed_by.get('lastName') + first_name = filed_by.get('firstName') + if not first_name and not last_name: + last_name = filed_by.get('userName') # if no names, use userName for last name + submitting_party_query = \ """ - insert into submitting_party (event_id, mailing_addr_id, last_nme) - values (:event_id, :mailing_addr_id, :last_nme) + insert into submitting_party (event_id, mailing_addr_id, last_nme, first_nme) + values (:event_id, :mailing_addr_id, :last_nme, :first_nme) """ cursor.execute( submitting_party_query, event_id=filing.event_id, mailing_addr_id=mailing_addr_id, - last_nme=filing.get_certified_by()[:20] + last_nme=last_name[:20], + first_nme=first_name[:20] ) # pylint: disable=too-many-branches, too-many-locals, too-many-statements, too-many-nested-blocks; From 7c1e2c1110ff36bdcdf2c5116f5cbd8040e7c6f5 Mon Sep 17 00:00:00 2001 From: Argus Chiu Date: Tue, 24 Mar 2026 11:38:07 -0700 Subject: [PATCH 45/46] 32908 Fix COLIN extract refresh script edge case to delete orphaned data (#4198) --- .../scripts/README_COLIN_Corps_Extract.md | 4 ++ .../scripts/generate_cprd_subset_extract.py | 49 +++++++++++++++ .../subset_pg_acquire_advisory_lock.sql | 7 +++ .../subset_pg_cleanup_orphan_children.sql | 62 +++++++++++++++++++ .../subset_pg_release_advisory_lock.sql | 7 +++ data-tool/scripts/transfer_cprd_corps.sql | 12 ++++ 6 files changed, 141 insertions(+) create mode 100644 data-tool/scripts/subset/subset_pg_acquire_advisory_lock.sql create mode 100644 data-tool/scripts/subset/subset_pg_cleanup_orphan_children.sql create mode 100644 data-tool/scripts/subset/subset_pg_release_advisory_lock.sql diff --git a/data-tool/scripts/README_COLIN_Corps_Extract.md b/data-tool/scripts/README_COLIN_Corps_Extract.md index 77cec3ee42..6d6e483531 100644 --- a/data-tool/scripts/README_COLIN_Corps_Extract.md +++ b/data-tool/scripts/README_COLIN_Corps_Extract.md @@ -151,6 +151,8 @@ Core templates (always used in subset workflow): - `data-tool/scripts/subset/subset_enable_triggers.sql` - `data-tool/scripts/subset/subset_delete_chunk.sql` - `data-tool/scripts/subset/subset_transfer_chunk.sql` +- `data-tool/scripts/subset/subset_pg_acquire_advisory_lock.sql` / `subset_pg_release_advisory_lock.sql` (serialize subset runs per target DB) +- `data-tool/scripts/subset/subset_pg_cleanup_orphan_children.sql` (refresh-only orphan child cleanup before chunked deletes) - `data-tool/scripts/subset/subset_pg_boolean_casts.sql` (required so DbSchemaCLI can insert 't'/'f' into boolean columns) - `data-tool/scripts/subset/subset_pg_purge_bcomps_excluded.sql` (run once after transfer) @@ -206,10 +208,12 @@ Generated scripts (gitignored by default): - `--pg-disable-method` currently accepts only `table_triggers` and `replica_role`. - The actual generator default is `table_triggers`, not `replica_role`. - In refresh mode, preserved rows in `corp_processing`, `auth_processing`, `affiliation_processing`, and `colin_tracking` still reference `corporation` / `event`, so FK enforcement must stay suppressed across delete/reload. The generator now adds refresh-only trigger suppression for those preserved FK-owning tables when `--pg-disable-method table_triggers` is used. + - Subset load/refresh scripts now acquire a session-level Postgres advisory lock at the start and release it at the end so overlapping subset runs on the same target DB serialize instead of interleaving. The same lock key is also used by the full refresh script. - `table_triggers` changes table trigger state globally while the refresh runs, so use it against a quiesced/disposable extract DB and with a role that can disable the relevant triggers. - If you use `replica_role`, remember it is session-local. If FK errors still occur, verify `current_setting('session_replication_role')` inside the nested delete/purge scripts being executed by DbSchemaCLI. - `address` is treated as a shared/global table during subset refresh/load. The generator now prepares a helper staging table, transfers incoming Oracle addresses into it, and merges them into `public.address` by `addr_id` instead of deleting/reinserting address rows directly. The address extract also includes `notification_resend` references. - The helper stage table is `public.subset_address_stage`. Do not overlap subset runs against the same target DB, and ensure the runtime role can create/drop that helper table. + - Refresh mode now pre-cleans orphan event/corp-party child rows that can survive the parent-keyed delete phase from earlier failed/interleaved runs (for example a stale `filing` row whose parent `event` row is missing in target). - For diagnostics, add `--pg-debug-session-probes` (inline mode only). The generated SQL will print `pg_backend_pid()`, `current_user`, and `current_setting('session_replication_role')` in the master script and nested execute files so you can verify the master/nested `execute` session context during refresh. This does not directly instrument any separate DbSchemaCLI transfer writer sessions. 3. Run the generated main script with DbSchemaCLI: diff --git a/data-tool/scripts/generate_cprd_subset_extract.py b/data-tool/scripts/generate_cprd_subset_extract.py index ff932e2977..64e19632ea 100644 --- a/data-tool/scripts/generate_cprd_subset_extract.py +++ b/data-tool/scripts/generate_cprd_subset_extract.py @@ -107,8 +107,11 @@ class tmpl_TemplateSpec: @dataclass(frozen=True) class tmpl_TemplateBundle: + pg_acquire_advisory_lock: tmpl_TemplateSpec + pg_release_advisory_lock: tmpl_TemplateSpec pg_prepare_address_stage: tmpl_TemplateSpec pg_cleanup_address_stage: tmpl_TemplateSpec + pg_cleanup_orphan_children: tmpl_TemplateSpec disable_triggers: tmpl_TemplateSpec enable_triggers: tmpl_TemplateSpec pg_boolean_casts: tmpl_TemplateSpec @@ -283,6 +286,14 @@ def sql_render_pg_session_probe(label: str) -> str: def tmpl_default_bundle(repo_root: Path) -> tmpl_TemplateBundle: subset_dir = repo_root / "data-tool" / "scripts" / "subset" + pg_acquire_advisory_lock = tmpl_TemplateSpec( + name="subset_pg_acquire_advisory_lock", + path=subset_dir / "subset_pg_acquire_advisory_lock.sql", + ) + pg_release_advisory_lock = tmpl_TemplateSpec( + name="subset_pg_release_advisory_lock", + path=subset_dir / "subset_pg_release_advisory_lock.sql", + ) pg_prepare_address_stage = tmpl_TemplateSpec( name="subset_pg_prepare_address_stage", path=subset_dir / "subset_pg_prepare_address_stage.sql", @@ -291,6 +302,10 @@ def tmpl_default_bundle(repo_root: Path) -> tmpl_TemplateBundle: name="subset_pg_cleanup_address_stage", path=subset_dir / "subset_pg_cleanup_address_stage.sql", ) + pg_cleanup_orphan_children = tmpl_TemplateSpec( + name="subset_pg_cleanup_orphan_children", + path=subset_dir / "subset_pg_cleanup_orphan_children.sql", + ) disable_triggers = tmpl_TemplateSpec( name="subset_disable_triggers", path=subset_dir / "subset_disable_triggers.sql", @@ -335,8 +350,11 @@ def tmpl_default_bundle(repo_root: Path) -> tmpl_TemplateBundle: ) return tmpl_TemplateBundle( + pg_acquire_advisory_lock=pg_acquire_advisory_lock, + pg_release_advisory_lock=pg_release_advisory_lock, pg_prepare_address_stage=pg_prepare_address_stage, pg_cleanup_address_stage=pg_cleanup_address_stage, + pg_cleanup_orphan_children=pg_cleanup_orphan_children, disable_triggers=disable_triggers, enable_triggers=enable_triggers, pg_boolean_casts=pg_boolean_casts, @@ -604,6 +622,9 @@ def gen_build_master_script_inline( lines.append("vset format.timestamp=YYYY-MM-dd'T'hh:mm:ss'Z'") lines.append("") lines.append(f"connect {cfg.target_connection};") + lines.append("-- Serialize subset runs on this target DB.") + lines.append(f"execute {templates.pg_acquire_advisory_lock.path.as_posix()}") + lines.append("") lines.append("-- Prepare shared address staging table before learning schema") lines.append(f"execute {templates.pg_prepare_address_stage.path.as_posix()}") lines.append(f"learn schema {cfg.target_schema};") @@ -620,6 +641,10 @@ def gen_build_master_script_inline( lines.append("select 't'::varchar::boolean;") lines.append("select 'f'::bpchar::boolean;") lines.append("") + if cfg.mode == cfg_GenerationMode.REFRESH: + lines.append("-- Cleanup stale orphan child rows before entering the trigger-suppressed refresh window.") + lines.append(f"execute {templates.pg_cleanup_orphan_children.path.as_posix()}") + lines.append("") _gen_emit_pg_disable_begin(lines, cfg=cfg, templates=templates) _gen_emit_refresh_fk_note(lines, cfg=cfg) @@ -658,6 +683,9 @@ def gen_build_master_script_inline( lines.append("-- Cleanup shared address staging table") lines.append(f"execute {templates.pg_cleanup_address_stage.path.as_posix()}") lines.append("") + lines.append("-- Release subset-run advisory lock") + lines.append(f"execute {templates.pg_release_advisory_lock.path.as_posix()}") + lines.append("") if cfg.pg_fastload: lines.append("-- Reset Postgres fast-load session settings") @@ -693,6 +721,9 @@ def gen_build_master_script_vset( lines.append("vset format.timestamp=YYYY-MM-dd'T'hh:mm:ss'Z'") lines.append("") lines.append(f"connect {cfg.target_connection};") + lines.append("-- Serialize subset runs on this target DB.") + lines.append(f"execute {templates.pg_acquire_advisory_lock.path.as_posix()}") + lines.append("") lines.append("-- Prepare shared address staging table before learning schema") lines.append(f"execute {templates.pg_prepare_address_stage.path.as_posix()}") lines.append(f"learn schema {cfg.target_schema};") @@ -709,9 +740,17 @@ def gen_build_master_script_vset( lines.append("select 't'::varchar::boolean;") lines.append("select 'f'::bpchar::boolean;") lines.append("") + if cfg.mode == cfg_GenerationMode.REFRESH: + lines.append("-- Cleanup stale orphan child rows before entering the trigger-suppressed refresh window.") + lines.append(f"execute {templates.pg_cleanup_orphan_children.path.as_posix()}") + lines.append("") _gen_emit_pg_disable_begin(lines, cfg=cfg, templates=templates) _gen_emit_refresh_fk_note(lines, cfg=cfg) + if cfg.mode == cfg_GenerationMode.REFRESH: + lines.append("-- Cleanup stale orphan child rows before chunked refresh deletes.") + lines.append(f"execute {templates.pg_cleanup_orphan_children.path.as_posix()}") + lines.append("") if cfg.include_cars: lines.append("-- global cars* refresh (not corp-scoped; full dataset truncate + reload)") @@ -772,6 +811,9 @@ def _vset_predicates(target_values: Sequence[str], oracle_values: Sequence[str]) lines.append("-- Cleanup shared address staging table") lines.append(f"execute {templates.pg_cleanup_address_stage.path.as_posix()}") lines.append("") + lines.append("-- Release subset-run advisory lock") + lines.append(f"execute {templates.pg_release_advisory_lock.path.as_posix()}") + lines.append("") if cfg.pg_fastload: lines.append("-- Reset Postgres fast-load session settings") @@ -968,8 +1010,11 @@ def run(cfg: cfg_GenerationConfig) -> int: # Ensure the execute-only templates exist too (even though we don't render them). for spec in ( + templates.pg_acquire_advisory_lock, + templates.pg_release_advisory_lock, templates.pg_prepare_address_stage, templates.pg_cleanup_address_stage, + templates.pg_cleanup_orphan_children, templates.disable_triggers, templates.enable_triggers, templates.pg_boolean_casts, @@ -1092,6 +1137,7 @@ def run(cfg: cfg_GenerationConfig) -> int: print(" - cars* tables will NOT be refreshed (--no-cars was set).") print(f" - Postgres fast-load session settings: {'ENABLED' if cfg.pg_fastload else 'disabled'} (--pg-fastload)") print(f" - Postgres trigger suppression: {cfg.pg_disable_method.value} (--pg-disable-method)") + print(" - subset runs acquire a session-level advisory lock on the target DB to prevent overlap.") print(" - Address loads use shared staging table public.subset_address_stage and merge into public.address by addr_id.") print(" - subset runs should not overlap on the same target DB, and the runtime role must be able to create/drop that helper table.") if cfg.pg_debug_session_probes: @@ -1100,6 +1146,7 @@ def run(cfg: cfg_GenerationConfig) -> int: print(" - probes confirm master/nested execute session context; DbSchemaCLI transfer writer sessions may still differ.") if cfg.mode == cfg_GenerationMode.REFRESH: print(" - refresh preserves processing/tracking rows while deleting/reloading corporation/event rows.") + print(" - refresh also pre-cleans orphan event/corp-party child rows before entering the trigger-suppressed window.") if cfg.pg_disable_method == cfg_PgDisableMethod.TABLE_TRIGGERS: print(" - table_triggers mode adds refresh-only trigger suppression for the preserved FK-owning tables.") print(" - table_triggers changes table state globally while the refresh runs; use it against a quiesced extract DB with sufficient privileges.") @@ -1143,6 +1190,7 @@ def run(cfg: cfg_GenerationConfig) -> int: print(" - Prefer --render-mode inline for faster runs (inline generates static SQL per chunk).") print(f" - Postgres fast-load session settings: {'ENABLED' if cfg.pg_fastload else 'disabled'} (--pg-fastload)") print(f" - Postgres trigger suppression: {cfg.pg_disable_method.value} (--pg-disable-method)") + print(" - subset runs acquire a session-level advisory lock on the target DB to prevent overlap.") print(" - Address loads use shared staging table public.subset_address_stage and merge into public.address by addr_id.") print(" - subset runs should not overlap on the same target DB, and the runtime role must be able to create/drop that helper table.") if cfg.pg_debug_session_probes: @@ -1151,6 +1199,7 @@ def run(cfg: cfg_GenerationConfig) -> int: print(" - probes confirm master/nested execute session context; DbSchemaCLI transfer writer sessions may still differ.") if cfg.mode == cfg_GenerationMode.REFRESH: print(" - refresh preserves processing/tracking rows while deleting/reloading corporation/event rows.") + print(" - refresh also pre-cleans orphan event/corp-party child rows that can survive parent-keyed deletes.") if cfg.pg_disable_method == cfg_PgDisableMethod.TABLE_TRIGGERS: print(" - table_triggers mode adds refresh-only trigger suppression for the preserved FK-owning tables.") print(" - table_triggers changes table state globally while the refresh runs; use it against a quiesced extract DB with sufficient privileges.") diff --git a/data-tool/scripts/subset/subset_pg_acquire_advisory_lock.sql b/data-tool/scripts/subset/subset_pg_acquire_advisory_lock.sql new file mode 100644 index 0000000000..6f4d3c400e --- /dev/null +++ b/data-tool/scripts/subset/subset_pg_acquire_advisory_lock.sql @@ -0,0 +1,7 @@ +-- Acquire a session-level advisory lock so subset load/refresh runs do not overlap on the same target DB. +-- Advisory locks are scoped to the current Postgres database, so the same keys are safe across separate DBs. + +SELECT pg_advisory_lock( + hashtext('lear:data-tool:colin_subset_extract'), + hashtext('subset_run') +); diff --git a/data-tool/scripts/subset/subset_pg_cleanup_orphan_children.sql b/data-tool/scripts/subset/subset_pg_cleanup_orphan_children.sql new file mode 100644 index 0000000000..345a6e55e1 --- /dev/null +++ b/data-tool/scripts/subset/subset_pg_cleanup_orphan_children.sql @@ -0,0 +1,62 @@ +-- Cleanup stale child rows that no longer have the parent rows used by refresh-mode deletes. +-- +-- Why this exists: +-- - refresh-mode chunk deletes remove event-scoped rows by first looking up event_id in target `event` +-- - and remove corp-party child rows by first looking up corp_party_id in target `corp_party` +-- - so a prior failed/interleaved run can leave stale child rows behind when the parent row is missing +-- - those orphans can then collide with the next reload (for example, unique `filing.event_id`) +-- +-- This cleanup is intentionally narrow: +-- - only rows whose normal refresh delete path traverses a parent lookup are removed here +-- - corp-scoped rows deleted directly by corp_num are left to the regular chunk deletes + +-- Event-scoped children whose parent event row is missing. +DELETE FROM notification_resend t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM notification t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM filing_user t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM payment t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM ledger_text t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM conv_ledger t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM conv_event t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM completing_party t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM submitting_party t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM corp_involved_amalgamating t +WHERE t.event_id IS NOT NULL + AND NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM corp_involved_cont_in t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM correction t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +DELETE FROM filing t +WHERE NOT EXISTS (SELECT 1 FROM event e WHERE e.event_id = t.event_id); + +-- Corp-party children whose parent corp_party row is missing. +DELETE FROM party_notification t +WHERE NOT EXISTS (SELECT 1 FROM corp_party cp WHERE cp.corp_party_id = t.party_id); + +DELETE FROM offices_held t +WHERE NOT EXISTS (SELECT 1 FROM corp_party cp WHERE cp.corp_party_id = t.corp_party_id); + +DELETE FROM corp_party_relationship t +WHERE NOT EXISTS (SELECT 1 FROM corp_party cp WHERE cp.corp_party_id = t.corp_party_id); diff --git a/data-tool/scripts/subset/subset_pg_release_advisory_lock.sql b/data-tool/scripts/subset/subset_pg_release_advisory_lock.sql new file mode 100644 index 0000000000..180211c0b0 --- /dev/null +++ b/data-tool/scripts/subset/subset_pg_release_advisory_lock.sql @@ -0,0 +1,7 @@ +-- Release the session-level advisory lock used to serialize subset load/refresh runs. +-- If the session ends unexpectedly, Postgres releases the advisory lock automatically. + +SELECT pg_advisory_unlock( + hashtext('lear:data-tool:colin_subset_extract'), + hashtext('subset_run') +); diff --git a/data-tool/scripts/transfer_cprd_corps.sql b/data-tool/scripts/transfer_cprd_corps.sql index a72081e8e2..9bcdb402cd 100644 --- a/data-tool/scripts/transfer_cprd_corps.sql +++ b/data-tool/scripts/transfer_cprd_corps.sql @@ -5,6 +5,12 @@ vset format.timestamp=YYYY-MM-dd'T'hh:mm:ss'Z' connect cprd_pg; +-- Serialize COLIN extract refreshes on this target DB so subset/full runs cannot overlap. +SELECT pg_advisory_lock( + hashtext('lear:data-tool:colin_subset_extract'), + hashtext('subset_run') +); + learn schema public; @@ -1524,3 +1530,9 @@ ALTER TABLE share_struct_cls ENABLE TRIGGER ALL; ALTER TABLE notification ENABLE TRIGGER ALL; ALTER TABLE notification_resend ENABLE TRIGGER ALL; ALTER TABLE party_notification ENABLE TRIGGER ALL; + +-- Release COLIN extract refresh advisory lock. +SELECT pg_advisory_unlock( + hashtext('lear:data-tool:colin_subset_extract'), + hashtext('subset_run') +); From bf4dc62ae701a7029c850c108cab40987bb3cf14 Mon Sep 17 00:00:00 2001 From: ketaki-deodhar Date: Thu, 26 Mar 2026 09:14:22 -0700 Subject: [PATCH 46/46] update 1 --- .../migration_status_tracking.ipynb | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/data-tool/notebooks/corps_onboarding_process_flow/migration_status_tracking.ipynb b/data-tool/notebooks/corps_onboarding_process_flow/migration_status_tracking.ipynb index 12df02799f..fe86b411cf 100644 --- a/data-tool/notebooks/corps_onboarding_process_flow/migration_status_tracking.ipynb +++ b/data-tool/notebooks/corps_onboarding_process_flow/migration_status_tracking.ipynb @@ -23,7 +23,9 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ "%pip install pandas\n", "%pip install sqlalchemy>=2.0\n", @@ -31,9 +33,7 @@ "%pip install dotenv\n", "%pip install psycopg2-binary\n", "%pip install openpyxl" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -45,8 +45,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "import oracledb\n", "import os\n", @@ -152,9 +154,7 @@ " raise ValueError(\"DATABASE_COLIN_ORACLE_SCHEMA is not set.\")\n", "\n", "print(\"Libraries imported and configuration loaded successfully.\")" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -167,7 +167,9 @@ }, { "cell_type": "code", + "execution_count": null, "metadata": {}, + "outputs": [], "source": [ "DATABASE_CONFIG = {\n", " 'colin_extract': {\n", @@ -218,9 +220,7 @@ " DATABASE_CONFIG[db_key] = {'uri': uri}\n", "\n", "print(\"Database configurations successfully.\")\n" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -232,8 +232,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "oracledb.init_oracle_client()\n", "\n", @@ -266,9 +268,7 @@ "ENGINE_NAMES = {engine: key for key, engine in engines.items()}\n", "\n", "print(\"All database engines ready for use.\")\n" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -280,8 +280,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "colin_extract_query = f\"\"\"\n", "SELECT\n", @@ -304,7 +306,7 @@ "LEFT JOIN corporation c ON mcb.corp_num = c.corp_num\n", "LEFT JOIN corp_processing cp\n", " ON mcb.corp_num = cp.corp_num\n", - " AND cp.environment = 'prod'\n", + " AND cp.environment = 'test'\n", "LEFT JOIN corp_name cn ON c.corp_num = cn.corp_num\n", " AND cn.corp_name_typ_cd IN ('CO', 'NB')\n", " AND cn.end_event_id IS NULL\n", @@ -328,9 +330,7 @@ "# Display results\n", "with pd.option_context('display.max_rows', None):\n", " display(colin_extract_df)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -341,8 +341,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "def batch_query(query_sql, db_engine, batch_size, columns, is_colin_oracle=False, additional_params=None, dedup=True):\n", " # Get unique corporation numbers from the dataset\n", @@ -401,9 +403,7 @@ " print(f\"No records fetched\")\n", "\n", " return combined_df" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -415,8 +415,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "lear_combined_query = f\"\"\"\n", "SELECT\n", @@ -445,13 +447,13 @@ "# Display results\n", "with pd.option_context('display.max_rows', None):\n", " display(lear_combined_df)" - ], - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "def validate_and_fill_missing_data(colin_extract_df, source_df, source_name, default_values):\n", " \"\"\"Data validation and default value filling method\"\"\"\n", @@ -480,9 +482,7 @@ " source_df = pd.concat([source_df, missing_df], ignore_index=True)\n", "\n", " return source_df" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -494,8 +494,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "auth_query = f\"\"\"\n", "SELECT\n", @@ -545,9 +547,7 @@ "# Display results\n", "with pd.option_context('display.max_rows', None):\n", " display(auth_combined_df)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -559,8 +559,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "colin_oracle_query = f\"\"\"\n", "SELECT\n", @@ -599,9 +601,7 @@ "# Display results\n", "with pd.option_context('display.max_rows', None):\n", " display(colin_oracle_combined_df)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -611,8 +611,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "# Get colin_event_ids and file_keys for migrated corps from LEAR\n", "lear_colin_event_detail_query = f\"\"\"\n", @@ -641,13 +643,13 @@ "# Display LEAR query results\n", "with pd.option_context('display.max_rows', None):\n", " display(lear_colin_event_detail_df)" - ], - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "# Query DRS, match both entity_id and event_id\n", "drs_detail_query = f\"\"\"\n", @@ -680,13 +682,13 @@ "else:\n", " drs_detail_df = pd.DataFrame(columns=[COLUMN_NAMES['corp_num'], 'event_id', 'document_service_id'])\n", " print(\"No colin event data found in LEAR, skipping DRS query.\")" - ], - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "def calculate_legacy_status_logic(corp_num, corp_colin_event_lear, corp_drs):\n", " # DRS legacy outputs status calculation\n", @@ -747,13 +749,13 @@ " lear_status = FLAG_STATUS['NO']\n", "\n", " return drs_status, lear_status" - ], - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "def calculate_corp_legacy_outputs_status():\n", " processed_corps = colin_extract_df[\n", @@ -824,9 +826,7 @@ "print(f\"Generated legacy outputs status for {len(legacy_outputs_df)} corporations.\")\n", "with pd.option_context('display.max_rows', None):\n", " display(legacy_outputs_df)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -838,8 +838,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "try:\n", " result = (colin_extract_df\n", @@ -868,9 +870,7 @@ "# Display merged results\n", "with pd.option_context('display.max_rows', None):\n", " display(merged_df)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -882,8 +882,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "batch_summary_query = f\"\"\"\n", "WITH batch_status AS (\n", @@ -947,9 +949,7 @@ "\n", "with pd.option_context('display.max_rows', None):\n", " display(batch_summary_df)" - ], - "outputs": [], - "execution_count": null + ] }, { "cell_type": "markdown", @@ -961,8 +961,10 @@ ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "# Define highlighting rules for Migration Status tab\n", "MIGRATION_STATUS_HIGHLIGHTING_RULES = [\n", @@ -1037,13 +1039,13 @@ " ]\n", " }\n", "]" - ], - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "from openpyxl.styles import Font, PatternFill, Alignment\n", "\n", @@ -1160,13 +1162,13 @@ "\n", " adjusted_width = min(max_length + 12, CONFIG['excel_export']['max_column_width'])\n", " worksheet.column_dimensions[column_letter].width = adjusted_width\n" - ], - "outputs": [], - "execution_count": null + ] }, { - "metadata": {}, "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], "source": [ "if merged_df.empty:\n", " raise ValueError(\"Data is empty, cannot export\")\n", @@ -1215,14 +1217,12 @@ "except Exception as e:\n", " print(f\"Excel export failed: {e}\")\n", " raise" - ], - "outputs": [], - "execution_count": null + ] } ], "metadata": { "kernelspec": { - "display_name": "3.8.17", + "display_name": "venv (3.11.15)", "language": "python", "name": "python3" }, @@ -1236,7 +1236,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.17" + "version": "3.11.15" } }, "nbformat": 4,