1
0
mirror of synced 2025-12-25 02:09:19 -05:00

Source Zendesk Support: migration to low-code (#56354)

This commit is contained in:
Maxime Carbonneau-Leclerc
2025-03-25 08:52:19 -04:00
committed by GitHub
parent 432f6d2c03
commit c35002acd2
48 changed files with 1867 additions and 2950 deletions

View File

@@ -32,6 +32,8 @@ acceptance_tests:
bypass_reason: "not available in current subscription plan"
- name: "post_comment_votes"
bypass_reason: "not available in current subscription plan"
- name: "ticket_activities"
bypass_reason: "There is a retention period which requires too much maintenance"
- name: "tags"
bypass_reason: "API issue" # TODO: remove this after all changes being merged
incremental:

View File

@@ -32,7 +32,6 @@
{"stream": "satisfaction_ratings", "data": {"url": "https://d3v-airbyte.zendesk.com/api/v2/satisfaction_ratings/7235633102607.json", "id": 7235633102607, "assignee_id": null, "group_id": null, "requester_id": 361089721035, "ticket_id": 146, "score": "offered", "created_at": "2023-06-19T18:01:40Z", "updated_at": "2023-06-19T18:01:40Z", "comment": null}, "emitted_at": 1720179592962}
{"stream": "satisfaction_ratings", "data": {"url": "https://d3v-airbyte.zendesk.com/api/v2/satisfaction_ratings/8178725484175.json", "id": 8178725484175, "assignee_id": null, "group_id": null, "requester_id": 8178212241935, "ticket_id": 158, "score": "offered", "created_at": "2023-10-20T12:01:58Z", "updated_at": "2023-10-20T12:01:58Z", "comment": null}, "emitted_at": 1720179592971}
{"stream": "satisfaction_ratings", "data": {"url": "https://d3v-airbyte.zendesk.com/api/v2/satisfaction_ratings/9862120719631.json", "id": 9862120719631, "assignee_id": null, "group_id": null, "requester_id": 9861847678735, "ticket_id": 161, "score": "offered", "created_at": "2024-05-28T21:01:33Z", "updated_at": "2024-05-28T21:01:33Z", "comment": null}, "emitted_at": 1720179592979}
{"stream":"ticket_activities","data":{"url":"https://d3v-airbyte.zendesk.com/api/v2/activities/11836244663055.json","id":11836244663055,"title":"Danylo commented on ticket #160: I hope so!.","verb":"tickets.comment","user_id":360786799676,"actor_id":9515132940047,"updated_at":"2025-01-29T18:21:22Z","created_at":"2025-01-29T18:21:22Z","object":{"comment":{"value":"I hope so!","public":true}},"target":{"ticket":{"id":160,"subject":"Stream filling request"}},"user":{"id":360786799676,"url":"https://d3v-airbyte.zendesk.com/api/v2/users/360786799676.json","name":"Team Airbyte","email":"integration-test@airbyte.io","created_at":"2020-11-17T23:55:24Z","updated_at":"2025-01-31T21:22:08Z","time_zone":"Pacific/Noumea","iana_time_zone":"Pacific/Noumea","phone":null,"shared_phone_number":null,"photo":{"url":"https://d3v-airbyte.zendesk.com/api/v2/attachments/7282857066895.json","id":7282857066895,"file_name":"Airbyte_logo_220x220.png","content_url":"https://d3v-airbyte.zendesk.com/system/photos/7282857066895/Airbyte_logo_220x220.png","mapped_content_url":"https://d3v-airbyte.zendesk.com/system/photos/7282857066895/Airbyte_logo_220x220.png","content_type":"image/png","size":5442,"width":80,"height":80,"inline":false,"deleted":false,"thumbnails":[{"url":"https://d3v-airbyte.zendesk.com/api/v2/attachments/7282824912911.json","id":7282824912911,"file_name":"Airbyte_logo_220x220_thumb.png","content_url":"https://d3v-airbyte.zendesk.com/system/photos/7282857066895/Airbyte_logo_220x220_thumb.png","mapped_content_url":"https://d3v-airbyte.zendesk.com/system/photos/7282857066895/Airbyte_logo_220x220_thumb.png","content_type":"image/png","size":1422,"width":32,"height":32,"inline":false,"deleted":false}]},"locale_id":1,"locale":"en-US","organization_id":360033549136,"role":"admin","verified":true,"external_id":null,"tags":[],"alias":"Team Airbyte","active":true,"shared":false,"shared_agent":false,"last_login_at":"2025-01-31T21:22:08Z","two_factor_auth_enabled":null,"signature":null,"details":null,"notes":null,"role_type":4,"custom_role_id":360006308896,"moderator":true,"ticket_restriction":null,"only_private_comments":false,"restricted_agent":false,"suspended":false,"default_group_id":360003074836,"report_csv":true,"user_fields":{"test_display_name_checkbox_field":false,"test_display_name_decimal_field":null,"test_display_name_text_field":null}},"actor":{"id":9515132940047,"url":"https://d3v-airbyte.zendesk.com/api/v2/users/9515132940047.json","name":"Danylo","email":"gl_danylo.jablonski@airbyte.io","created_at":"2024-04-12T13:38:07Z","updated_at":"2024-04-12T13:38:07Z","time_zone":"Pacific/Noumea","iana_time_zone":"Pacific/Noumea","phone":null,"shared_phone_number":null,"photo":null,"locale_id":1,"locale":"en-US","organization_id":null,"role":"end-user","verified":false,"external_id":null,"tags":[],"alias":"","active":true,"shared":false,"shared_agent":false,"last_login_at":null,"two_factor_auth_enabled":null,"signature":null,"details":"","notes":"","role_type":null,"custom_role_id":null,"moderator":false,"ticket_restriction":"requested","only_private_comments":false,"restricted_agent":true,"suspended":false,"default_group_id":null,"report_csv":false,"user_fields":{"test_display_name_checkbox_field":false,"test_display_name_decimal_field":null,"test_display_name_text_field":null}}},"emitted_at":1738602991286}
{"stream": "ticket_audits", "data": {"id": 10021116193295, "ticket_id": 160, "created_at": "2024-06-19T09:49:54Z", "author_id": 9515132940047, "metadata": {"system": {"message_id": "<00a201dac22e$068cdcf0$13a696d0$@airbyte.io>", "client": "Microsoft Outlook 16.0", "email_id": "01J0QYE6SGCX3Z936BFETHRR8P", "ip_address": "024.06.19.02", "raw_email_identifier": "10414779/4fe5f3c6-857a-4560-a065-aae562a36b53.eml", "json_email_identifier": "10414779/4fe5f3c6-857a-4560-a065-aae562a36b53.json", "eml_redacted": false, "location": "Mountain View, CA, United States", "latitude": 37.3859, "longitude": -122.0882}, "custom": {}, "flags": [15], "flags_options": {"15": {"trusted": true}}, "trusted": true, "suspension_type_id": null}, "events": [{"id": 10021099820047, "type": "Comment", "author_id": 9515132940047, "body": "\n\n\n\nI hope so!", "html_body": "<div class=\"zd-comment zd-comment-pre-styled\" dir=\"auto\"><div style=\"page: WordSection1;\"><p style=\"font-size: 11.0pt; margin: 0in 0in .0001pt;\" dir=\"auto\">&nbsp;</p><p style=\"font-size: 11.0pt; margin: 0in 0in .0001pt;\" dir=\"auto\">&nbsp;</p><p style=\"font-size: 11.0pt; margin: 0in 0in .0001pt;\" dir=\"auto\">I hope so!</p><p style=\"font-size: 11.0pt; margin: 0in 0in .0001pt;\" dir=\"auto\">&nbsp;</p></div></div>", "plain_body": "&nbsp; &nbsp; I hope so! &nbsp;", "public": true, "attachments": [], "audit_id": 10021116193295}, {"id": 10021116193423, "type": "Notification", "via": {"channel": "rule", "source": {"from": {"deleted": false, "title": "Notify assignee of comment update", "id": 360011363236, "revision_id": 1}, "rel": "trigger"}}, "subject": "[{{ticket.account}}] Re: {{ticket.title}}", "body": "This ticket (#{{ticket.id}}) has been updated.\n\n{{ticket.comments_formatted}}", "recipients": [360786799676]}], "via": {"channel": "email", "source": {"from": {"address": "gl_danylo.jablonski@airbyte.io", "name": "Danylo", "original_recipients": ["gl_danylo.jablonski@airbyte.io", "support+id160@d3v-airbyte.zendesk.com"]}, "to": {"name": "Airbyte", "address": "support+id160@d3v-airbyte.zendesk.com"}, "rel": null}}}, "emitted_at": 1720179595459}
{"stream": "ticket_audits", "data": {"id": 10020996855439, "ticket_id": 160, "created_at": "2024-06-19T09:28:21Z", "author_id": 360786799676, "metadata": {"system": {"client": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", "ip_address": "45.89.90.157", "location": "Lviv, 46, Ukraine", "latitude": 49.839, "longitude": 24.0191}, "custom": {}}, "events": [{"id": 10020996855567, "type": "Change", "value": "high", "field_name": "priority", "previous_value": "normal"}], "via": {"channel": "web", "source": {"from": {}, "to": {}, "rel": null}}}, "emitted_at": 1720179595472}
{"stream": "ticket_audits", "data": {"id": 10020982311311, "ticket_id": 160, "created_at": "2024-06-19T09:27:57Z", "author_id": 360786799676, "metadata": {"system": {"client": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36", "ip_address": "45.89.90.157", "location": "Lviv, 46, Ukraine", "latitude": 49.839, "longitude": 24.0191}, "custom": {}}, "events": [{"id": 10020982311439, "type": "Change", "value": "normal", "field_name": "priority", "previous_value": "high"}], "via": {"channel": "web", "source": {"from": {}, "to": {}, "rel": null}}}, "emitted_at": 1720179595483}

View File

@@ -11,7 +11,7 @@ data:
connectorSubtype: api
connectorType: source
definitionId: 79c1aa37-dae3-42ae-b333-d1c105477715
dockerImageTag: 4.7.1
dockerImageTag: 4.8.0
dockerRepository: airbyte/source-zendesk-support
documentationUrl: https://docs.airbyte.com/integrations/sources/zendesk-support
githubIssueLabel: source-zendesk-support

View File

@@ -2,22 +2,23 @@
[[package]]
name = "airbyte-cdk"
version = "6.12.1"
version = "6.41.1"
description = "A framework for writing Airbyte Connectors."
optional = false
python-versions = "<3.13,>=3.10"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "airbyte_cdk-6.12.1-py3-none-any.whl", hash = "sha256:70e9b6e3ab76105c689dcd222a2d092af9a9d7574a31d21145f3c34514314fff"},
{file = "airbyte_cdk-6.12.1.tar.gz", hash = "sha256:3dca699c6e001fc4122a29d30a5568bfa9e625c0e6e81232006eeaeaef1e1a59"},
{file = "airbyte_cdk-6.41.1-py3-none-any.whl", hash = "sha256:1fefae879211822cab2877dc146a7911b88b6e925da690e8a5d5ae4e2fbcb579"},
{file = "airbyte_cdk-6.41.1.tar.gz", hash = "sha256:a444ccfa456e3e4753920e2b2f5454179cbd189e6ee3fef75520cedaacb819c8"},
]
[package.dependencies]
airbyte-protocol-models-dataclasses = ">=0.14,<0.15"
anyascii = ">=0.3.2,<0.4.0"
backoff = "*"
cachetools = "*"
cryptography = ">=42.0.5,<44.0.0"
cryptography = ">=44.0.0,<45.0.0"
dpath = ">=2.1.6,<3.0.0"
dunamai = ">=1.22.0,<2.0.0"
genson = "1.3.0"
@@ -29,26 +30,27 @@ langchain_core = "0.1.42"
nltk = "3.9.1"
numpy = "<2"
orjson = ">=3.10.7,<4.0.0"
packaging = "*"
pandas = "2.2.2"
pendulum = "<3.0.0"
psutil = "6.1.0"
pydantic = ">=2.7,<3.0"
pyjwt = ">=2.8.0,<3.0.0"
pyrate-limiter = ">=3.1.0,<3.2.0"
python-dateutil = "*"
python-dateutil = ">=2.9.0,<3.0.0"
python-ulid = ">=3.0.0,<4.0.0"
pytz = "2024.1"
pytz = "2024.2"
PyYAML = ">=6.0.1,<7.0.0"
rapidfuzz = ">=3.10.1,<4.0.0"
requests = "*"
requests_cache = "*"
serpyco-rs = ">=1.10.2,<2.0.0"
Unidecode = ">=1.3,<2.0"
typing-extensions = "*"
wcmatch = "10.0"
xmltodict = ">=0.13.0,<0.14.0"
whenever = ">=0.6.16,<0.7.0"
xmltodict = ">=0.13,<0.15"
[package.extras]
file-based = ["avro (>=1.11.2,<1.12.0)", "fastavro (>=1.8.0,<1.9.0)", "markdown", "pdf2image (==1.16.3)", "pdfminer.six (==20221105)", "pyarrow (>=15.0.0,<15.1.0)", "pytesseract (==0.3.10)", "python-calamine (==0.2.3)", "python-snappy (==0.7.3)", "unstructured.pytesseract (>=0.3.12)", "unstructured[docx,pptx] (==0.10.27)"]
file-based = ["avro (>=1.11.2,<1.13.0)", "fastavro (>=1.8.0,<1.9.0)", "markdown", "pdf2image (==1.16.3)", "pdfminer.six (==20221105)", "pyarrow (>=19.0.0,<20.0.0)", "pytesseract (==0.3.10)", "python-calamine (==0.2.3)", "python-snappy (==0.7.3)", "unstructured.pytesseract (>=0.3.12)", "unstructured[docx,pptx] (==0.10.27)"]
sql = ["sqlalchemy (>=2.0,!=2.0.36,<3.0)"]
vector-db-based = ["cohere (==4.21)", "langchain (==0.1.16)", "openai[embeddings] (==0.27.9)", "tiktoken (==0.8.0)"]
@@ -78,6 +80,19 @@ files = [
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyascii"
version = "0.3.2"
description = "Unicode to ASCII transliteration"
optional = false
python-versions = ">=3.3"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "anyascii-0.3.2-py3-none-any.whl", hash = "sha256:3b3beef6fc43d9036d3b0529050b0c48bfad8bc960e9e562d7223cfb94fe45d4"},
{file = "anyascii-0.3.2.tar.gz", hash = "sha256:9d5d32ef844fe225b8bc7cba7f950534fae4da27a9bf3a6bea2cb0ea46ce4730"},
]
[[package]]
name = "anyio"
version = "4.8.0"
@@ -164,15 +179,15 @@ files = [
[[package]]
name = "cachetools"
version = "5.5.1"
version = "5.5.2"
description = "Extensible memoizing collections and decorators"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"},
{file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"},
{file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"},
{file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"},
]
[[package]]
@@ -431,53 +446,61 @@ markers = {main = "python_version <= \"3.11\" and platform_system == \"Windows\"
[[package]]
name = "cryptography"
version = "43.0.3"
version = "44.0.2"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = ">=3.7"
python-versions = "!=3.9.0,!=3.9.1,>=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
{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"},
]
[package.dependencies]
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
docs = ["sphinx (>=5.3.0)", "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)"]
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
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-randomorder = ["pytest-randomly"]
[[package]]
@@ -663,15 +686,15 @@ six = "*"
[[package]]
name = "jinja2"
version = "3.1.5"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
@@ -1459,15 +1482,15 @@ files = [
[[package]]
name = "pytest"
version = "8.3.4"
version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
markers = "python_version <= \"3.11\""
files = [
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
]
[package.dependencies]
@@ -1534,15 +1557,15 @@ pydantic = ["pydantic (>=2.0)"]
[[package]]
name = "pytz"
version = "2024.1"
version = "2024.2"
description = "World timezone definitions, modern and historical"
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
{file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
{file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
{file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
]
[[package]]
@@ -1624,101 +1647,107 @@ files = [
[[package]]
name = "rapidfuzz"
version = "3.12.1"
version = "3.12.2"
description = "rapid fuzzy string matching"
optional = false
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "rapidfuzz-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dbb7ea2fd786e6d66f225ef6eef1728832314f47e82fee877cb2a793ebda9579"},
{file = "rapidfuzz-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1ae41361de05762c1eaa3955e5355de7c4c6f30d1ef1ea23d29bf738a35809ab"},
{file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc3c39e0317e7f68ba01bac056e210dd13c7a0abf823e7b6a5fe7e451ddfc496"},
{file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69f2520296f1ae1165b724a3aad28c56fd0ac7dd2e4cff101a5d986e840f02d4"},
{file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34dcbf5a7daecebc242f72e2500665f0bde9dd11b779246c6d64d106a7d57c99"},
{file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:773ab37fccf6e0513891f8eb4393961ddd1053c6eb7e62eaa876e94668fc6d31"},
{file = "rapidfuzz-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ecf0e6de84c0bc2c0f48bc03ba23cef2c5f1245db7b26bc860c11c6fd7a097c"},
{file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4dc2ebad4adb29d84a661f6a42494df48ad2b72993ff43fad2b9794804f91e45"},
{file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8389d98b9f54cb4f8a95f1fa34bf0ceee639e919807bb931ca479c7a5f2930bf"},
{file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:165bcdecbfed9978962da1d3ec9c191b2ff9f1ccc2668fbaf0613a975b9aa326"},
{file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:129d536740ab0048c1a06ccff73c683f282a2347c68069affae8dbc423a37c50"},
{file = "rapidfuzz-3.12.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b67e390261ffe98ec86c771b89425a78b60ccb610c3b5874660216fcdbded4b"},
{file = "rapidfuzz-3.12.1-cp310-cp310-win32.whl", hash = "sha256:a66520180d3426b9dc2f8d312f38e19bc1fc5601f374bae5c916f53fa3534a7d"},
{file = "rapidfuzz-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:82260b20bc7a76556cecb0c063c87dad19246a570425d38f8107b8404ca3ac97"},
{file = "rapidfuzz-3.12.1-cp310-cp310-win_arm64.whl", hash = "sha256:3a860d103bbb25c69c2e995fdf4fac8cb9f77fb69ec0a00469d7fd87ff148f46"},
{file = "rapidfuzz-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6d9afad7b16d01c9e8929b6a205a18163c7e61b6cd9bcf9c81be77d5afc1067a"},
{file = "rapidfuzz-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb424ae7240f2d2f7d8dda66a61ebf603f74d92f109452c63b0dbf400204a437"},
{file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42149e6d13bd6d06437d2a954dae2184dadbbdec0fdb82dafe92860d99f80519"},
{file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:760ac95d788f2964b73da01e0bdffbe1bf2ad8273d0437565ce9092ae6ad1fbc"},
{file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cf27e8e4bf7bf9d92ef04f3d2b769e91c3f30ba99208c29f5b41e77271a2614"},
{file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00ceb8ff3c44ab0d6014106c71709c85dee9feedd6890eff77c814aa3798952b"},
{file = "rapidfuzz-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b61c558574fbc093d85940c3264c08c2b857b8916f8e8f222e7b86b0bb7d12"},
{file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:346a2d8f17224e99f9ef988606c83d809d5917d17ad00207237e0965e54f9730"},
{file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d60d1db1b7e470e71ae096b6456e20ec56b52bde6198e2dbbc5e6769fa6797dc"},
{file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:2477da227e266f9c712f11393182c69a99d3c8007ea27f68c5afc3faf401cc43"},
{file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:8499c7d963ddea8adb6cffac2861ee39a1053e22ca8a5ee9de1197f8dc0275a5"},
{file = "rapidfuzz-3.12.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:12802e5c4d8ae104fb6efeeb436098325ce0dca33b461c46e8df015c84fbef26"},
{file = "rapidfuzz-3.12.1-cp311-cp311-win32.whl", hash = "sha256:e1061311d07e7cdcffa92c9b50c2ab4192907e70ca01b2e8e1c0b6b4495faa37"},
{file = "rapidfuzz-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6e4ed63e204daa863a802eec09feea5448617981ba5d150f843ad8e3ae071a4"},
{file = "rapidfuzz-3.12.1-cp311-cp311-win_arm64.whl", hash = "sha256:920733a28c3af47870835d59ca9879579f66238f10de91d2b4b3f809d1ebfc5b"},
{file = "rapidfuzz-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f6235b57ae3faa3f85cb3f90c9fee49b21bd671b76e90fc99e8ca2bdf0b5e4a3"},
{file = "rapidfuzz-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af4585e5812632c357fee5ab781c29f00cd06bea58f8882ff244cc4906ba6c9e"},
{file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5942dc4460e5030c5f9e1d4c9383de2f3564a2503fe25e13e89021bcbfea2f44"},
{file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b31ab59e1a0df5afc21f3109b6cfd77b34040dbf54f1bad3989f885cfae1e60"},
{file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97c885a7a480b21164f57a706418c9bbc9a496ec6da087e554424358cadde445"},
{file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d844c0587d969ce36fbf4b7cbf0860380ffeafc9ac5e17a7cbe8abf528d07bb"},
{file = "rapidfuzz-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93c95dce8917bf428064c64024de43ffd34ec5949dd4425780c72bd41f9d969"},
{file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:834f6113d538af358f39296604a1953e55f8eeffc20cb4caf82250edbb8bf679"},
{file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a940aa71a7f37d7f0daac186066bf6668d4d3b7e7ef464cb50bc7ba89eae1f51"},
{file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ec9eaf73501c9a7de2c6938cb3050392e2ee0c5ca3921482acf01476b85a7226"},
{file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3c5ec360694ac14bfaeb6aea95737cf1a6cf805b5fe8ea7fd28814706c7fa838"},
{file = "rapidfuzz-3.12.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6b5e176524653ac46f1802bdd273a4b44a5f8d0054ed5013a8e8a4b72f254599"},
{file = "rapidfuzz-3.12.1-cp312-cp312-win32.whl", hash = "sha256:6f463c6f1c42ec90e45d12a6379e18eddd5cdf74138804d8215619b6f4d31cea"},
{file = "rapidfuzz-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:b894fa2b30cd6498a29e5c470cb01c6ea898540b7e048a0342775a5000531334"},
{file = "rapidfuzz-3.12.1-cp312-cp312-win_arm64.whl", hash = "sha256:43bb17056c5d1332f517b888c4e57846c4b5f936ed304917eeb5c9ac85d940d4"},
{file = "rapidfuzz-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:97f824c15bc6933a31d6e3cbfa90188ba0e5043cf2b6dd342c2b90ee8b3fd47c"},
{file = "rapidfuzz-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a973b3f5cabf931029a3ae4a0f72e3222e53d412ea85fc37ddc49e1774f00fbf"},
{file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df7880e012228722dec1be02b9ef3898ed023388b8a24d6fa8213d7581932510"},
{file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c78582f50e75e6c2bc38c791ed291cb89cf26a3148c47860c1a04d6e5379c8e"},
{file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d7d9e6a04d8344b0198c96394c28874086888d0a2b2f605f30d1b27b9377b7d"},
{file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5620001fd4d6644a2f56880388179cc8f3767670f0670160fcb97c3b46c828af"},
{file = "rapidfuzz-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0666ab4c52e500af7ba5cc17389f5d15c0cdad06412c80312088519fdc25686d"},
{file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:27b4d440fa50b50c515a91a01ee17e8ede719dca06eef4c0cccf1a111a4cfad3"},
{file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83dccfd5a754f2a0e8555b23dde31f0f7920601bfa807aa76829391ea81e7c67"},
{file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b572b634740e047c53743ed27a1bb3b4f93cf4abbac258cd7af377b2c4a9ba5b"},
{file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7fa7b81fb52902d5f78dac42b3d6c835a6633b01ddf9b202a3ca8443be4b2d6a"},
{file = "rapidfuzz-3.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b1d4fbff980cb6baef4ee675963c081f7b5d6580a105d6a4962b20f1f880e1fb"},
{file = "rapidfuzz-3.12.1-cp313-cp313-win32.whl", hash = "sha256:3fe8da12ea77271097b303fa7624cfaf5afd90261002314e3b0047d36f4afd8d"},
{file = "rapidfuzz-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:6f7e92fc7d2a7f02e1e01fe4f539324dfab80f27cb70a30dd63a95445566946b"},
{file = "rapidfuzz-3.12.1-cp313-cp313-win_arm64.whl", hash = "sha256:e31be53d7f4905a6a038296d8b773a79da9ee9f0cd19af9490c5c5a22e37d2e5"},
{file = "rapidfuzz-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bef5c91d5db776523530073cda5b2a276283258d2f86764be4a008c83caf7acd"},
{file = "rapidfuzz-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:841e0c2a5fbe8fc8b9b1a56e924c871899932c0ece7fbd970aa1c32bfd12d4bf"},
{file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:046fc67f3885d94693a2151dd913aaf08b10931639cbb953dfeef3151cb1027c"},
{file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4d2d39b2e76c17f92edd6d384dc21fa020871c73251cdfa017149358937a41d"},
{file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5857dda85165b986c26a474b22907db6b93932c99397c818bcdec96340a76d5"},
{file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c26cd1b9969ea70dbf0dbda3d2b54ab4b2e683d0fd0f17282169a19563efeb1"},
{file = "rapidfuzz-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf56ea4edd69005786e6c80a9049d95003aeb5798803e7a2906194e7a3cb6472"},
{file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fbe7580b5fb2db8ebd53819171ff671124237a55ada3f64d20fc9a149d133960"},
{file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:018506a53c3b20dcbda8c93d4484b9eb1764c93d5ea16be103cf6b0d8b11d860"},
{file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:325c9c71b737fcd32e2a4e634c430c07dd3d374cfe134eded3fe46e4c6f9bf5d"},
{file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:930756639643e3aa02d3136b6fec74e5b9370a24f8796e1065cd8a857a6a6c50"},
{file = "rapidfuzz-3.12.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0acbd27543b158cb915fde03877383816a9e83257832818f1e803bac9b394900"},
{file = "rapidfuzz-3.12.1-cp39-cp39-win32.whl", hash = "sha256:80ff9283c54d7d29b2d954181e137deee89bec62f4a54675d8b6dbb6b15d3e03"},
{file = "rapidfuzz-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:fd37e53f0ed239d0cec27b250cec958982a8ba252ce64aa5e6052de3a82fa8db"},
{file = "rapidfuzz-3.12.1-cp39-cp39-win_arm64.whl", hash = "sha256:4a4422e4f73a579755ab60abccb3ff148b5c224b3c7454a13ca217dfbad54da6"},
{file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b7cba636c32a6fc3a402d1cb2c70c6c9f8e6319380aaf15559db09d868a23e56"},
{file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b79286738a43e8df8420c4b30a92712dec6247430b130f8e015c3a78b6d61ac2"},
{file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dc1937198e7ff67e217e60bfa339f05da268d91bb15fec710452d11fe2fdf60"},
{file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b85817a57cf8db32dd5d2d66ccfba656d299b09eaf86234295f89f91be1a0db2"},
{file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:04283c6f3e79f13a784f844cd5b1df4f518ad0f70c789aea733d106c26e1b4fb"},
{file = "rapidfuzz-3.12.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a718f740553aad5f4daef790191511da9c6eae893ee1fc2677627e4b624ae2db"},
{file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cbdf145c7e4ebf2e81c794ed7a582c4acad19e886d5ad6676086369bd6760753"},
{file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:0d03ad14a26a477be221fddc002954ae68a9e2402b9d85433f2d0a6af01aa2bb"},
{file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1187aeae9c89e838d2a0a2b954b4052e4897e5f62e5794ef42527bf039d469e"},
{file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd47dfb1bca9673a48b923b3d988b7668ee8efd0562027f58b0f2b7abf27144c"},
{file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187cdb402e223264eebed2fe671e367e636a499a7a9c82090b8d4b75aa416c2a"},
{file = "rapidfuzz-3.12.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6899b41bf6c30282179f77096c1939f1454836440a8ab05b48ebf7026a3b590"},
{file = "rapidfuzz-3.12.1.tar.gz", hash = "sha256:6a98bbca18b4a37adddf2d8201856441c26e9c981d8895491b5bc857b5f780eb"},
{file = "rapidfuzz-3.12.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b9a75e0385a861178adf59e86d6616cbd0d5adca7228dc9eeabf6f62cf5b0b1"},
{file = "rapidfuzz-3.12.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6906a7eb458731e3dd2495af1d0410e23a21a2a2b7ced535e6d5cd15cb69afc5"},
{file = "rapidfuzz-3.12.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4b3334a8958b689f292d5ce8a928140ac98919b51e084f04bf0c14276e4c6ba"},
{file = "rapidfuzz-3.12.2-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:85a54ce30345cff2c79cbcffa063f270ad1daedd0d0c3ff6e541d3c3ba4288cf"},
{file = "rapidfuzz-3.12.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb63c5072c08058f8995404201a52fc4e1ecac105548a4d03c6c6934bda45a3"},
{file = "rapidfuzz-3.12.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5385398d390c6571f0f2a7837e6ddde0c8b912dac096dc8c87208ce9aaaa7570"},
{file = "rapidfuzz-3.12.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5032cbffa245b4beba0067f8ed17392ef2501b346ae3c1f1d14b950edf4b6115"},
{file = "rapidfuzz-3.12.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:195adbb384d89d6c55e2fd71e7fb262010f3196e459aa2f3f45f31dd7185fe72"},
{file = "rapidfuzz-3.12.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f43b773a4d4950606fb25568ecde5f25280daf8f97b87eb323e16ecd8177b328"},
{file = "rapidfuzz-3.12.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:55a43be0e0fa956a919043c19d19bd988991d15c59f179d413fe5145ed9deb43"},
{file = "rapidfuzz-3.12.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:71cf1ea16acdebe9e2fb62ee7a77f8f70e877bebcbb33b34e660af2eb6d341d9"},
{file = "rapidfuzz-3.12.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a3692d4ab36d44685f61326dca539975a4eda49b2a76f0a3df177d8a2c0de9d2"},
{file = "rapidfuzz-3.12.2-cp310-cp310-win32.whl", hash = "sha256:09227bd402caa4397ba1d6e239deea635703b042dd266a4092548661fb22b9c6"},
{file = "rapidfuzz-3.12.2-cp310-cp310-win_amd64.whl", hash = "sha256:0f05b7b95f9f87254b53fa92048367a8232c26cee7fc8665e4337268c3919def"},
{file = "rapidfuzz-3.12.2-cp310-cp310-win_arm64.whl", hash = "sha256:6938738e00d9eb6e04097b3f565097e20b0c398f9c58959a2bc64f7f6be3d9da"},
{file = "rapidfuzz-3.12.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e9c4d984621ae17404c58f8d06ed8b025e167e52c0e6a511dfec83c37e9220cd"},
{file = "rapidfuzz-3.12.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f9132c55d330f0a1d34ce6730a76805323a6250d97468a1ca766a883d6a9a25"},
{file = "rapidfuzz-3.12.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b343b6cb4b2c3dbc8d2d4c5ee915b6088e3b144ddf8305a57eaab16cf9fc74"},
{file = "rapidfuzz-3.12.2-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:24081077b571ec4ee6d5d7ea0e49bc6830bf05b50c1005028523b9cd356209f3"},
{file = "rapidfuzz-3.12.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c988a4fc91856260355773bf9d32bebab2083d4c6df33fafeddf4330e5ae9139"},
{file = "rapidfuzz-3.12.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:780b4469ee21cf62b1b2e8ada042941fd2525e45d5fb6a6901a9798a0e41153c"},
{file = "rapidfuzz-3.12.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edd84b0a323885493c893bad16098c5e3b3005d7caa995ae653da07373665d97"},
{file = "rapidfuzz-3.12.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efa22059c765b3d8778083805b199deaaf643db070f65426f87d274565ddf36a"},
{file = "rapidfuzz-3.12.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:095776b11bb45daf7c2973dd61cc472d7ea7f2eecfa454aef940b4675659b92f"},
{file = "rapidfuzz-3.12.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7e2574cf4aa86065600b664a1ac7b8b8499107d102ecde836aaaa403fc4f1784"},
{file = "rapidfuzz-3.12.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d5a3425a6c50fd8fbd991d8f085ddb504791dae6ef9cc3ab299fea2cb5374bef"},
{file = "rapidfuzz-3.12.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97fb05e1ddb7b71a054040af588b0634214ee87cea87900d309fafc16fd272a4"},
{file = "rapidfuzz-3.12.2-cp311-cp311-win32.whl", hash = "sha256:b4c5a0413589aef936892fbfa94b7ff6f7dd09edf19b5a7b83896cc9d4e8c184"},
{file = "rapidfuzz-3.12.2-cp311-cp311-win_amd64.whl", hash = "sha256:58d9ae5cf9246d102db2a2558b67fe7e73c533e5d769099747921232d88b9be2"},
{file = "rapidfuzz-3.12.2-cp311-cp311-win_arm64.whl", hash = "sha256:7635fe34246cd241c8e35eb83084e978b01b83d5ef7e5bf72a704c637f270017"},
{file = "rapidfuzz-3.12.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1d982a651253ffe8434d9934ff0c1089111d60502228464721a2a4587435e159"},
{file = "rapidfuzz-3.12.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:02e6466caa0222d5233b1f05640873671cd99549a5c5ba4c29151634a1e56080"},
{file = "rapidfuzz-3.12.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e956b3f053e474abae69ac693a52742109d860ac2375fe88e9387d3277f4c96c"},
{file = "rapidfuzz-3.12.2-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2dee7d740a2d5418d4f964f39ab8d89923e6b945850db833e798a1969b19542a"},
{file = "rapidfuzz-3.12.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a057cdb0401e42c84b6516c9b1635f7aedd5e430c6e388bd5f6bcd1d6a0686bb"},
{file = "rapidfuzz-3.12.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dccf8d4fb5b86d39c581a59463c596b1d09df976da26ff04ae219604223d502f"},
{file = "rapidfuzz-3.12.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21d5b3793c6f5aecca595cd24164bf9d3c559e315ec684f912146fc4e769e367"},
{file = "rapidfuzz-3.12.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:46a616c0e13cff2de1761b011e0b14bb73b110182f009223f1453d505c9a975c"},
{file = "rapidfuzz-3.12.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19fa5bc4301a1ee55400d4a38a8ecf9522b0391fc31e6da5f4d68513fe5c0026"},
{file = "rapidfuzz-3.12.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:544a47190a0d25971658a9365dba7095397b4ce3e897f7dd0a77ca2cf6fa984e"},
{file = "rapidfuzz-3.12.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f21af27c5e001f0ba1b88c36a0936437dfe034c452548d998891c21125eb640f"},
{file = "rapidfuzz-3.12.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b63170d9db00629b5b3f2862114d8d6ee19127eaba0eee43762d62a25817dbe0"},
{file = "rapidfuzz-3.12.2-cp312-cp312-win32.whl", hash = "sha256:6c7152d77b2eb6bfac7baa11f2a9c45fd5a2d848dbb310acd0953b3b789d95c9"},
{file = "rapidfuzz-3.12.2-cp312-cp312-win_amd64.whl", hash = "sha256:1a314d170ee272ac87579f25a6cf8d16a031e1f7a7b07663434b41a1473bc501"},
{file = "rapidfuzz-3.12.2-cp312-cp312-win_arm64.whl", hash = "sha256:d41e8231326e94fd07c4d8f424f6bed08fead6f5e6688d1e6e787f1443ae7631"},
{file = "rapidfuzz-3.12.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:941f31038dba5d3dedcfcceba81d61570ad457c873a24ceb13f4f44fcb574260"},
{file = "rapidfuzz-3.12.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fe2dfc454ee51ba168a67b1e92b72aad251e45a074972cef13340bbad2fd9438"},
{file = "rapidfuzz-3.12.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78fafaf7f5a48ee35ccd7928339080a0136e27cf97396de45259eca1d331b714"},
{file = "rapidfuzz-3.12.2-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e0c7989ff32c077bb8fd53253fd6ca569d1bfebc80b17557e60750e6909ba4fe"},
{file = "rapidfuzz-3.12.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:96fa00bc105caa34b6cd93dca14a29243a3a7f0c336e4dcd36348d38511e15ac"},
{file = "rapidfuzz-3.12.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bccfb30c668620c5bc3490f2dc7d7da1cca0ead5a9da8b755e2e02e2ef0dff14"},
{file = "rapidfuzz-3.12.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f9b0adc3d894beb51f5022f64717b6114a6fabaca83d77e93ac7675911c8cc5"},
{file = "rapidfuzz-3.12.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32691aa59577f42864d5535cb6225d0f47e2c7bff59cf4556e5171e96af68cc1"},
{file = "rapidfuzz-3.12.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:758b10380ad34c1f51753a070d7bb278001b5e6fcf544121c6df93170952d705"},
{file = "rapidfuzz-3.12.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:50a9c54c0147b468363119132d514c5024fbad1ed8af12bd8bd411b0119f9208"},
{file = "rapidfuzz-3.12.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e3ceb87c11d2d0fbe8559bb795b0c0604b84cfc8bb7b8720b5c16e9e31e00f41"},
{file = "rapidfuzz-3.12.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f7c9a003002434889255ff5676ca0f8934a478065ab5e702f75dc42639505bba"},
{file = "rapidfuzz-3.12.2-cp313-cp313-win32.whl", hash = "sha256:cf165a76870cd875567941cf861dfd361a0a6e6a56b936c5d30042ddc9def090"},
{file = "rapidfuzz-3.12.2-cp313-cp313-win_amd64.whl", hash = "sha256:55bcc003541f5f16ec0a73bf6de758161973f9e8d75161954380738dd147f9f2"},
{file = "rapidfuzz-3.12.2-cp313-cp313-win_arm64.whl", hash = "sha256:69f6ecdf1452139f2b947d0c169a605de578efdb72cbb2373cb0a94edca1fd34"},
{file = "rapidfuzz-3.12.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c4c852cd8bed1516a64fd6e2d4c6f270d4356196ee03fda2af1e5a9e13c34643"},
{file = "rapidfuzz-3.12.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42e7f747b55529a6d0d1588695d71025e884ab48664dca54b840413dea4588d8"},
{file = "rapidfuzz-3.12.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a749fd2690f24ef256b264a781487746bbb95344364fe8fe356f0eef7ef206ba"},
{file = "rapidfuzz-3.12.2-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a11e1d036170bbafa43a9e63d8c309273564ec5bdfc5439062f439d1a16965a"},
{file = "rapidfuzz-3.12.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dfb337f1832c1231e3d5621bd0ebebb854e46036aedae3e6a49c1fc08f16f249"},
{file = "rapidfuzz-3.12.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e88c6e68fca301722fa3ab7fd3ca46998012c14ada577bc1e2c2fc04f2067ca6"},
{file = "rapidfuzz-3.12.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17e1a3a8b4b5125cfb63a6990459b25b87ea769bdaf90d05bb143f8febef076a"},
{file = "rapidfuzz-3.12.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:b9f8177b24ccc0a843e85932b1088c5e467a7dd7a181c13f84c684b796bea815"},
{file = "rapidfuzz-3.12.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:6c506bdc2f304051592c0d3b0e82eed309248ec10cdf802f13220251358375ea"},
{file = "rapidfuzz-3.12.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:30bf15c1ecec2798b713d551df17f23401a3e3653ad9ed4e83ad1c2b06e86100"},
{file = "rapidfuzz-3.12.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:bd9a67cfc83e8453ef17ddd1c2c4ce4a74d448a197764efb54c29f29fb41f611"},
{file = "rapidfuzz-3.12.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a6eaec2ef658dd650c6eb9b36dff7a361ebd7d8bea990ce9d639b911673b2cb"},
{file = "rapidfuzz-3.12.2-cp39-cp39-win32.whl", hash = "sha256:d7701769f110332cde45c41759cb2a497de8d2dca55e4c519a46aed5fbb19d1a"},
{file = "rapidfuzz-3.12.2-cp39-cp39-win_amd64.whl", hash = "sha256:296bf0fd4f678488670e262c87a3e4f91900b942d73ae38caa42a417e53643b1"},
{file = "rapidfuzz-3.12.2-cp39-cp39-win_arm64.whl", hash = "sha256:7957f5d768de14f6b2715303ccdf224b78416738ee95a028a2965c95f73afbfb"},
{file = "rapidfuzz-3.12.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e5fd3ce849b27d063755829cda27a9dab6dbd63be3801f2a40c60ec563a4c90f"},
{file = "rapidfuzz-3.12.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:54e53662d71ed660c83c5109127c8e30b9e607884b7c45d2aff7929bbbd00589"},
{file = "rapidfuzz-3.12.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b9e43cf2213e524f3309d329f1ad8dbf658db004ed44f6ae1cd2919aa997da5"},
{file = "rapidfuzz-3.12.2-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29ca445e320e5a8df3bd1d75b4fa4ecfa7c681942b9ac65b55168070a1a1960e"},
{file = "rapidfuzz-3.12.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:83eb7ef732c2f8533c6b5fbe69858a722c218acc3e1fc190ab6924a8af7e7e0e"},
{file = "rapidfuzz-3.12.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:648adc2dd2cf873efc23befcc6e75754e204a409dfa77efd0fea30d08f22ef9d"},
{file = "rapidfuzz-3.12.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9b1e6f48e1ffa0749261ee23a1c6462bdd0be5eac83093f4711de17a42ae78ad"},
{file = "rapidfuzz-3.12.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1ae9ded463f2ca4ba1eb762913c5f14c23d2e120739a62b7f4cc102eab32dc90"},
{file = "rapidfuzz-3.12.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dda45f47b559be72ecbce45c7f71dc7c97b9772630ab0f3286d97d2c3025ab71"},
{file = "rapidfuzz-3.12.2-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3745c6443890265513a3c8777f2de4cb897aeb906a406f97741019be8ad5bcc"},
{file = "rapidfuzz-3.12.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36d3ef4f047ed1bc96fa29289f9e67a637ddca5e4f4d3dc7cb7f50eb33ec1664"},
{file = "rapidfuzz-3.12.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:54bb69ebe5ca0bd7527357e348f16a4c0c52fe0c2fcc8a041010467dcb8385f7"},
{file = "rapidfuzz-3.12.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3f2ddd5b99b254039a8c82be5749d4d75943f62eb2c2918acf6ffd586852834f"},
{file = "rapidfuzz-3.12.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:8117dab9b26a1aaffab59b4e30f80ac4d55e61ad4139a637c149365960933bee"},
{file = "rapidfuzz-3.12.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40c0f16d62d6553527de3dab2fb69709c4383430ea44bce8fb4711ed4cbc6ae3"},
{file = "rapidfuzz-3.12.2-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f177e1eb6e4f5261a89c475e21bce7a99064a8f217d2336fb897408f46f0ceaf"},
{file = "rapidfuzz-3.12.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df0cecc2852fcb078ed1b4482fac4fc2c2e7787f3edda8920d9a4c0f51b1c95"},
{file = "rapidfuzz-3.12.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b3c4df0321df6f8f0b61afbaa2ced9622750ee1e619128db57a18533d139820"},
{file = "rapidfuzz-3.12.2.tar.gz", hash = "sha256:b0ba1ccc22fff782e7152a3d3d0caca44ec4e32dc48ba01c560b8593965b5aa3"},
]
[package.extras]
@@ -1886,24 +1915,22 @@ yaml = ["pyyaml (>=6.0.1)"]
[[package]]
name = "requests-mock"
version = "1.9.3"
version = "1.12.1"
description = "Mock out responses from the requests package"
optional = false
python-versions = "*"
python-versions = ">=3.5"
groups = ["dev"]
markers = "python_version <= \"3.11\""
files = [
{file = "requests-mock-1.9.3.tar.gz", hash = "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"},
{file = "requests_mock-1.9.3-py2.py3-none-any.whl", hash = "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970"},
{file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"},
{file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"},
]
[package.dependencies]
requests = ">=2.3,<3"
six = "*"
requests = ">=2.22,<3"
[package.extras]
fixture = ["fixtures"]
test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.18)", "testtools"]
[[package]]
name = "requests-toolbelt"
@@ -2112,19 +2139,6 @@ files = [
{file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"},
]
[[package]]
name = "unidecode"
version = "1.3.8"
description = "ASCII transliterations of Unicode text"
optional = false
python-versions = ">=3.5"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"},
{file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"},
]
[[package]]
name = "url-normalize"
version = "1.4.3"
@@ -2177,19 +2191,104 @@ files = [
bracex = ">=2.1.1"
[[package]]
name = "xmltodict"
version = "0.13.0"
description = "Makes working with XML feel like you are working with JSON"
name = "whenever"
version = "0.6.17"
description = "Modern datetime library for Python"
optional = false
python-versions = ">=3.4"
python-versions = ">=3.9"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"},
{file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"},
{file = "whenever-0.6.17-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8e9e905fd19b0679e5ab1a0d0110a1974b89bf4cbd1ff22c9e352db381e4ae4f"},
{file = "whenever-0.6.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cd615e60f992fb9ae9d73fc3581ac63de981e51013b0fffbf8e2bd748c71e3df"},
{file = "whenever-0.6.17-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd717faa660771bf6f2fda4f75f2693cd79f2a7e975029123284ea3859fb329c"},
{file = "whenever-0.6.17-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2ea744d9666be8880062da0d6dee690e8f70a2bc2a42b96ee17e10e36b0b5266"},
{file = "whenever-0.6.17-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6b32593b44332660402c7e4c681cce6d7859b15a609d66ac3a28a6ad6357c2f"},
{file = "whenever-0.6.17-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a01e4daaac24e0be48a6cb0bb03fa000a40126b1e9cb8d721ee116b2f44c1bb1"},
{file = "whenever-0.6.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e88fe9fccb868ee88bb2ee8bfcbc55937d0b40747069f595f10b4832ff1545"},
{file = "whenever-0.6.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2dce7b9faf23325b38ca713b2c7a150a8befc832995213a8ec46fe15af6a03e7"},
{file = "whenever-0.6.17-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0925f7bf3448ef4f8c9b93de2d1270b82450a81b5d025a89f486ea61aa94319"},
{file = "whenever-0.6.17-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:82203a572049070d685499dd695ff1914fee62f32aefa9e9952a60762217aa9e"},
{file = "whenever-0.6.17-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c30e5b5b82783bc85169c8208ab3acf58648092515017b2a185a598160503dbb"},
{file = "whenever-0.6.17-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:763e59062adc9adfbde45c3ad8b5f472b337cc5cebc70760627d004a4c286d33"},
{file = "whenever-0.6.17-cp310-cp310-win32.whl", hash = "sha256:f71387bbe95cd98fc78653b942c6e02ff4245b6add012b3f11796220272984ce"},
{file = "whenever-0.6.17-cp310-cp310-win_amd64.whl", hash = "sha256:996ab1f6f09bc9e0c699fa58937b5adc25e39e979ebbebfd77bae09221350f3d"},
{file = "whenever-0.6.17-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:87e28378945182e822e211fcea9e89c7428749fd440b616d6d81365202cbed09"},
{file = "whenever-0.6.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0cf4ee3e8d5a55d788e8a79aeff29482dd4facc38241901f18087c3e662d16ba"},
{file = "whenever-0.6.17-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97ffc43cd278f6f58732cd9d83c822faff3b1987c3b7b448b59b208cf6b6293"},
{file = "whenever-0.6.17-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ce99533865fd63029fa64aef1cfbd42be1d2ced33da38c82f8c763986583982"},
{file = "whenever-0.6.17-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68b88e023d64e8ccfabe04028738d8041eccd5a078843cd9b506e51df3375e84"},
{file = "whenever-0.6.17-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9159bae31f2edaf5e70e4437d871e52f51e7e90f1b9faaac19a8c2bccba5170a"},
{file = "whenever-0.6.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f9c4ee1f1e85f857507d146d56973db28d148f50883babf1da3d24a40bbcf60"},
{file = "whenever-0.6.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0acd8b3238aa28a20d1f93c74fd84c9b59e2662e553a55650a0e663a81d2908d"},
{file = "whenever-0.6.17-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ae238cd46567b5741806517d307a81cca45fd49902312a9bdde27db5226e8825"},
{file = "whenever-0.6.17-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:99f72853e8292284c2a89a06ab826892216c04540a0ca84b3d3eaa9317dbe026"},
{file = "whenever-0.6.17-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ccb6c77b497d651a283ef0f40ada326602b313ee71d22015f53d5496124dfc10"},
{file = "whenever-0.6.17-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a1918c9836dc331cd9a39175806668b57b93d538d288469ad8bedb144ec11b"},
{file = "whenever-0.6.17-cp311-cp311-win32.whl", hash = "sha256:72492f130a8c5b8abb2d7b16cec33b6d6ed9e294bb63c56ab1030623de4ae343"},
{file = "whenever-0.6.17-cp311-cp311-win_amd64.whl", hash = "sha256:88dc4961f8f6cd16d9b70db022fd6c86193fad429f98daeb82c8e9ba0ca27e5c"},
{file = "whenever-0.6.17-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:d72c2413e32e3f382f6def337961ea7f20e66d0452ebc02e2fa215e1c45df73e"},
{file = "whenever-0.6.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d12b891d780d9c98585b507e9f85097085337552b75f160ce6930af96509faa1"},
{file = "whenever-0.6.17-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:503aaf2acfd5a7926ca5c6dc6ec09fc6c2891f536ab9cbd26a072c94bda3927f"},
{file = "whenever-0.6.17-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6de09bcddfeb61c822019e88d8abed9ccc1d4f9d1a3a5d62d28d94d2fb6daff5"},
{file = "whenever-0.6.17-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fdfe430df7f336d8793b6b844f0d2552e1589e39e72b7414ba67139b9b402bed"},
{file = "whenever-0.6.17-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99776635ac174a3df4a372bfae7420b3de965044d69f2bee08a7486cabba0aaa"},
{file = "whenever-0.6.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdbb6d8dae94b492370949c8d8bf818f9ee0b4a08f304dadf9d6d892b7513676"},
{file = "whenever-0.6.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:45d66e68cdca52ca3e6e4990515d32f6bc4eb6a24ff8cbcbe4df16401dd2d3c7"},
{file = "whenever-0.6.17-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73947bd633bc658f8a8e2ff2bff34ee7caabd6edd9951bb2d778e6071c772df4"},
{file = "whenever-0.6.17-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:9f9d5b108f9abf39471e3d5ef22ff2fed09cc51a0cfa63c833c393b21b8bdb81"},
{file = "whenever-0.6.17-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a42231e7623b50a60747a752a97499f6ad03e03ce128bf97ded84e12b0f4a77e"},
{file = "whenever-0.6.17-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a6d9458d544006131e1210343bf660019abfa11d46f5be8ad2d7616dc82340f4"},
{file = "whenever-0.6.17-cp312-cp312-win32.whl", hash = "sha256:ca1eda94ca2ef7ad1a1249ea80949be252e78a0f10463e12c81ad126ec6b99e5"},
{file = "whenever-0.6.17-cp312-cp312-win_amd64.whl", hash = "sha256:fd7de20d6bbb74c6bad528c0346ef679957db21ce8a53f118e53b5f60f76495b"},
{file = "whenever-0.6.17-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ca9ee5b2b04c5a65112f55ff4a4efcba185f45b95766b669723e8b9a28bdb50b"},
{file = "whenever-0.6.17-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bef0cf1cd4282044d98e4af9969239dc139e5b192896d4110d0d3f4139bdb30"},
{file = "whenever-0.6.17-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04ac4e1fc1bc0bfb35f2c6a05d52de9fec297ea84ee60c655dec258cca1e6eb7"},
{file = "whenever-0.6.17-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c792f96d021ba2883e6f4b70cc58b5d970f026eb156ff93866686e27a7cce93"},
{file = "whenever-0.6.17-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7a7f938b5533e751702de95a615b7903457a7618b94aef72c062fa871ad691b"},
{file = "whenever-0.6.17-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:47d2dbb85c512e28c14eede36a148afbb90baa340e113b39b2b9f0e9a3b192dd"},
{file = "whenever-0.6.17-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea2b49a91853c133e8954dffbf180adca539b3719fd269565bf085ba97b47f5f"},
{file = "whenever-0.6.17-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:91fcb2f42381a8ad763fc7ee2259375b1ace1306a02266c195af27bd3696e0da"},
{file = "whenever-0.6.17-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:32e4d5e3429015a5082cd171ceea633c6ea565d90491005cdcef49a7d6a17c99"},
{file = "whenever-0.6.17-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:f05731f530e4af29582a70cf02f8441027a4534e67b7c484efdf210fc09d0421"},
{file = "whenever-0.6.17-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0d417b7de29aea2cfa7ea47f344848491d44291f28c038df869017ae66a50b48"},
{file = "whenever-0.6.17-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8208333ece7f2e0c232feeecbd21bde3888c6782d3b08372ae8b5269938645b3"},
{file = "whenever-0.6.17-cp313-cp313-win32.whl", hash = "sha256:c4912104731fd2be89cd031d8d34227225f1fae5181f931b91f217e69ded48ff"},
{file = "whenever-0.6.17-cp313-cp313-win_amd64.whl", hash = "sha256:4f46ad87fab336d7643e0c2248dcd27a0f4ae42ac2c5e864a9d06a8f5538efd0"},
{file = "whenever-0.6.17-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:53f03ae8c54aa60f5f22c790eb63ad644e97f8fba4b22337572a4e16bc4abb73"},
{file = "whenever-0.6.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42fce832892578455d46870dc074521e627ba9272b839a8297784059170030f5"},
{file = "whenever-0.6.17-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ac0786d6cb479275ea627d84536f38b6a408348961856e2e807d82d4dc768ed"},
{file = "whenever-0.6.17-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3e2f490b5e90b314cf7615435e24effe2356b57fa907fedb98fe58d49c6109c5"},
{file = "whenever-0.6.17-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c1f25ab893cfa724b319a838ef60b918bd35be8f3f6ded73e6fd6e508b5237e"},
{file = "whenever-0.6.17-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac5f644d0d3228e806b5129cebfb824a5e26553a0d47d89fc9e962cffa1b99ed"},
{file = "whenever-0.6.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e185309314b1abcc14c18597dd0dfe7fd8b39670f63a7d9357544994cba0e251"},
{file = "whenever-0.6.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cc78b8a73a71241bf356743dd76133ccf796616823d8bbe170701a51d10b9fd3"},
{file = "whenever-0.6.17-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0ea05123a0b3673c7cf3ea1fe3d8aa9362571db59f8ea15d7a8fb05d885fd756"},
{file = "whenever-0.6.17-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:9f0c874dbb49c3a733ce4dde86ffa243f166b9d1db4195e05127ec352b49d617"},
{file = "whenever-0.6.17-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:86cfbd724b11e8a419056211381bde4c1d35ead4bea8d498c85bee3812cf4e7c"},
{file = "whenever-0.6.17-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e1514f4a3094f11e1ad63b9defadf375d953709c7806cc1d2396634a7b00a009"},
{file = "whenever-0.6.17-cp39-cp39-win32.whl", hash = "sha256:715ed172e929327c1b68e107f0dc9520237d92e11c26db95fd05869724f3e9d9"},
{file = "whenever-0.6.17-cp39-cp39-win_amd64.whl", hash = "sha256:5fed15042b2b0ea44cafb8b7426e99170d3f4cd64dbeb966c77f14985e724d82"},
{file = "whenever-0.6.17.tar.gz", hash = "sha256:9c4bfe755c8f06726c4031dbbecd0a7710e2058bc2f3b4e4e331755af015f55f"},
]
[package.dependencies]
tzdata = {version = ">=2020.1", markers = "sys_platform == \"win32\""}
[[package]]
name = "xmltodict"
version = "0.14.2"
description = "Makes working with XML feel like you are working with JSON"
optional = false
python-versions = ">=3.6"
groups = ["main"]
markers = "python_version <= \"3.11\""
files = [
{file = "xmltodict-0.14.2-py2.py3-none-any.whl", hash = "sha256:20cc7d723ed729276e808f26fb6b3599f786cbc37e06c65e192ba77c40f20aac"},
{file = "xmltodict-0.14.2.tar.gz", hash = "sha256:201e7c28bb210e374999d1dde6382923ab0ed1a8a5faeece48ab525b7810a553"},
]
[metadata]
lock-version = "2.1"
python-versions = "^3.10,<3.12"
content-hash = "c56c7ddabd7f85cb2e3e416d14f095706ca468ab22d75b77390d69164da516bd"
content-hash = "1ae467862b0ad7402b948894dfc342e0aca8c42acda8fe58bfd8e7a35061f050"

View File

@@ -3,7 +3,7 @@ requires = [ "poetry-core>=1.0.0",]
build-backend = "poetry.core.masonry.api"
[tool.poetry]
version = "4.7.1"
version = "4.8.0"
name = "source-zendesk-support"
description = "Source implementation for Zendesk Support."
authors = [ "Airbyte <contact@airbyte.io>",]
@@ -18,7 +18,7 @@ include = "source_zendesk_support"
[tool.poetry.dependencies]
python = "^3.10,<3.12"
airbyte-cdk = "^6"
pytz = "==2024.1"
pendulum = "<3.0.0"
[tool.poetry.scripts]
source-zendesk-support = "source_zendesk_support.run:run"

View File

@@ -8,10 +8,6 @@ check:
- Tags
definitions:
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/{{ parameters.get('schema_name') or parameters['name'] }}.json"
bearer_authenticator:
type: BearerAuthenticator
api_token: "{{ config['credentials']['access_token'] }}"
@@ -66,7 +62,7 @@ definitions:
base_stream:
type: DeclarativeStream
schema_loader:
$ref: "#/definitions/schema_loader"
type: JsonFileSchemaLoader
retriever:
$ref: "#/definitions/retriever"
@@ -233,6 +229,145 @@ definitions:
primary_key: "id"
# Incremental cursor-based streams
articles_stream:
$ref: "#/definitions/base_incremental_stream"
name: "articles"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/articles.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "help_center/incremental/articles"
paginator:
type: DefaultPaginator
pagination_strategy:
type: CursorPagination
cursor_value: '{{ response.get("next_page", {}) }}'
stop_condition: "{{ last_page_size == 0 }}"
page_token_option:
type: RequestPath
record_selector:
extractor:
type: DpathExtractor
field_path: ["articles"]
article_comments_stream:
$ref: "#/definitions/base_incremental_stream"
name: "article_comments"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/article_comments.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "help_center/articles/{{ stream_partition.article_id }}/comments"
paginator:
$ref: "#/definitions/links_next_paginator"
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
parent_key: "id"
partition_field: "article_id"
stream:
$ref: "#/definitions/articles_stream"
incremental_dependency: true
record_selector:
extractor:
type: DpathExtractor
field_path: ["comments"]
transformations:
- type: AddFields
fields:
- path:
- _airbyte_parent_id # used for article_comment_votes stream
value: "{{ { 'article_id': record['source_id'], 'comment_id': record['id'] } }}"
article_votes_stream:
$ref: "#/definitions/base_incremental_stream"
name: "article_votes"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/votes.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "help_center/articles/{{ stream_partition.article_id }}/votes"
paginator:
$ref: "#/definitions/links_next_paginator"
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
parent_key: "id"
partition_field: "article_id"
stream:
$ref: "#/definitions/articles_stream"
incremental_dependency: true
record_selector:
extractor:
type: DpathExtractor
field_path: ["votes"]
article_comment_votes_stream:
$ref: "#/definitions/base_incremental_stream"
name: "article_comment_votes"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/votes.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "help_center/articles/{{ stream_partition.id.article_id }}/comments/{{ stream_partition.id.comment_id }}/votes"
paginator:
$ref: "#/definitions/links_next_paginator"
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
parent_key: "_airbyte_parent_id"
partition_field: "id"
stream:
$ref: "#/definitions/article_comments_stream"
incremental_dependency: true
record_selector:
extractor:
type: DpathExtractor
field_path: ["votes"]
audit_logs_stream:
$ref: "#/definitions/base_incremental_stream"
retriever:
@@ -353,6 +488,140 @@ definitions:
cursor_filter: "start_time"
primary_key: "id"
posts_stream:
$ref: "#/definitions/base_incremental_stream"
name: "posts"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/posts.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "community/posts"
paginator:
$ref: "#/definitions/links_next_paginator"
record_selector:
extractor:
type: DpathExtractor
field_path: ["posts"]
schema_normalization: None
post_comments_stream:
$ref: "#/definitions/base_incremental_stream"
name: "post_comments"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/post_comments.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "community/posts/{{ stream_partition.post_id }}/comments"
paginator:
$ref: "#/definitions/links_next_paginator"
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
parent_key: id
partition_field: post_id
stream:
$ref: "#/definitions/posts_stream"
incremental_dependency: true
record_selector:
extractor:
type: DpathExtractor
field_path: ["comments"]
transformations:
- type: AddFields
fields:
- path:
- _airbyte_parent_id # used for post_comment_votes stream
value: "{{ { 'post_id': record['post_id'], 'comment_id': record['id'] } }}"
post_votes_stream:
$ref: "#/definitions/base_incremental_stream"
name: "post_votes"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/votes.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "community/posts/{{ stream_partition.post_id }}/votes"
paginator:
$ref: "#/definitions/links_next_paginator"
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
parent_key: id
partition_field: post_id
stream:
$ref: "#/definitions/posts_stream"
incremental_dependency: true
record_selector:
extractor:
type: DpathExtractor
field_path: ["votes"]
post_comment_votes_stream:
$ref: "#/definitions/base_incremental_stream"
name: "post_comment_votes"
primary_key: "id"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/votes.json"
incremental_sync:
$ref: "#/definitions/cursor_incremental_sync"
start_time_option:
$ref: "#/definitions/cursor_incremental_sync/start_time_option"
field_name: "start_time"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "community/posts/{{ stream_partition.id.post_id }}/comments/{{ stream_partition.id.comment_id }}/votes"
paginator:
$ref: "#/definitions/links_next_paginator"
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
parent_key: "_airbyte_parent_id"
partition_field: id
stream:
$ref: "#/definitions/post_comments_stream"
incremental_dependency: true
record_selector:
extractor:
type: DpathExtractor
field_path: ["votes"]
satisfaction_ratings_stream:
$ref: "#/definitions/base_incremental_stream"
retriever:
@@ -567,6 +836,121 @@ definitions:
cursor_field: "updated_at"
primary_key: "id"
tickets_stream:
$ref: "#/definitions/base_incremental_stream"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
paginator:
$ref: "#/definitions/after_url_paginator"
$parameters:
name: "tickets"
path: "incremental/tickets/cursor.json"
cursor_field: "generated_timestamp"
cursor_filter: "start_time"
primary_key: "id"
ticket_metrics_stream:
type: StateDelegatingStream
name: "ticket_metrics"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/ticket_metrics.json"
full_refresh_stream:
$ref: "#/definitions/base_incremental_stream"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/ticket_metrics.json"
name: "ticket_metrics"
primary_key: "id"
incremental_sync:
type: DatetimeBasedCursor
cursor_datetime_formats:
- "%s"
datetime_format: "%s"
cursor_field: "_ab_updated_at"
start_datetime:
datetime: "0" # not used as the API does not take filters in and we don't define a step so there is only one request
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "ticket_metrics"
paginator:
$ref: "#/definitions/links_next_paginator"
record_selector:
type: RecordSelector
extractor:
type: DpathExtractor
field_path: ["ticket_metrics"]
transformations:
- type: AddFields
fields:
- path:
- "_ab_updated_at"
value: "{{ format_datetime(record['updated_at'], '%s') }}"
incremental_stream:
$ref: "#/definitions/base_incremental_stream"
schema_loader:
type: JsonFileSchemaLoader
file_path: "./source_zendesk_support/schemas/ticket_metrics.json"
name: "ticket_metrics"
primary_key: "id"
incremental_sync:
type: DatetimeBasedCursor
cursor_datetime_formats:
- "%s"
datetime_format: "%s"
cursor_field: "_ab_updated_at"
start_datetime:
datetime: "0" # not used as we should start from the state value
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
requester:
$ref: "#/definitions/retriever/requester"
path: "tickets/{{ stream_partition.ticket_id }}/metrics"
error_handler:
type: DefaultErrorHandler
backoff_strategies:
- type: WaitTimeFromHeader
header: Retry-After
response_filters:
- http_codes: [403]
action: IGNORE
failure_type: config_error
error_message: "Please ensure the authenticated user has access to stream: {self.name}. If the issue persists, contact Zendesk support."
- http_codes: [404]
action: IGNORE
failure_type: config_error
error_message: "Not found. Ticket was deleted. If the issue persists, contact Zendesk support."
paginator:
type: NoPagination
record_selector:
type: RecordSelector
extractor:
type: DpathExtractor
field_path: ["ticket_metric"]
partition_router:
type: SubstreamPartitionRouter
parent_stream_configs:
- type: ParentStreamConfig
parent_key: "id"
partition_field: "ticket_id"
extra_fields:
- ["generated_timestamp"]
stream:
$ref: "#/definitions/tickets_stream"
incremental_dependency: true
transformations:
- type: AddFields
fields:
- path:
- "_ab_updated_at"
value: "{{ record['generated_timestamp'] if 'generated_timestamp' in record else stream_slice.extra_fields['generated_timestamp'] }}"
value_type: "integer"
topics_stream:
$ref: "#/definitions/semi_incremental_stream"
retriever:
@@ -601,6 +985,25 @@ definitions:
cursor_filter: "start_time"
primary_key: "id"
users_identities_stream:
$ref: "#/definitions/base_incremental_stream"
retriever:
$ref: "#/definitions/retriever"
ignore_stream_slicer_parameters_on_paginated_requests: true
paginator:
$ref: "#/definitions/after_url_paginator"
requester:
$ref: "#/definitions/retriever/requester"
request_parameters:
include: "identities"
$parameters:
name: "user_identities"
data_path: "identities"
path: "incremental/users/cursor.json"
cursor_field: "updated_at"
cursor_filter: "start_time"
primary_key: "id"
streams:
# Full refresh streams
- $ref: "#/definitions/account_attributes_stream"
@@ -618,6 +1021,10 @@ streams:
- $ref: "#/definitions/categories_stream"
- $ref: "#/definitions/sections_stream"
# Incremental streams
- $ref: "#/definitions/articles_stream"
- $ref: "#/definitions/article_comments_stream"
- $ref: "#/definitions/article_votes_stream"
- $ref: "#/definitions/article_comment_votes_stream"
- $ref: "#/definitions/audit_logs_stream"
- $ref: "#/definitions/groups_stream"
- $ref: "#/definitions/group_memberships_stream"
@@ -625,14 +1032,21 @@ streams:
- $ref: "#/definitions/organization_fields_stream"
- $ref: "#/definitions/organization_memberships_stream"
- $ref: "#/definitions/organizations_stream"
- $ref: "#/definitions/posts_stream"
- $ref: "#/definitions/post_comment_votes_stream"
- $ref: "#/definitions/post_comments_stream"
- $ref: "#/definitions/post_votes_stream"
- $ref: "#/definitions/satisfaction_ratings_stream"
- $ref: "#/definitions/ticket_activities_stream"
- $ref: "#/definitions/ticket_audits_stream"
- $ref: "#/definitions/ticket_comments_stream"
- $ref: "#/definitions/ticket_metric_events_stream"
- $ref: "#/definitions/ticket_skips_stream"
- $ref: "#/definitions/tickets_stream"
- $ref: "#/definitions/ticket_metrics_stream"
- $ref: "#/definitions/triggers_stream"
- $ref: "#/definitions/users_stream"
- $ref: "#/definitions/users_identities_stream"
# Zendesk Support offers four tiers of rate limits:
# - Team: 200 req/min (3.3 req/sec)

View File

@@ -4,32 +4,15 @@
import base64
import logging
from datetime import datetime
from typing import Any, List, Mapping, Optional, Tuple
import pendulum
from airbyte_cdk import AirbyteTracedException, FailureType
from airbyte_cdk.models import ConfiguredAirbyteCatalog, SyncMode
from airbyte_cdk.sources.declarative.yaml_declarative_source import YamlDeclarativeSource
from airbyte_cdk.sources.source import TState
from airbyte_cdk.sources.streams import Stream
from airbyte_cdk.sources.streams.http import HttpClient
from airbyte_cdk.sources.streams.http.requests_native_auth import TokenAuthenticator
from source_zendesk_support.streams import DATETIME_FORMAT, ZendeskConfigException
from .streams import (
ArticleComments,
ArticleCommentVotes,
Articles,
ArticleVotes,
PostComments,
PostCommentVotes,
Posts,
PostVotes,
TicketMetrics,
Tickets,
UserIdentities,
UserSettingsStream,
)
logger = logging.getLogger("airbyte")
@@ -49,21 +32,6 @@ class SourceZendeskSupport(YamlDeclarativeSource):
def __init__(self, catalog: Optional[ConfiguredAirbyteCatalog], config: Optional[Mapping[str, Any]], state: TState, **kwargs):
super().__init__(catalog=catalog, config=config, state=state, **{"path_to_yaml": "manifest.yaml"})
@classmethod
def get_default_start_date(cls) -> str:
"""
Gets the default start date for data retrieval.
The default date is set to the current date and time in UTC minus 2 years.
Returns:
str: The default start date in 'YYYY-MM-DDTHH:mm:ss[Z]' format.
Note:
Start Date is a required request parameter for Zendesk Support API streams.
"""
return pendulum.now(tz="UTC").subtract(years=2).format("YYYY-MM-DDTHH:mm:ss[Z]")
@classmethod
def get_authenticator(cls, config: Mapping[str, Any]) -> [TokenAuthenticator, BasicApiTokenAuthenticator]:
# new authentication flow
@@ -74,7 +42,10 @@ class SourceZendeskSupport(YamlDeclarativeSource):
elif auth.get("credentials") == "api_token":
return BasicApiTokenAuthenticator(config["credentials"]["email"], config["credentials"]["api_token"])
else:
raise ZendeskConfigException(message=f"Not implemented authorization method: {config['credentials']}")
raise AirbyteTracedException(
failure_type=FailureType.config_error,
message=f"Not implemented authorization method: {config['credentials']}",
)
def check_connection(self, logger, config) -> Tuple[bool, any]:
"""Connection check to validate that the user-provided config can be used to connect to the underlying API
@@ -84,13 +55,21 @@ class SourceZendeskSupport(YamlDeclarativeSource):
:return Tuple[bool, any]: (True, None) if the input config can be used to connect to the API successfully,
(False, error) otherwise.
"""
auth = self.get_authenticator(config)
http_client = HttpClient(
name="user_settings",
logger=self.logger,
authenticator=self.get_authenticator(config),
)
try:
start_date = datetime.strptime(config["start_date"], DATETIME_FORMAT) if config["start_date"] else None
settings = UserSettingsStream(config["subdomain"], authenticator=auth, start_date=start_date).get_settings()
_, response = http_client.send_request(
http_method="GET",
url=f"https://{config['subdomain']}.zendesk.com/api/v2/account/settings.json",
request_kwargs={},
)
except Exception as e:
return False, e
active_features = [k for k, v in settings.get("active_features", {}).items() if v]
active_features = [k for k, v in response.json().get("settings", {}).get("active_features", {}).items() if v]
if "organization_access_enabled" not in active_features:
return (
False,
@@ -99,41 +78,6 @@ class SourceZendeskSupport(YamlDeclarativeSource):
)
return True, None
@classmethod
def convert_config2stream_args(cls, config: Mapping[str, Any]) -> Mapping[str, Any]:
"""Convert input configs to parameters of the future streams
This function is used by unit tests too
"""
return {
"subdomain": config["subdomain"],
"start_date": config.get("start_date", cls.get_default_start_date()),
"authenticator": cls.get_authenticator(config),
"ignore_pagination": config.get("ignore_pagination", False),
}
def get_nested_streams(self, config: Mapping[str, Any]) -> List[Stream]:
"""Returns relevant a list of available streams
:param config: A Mapping of the user input configuration as defined in the connector spec.
"""
args = self.convert_config2stream_args(config)
tickets = Tickets(**args)
streams = [
Articles(**args),
ArticleComments(**args),
ArticleCommentVotes(**args),
ArticleVotes(**args),
Posts(**args),
PostComments(**args),
PostCommentVotes(**args),
PostVotes(**args),
tickets,
TicketMetrics(**args),
UserIdentities(**args),
]
return streams
def check_enterprise_streams(self, declarative_streams: List[Stream]) -> List[Stream]:
"""Returns relevant a list of available streams
:param config: A Mapping of the user input configuration as defined in the connector spec.
@@ -159,9 +103,4 @@ class SourceZendeskSupport(YamlDeclarativeSource):
def streams(self, config: Mapping[str, Any]) -> List[Stream]:
declarative_streams = super().streams(config)
nested_streams = self.get_nested_streams(config)
declarative_streams.extend(nested_streams)
declarative_streams = self.check_enterprise_streams(declarative_streams)
return declarative_streams
return self.check_enterprise_streams(declarative_streams)

View File

@@ -89,8 +89,8 @@
"ignore_pagination": {
"type": "boolean",
"default": false,
"description": "Makes each stream read a single page of data.",
"title": "Should the connector read the second and further pages of data.",
"description": "[Deprecated] Makes each stream read a single page of data.",
"title": "[Deprecated] Should the connector read the second and further pages of data.",
"airbyte_hidden": true
},
"num_workers": {

View File

@@ -1,16 +1,17 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
import base64
from typing import Any, Dict
from typing import Any, Dict, Optional
from pendulum.datetime import DateTime
class ConfigBuilder:
def __init__(self) -> None:
self._subdomain: str = None
self._start_date: str = None
self._subdomain: Optional[str] = None
self._start_date: Optional[str] = None
self._credentials: Dict[str, str] = {}
self._ignore_pagination: Optional[bool] = None
def with_subdomain(self, subdomain: str) -> "ConfigBuilder":
self._subdomain = subdomain
@@ -31,6 +32,10 @@ class ConfigBuilder:
self._start_date = start_date.format("YYYY-MM-DDTHH:mm:ss[Z]")
return self
def with_ignore_pagination(self) -> "ConfigBuilder":
self._ignore_pagination = True
return self
def build(self) -> Dict[str, Any]:
config = {}
if self._subdomain:
@@ -39,4 +44,6 @@ class ConfigBuilder:
config["start_date"] = self._start_date
if self._credentials:
config["credentials"] = self._credentials
if self._ignore_pagination:
config["ignore_pagination"] = self._ignore_pagination
return config

View File

@@ -1,4 +1,5 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Optional
import pendulum
from pendulum.datetime import DateTime
@@ -47,12 +48,14 @@ def given_ticket_forms(
return ticket_forms_record_builder
def given_posts(http_mocker: HttpMocker, start_date: DateTime, api_token_authenticator: ApiTokenAuthenticator) -> PostsRecordBuilder:
def given_posts(
http_mocker: HttpMocker, start_date: DateTime, api_token_authenticator: ApiTokenAuthenticator, updated_at: Optional[DateTime] = None
) -> PostsRecordBuilder:
"""
Posts requests setup
"""
posts_record_builder = PostsRecordBuilder.posts_record().with_field(
FieldPath("updated_at"), datetime_to_string(start_date.add(seconds=1))
FieldPath("updated_at"), datetime_to_string(updated_at if updated_at else start_date.add(seconds=1))
)
http_mocker.get(
PostsRequestBuilder.posts_endpoint(api_token_authenticator)
@@ -65,13 +68,17 @@ def given_posts(http_mocker: HttpMocker, start_date: DateTime, api_token_authent
def given_post_comments(
http_mocker: HttpMocker, start_date: DateTime, post_id: int, api_token_authenticator: ApiTokenAuthenticator
http_mocker: HttpMocker,
start_date: DateTime,
post_id: int,
api_token_authenticator: ApiTokenAuthenticator,
updated_at: Optional[DateTime] = None,
) -> PostsCommentsRecordBuilder:
"""
Post Comments requests setup
"""
post_comments_record_builder = PostsCommentsRecordBuilder.posts_commetns_record().with_field(
FieldPath("updated_at"), datetime_to_string(start_date.add(seconds=1))
post_comments_record_builder = PostsCommentsRecordBuilder.posts_comments_record().with_field(
FieldPath("updated_at"), datetime_to_string(updated_at if updated_at else start_date.add(seconds=1))
)
http_mocker.get(
PostsCommentsRequestBuilder.posts_comments_endpoint(api_token_authenticator, post_id)

View File

@@ -0,0 +1,111 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from datetime import datetime, timezone
from unittest import TestCase
import freezegun
import pendulum
from airbyte_cdk.models import AirbyteStateBlob, SyncMode
from airbyte_cdk.test.mock_http import HttpMocker
from airbyte_cdk.test.state_builder import StateBuilder
from .config import ConfigBuilder
from .utils import datetime_to_string, read_stream
from .zs_requests.articles_request_builder import ArticlesRequestBuilder
from .zs_requests.request_authenticators import ApiTokenAuthenticator
from .zs_responses.articles_response_builder import ArticlesResponseBuilder
from .zs_responses.records.articles_records_builder import ArticlesRecordBuilder
_NOW = datetime.now(timezone.utc)
_START_DATE = pendulum.now(tz="UTC").subtract(years=2)
@freezegun.freeze_time(_NOW.isoformat())
class TestArticlesStream(TestCase):
def _config(self) -> ConfigBuilder:
return (
ConfigBuilder()
.with_basic_auth_credentials("user@example.com", "password")
.with_subdomain("d3v-airbyte")
.with_start_date(pendulum.now(tz="UTC").subtract(years=2))
)
def _get_authenticator(self, config):
return ApiTokenAuthenticator(email=config["credentials"]["email"], password=config["credentials"]["api_token"])
@HttpMocker()
def test_given_one_page_when_read_then_return_records(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
http_mocker.get(
ArticlesRequestBuilder.articles_endpoint(api_token_authenticator).with_start_time(_START_DATE).build(),
ArticlesResponseBuilder.response()
.with_record(ArticlesRecordBuilder.record())
.with_record(ArticlesRecordBuilder.record())
.build(),
)
output = read_stream("articles", SyncMode.full_refresh, config)
assert len(output.records) == 2
@HttpMocker()
def test_given_next_page_when_read_then_paginate(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
next_page_http_request = (
ArticlesRequestBuilder.articles_endpoint(api_token_authenticator).with_start_time(_START_DATE.add(days=10)).build()
)
http_mocker.get(
ArticlesRequestBuilder.articles_endpoint(api_token_authenticator).with_start_time(_START_DATE).build(),
ArticlesResponseBuilder.response(next_page_http_request)
.with_record(ArticlesRecordBuilder.record())
.with_record(ArticlesRecordBuilder.record())
.with_pagination()
.build(),
)
http_mocker.get(
next_page_http_request,
ArticlesResponseBuilder.response().with_record(ArticlesRecordBuilder.record()).build(),
)
output = read_stream("articles", SyncMode.full_refresh, config)
assert len(output.records) == 3
@HttpMocker()
def test_when_read_then_set_state_value_to_most_recent_cursor_value(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
most_recent_cursor_value = _START_DATE.add(days=2)
http_mocker.get(
ArticlesRequestBuilder.articles_endpoint(api_token_authenticator).with_start_time(_START_DATE).build(),
ArticlesResponseBuilder.response()
.with_record(ArticlesRecordBuilder.record().with_cursor(datetime_to_string(most_recent_cursor_value)))
.build(),
)
output = read_stream("articles", SyncMode.full_refresh, config)
assert output.most_recent_state.stream_state.__dict__ == {"updated_at": str(most_recent_cursor_value.int_timestamp)}
@HttpMocker()
def test_given_input_state_when_read_then_set_state_value_to_most_recent_cursor_value(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
state_cursor_value = _START_DATE.add(days=2)
http_mocker.get(
ArticlesRequestBuilder.articles_endpoint(api_token_authenticator).with_start_time(state_cursor_value).build(),
ArticlesResponseBuilder.response().with_record(ArticlesRecordBuilder.record()).build(),
)
output = read_stream(
"articles",
SyncMode.full_refresh,
config,
StateBuilder().with_stream_state("articles", {"updated_at": datetime_to_string(state_cursor_value)}).build(),
)
assert len(output.records) == 1

View File

@@ -7,7 +7,7 @@ from unittest.mock import patch
import freezegun
import pendulum
from airbyte_cdk.models import AirbyteStateBlob, SyncMode
from airbyte_cdk.models import AirbyteStateBlob, AirbyteStreamStatus, SyncMode
from airbyte_cdk.models import Level as LogLevel
from airbyte_cdk.test.mock_http import HttpMocker
from airbyte_cdk.test.mock_http.response_builder import FieldPath
@@ -26,7 +26,7 @@ _NOW = datetime.now(timezone.utc)
@freezegun.freeze_time(_NOW.isoformat())
class TestPostsCommentsVotesStreamFullRefresh(TestCase):
class TestPostsCommentVotesStreamFullRefresh(TestCase):
@property
def _config(self):
return (
@@ -95,13 +95,11 @@ class TestPostsCommentsVotesStreamFullRefresh(TestCase):
output = read_stream("post_comment_votes", SyncMode.full_refresh, self._config)
assert len(output.records) == 0
info_logs = get_log_messages_by_log_level(output.logs, LogLevel.INFO)
assert output.get_stream_statuses("post_comment_votes")[-1] == AirbyteStreamStatus.INCOMPLETE
assert any(
[
"Forbidden. Please ensure the authenticated user has access to this stream. If the issue persists, contact Zendesk support."
in error
for error in info_logs
"failed with status code '403' and error message" in error
for error in get_log_messages_by_log_level(output.logs, LogLevel.ERROR)
]
)
@@ -131,13 +129,11 @@ class TestPostsCommentsVotesStreamFullRefresh(TestCase):
output = read_stream("post_comment_votes", SyncMode.full_refresh, self._config)
assert len(output.records) == 0
info_logs = get_log_messages_by_log_level(output.logs, LogLevel.INFO)
assert output.get_stream_statuses("post_comment_votes")[-1] == AirbyteStreamStatus.INCOMPLETE
assert any(
[
"Not found. Please ensure the authenticated user has access to this stream. If the issue persists, contact Zendesk support."
in error
for error in info_logs
"failed with status code '404' and error message" in error
for error in get_log_messages_by_log_level(output.logs, LogLevel.ERROR)
]
)
@@ -175,7 +171,7 @@ class TestPostsCommentsVotesStreamFullRefresh(TestCase):
@freezegun.freeze_time(_NOW.isoformat())
class TestPostsCommentsStreamIncremental(TestCase):
class TestPostsCommentVotesStreamIncremental(TestCase):
@property
def _config(self):
return (
@@ -197,13 +193,21 @@ class TestPostsCommentsStreamIncremental(TestCase):
api_token_authenticator = self._get_authenticator(self._config)
_ = given_ticket_forms(http_mocker, string_to_datetime(self._config["start_date"]), api_token_authenticator)
posts_record_builder = given_posts(http_mocker, string_to_datetime(self._config["start_date"]), api_token_authenticator)
post_updated_at = string_to_datetime(self._config["start_date"]).add(minutes=5)
posts_record_builder = given_posts(
http_mocker, string_to_datetime(self._config["start_date"]), api_token_authenticator, post_updated_at
)
post = posts_record_builder.build()
posts_comments_record_builder = given_post_comments(
http_mocker, string_to_datetime(self._config["start_date"]), post["id"], api_token_authenticator
post_comments_updated_at = string_to_datetime(self._config["start_date"]).add(minutes=10)
post_comments_record_builder = given_post_comments(
http_mocker,
string_to_datetime(self._config["start_date"]),
post["id"],
api_token_authenticator,
post_comments_updated_at,
)
post_comment = posts_comments_record_builder.build()
post_comment = post_comments_record_builder.build()
post_comment_votes_record_builder = PostCommentVotesRecordBuilder.post_commetn_votes_record()
post_comment_votes = post_comment_votes_record_builder.build()
@@ -222,7 +226,37 @@ class TestPostsCommentsStreamIncremental(TestCase):
assert len(output.records) == 1
assert output.most_recent_state.stream_descriptor.name == "post_comment_votes"
assert output.most_recent_state.stream_state == AirbyteStateBlob({"updated_at": post_comment_votes["updated_at"]})
post_comment_votes_state_value = str(string_to_datetime(post_comment_votes["updated_at"]).int_timestamp)
assert output.most_recent_state.stream_state == AirbyteStateBlob(
{
"use_global_cursor": False,
"states": [
{
"partition": {
"id": {"comment_id": post_comment["id"], "post_id": post["id"]},
"parent_slice": {"parent_slice": {}, "post_id": post["id"]},
},
"cursor": {"updated_at": post_comment_votes_state_value},
}
],
"state": {"updated_at": post_comment_votes_state_value},
"lookback_window": 0,
"parent_state": {
"post_comments": {
"use_global_cursor": False,
"state": {"updated_at": datetime_to_string(post_comments_updated_at)},
"lookback_window": 0,
"states": [
{
"partition": {"parent_slice": {}, "post_id": post["id"]},
"cursor": {"updated_at": datetime_to_string(post_comments_updated_at)},
}
],
"parent_state": {"posts": {"updated_at": datetime_to_string(post_updated_at)}},
}
},
}
)
@HttpMocker()
def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker):
@@ -237,10 +271,18 @@ class TestPostsCommentsStreamIncremental(TestCase):
state = {"updated_at": datetime_to_string(state_start_date)}
posts_record_builder = given_posts(http_mocker, state_start_date, api_token_authenticator)
post_updated_at = state_start_date.add(minutes=5)
posts_record_builder = given_posts(http_mocker, state_start_date, api_token_authenticator, post_updated_at)
post = posts_record_builder.build()
post_comments_record_builder = given_post_comments(http_mocker, state_start_date, post["id"], api_token_authenticator)
post_comments_updated_at = state_start_date.add(minutes=10)
post_comments_record_builder = given_post_comments(
http_mocker,
state_start_date,
post["id"],
api_token_authenticator,
post_comments_updated_at,
)
post_comment = post_comments_record_builder.build()
post_comment_votes_first_record_builder = PostCommentVotesRecordBuilder.post_commetn_votes_record().with_field(
@@ -253,7 +295,11 @@ class TestPostsCommentsStreamIncremental(TestCase):
.with_start_time(datetime_to_string(state_start_date))
.with_page_size(100)
.build(),
PostCommentVotesResponseBuilder.post_comment_votes_response()
PostCommentVotesResponseBuilder.post_comment_votes_response(
PostCommentVotesRequestBuilder.post_comment_votes_endpoint(api_token_authenticator, post["id"], post_comment["id"])
.with_page_size(100)
.build()
)
.with_pagination()
.with_record(post_comment_votes_first_record_builder)
.build(),
@@ -280,4 +326,34 @@ class TestPostsCommentsStreamIncremental(TestCase):
assert len(output.records) == 2
assert output.most_recent_state.stream_descriptor.name == "post_comment_votes"
assert output.most_recent_state.stream_state == AirbyteStateBlob({"updated_at": datetime_to_string(last_page_record_updated_at)})
post_comment_votes_state_value = str(string_to_datetime(post_comment_votes_last_record_builder.build()["updated_at"]).int_timestamp)
assert output.most_recent_state.stream_state == AirbyteStateBlob(
{
"use_global_cursor": False,
"states": [
{
"partition": {
"id": {"comment_id": post_comment["id"], "post_id": post["id"]},
"parent_slice": {"parent_slice": {}, "post_id": post["id"]},
},
"cursor": {"updated_at": post_comment_votes_state_value},
}
],
"state": {"updated_at": post_comment_votes_state_value},
"lookback_window": 0,
"parent_state": {
"post_comments": {
"use_global_cursor": False,
"state": {"updated_at": datetime_to_string(post_comments_updated_at)},
"lookback_window": 0,
"states": [
{
"partition": {"parent_slice": {}, "post_id": post["id"]},
"cursor": {"updated_at": datetime_to_string(post_comments_updated_at)},
}
],
"parent_state": {"posts": {"updated_at": datetime_to_string(post_updated_at)}},
}
},
}
)

View File

@@ -7,7 +7,7 @@ from unittest.mock import patch
import freezegun
import pendulum
from airbyte_cdk.models import AirbyteStateBlob, SyncMode
from airbyte_cdk.models import AirbyteStateBlob, AirbyteStreamStatus, SyncMode
from airbyte_cdk.models import Level as LogLevel
from airbyte_cdk.test.mock_http import HttpMocker
from airbyte_cdk.test.mock_http.response_builder import FieldPath
@@ -23,6 +23,7 @@ from .zs_responses.records import PostsCommentsRecordBuilder
_NOW = datetime.now(timezone.utc)
_START_DATE = pendulum.now(tz="UTC").subtract(years=2)
@freezegun.freeze_time(_NOW.isoformat())
@@ -33,7 +34,7 @@ class TestPostsCommentsStreamFullRefresh(TestCase):
ConfigBuilder()
.with_basic_auth_credentials("user@example.com", "password")
.with_subdomain("d3v-airbyte")
.with_start_date(pendulum.now(tz="UTC").subtract(years=2))
.with_start_date(_START_DATE)
.build()
)
@@ -56,7 +57,7 @@ class TestPostsCommentsStreamFullRefresh(TestCase):
.with_start_time(self._config["start_date"])
.with_page_size(100)
.build(),
PostsCommentsResponseBuilder.posts_comments_response().with_record(PostsCommentsRecordBuilder.posts_commetns_record()).build(),
PostsCommentsResponseBuilder.posts_comments_response().with_record(PostsCommentsRecordBuilder.posts_comments_record()).build(),
)
output = read_stream("post_comments", SyncMode.full_refresh, self._config)
@@ -83,13 +84,11 @@ class TestPostsCommentsStreamFullRefresh(TestCase):
output = read_stream("post_comments", SyncMode.full_refresh, self._config)
assert len(output.records) == 0
info_logs = get_log_messages_by_log_level(output.logs, LogLevel.INFO)
assert output.get_stream_statuses("post_comments")[-1] == AirbyteStreamStatus.INCOMPLETE
assert any(
[
"Forbidden. Please ensure the authenticated user has access to this stream. If the issue persists, contact Zendesk support."
in error
for error in info_logs
"failed with status code '403' and error message" in error
for error in get_log_messages_by_log_level(output.logs, LogLevel.ERROR)
]
)
@@ -112,15 +111,13 @@ class TestPostsCommentsStreamFullRefresh(TestCase):
ErrorResponseBuilder.response_with_status(404).build(),
)
output = read_stream("post_comments", SyncMode.full_refresh, self._config)
output = read_stream("post_comments", SyncMode.full_refresh, self._config, expecting_exception=True)
assert len(output.records) == 0
info_logs = get_log_messages_by_log_level(output.logs, LogLevel.INFO)
assert output.get_stream_statuses("post_comments")[-1] == AirbyteStreamStatus.INCOMPLETE
assert any(
[
"Not found. Please ensure the authenticated user has access to this stream. If the issue persists, contact Zendesk support."
in error
for error in info_logs
"failed with status code '404' and error message" in error
for error in get_log_messages_by_log_level(output.logs, LogLevel.ERROR)
]
)
@@ -160,7 +157,7 @@ class TestPostsCommentsStreamIncremental(TestCase):
ConfigBuilder()
.with_basic_auth_credentials("user@example.com", "password")
.with_subdomain("d3v-airbyte")
.with_start_date(pendulum.now(tz="UTC").subtract(years=2))
.with_start_date(_START_DATE)
.build()
)
@@ -177,7 +174,7 @@ class TestPostsCommentsStreamIncremental(TestCase):
posts_record_builder = given_posts(http_mocker, string_to_datetime(self._config["start_date"]), api_token_authenticator)
post = posts_record_builder.build()
post_comments_record_builder = PostsCommentsRecordBuilder.posts_commetns_record()
post_comments_record_builder = PostsCommentsRecordBuilder.posts_comments_record()
http_mocker.get(
PostsCommentsRequestBuilder.posts_comments_endpoint(api_token_authenticator, post["id"])
@@ -191,8 +188,32 @@ class TestPostsCommentsStreamIncremental(TestCase):
assert len(output.records) == 1
post_comment = post_comments_record_builder.build()
assert output.most_recent_state.stream_descriptor.name == "post_comments"
assert output.most_recent_state.stream_state == AirbyteStateBlob({"updated_at": post_comment["updated_at"]})
assert output.most_recent_state.stream_descriptor.name == "post_comments" # 1687393942.0
post_comments_state_value = str(string_to_datetime(post_comment["updated_at"]).int_timestamp)
assert (
output.most_recent_state.stream_state
== AirbyteStateBlob(
{
"lookback_window": 0,
"parent_state": {
"posts": {"updated_at": post["updated_at"]}
}, # note that this state does not have the concurrent format because SubstreamPartitionRouter is still relying on the declarative cursor
"state": {"updated_at": post_comments_state_value},
"states": [
{
"partition": {
"parent_slice": {},
"post_id": post["id"],
},
"cursor": {
"updated_at": post_comments_state_value,
},
}
],
"use_global_cursor": False,
}
)
)
@HttpMocker()
def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker):
@@ -210,7 +231,7 @@ class TestPostsCommentsStreamIncremental(TestCase):
posts_record_builder = given_posts(http_mocker, state_start_date, api_token_authenticator)
post = posts_record_builder.build()
post_comments_first_record_builder = PostsCommentsRecordBuilder.posts_commetns_record().with_field(
post_comments_first_record_builder = PostsCommentsRecordBuilder.posts_comments_record().with_field(
FieldPath("updated_at"), datetime_to_string(first_page_record_updated_at)
)
@@ -220,16 +241,16 @@ class TestPostsCommentsStreamIncremental(TestCase):
.with_start_time(datetime_to_string(state_start_date))
.with_page_size(100)
.build(),
PostsCommentsResponseBuilder.posts_comments_response()
PostsCommentsResponseBuilder.posts_comments_response(
PostsCommentsRequestBuilder.posts_comments_endpoint(api_token_authenticator, post["id"]).with_page_size(100).build()
)
.with_pagination()
.with_record(post_comments_first_record_builder)
.build(),
)
post_comments_last_record_builder = (
PostsCommentsRecordBuilder.posts_commetns_record()
.with_id("last_record_id_from_last_page")
.with_field(FieldPath("updated_at"), datetime_to_string(last_page_record_updated_at))
post_comments_last_record_builder = PostsCommentsRecordBuilder.posts_comments_record().with_field(
FieldPath("updated_at"), datetime_to_string(last_page_record_updated_at)
)
# Read second page request mock
@@ -247,4 +268,24 @@ class TestPostsCommentsStreamIncremental(TestCase):
assert len(output.records) == 2
assert output.most_recent_state.stream_descriptor.name == "post_comments"
assert output.most_recent_state.stream_state == AirbyteStateBlob({"updated_at": datetime_to_string(last_page_record_updated_at)})
post_comments_state_value = str(last_page_record_updated_at.int_timestamp)
assert output.most_recent_state.stream_state == AirbyteStateBlob(
{
"lookback_window": 0,
"parent_state": {"posts": {"updated_at": post["updated_at"]}},
# note that this state does not have the concurrent format because SubstreamPartitionRouter is still relying on the declarative cursor
"state": {"updated_at": post_comments_state_value},
"states": [
{
"partition": {
"parent_slice": {},
"post_id": post["id"],
},
"cursor": {
"updated_at": post_comments_state_value,
},
}
],
"use_global_cursor": False,
}
)

View File

@@ -7,7 +7,7 @@ from unittest.mock import patch
import freezegun
import pendulum
from airbyte_cdk.models import AirbyteStateBlob, SyncMode
from airbyte_cdk.models import AirbyteStateBlob, AirbyteStreamStatus, SyncMode
from airbyte_cdk.models import Level as LogLevel
from airbyte_cdk.test.mock_http import HttpMocker
from airbyte_cdk.test.mock_http.response_builder import FieldPath
@@ -83,13 +83,11 @@ class TestPostsVotesStreamFullRefresh(TestCase):
output = read_stream("post_votes", SyncMode.full_refresh, self._config)
assert len(output.records) == 0
info_logs = get_log_messages_by_log_level(output.logs, LogLevel.INFO)
assert output.get_stream_statuses("post_votes")[-1] == AirbyteStreamStatus.INCOMPLETE
assert any(
[
"Forbidden. Please ensure the authenticated user has access to this stream. If the issue persists, contact Zendesk support."
in error
for error in info_logs
"failed with status code '403' and error message" in error
for error in get_log_messages_by_log_level(output.logs, LogLevel.ERROR)
]
)
@@ -114,13 +112,11 @@ class TestPostsVotesStreamFullRefresh(TestCase):
output = read_stream("post_votes", SyncMode.full_refresh, self._config)
assert len(output.records) == 0
info_logs = get_log_messages_by_log_level(output.logs, LogLevel.INFO)
assert output.get_stream_statuses("post_votes")[-1] == AirbyteStreamStatus.INCOMPLETE
assert any(
[
"Not found. Please ensure the authenticated user has access to this stream. If the issue persists, contact Zendesk support."
in error
for error in info_logs
"failed with status code '404' and error message" in error
for error in get_log_messages_by_log_level(output.logs, LogLevel.ERROR)
]
)
@@ -177,22 +173,46 @@ class TestPostsVotesStreamIncremental(TestCase):
posts_record_builder = given_posts(http_mocker, string_to_datetime(self._config["start_date"]), api_token_authenticator)
post = posts_record_builder.build()
post_comments_record_builder = PostsVotesRecordBuilder.posts_votes_record()
post_votes_record_builder = PostsVotesRecordBuilder.posts_votes_record()
http_mocker.get(
PostsVotesRequestBuilder.posts_votes_endpoint(api_token_authenticator, post["id"])
.with_start_time(self._config["start_date"])
.with_page_size(100)
.build(),
PostsVotesResponseBuilder.posts_votes_response().with_record(post_comments_record_builder).build(),
PostsVotesResponseBuilder.posts_votes_response().with_record(post_votes_record_builder).build(),
)
output = read_stream("post_votes", SyncMode.incremental, self._config)
assert len(output.records) == 1
post_comment = post_comments_record_builder.build()
post_vote = post_votes_record_builder.build()
assert output.most_recent_state.stream_descriptor.name == "post_votes"
assert output.most_recent_state.stream_state == AirbyteStateBlob({"updated_at": post_comment["updated_at"]})
post_comments_state_value = str(string_to_datetime(post_vote["updated_at"]).int_timestamp)
assert (
output.most_recent_state.stream_state
== AirbyteStateBlob(
{
"lookback_window": 0,
"parent_state": {
"posts": {"updated_at": post["updated_at"]}
}, # note that this state does not have the concurrent format because SubstreamPartitionRouter is still relying on the declarative cursor
"state": {"updated_at": post_comments_state_value},
"states": [
{
"partition": {
"parent_slice": {},
"post_id": post["id"],
},
"cursor": {
"updated_at": post_comments_state_value,
},
}
],
"use_global_cursor": False,
}
)
)
@HttpMocker()
def test_given_state_and_pagination_when_read_then_return_records(self, http_mocker):
@@ -210,7 +230,7 @@ class TestPostsVotesStreamIncremental(TestCase):
posts_record_builder = given_posts(http_mocker, state_start_date, api_token_authenticator)
post = posts_record_builder.build()
post_comments_first_record_builder = PostsVotesRecordBuilder.posts_votes_record().with_field(
post_votes_first_record_builder = PostsVotesRecordBuilder.posts_votes_record().with_field(
FieldPath("updated_at"), datetime_to_string(first_page_record_updated_at)
)
@@ -220,10 +240,15 @@ class TestPostsVotesStreamIncremental(TestCase):
.with_start_time(datetime_to_string(state_start_date))
.with_page_size(100)
.build(),
PostsVotesResponseBuilder.posts_votes_response().with_pagination().with_record(post_comments_first_record_builder).build(),
PostsVotesResponseBuilder.posts_votes_response(
PostsVotesRequestBuilder.posts_votes_endpoint(api_token_authenticator, post["id"]).with_page_size(100).build()
)
.with_pagination()
.with_record(post_votes_first_record_builder)
.build(),
)
post_comments_last_record_builder = (
post_votes_last_record_builder = (
PostsVotesRecordBuilder.posts_votes_record()
.with_id("last_record_id_from_last_page")
.with_field(FieldPath("updated_at"), datetime_to_string(last_page_record_updated_at))
@@ -235,7 +260,7 @@ class TestPostsVotesStreamIncremental(TestCase):
.with_page_after("after-cursor")
.with_page_size(100)
.build(),
PostsVotesResponseBuilder.posts_votes_response().with_record(post_comments_last_record_builder).build(),
PostsVotesResponseBuilder.posts_votes_response().with_record(post_votes_last_record_builder).build(),
)
output = read_stream(
@@ -244,4 +269,24 @@ class TestPostsVotesStreamIncremental(TestCase):
assert len(output.records) == 2
assert output.most_recent_state.stream_descriptor.name == "post_votes"
assert output.most_recent_state.stream_state == AirbyteStateBlob({"updated_at": datetime_to_string(last_page_record_updated_at)})
post_comments_state_value = str(last_page_record_updated_at.int_timestamp)
assert output.most_recent_state.stream_state == AirbyteStateBlob(
{
"lookback_window": 0,
"parent_state": {"posts": {"updated_at": post["updated_at"]}},
# note that this state does not have the concurrent format because SubstreamPartitionRouter is still relying on the declarative cursor
"state": {"updated_at": post_comments_state_value},
"states": [
{
"partition": {
"parent_slice": {},
"post_id": post["id"],
},
"cursor": {
"updated_at": post_comments_state_value,
},
}
],
"use_global_cursor": False,
}
)

View File

@@ -0,0 +1,135 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from datetime import datetime, timezone
from unittest import TestCase
import freezegun
import pendulum
from airbyte_cdk.models import AirbyteStateBlob, SyncMode
from airbyte_cdk.test.mock_http import HttpMocker
from airbyte_cdk.test.state_builder import StateBuilder
from .config import ConfigBuilder
from .utils import datetime_to_string, read_stream
from .zs_requests import PostsRequestBuilder
from .zs_requests.request_authenticators import ApiTokenAuthenticator
from .zs_requests.request_authenticators.authenticator import Authenticator
from .zs_responses import PostsResponseBuilder
from .zs_responses.records import PostsRecordBuilder
_NOW = datetime.now(timezone.utc)
_START_DATE = pendulum.now(tz="UTC").subtract(years=2)
@freezegun.freeze_time(_NOW.isoformat())
class TestPostsStream(TestCase):
def _config(self) -> ConfigBuilder:
return (
ConfigBuilder()
.with_basic_auth_credentials("user@example.com", "password")
.with_subdomain("d3v-airbyte")
.with_start_date(pendulum.now(tz="UTC").subtract(years=2))
)
def _get_authenticator(self, config):
return ApiTokenAuthenticator(email=config["credentials"]["email"], password=config["credentials"]["api_token"])
def _base_posts_request(self, authenticator: Authenticator) -> PostsRequestBuilder:
return PostsRequestBuilder.posts_endpoint(authenticator).with_page_size(100)
@HttpMocker()
def test_given_one_page_when_read_then_return_records(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
http_mocker.get(
self._base_posts_request(api_token_authenticator).with_start_time(datetime_to_string(_START_DATE)).build(),
PostsResponseBuilder.posts_response()
.with_record(PostsRecordBuilder.posts_record())
.with_record(PostsRecordBuilder.posts_record())
.build(),
)
output = read_stream("posts", SyncMode.full_refresh, config)
assert len(output.records) == 2
@HttpMocker()
def test_given_has_more_when_read_then_paginate(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
http_mocker.get(
self._base_posts_request(api_token_authenticator).with_start_time(datetime_to_string(_START_DATE)).build(),
PostsResponseBuilder.posts_response(self._base_posts_request(api_token_authenticator).build())
.with_record(PostsRecordBuilder.posts_record())
.with_record(PostsRecordBuilder.posts_record())
.with_pagination()
.build(),
)
http_mocker.get(
self._base_posts_request(api_token_authenticator).with_after_cursor("after-cursor").build(),
PostsResponseBuilder.posts_response().with_record(PostsRecordBuilder.posts_record()).build(),
)
output = read_stream("posts", SyncMode.full_refresh, config)
assert len(output.records) == 3
@HttpMocker()
def test_when_read_then_set_state_value_to_most_recent_cursor_value(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
most_recent_cursor_value = _START_DATE.add(days=2)
http_mocker.get(
PostsRequestBuilder.posts_endpoint(api_token_authenticator)
.with_start_time(datetime_to_string(_START_DATE))
.with_page_size(100)
.build(),
PostsResponseBuilder.posts_response()
.with_record(PostsRecordBuilder.posts_record().with_cursor(datetime_to_string(most_recent_cursor_value)))
.with_record(PostsRecordBuilder.posts_record().with_cursor(datetime_to_string(_START_DATE.add(days=1))))
.build(),
)
output = read_stream("posts", SyncMode.full_refresh, config)
assert output.most_recent_state.stream_state == AirbyteStateBlob({"updated_at": str(most_recent_cursor_value.int_timestamp)})
@HttpMocker()
def test_given_input_state_as_old_format_when_read_then_set_state_value_to_most_recent_cursor_value(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
state_cursor_value = datetime_to_string(_START_DATE.add(days=2))
http_mocker.get(
PostsRequestBuilder.posts_endpoint(api_token_authenticator).with_start_time(state_cursor_value).with_page_size(100).build(),
PostsResponseBuilder.posts_response().with_record(PostsRecordBuilder.posts_record()).build(),
)
output = read_stream(
"posts", SyncMode.full_refresh, config, StateBuilder().with_stream_state("posts", {"updated_at": state_cursor_value}).build()
)
assert len(output.records) == 1
@HttpMocker()
def test_given_input_state_when_read_then_set_state_value_to_most_recent_cursor_value(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
state_cursor_value = _START_DATE.add(days=2)
http_mocker.get(
PostsRequestBuilder.posts_endpoint(api_token_authenticator)
.with_start_time(datetime_to_string(state_cursor_value))
.with_page_size(100)
.build(),
PostsResponseBuilder.posts_response().with_record(PostsRecordBuilder.posts_record()).build(),
)
output = read_stream(
"posts",
SyncMode.full_refresh,
config,
StateBuilder().with_stream_state("posts", {"updated_at": str(state_cursor_value.int_timestamp)}).build(),
)
assert len(output.records) == 1

View File

@@ -5,14 +5,14 @@ from unittest import TestCase
import freezegun
import pendulum
from airbyte_cdk.models.airbyte_protocol import AirbyteStateBlob, SyncMode
from airbyte_cdk.models.airbyte_protocol import SyncMode
from airbyte_cdk.test.mock_http import HttpMocker
from airbyte_cdk.test.mock_http.response_builder import FieldPath
from airbyte_cdk.test.state_builder import StateBuilder
from .config import ConfigBuilder
from .helpers import given_ticket_forms, given_tickets_with_state
from .utils import read_stream, string_to_datetime
from .helpers import given_tickets_with_state
from .utils import read_stream
from .zs_requests import TicketMetricsRequestBuilder
from .zs_requests.request_authenticators import ApiTokenAuthenticator
from .zs_responses import TicketMetricsResponseBuilder
@@ -42,8 +42,6 @@ class TestTicketMetricsIncremental(TestCase):
def test_given_no_state_and_successful_sync_when_read_then_set_state_to_most_recently_read_record_cursor(self, http_mocker):
record_updated_at: str = pendulum.now(tz="UTC").subtract(days=1).format("YYYY-MM-DDThh:mm:ss") + "Z"
api_token_authenticator = self._get_authenticator(self._config)
_ = given_ticket_forms(http_mocker, string_to_datetime(self._config["start_date"]), api_token_authenticator)
state = StateBuilder().with_stream_state("ticket_metrics", state={}).build()
ticket_metrics_record_builder = TicketMetricsRecordBuilder.stateless_ticket_metrics_record().with_cursor(record_updated_at)
http_mocker.get(
@@ -51,28 +49,29 @@ class TestTicketMetricsIncremental(TestCase):
TicketMetricsResponseBuilder.stateless_ticket_metrics_response().with_record(ticket_metrics_record_builder).build(),
)
output = read_stream("ticket_metrics", SyncMode.incremental, self._config, state)
output = read_stream("ticket_metrics", SyncMode.incremental, self._config)
assert len(output.records) == 1
assert output.most_recent_state.stream_descriptor.name == "ticket_metrics"
assert output.most_recent_state.stream_state.__dict__ == {"_ab_updated_at": pendulum.parse(record_updated_at).int_timestamp}
assert output.most_recent_state.stream_state.__dict__ == {"_ab_updated_at": str(pendulum.parse(record_updated_at).int_timestamp)}
@HttpMocker()
def test_given_state_and_successful_sync_when_read_then_return_record(self, http_mocker):
def test_given_state_when_read_then_migrate_state_to_per_partition(self, http_mocker):
api_token_authenticator = self._get_authenticator(self._config)
state_cursor_value = pendulum.now(tz="UTC").subtract(days=2).int_timestamp
state = StateBuilder().with_stream_state("ticket_metrics", state={"_ab_updated_at": state_cursor_value}).build()
record_cursor_value = pendulum.now(tz="UTC").subtract(days=1)
parent_cursor_value = pendulum.now(tz="UTC").subtract(days=2)
tickets_records_builder = given_tickets_with_state(
http_mocker, pendulum.from_timestamp(state_cursor_value), record_cursor_value, api_token_authenticator
http_mocker, pendulum.from_timestamp(state_cursor_value), parent_cursor_value, api_token_authenticator
)
ticket = tickets_records_builder.build()
child_cursor_value = pendulum.now(tz="UTC").subtract(days=1)
ticket_metrics_first_record_builder = (
TicketMetricsRecordBuilder.stateful_ticket_metrics_record()
.with_field(FieldPath("ticket_id"), ticket["id"])
.with_cursor(ticket["generated_timestamp"])
.with_cursor(child_cursor_value.int_timestamp)
)
http_mocker.get(
@@ -84,4 +83,12 @@ class TestTicketMetricsIncremental(TestCase):
assert len(output.records) == 1
assert output.most_recent_state.stream_descriptor.name == "ticket_metrics"
assert output.most_recent_state.stream_state.__dict__ == {"_ab_updated_at": record_cursor_value.int_timestamp}
assert output.most_recent_state.stream_state.__dict__ == {
"lookback_window": 0,
"parent_state": {"tickets": {"generated_timestamp": parent_cursor_value.int_timestamp}},
"state": {"_ab_updated_at": str(child_cursor_value.int_timestamp)},
"states": [
{"cursor": {"_ab_updated_at": str(child_cursor_value.int_timestamp)}, "partition": {"parent_slice": {}, "ticket_id": 35436}}
],
"use_global_cursor": False,
}

View File

@@ -0,0 +1,109 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from datetime import datetime, timezone
from unittest import TestCase
import freezegun
import pendulum
from airbyte_cdk.models import SyncMode
from airbyte_cdk.test.mock_http import HttpMocker
from airbyte_cdk.test.state_builder import StateBuilder
from .config import ConfigBuilder
from .utils import datetime_to_string, read_stream
from .zs_requests.request_authenticators import ApiTokenAuthenticator
from .zs_requests.users_request_builder import UsersRequestBuilder
from .zs_responses.records.users_records_builder import UsersRecordBuilder
from .zs_responses.users_response_builder import UsersResponseBuilder
_NOW = datetime.now(timezone.utc)
_START_DATE = pendulum.now(tz="UTC").subtract(years=2)
_A_CURSOR = "a_cursor"
@freezegun.freeze_time(_NOW.isoformat())
class TestUserIdentitiesStream(TestCase):
def _config(self) -> ConfigBuilder:
return (
ConfigBuilder()
.with_basic_auth_credentials("user@example.com", "password")
.with_subdomain("d3v-airbyte")
.with_start_date(pendulum.now(tz="UTC").subtract(years=2))
)
def _get_authenticator(self, config):
return ApiTokenAuthenticator(email=config["credentials"]["email"], password=config["credentials"]["api_token"])
@HttpMocker()
def test_given_one_page_when_read_then_return_records(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
http_mocker.get(
UsersRequestBuilder.endpoint(api_token_authenticator).with_include("identities").with_start_time(_START_DATE).build(),
UsersResponseBuilder.identities_response()
.with_record(UsersRecordBuilder.record())
.with_record(UsersRecordBuilder.record())
.build(),
)
output = read_stream("user_identities", SyncMode.full_refresh, config)
assert len(output.records) == 2
@HttpMocker()
def test_given_next_page_when_read_then_paginate(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
http_mocker.get(
UsersRequestBuilder.endpoint(api_token_authenticator).with_include("identities").with_start_time(_START_DATE).build(),
UsersResponseBuilder.identities_response(UsersRequestBuilder.endpoint(api_token_authenticator).build(), _A_CURSOR)
.with_record(UsersRecordBuilder.record())
.with_record(UsersRecordBuilder.record())
.with_pagination()
.build(),
)
http_mocker.get(
UsersRequestBuilder.endpoint(api_token_authenticator).with_include("identities").with_cursor(_A_CURSOR).build(),
UsersResponseBuilder.identities_response().with_record(UsersRecordBuilder.record()).build(),
)
output = read_stream("user_identities", SyncMode.full_refresh, config)
assert len(output.records) == 3
@HttpMocker()
def test_when_read_then_set_state_value_to_most_recent_cursor_value(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
most_recent_cursor_value = _START_DATE.add(days=2)
http_mocker.get(
UsersRequestBuilder.endpoint(api_token_authenticator).with_include("identities").with_start_time(_START_DATE).build(),
UsersResponseBuilder.identities_response()
.with_record(UsersRecordBuilder.record().with_cursor(datetime_to_string(most_recent_cursor_value)))
.build(),
)
output = read_stream("user_identities", SyncMode.full_refresh, config)
assert output.most_recent_state.stream_state.__dict__ == {"updated_at": str(most_recent_cursor_value.int_timestamp)}
@HttpMocker()
def test_given_input_state_when_read_then_set_state_value_to_most_recent_cursor_value(self, http_mocker):
config = self._config().with_start_date(_START_DATE).build()
api_token_authenticator = self._get_authenticator(config)
state_cursor_value = _START_DATE.add(days=2)
http_mocker.get(
UsersRequestBuilder.endpoint(api_token_authenticator).with_include("identities").with_start_time(state_cursor_value).build(),
UsersResponseBuilder.identities_response().with_record(UsersRecordBuilder.record()).build(),
)
output = read_stream(
"user_identities",
SyncMode.full_refresh,
config,
StateBuilder().with_stream_state("user_identities", {"updated_at": datetime_to_string(state_cursor_value)}).build(),
)
assert len(output.records) == 1

View File

@@ -7,6 +7,7 @@ import pendulum
from pendulum.datetime import DateTime
from source_zendesk_support import SourceZendeskSupport
from airbyte_cdk.connector_builder.models import HttpRequest
from airbyte_cdk.models import AirbyteMessage, AirbyteStateMessage, SyncMode
from airbyte_cdk.models import Level as LogLevel
from airbyte_cdk.test.catalog_builder import CatalogBuilder
@@ -34,3 +35,9 @@ def datetime_to_string(dt: DateTime) -> str:
def string_to_datetime(dt_string: str) -> DateTime:
return pendulum.parse(dt_string)
def http_request_to_str(http_request: Optional[HttpRequest]) -> Optional[str]:
if http_request is None:
return None
return http_request._parsed_url._replace(fragment="").geturl()

View File

@@ -0,0 +1,44 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
import calendar
from typing import Optional
import pendulum
from .base_request_builder import ZendeskSupportBaseRequestBuilder
from .request_authenticators.authenticator import Authenticator
class ArticlesRequestBuilder(ZendeskSupportBaseRequestBuilder):
@classmethod
def articles_endpoint(cls, authenticator: Authenticator) -> "ArticlesRequestBuilder":
return cls("d3v-airbyte", "help_center/incremental/articles").with_authenticator(authenticator)
def __init__(self, subdomain: str, resource: str) -> None:
super().__init__(subdomain, resource)
self._sort_by: Optional[str] = None
self._sort_order: Optional[str] = None
self._start_time: Optional[str] = None
@property
def query_params(self):
params = super().query_params or {}
if self._sort_by:
params["sort_by"] = self._sort_by
if self._sort_order:
params["sort_order"] = self._sort_order
if self._start_time:
params["start_time"] = self._start_time
return params
def with_sort_by(self, sort_by: str) -> "ArticlesRequestBuilder":
self._sort_by = sort_by
return self
def with_sort_order(self, sort_order: str) -> "ArticlesRequestBuilder":
self._sort_order = sort_order
return self
def with_start_time(self, start_time: pendulum.DateTime) -> "ArticlesRequestBuilder":
self._start_time = str(calendar.timegm(start_time.timetuple()))
return self

View File

@@ -24,6 +24,6 @@ class GroupsRequestBuilder(ZendeskSupportBaseRequestBuilder):
params["per_page"] = self._page_size
return params
def with_page_size(self, page_size: int) -> "PostCommentVotesRequestBuilder":
def with_page_size(self, page_size: int) -> "GroupsRequestBuilder":
self._page_size: int = page_size
return self

View File

@@ -32,7 +32,7 @@ class PostCommentVotesRequestBuilder(ZendeskSupportBaseRequestBuilder):
params["page[after]"] = self._page_after
return params
def with_start_time(self, start_time: int) -> "PostCommentVotesRequestBuilder":
def with_start_time(self, start_time: str) -> "PostCommentVotesRequestBuilder":
self._start_time: int = calendar.timegm(pendulum.parse(start_time).utctimetuple())
return self

View File

@@ -30,7 +30,7 @@ class PostsCommentsRequestBuilder(ZendeskSupportBaseRequestBuilder):
params["page[after]"] = self._page_after
return params
def with_start_time(self, start_time: int) -> "PostsCommentsRequestBuilder":
def with_start_time(self, start_time: str) -> "PostsCommentsRequestBuilder":
self._start_time: int = calendar.timegm(pendulum.parse(start_time).utctimetuple())
return self

View File

@@ -30,7 +30,7 @@ class PostsVotesRequestBuilder(ZendeskSupportBaseRequestBuilder):
params["page[after]"] = self._page_after
return params
def with_start_time(self, start_time: int) -> "PostsVotesRequestBuilder":
def with_start_time(self, start_time: str) -> "PostsVotesRequestBuilder":
self._start_time: int = calendar.timegm(pendulum.parse(start_time).utctimetuple())
return self

View File

@@ -1,6 +1,7 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
import calendar
from typing import Optional
import pendulum
@@ -15,22 +16,29 @@ class PostsRequestBuilder(ZendeskSupportBaseRequestBuilder):
def __init__(self, subdomain: str, resource: str) -> None:
super().__init__(subdomain, resource)
self._start_time: int = None
self._page_size: int = None
self._start_time: Optional[int] = None
self._page_size: Optional[int] = None
self._after_cursor: Optional[str] = None
@property
def query_params(self):
params = super().query_params or {}
if self._start_time:
if self._start_time is not None:
params["start_time"] = self._start_time
if self._page_size:
if self._page_size is not None:
params["page[size]"] = self._page_size
if self._after_cursor is not None:
params["page[after]"] = self._after_cursor
return params
def with_start_time(self, start_time: int) -> "PostsRequestBuilder":
def with_start_time(self, start_time: str) -> "PostsRequestBuilder":
self._start_time: int = calendar.timegm(pendulum.parse(start_time).utctimetuple())
return self
def with_page_size(self, page_size: int) -> "PostsRequestBuilder":
self._page_size: int = page_size
return self
def with_after_cursor(self, after_cursor: str) -> "PostsRequestBuilder":
self._after_cursor: str = after_cursor
return self

View File

@@ -0,0 +1,44 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
import calendar
from typing import Optional
import pendulum
from .base_request_builder import ZendeskSupportBaseRequestBuilder
from .request_authenticators.authenticator import Authenticator
class UsersRequestBuilder(ZendeskSupportBaseRequestBuilder):
@classmethod
def endpoint(cls, authenticator: Authenticator) -> "UsersRequestBuilder":
return cls("d3v-airbyte", "incremental/users/cursor.json").with_authenticator(authenticator)
def __init__(self, subdomain: str, resource: str) -> None:
super().__init__(subdomain, resource)
self._start_time: Optional[str] = None
self._cursor: Optional[str] = None
self._include: Optional[str] = None
@property
def query_params(self):
params = super().query_params or {}
if self._start_time:
params["start_time"] = self._start_time
if self._cursor:
params["cursor"] = self._cursor
if self._include:
params["include"] = self._include
return params
def with_start_time(self, start_time: pendulum.DateTime) -> "UsersRequestBuilder":
self._start_time = str(calendar.timegm(start_time.timetuple()))
return self
def with_cursor(self, cursor: str) -> "UsersRequestBuilder":
self._cursor = cursor
return self
def with_include(self, include: str) -> "UsersRequestBuilder":
self._include = include
return self

View File

@@ -0,0 +1,16 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Optional
from airbyte_cdk.connector_builder.models import HttpRequest
from airbyte_cdk.test.mock_http.response_builder import FieldPath, HttpResponseBuilder, find_template
from ..utils import http_request_to_str
from .pagination_strategies.next_page_pagination_strategy import NextPagePaginationStrategy
class ArticlesResponseBuilder(HttpResponseBuilder):
@classmethod
def response(cls, next_page_url: Optional[HttpRequest] = None) -> "ArticlesResponseBuilder":
return cls(
find_template("articles", __file__), FieldPath("articles"), NextPagePaginationStrategy(http_request_to_str(next_page_url))
)

View File

@@ -1 +1,2 @@
from .cursor_based_pagination_strategy import CursorBasedPaginationStrategy
from .end_of_stream_pagination_strategy import EndOfStreamPaginationStrategy

View File

@@ -1,13 +1,22 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Any, Dict
from typing import Any, Dict, Optional
from airbyte_cdk.test.mock_http.response_builder import PaginationStrategy
class CursorBasedPaginationStrategy(PaginationStrategy):
@staticmethod
def update(response: Dict[str, Any]) -> None:
def __init__(self, first_url: Optional[str] = None) -> None:
self._first_url = first_url
def update(self, response: Dict[str, Any]) -> None:
"""
Only allow for one page
"""
response["meta"]["has_more"] = True
response["meta"]["after_cursor"] = "after-cursor"
response["meta"]["before_cursor"] = "before-cursor"
if self._first_url:
response["links"]["next"] = (
self._first_url + "&page[after]=after-cursor" if "?" in self._first_url else self._first_url + "?page[after]=after-cursor"
)

View File

@@ -0,0 +1,19 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Any, Dict
from airbyte_cdk.test.mock_http.response_builder import PaginationStrategy
class EndOfStreamPaginationStrategy(PaginationStrategy):
def __init__(self, url: str, cursor) -> None:
self._next_page_url = url
self._cursor = cursor
def update(self, response: Dict[str, Any]) -> None:
"""
Only allow for one page
"""
response["after_url"] = f"{self._next_page_url}?cursor={self._cursor}"
response["after_cursor"] = self._cursor
response["end_of_stream"] = False

View File

@@ -0,0 +1,16 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Any, Dict
from airbyte_cdk.test.mock_http.response_builder import PaginationStrategy
class NextPagePaginationStrategy(PaginationStrategy):
def __init__(self, next_page_url: str) -> None:
self._next_page_url = next_page_url
def update(self, response: Dict[str, Any]) -> None:
"""
Only allow for one page
"""
response["next_page"] = self._next_page_url

View File

@@ -1,11 +1,20 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Optional
from airbyte_cdk.connector_builder.models import HttpRequest
from airbyte_cdk.test.mock_http.response_builder import FieldPath, HttpResponseBuilder, find_template
from ..utils import http_request_to_str
from .pagination_strategies import CursorBasedPaginationStrategy
class PostCommentVotesResponseBuilder(HttpResponseBuilder):
@classmethod
def post_comment_votes_response(cls) -> "PostCommentVotesResponseBuilder":
return cls(find_template("votes", __file__), FieldPath("votes"), CursorBasedPaginationStrategy())
def post_comment_votes_response(
cls, request_without_cursor_for_pagination: Optional[HttpRequest] = None
) -> "PostCommentVotesResponseBuilder":
return cls(
find_template("votes", __file__),
FieldPath("votes"),
CursorBasedPaginationStrategy(http_request_to_str(request_without_cursor_for_pagination)),
)

View File

@@ -1,11 +1,18 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Optional
from airbyte_cdk.connector_builder.models import HttpRequest
from airbyte_cdk.test.mock_http.response_builder import FieldPath, HttpResponseBuilder, find_template
from ..utils import http_request_to_str
from .pagination_strategies import CursorBasedPaginationStrategy
class PostsCommentsResponseBuilder(HttpResponseBuilder):
@classmethod
def posts_comments_response(cls) -> "PostsCommentsResponseBuilder":
return cls(find_template("post_comments", __file__), FieldPath("comments"), CursorBasedPaginationStrategy())
def posts_comments_response(cls, request_without_cursor_for_pagination: Optional[HttpRequest] = None) -> "PostsCommentsResponseBuilder":
return cls(
find_template("post_comments", __file__),
FieldPath("comments"),
CursorBasedPaginationStrategy(http_request_to_str(request_without_cursor_for_pagination)),
)

View File

@@ -1,11 +1,18 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Optional
from airbyte_cdk.connector_builder.models import HttpRequest
from airbyte_cdk.test.mock_http.response_builder import FieldPath, HttpResponseBuilder, find_template
from ..utils import http_request_to_str
from .pagination_strategies import CursorBasedPaginationStrategy
class PostsVotesResponseBuilder(HttpResponseBuilder):
@classmethod
def posts_votes_response(cls) -> "PostsVotesResponseBuilder":
return cls(find_template("votes", __file__), FieldPath("votes"), CursorBasedPaginationStrategy())
def posts_votes_response(cls, request_without_cursor_for_pagination: Optional[HttpRequest] = None) -> "PostsVotesResponseBuilder":
return cls(
find_template("votes", __file__),
FieldPath("votes"),
CursorBasedPaginationStrategy(http_request_to_str(request_without_cursor_for_pagination)),
)

View File

@@ -1,11 +1,18 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Optional
from airbyte_cdk.connector_builder.models import HttpRequest
from airbyte_cdk.test.mock_http.response_builder import FieldPath, HttpResponseBuilder, find_template
from ..utils import http_request_to_str
from .pagination_strategies import CursorBasedPaginationStrategy
class PostsResponseBuilder(HttpResponseBuilder):
@classmethod
def posts_response(cls) -> "PostsResponseBuilder":
return cls(find_template("posts", __file__), FieldPath("posts"), CursorBasedPaginationStrategy())
def posts_response(cls, request_without_cursor_for_pagination: Optional[HttpRequest] = None) -> "PostsResponseBuilder":
return cls(
find_template("posts", __file__),
FieldPath("posts"),
CursorBasedPaginationStrategy(http_request_to_str(request_without_cursor_for_pagination)),
)

View File

@@ -0,0 +1,12 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
from airbyte_cdk.test.mock_http.response_builder import FieldPath, NestedPath
from .records_builder import ZendeskSupportRecordBuilder
class ArticlesRecordBuilder(ZendeskSupportRecordBuilder):
@classmethod
def record(cls) -> "ArticlesRecordBuilder":
record_template = cls.extract_record("articles", __file__, NestedPath(["articles", 0]))
return cls(record_template, FieldPath("id"), FieldPath("updated_at"))

View File

@@ -7,6 +7,6 @@ from .records_builder import ZendeskSupportRecordBuilder
class PostsCommentsRecordBuilder(ZendeskSupportRecordBuilder):
@classmethod
def posts_commetns_record(cls) -> "PostsCommentsRecordBuilder":
def posts_comments_record(cls) -> "PostsCommentsRecordBuilder":
record_template = cls.extract_record("post_comments", __file__, NestedPath(["comments", 0]))
return cls(record_template, FieldPath("id"), FieldPath("updated_at"))

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from airbyte_cdk.test.mock_http.response_builder import FieldPath, RecordBuilder, find_template
from airbyte_cdk.test.mock_http.response_builder import Path, RecordBuilder, find_template
class ZendeskSupportRecordBuilder(RecordBuilder):
@staticmethod
def extract_record(resource: str, execution_folder: str, data_field: FieldPath):
def extract_record(resource: str, execution_folder: str, data_field: Path):
return data_field.extract(find_template(resource=resource, execution_folder=execution_folder))

View File

@@ -0,0 +1,12 @@
# Copyright (c) 2024 Airbyte, Inc., all rights reserved.
from airbyte_cdk.test.mock_http.response_builder import FieldPath, NestedPath
from .records_builder import ZendeskSupportRecordBuilder
class UsersRecordBuilder(ZendeskSupportRecordBuilder):
@classmethod
def record(cls) -> "UsersRecordBuilder":
record_template = cls.extract_record("users", __file__, NestedPath(["users", 0]))
return cls(record_template, FieldPath("id"), FieldPath("updated_at"))

View File

@@ -0,0 +1,20 @@
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
from typing import Optional
from airbyte_cdk.connector_builder.models import HttpRequest
from airbyte_cdk.test.mock_http.response_builder import FieldPath, HttpResponseBuilder, find_template
from ..utils import http_request_to_str
from .pagination_strategies import EndOfStreamPaginationStrategy
class UsersResponseBuilder(HttpResponseBuilder):
@classmethod
def response(cls, url: Optional[HttpRequest] = None, cursor: Optional[str] = None) -> "UsersResponseBuilder":
return cls(find_template("users", __file__), FieldPath("users"), EndOfStreamPaginationStrategy(http_request_to_str(url), cursor))
@classmethod
def identities_response(cls, url: Optional[HttpRequest] = None, cursor: Optional[str] = None) -> "UsersResponseBuilder":
return cls(
find_template("users", __file__), FieldPath("identities"), EndOfStreamPaginationStrategy(http_request_to_str(url), cursor)
)

View File

@@ -0,0 +1,35 @@
{
"count": 6,
"next_page": null,
"end_time": 1720784511,
"articles": [
{
"id": 7253351877519,
"url": "https://d3v-airbyte.zendesk.com/api/v2/help_center/en-us/articles/7253351877519.json",
"html_url": "https://d3v-airbyte.zendesk.com/hc/en-us/articles/7253351877519-Sample-article-Stellar-Skyonomy-refund-policies",
"author_id": 360786799676,
"comments_disabled": false,
"draft": true,
"promoted": false,
"position": 0,
"vote_sum": 0,
"vote_count": 0,
"section_id": 7253394933775,
"created_at": "2023-06-22T00:32:20Z",
"updated_at": "2023-06-22T00:32:20Z",
"name": "Sample article: Stellar Skyonomy refund policies",
"title": "Sample article: Stellar Skyonomy refund policies",
"source_locale": "en-us",
"locale": "en-us",
"outdated": false,
"outdated_locales": [],
"edited_at": "2023-06-22T00:32:20Z",
"user_segment_id": null,
"permission_group_id": 7253379449487,
"content_tag_ids": [],
"label_names": [],
"body": "\u003cp\u003eAll \u003cstrong\u003eStellar Skyonomy\u003c/strong\u003e merchandise purchases are backed by our 30-day satisfaction guarantee, no questions asked. We even pay to have it shipped back to us. Additionally, you can cancel your \u003cstrong\u003eStellar Skyonomy\u003c/strong\u003e subscription at any time. Before you cancel, review our refund policies in this article.\u003c/p\u003e\u003cbr\u003e\u003cp\u003e\u003cstrong\u003eRefund policy\u003c/strong\u003e\u003c/p\u003e\u003cp\u003eWe automatically issue a full refund when you \u003ca\u003einitiate a return\u003c/a\u003e within 30 days of delivery.\u003cbr\u003e\u003cbr\u003eTo \u003ca\u003ecancel an annual website subscription\u003c/a\u003e you can do so at any time and your refund will be prorated based on the cancellation date.\u003c/p\u003e\u003cbr\u003e\u003cp\u003e\u003cstrong\u003eRequest a refund\u003c/strong\u003e\u003c/p\u003e\u003cp\u003eIf you believe youre eligible for a refund but havent received one, contact us by completing a \u003ca\u003erefund request form.\u003c/a\u003e We review every refund and aim to respond within two business days.\u003cbr\u003e\u003cbr\u003eIf you haven't received a refund you're expecting, note that it can take up to 10 business days to appear on your card statement.\u003c/p\u003e",
"user_segment_ids": []
}
]
}

View File

@@ -42,5 +42,12 @@
},
"custom_status_updated_at": "2022-09-19T14:53:49Z",
"generated_timestamp": 1681308296
},
"meta": {
"has_more": false
},
"links": {
"first": "https://d3v-airbyte.zendesk.com/api/v2/help_center/community/posts/7253351904271/comments?page%5Bsize%5D=100",
"last": "https://d3v-airbyte.zendesk.com/api/v2/help_center/community/posts/7253351904271/comments?page%5Bbefore%5D=bGFzdF9wYWdl&page%5Bsize%5D=100"
}
}

View File

@@ -43,5 +43,12 @@
},
"custom_status_updated_at": "2022-09-19T14:53:49Z"
}
]
],
"meta": {
"has_more": false
},
"links": {
"first": "https://d3v-airbyte.zendesk.com/api/v2/help_center/community/posts/7253351904271/comments?page%5Bsize%5D=100",
"last": "https://d3v-airbyte.zendesk.com/api/v2/help_center/community/posts/7253351904271/comments?page%5Bbefore%5D=bGFzdF9wYWdl&page%5Bsize%5D=100"
}
}

View File

@@ -0,0 +1,67 @@
{
"users": [
{
"id": 6128164851599,
"url": "https://d3v-airbyte.zendesk.com/api/v2/users/6128164851599.json",
"name": "AmberBlanchard",
"email": "integration-test+amberblanchard@airbyte.io",
"created_at": "2022-12-29T08:56:24Z",
"updated_at": "2023-02-01T07:41:46Z",
"time_zone": "Pacific/Noumea",
"iana_time_zone": "Pacific/Noumea",
"phone": null,
"shared_phone_number": null,
"photo": null,
"locale_id": 1,
"locale": "en-US",
"organization_id": null,
"role": "end-user",
"verified": false,
"external_id": null,
"tags": ["tag", "test"],
"alias": "",
"active": true,
"shared": false,
"shared_agent": false,
"last_login_at": null,
"two_factor_auth_enabled": null,
"signature": null,
"details": "",
"notes": "",
"role_type": null,
"custom_role_id": null,
"moderator": false,
"ticket_restriction": "requested",
"only_private_comments": false,
"restricted_agent": true,
"suspended": false,
"default_group_id": null,
"report_csv": false,
"user_fields": {
"test_display_name_checkbox_field": false,
"test_display_name_decimal_field": null,
"test_display_name_text_field": null
}
}
],
"identities": [
{
"url": "https://d3v-airbyte.zendesk.com/api/v2/users/6128164851599/identities/6128164851983.json",
"id": 6128164851983,
"user_id": 6128164851599,
"type": "email",
"value": "integration-test+amberblanchard@airbyte.io",
"verified": false,
"primary": true,
"created_at": "2022-12-29T08:56:24Z",
"updated_at": "2022-12-29T08:56:24Z",
"undeliverable_count": 0,
"deliverable_state": "deliverable"
}
],
"after_url": null,
"before_url": null,
"after_cursor": null,
"before_cursor": null,
"end_of_stream": true
}

View File

@@ -11,8 +11,8 @@
"id": 35467,
"user_id": 888887,
"value": -1,
"created_at": "2023-06-22T00:32:22Z",
"updated_at": "2023-06-22T00:32:22Z"
"created_at": "2023-06-22T00:32:23Z",
"updated_at": "2023-06-22T00:32:23Z"
}
]
}

View File

@@ -1,50 +0,0 @@
#
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
#
from typing import Dict
import pytest
import requests
from source_zendesk_support.source import SourceZendeskSupport
from source_zendesk_support.streams import Users
_ANY_ATTEMPT_COUNT = 10
@pytest.fixture(scope="session", name="config")
def test_config():
config = {
"subdomain": "sandbox",
"start_date": "2021-06-01T00:00:00Z",
"credentials": {"credentials": "api_token", "email": "integration-test@airbyte.io", "api_token": "api_token"},
}
return config
def prepare_config(config: Dict):
return SourceZendeskSupport(config=config, catalog=None, state=None).convert_config2stream_args(config)
@pytest.mark.parametrize(
"x_rate_limit, retry_after, expected",
[("60", {}, 1), ("0", {}, None), ("0", {"Retry-After": "5"}, 5), ("0", {"Retry-After": "5, 4"}, 5)],
)
def test_backoff(requests_mock, config, x_rate_limit, retry_after, expected):
""" """
test_response_header = {"X-Rate-Limit": x_rate_limit} | retry_after
test_response_json = {"count": {"value": 1, "refreshed_at": "2022-03-29T10:10:51+00:00"}}
# create client
config = prepare_config(config)
# test stream
test_stream = Users(**config)
url = f"{test_stream.url_base}{test_stream.path()}/count.json"
requests_mock.get(url, json=test_response_json, headers=test_response_header, status_code=429)
test_response = requests.get(url)
actual = test_stream.get_backoff_strategy().backoff_time(test_response, _ANY_ATTEMPT_COUNT)
assert actual == expected

View File

@@ -187,140 +187,142 @@ The Zendesk connector ideally should not run into Zendesk API limitations under
<details>
<summary>Expand to review</summary>
| Version | Date | Pull Request | Subject |
|:--------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 4.7.1 | 2025-02-26 | [54696](https://github.com/airbytehq/airbyte/pull/54696) | Update requests-mock dependency versionb |
| 4.7.0 | 2025-02-24 | [54656](https://github.com/airbytehq/airbyte/pull/54656) | Promoting release candidate 4.7.0-rc.1 to a main version. |
| 4.7.0-rc.1 | 2025-02-13 | [53620](https://github.com/airbytehq/airbyte/pull/53620) | Replace ZendeskSupportAuditLogsIncrementalSync with low-code DatetimeBasedCursor |
| 4.6.0 | 2024-12-09 | [47939](https://github.com/airbytehq/airbyte/pull/47939) | Add `User Identities` stream |
| 4.5.0 | 2024-12-02 | [48761](https://github.com/airbytehq/airbyte/pull/48761) | Add `Categories` and `Sections` stream |
| 4.4.4 | 2025-02-08 | [51943](https://github.com/airbytehq/airbyte/pull/51943) | Update dependencies |
| 4.4.3 | 2025-02-03 | [52625](https://github.com/airbytehq/airbyte/pull/52625) | Update error message during check for `organization_access_enabled` |
| 4.4.2 | 2025-01-11 | [48309](https://github.com/airbytehq/airbyte/pull/48309) | Starting with this version, the Docker image is now rootless. Please note that this and future versions will not be compatible with Airbyte versions earlier than 0.64 |
| 4.4.1 | 2024-12-13 | [48889](https://github.com/airbytehq/airbyte/pull/48889) | Check if `start_date` exist in check operation |
| 4.4.0 | 2024-11-11 | [48379](https://github.com/airbytehq/airbyte/pull/48379) | Make DatetimeBasedCursor syncs concurrent |
| 4.3.3 | 2024-10-28 | [47663](https://github.com/airbytehq/airbyte/pull/47663) | Update dependencies |
| 4.3.2 | 2024-10-21 | [47202](https://github.com/airbytehq/airbyte/pull/47202) | Update dependencies and expected records |
| 4.3.1 | 2024-10-12 | [46794](https://github.com/airbytehq/airbyte/pull/46794) | Update dependencies |
| 4.3.0 | 2024-10-09 | [46096](https://github.com/airbytehq/airbyte/pull/46096) | Updates `TicketMetrics` stream for improved reliability for long syncs, updates state cursor field to `_ab_updated_at`, automatically migrates legacy state |
| 4.2.3 | 2024-10-05 | [46408](https://github.com/airbytehq/airbyte/pull/46408) | Update dependencies |
| 4.2.2 | 2024-09-28 | [45784](https://github.com/airbytehq/airbyte/pull/45784) | Update dependencies |
| 4.2.1 | 2024-09-14 | [45561](https://github.com/airbytehq/airbyte/pull/45561) | Update dependencies |
| 4.2.0 | 2024-09-10 | [44610](https://github.com/airbytehq/airbyte/pull/44610) | Add `Automations` and `Triggers` stream |
| 4.1.1 | 2024-09-07 | [45215](https://github.com/airbytehq/airbyte/pull/45215) | Update dependencies |
| 4.1.0 | 2024-09-06 | [45187](https://github.com/airbytehq/airbyte/pull/45187) | Migrate to CDK v5 |
| 4.0.2 | 2024-08-31 | [44965](https://github.com/airbytehq/airbyte/pull/44965) | Update dependencies |
| 4.0.1 | 2024-08-24 | [44692](https://github.com/airbytehq/airbyte/pull/44692) | Update dependencies |
| 4.0.0 | 2024-08-19 | [44096](https://github.com/airbytehq/airbyte/pull/44096) | Stream `Tags`: use cursor based pagination |
| 3.0.1 | 2024-08-17 | [44324](https://github.com/airbytehq/airbyte/pull/44324) | Update dependencies |
| 3.0.0 | 2024-08-13 | [43446](https://github.com/airbytehq/airbyte/pull/43446) | `TicketMetrics` stream: updates cursor field to `generated_timestamp` |
| 2.7.3 | 2024-08-12 | [43900](https://github.com/airbytehq/airbyte/pull/43900) | Update dependencies |
| 2.7.2 | 2024-08-10 | [43614](https://github.com/airbytehq/airbyte/pull/43614) | Update dependencies |
| 2.7.1 | 2024-08-03 | [41799](https://github.com/airbytehq/airbyte/pull/41799) | Update dependencies |
| 2.7.0 | 2024-08-02 | [42975](https://github.com/airbytehq/airbyte/pull/42975) | Migrate to CDK v4.3.0 |
| 2.6.13 | 2024-07-31 | [42892](https://github.com/airbytehq/airbyte/pull/42892) | Update BackoffStrategy interface to be up-to-date with latest parent interface. |
| 2.6.12 | 2024-07-25 | [42519](https://github.com/airbytehq/airbyte/pull/42519) | Update error message for permission issue. |
| 2.6.11 | 2024-07-18 | [42100](https://github.com/airbytehq/airbyte/pull/42100) | Raise config error on 403/404 status code. |
| 2.6.10 | 2024-07-10 | [41436](https://github.com/airbytehq/airbyte/pull/41436) | Fix unit test |
| 2.6.9 | 2024-07-10 | [41390](https://github.com/airbytehq/airbyte/pull/41390) | Update dependencies |
| 2.6.8 | 2024-07-09 | [40025](https://github.com/airbytehq/airbyte/pull/40025) | Update dependencies |
| 2.6.7 | 2024-07-09 | [41032](https://github.com/airbytehq/airbyte/pull/41032) | Use latest `CDK`: 3.0.0 |
| 2.6.6 | 2024-06-27 | [40592](https://github.com/airbytehq/airbyte/pull/40592) | Updated to use latest `CDK` version, fixed `cursor pagination` logic |
| 2.6.5 | 2024-05-23 | [38607](https://github.com/airbytehq/airbyte/pull/38607) | Migrate to cursor based pagination in stream `Organization memberships` |
| 2.6.4 | 2024-05-20 | [38310](https://github.com/airbytehq/airbyte/pull/38310) | Fix record filter for `Ticket Metrics` stream |
| 2.6.3 | 2024-05-02 | [36669](https://github.com/airbytehq/airbyte/pull/36669) | Schema descriptions |
| 2.6.2 | 2024-02-05 | [37761](https://github.com/airbytehq/airbyte/pull/37761) | Add stop condition for `Ticket Audits` when received old records; Ignore 403 and 404 status codes. |
| 2.6.1 | 2024-04-30 | [37723](https://github.com/airbytehq/airbyte/pull/37723) | Add %Y-%m-%dT%H:%M:%S%z to cursor_datetime_formats |
| 2.6.0 | 2024-04-29 | [36823](https://github.com/airbytehq/airbyte/pull/36823) | Migrate to low code; Add new stream `Ticket Activities` |
| 2.5.0 | 2024-04-25 | [36388](https://github.com/airbytehq/airbyte/pull/36388) | Fix data type of field in `Tickets` stream schema stream. |
| 2.4.1 | 2024-04-20 | [37450](https://github.com/airbytehq/airbyte/pull/37450) | Fix parsing response for `Ticket Metrics` stream. |
| 2.4.0 | 2024-04-09 | [36897](https://github.com/airbytehq/airbyte/pull/36897) | Fix long-running syncs for `Ticket Metrics`, `Ticket Audits` and `Satisfaction Ratings` streams. |
| 2.3.0 | 2024-03-26 | [36403](https://github.com/airbytehq/airbyte/pull/36403) | Unpin CDK version, add record counts to state messages |
| 2.2.8 | 2024-02-09 | [35083](https://github.com/airbytehq/airbyte/pull/35083) | Manage dependencies with Poetry. |
| 2.2.7 | 2024-02-05 | [34840](https://github.com/airbytehq/airbyte/pull/34840) | Fix missing fields in schema |
| 2.2.6 | 2024-01-11 | [34064](https://github.com/airbytehq/airbyte/pull/34064) | Skip 504 Error for stream `Ticket Audits` |
| 2.2.5 | 2024-01-08 | [34010](https://github.com/airbytehq/airbyte/pull/34010) | Prepare for airbyte-lib |
| 2.2.4 | 2023-12-20 | [33680](https://github.com/airbytehq/airbyte/pull/33680) | Fix pagination issue for streams related to incremental export sync |
| 2.2.3 | 2023-12-14 | [33435](https://github.com/airbytehq/airbyte/pull/33435) | Fix 504 Error for stream Ticket Audits |
| 2.2.2 | 2023-12-01 | [33012](https://github.com/airbytehq/airbyte/pull/33012) | Increase number of retries for backoff policy to 10 |
| 2.2.1 | 2023-11-10 | [32440](https://github.com/airbytehq/airbyte/pull/32440) | Made refactoring to improve code maintainability |
| 2.2.0 | 2023-10-31 | [31999](https://github.com/airbytehq/airbyte/pull/31999) | Extended the `CustomRoles` stream schema |
| 2.1.1 | 2023-10-23 | [31702](https://github.com/airbytehq/airbyte/pull/31702) | Base image migration: remove Dockerfile and use the python-connector-base image |
| 2.1.0 | 2023-10-19 | [31606](https://github.com/airbytehq/airbyte/pull/31606) | Added new field `reply_time_in_seconds` to the `Ticket Metrics` stream schema |
| 2.0.0 | 2023-09-15 | [30440](https://github.com/airbytehq/airbyte/pull/30440) | Remove stream `Deleted Tickets` |
| 1.7.0 | 2023-09-11 | [30259](https://github.com/airbytehq/airbyte/pull/30259) | Add stream `Deleted Tickets` |
| 1.6.0 | 2023-09-09 | [30168](https://github.com/airbytehq/airbyte/pull/30168) | Make `start_date` field optional |
| 1.5.1 | 2023-09-05 | [30142](https://github.com/airbytehq/airbyte/pull/30142) | Handle non-JSON Response |
| 1.5.0 | 2023-09-04 | [30138](https://github.com/airbytehq/airbyte/pull/30138) | Add new Streams: `Article Votes`, `Article Comments`, `Article Comment Votes` |
| 1.4.0 | 2023-09-04 | [30134](https://github.com/airbytehq/airbyte/pull/30134) | Add incremental support for streams: `custom Roles`, `Schedules`, `SLA Policies` |
| 1.3.0 | 2023-08-30 | [30031](https://github.com/airbytehq/airbyte/pull/30031) | Add new streams: `Articles`, `Organization Fields` |
| 1.2.2 | 2023-08-30 | [29998](https://github.com/airbytehq/airbyte/pull/29998) | Fix typo in stream `AttributeDefinitions`: field condition |
| 1.2.1 | 2023-08-30 | [29991](https://github.com/airbytehq/airbyte/pull/29991) | Remove Custom availability strategy |
| 1.2.0 | 2023-08-29 | [29940](https://github.com/airbytehq/airbyte/pull/29940) | Add undeclared fields to schemas |
| 1.1.1 | 2023-08-29 | [29904](https://github.com/airbytehq/airbyte/pull/29904) | Make `Organizations` stream incremental |
| 1.1.0 | 2023-08-28 | [29891](https://github.com/airbytehq/airbyte/pull/29891) | Add stream `UserFields` |
| 1.0.0 | 2023-07-27 | [28774](https://github.com/airbytehq/airbyte/pull/28774) | Fix retry logic & update cursor for `Tickets` stream |
| 0.11.0 | 2023-08-10 | [27208](https://github.com/airbytehq/airbyte/pull/27208) | Add stream `Topics` |
| 0.10.7 | 2023-08-09 | [29256](https://github.com/airbytehq/airbyte/pull/29256) | Update tooltip descriptions in spec |
| 0.10.6 | 2023-08-04 | [29031](https://github.com/airbytehq/airbyte/pull/29031) | Reverted `advancedAuth` spec changes |
| 0.10.5 | 2023-08-01 | [28910](https://github.com/airbytehq/airbyte/pull/28910) | Updated `advancedAuth` broken references |
| 0.10.4 | 2023-07-25 | [28397](https://github.com/airbytehq/airbyte/pull/28397) | Handle 404 Error |
| 0.10.3 | 2023-07-24 | [28612](https://github.com/airbytehq/airbyte/pull/28612) | Fix pagination for stream `TicketMetricEvents` |
| 0.10.2 | 2023-07-19 | [28487](https://github.com/airbytehq/airbyte/pull/28487) | Remove extra page from params |
| 0.10.1 | 2023-07-10 | [28096](https://github.com/airbytehq/airbyte/pull/28096) | Replace `offset` pagination with `cursor` pagination |
| 0.10.0 | 2023-07-06 | [27991](https://github.com/airbytehq/airbyte/pull/27991) | Add streams: `PostVotes`, `PostCommentVotes` |
| 0.9.0 | 2023-07-05 | [27961](https://github.com/airbytehq/airbyte/pull/27961) | Add stream: `Post Comments` |
| 0.8.1 | 2023-06-27 | [27765](https://github.com/airbytehq/airbyte/pull/27765) | Bugfix: Nonetype error while syncing more then 100000 organizations |
| 0.8.0 | 2023-06-09 | [27156](https://github.com/airbytehq/airbyte/pull/27156) | Add stream `Posts` |
| 0.7.0 | 2023-06-27 | [27436](https://github.com/airbytehq/airbyte/pull/27436) | Add Ticket Skips stream |
| 0.6.0 | 2023-06-27 | [27450](https://github.com/airbytehq/airbyte/pull/27450) | Add Skill Based Routing streams |
| 0.5.0 | 2023-06-26 | [27735](https://github.com/airbytehq/airbyte/pull/27735) | License Update: Elv2 stream stream |
| 0.4.0 | 2023-06-16 | [27431](https://github.com/airbytehq/airbyte/pull/27431) | Add Organization Memberships stream |
| 0.3.1 | 2023-06-02 | [26945](https://github.com/airbytehq/airbyte/pull/26945) | Make `Ticket Metrics` stream to use cursor pagination |
| 0.3.0 | 2023-05-23 | [26347](https://github.com/airbytehq/airbyte/pull/26347) | Add stream `Audit Logs` logs` |
| 0.2.30 | 2023-05-23 | [26414](https://github.com/airbytehq/airbyte/pull/26414) | Added missing handlers when `empty json` or `JSONDecodeError` is received |
| 0.2.29 | 2023-04-18 | [25214](https://github.com/airbytehq/airbyte/pull/25214) | Add missing fields to `Tickets` stream |
| 0.2.28 | 2023-03-21 | [24053](https://github.com/airbytehq/airbyte/pull/24053) | Fix stream `sla_policies` schema data type error (events.value) |
| 0.2.27 | 2023-03-22 | [22817](https://github.com/airbytehq/airbyte/pull/22817) | Specified date formatting in specification |
| 0.2.26 | 2023-03-20 | [24252](https://github.com/airbytehq/airbyte/pull/24252) | Handle invalid `start_date` when checking connection |
| 0.2.25 | 2023-02-28 | [22308](https://github.com/airbytehq/airbyte/pull/22308) | Add `AvailabilityStrategy` for all streams |
| 0.2.24 | 2023-02-17 | [23246](https://github.com/airbytehq/airbyte/pull/23246) | Handle `StartTimeTooRecent` error for Tickets stream |
| 0.2.23 | 2023-02-15 | [23035](https://github.com/airbytehq/airbyte/pull/23035) | Handle 403 Error |
| 0.2.22 | 2023-02-14 | [22483](https://github.com/airbytehq/airbyte/pull/22483) | Fix test; handle 400 error |
| 0.2.21 | 2023-01-27 | [22027](https://github.com/airbytehq/airbyte/pull/22027) | Set `AvailabilityStrategy` for streams explicitly to `None` |
| 0.2.20 | 2022-12-28 | [20900](https://github.com/airbytehq/airbyte/pull/20900) | Remove synchronous time.sleep, add logging, reduce backoff time |
| 0.2.19 | 2022-12-09 | [19967](https://github.com/airbytehq/airbyte/pull/19967) | Fix reading response for more than 100k records |
| 0.2.18 | 2022-11-29 | [19432](https://github.com/airbytehq/airbyte/pull/19432) | Revert changes from version 0.2.15, use a test read instead |
| 0.2.17 | 2022-11-24 | [19792](https://github.com/airbytehq/airbyte/pull/19792) | Transform `ticket_comments.via` "-" to null |
| 0.2.16 | 2022-09-28 | [17326](https://github.com/airbytehq/airbyte/pull/17326) | Migrate to per-stream states. |
| 0.2.15 | 2022-08-03 | [15233](https://github.com/airbytehq/airbyte/pull/15233) | Added `subscription plan` check on `streams discovery` step to remove streams that are not accessible for fetch due to subscription plan restrictions |
| 0.2.14 | 2022-07-27 | [15036](https://github.com/airbytehq/airbyte/pull/15036) | Convert `ticket_audits.previous_value` values to string |
| 0.2.13 | 2022-07-21 | [14829](https://github.com/airbytehq/airbyte/pull/14829) | Convert `tickets.custom_fields` values to string |
| 0.2.12 | 2022-06-30 | [14304](https://github.com/airbytehq/airbyte/pull/14304) | Fixed Pagination for Group Membership stream |
| 0.2.11 | 2022-06-24 | [14112](https://github.com/airbytehq/airbyte/pull/14112) | Fixed "Retry-After" non integer value |
| 0.2.10 | 2022-06-14 | [13757](https://github.com/airbytehq/airbyte/pull/13757) | Fixed the bug with `TicketMetrics` stream, HTTP Error 429, caused by lots of API requests |
| 0.2.9 | 2022-05-27 | [13261](https://github.com/airbytehq/airbyte/pull/13261) | Bugfix for the unhandled [ChunkedEncodingError](https://github.com/airbytehq/airbyte/issues/12591) and [ConnectionError](https://github.com/airbytehq/airbyte/issues/12155) |
| 0.2.8 | 2022-05-20 | [13055](https://github.com/airbytehq/airbyte/pull/13055) | Fixed minor issue for stream `ticket_audits` schema |
| 0.2.7 | 2022-04-27 | [12335](https://github.com/airbytehq/airbyte/pull/12335) | Adding fixtures to mock time.sleep for connectors that explicitly sleep |
| 0.2.6 | 2022-04-19 | [12122](https://github.com/airbytehq/airbyte/pull/12122) | Fixed the bug when only 100,000 Users are synced [11895](https://github.com/airbytehq/airbyte/issues/11895) and fixed bug when `start_date` is not used on user stream [12059](https://github.com/airbytehq/airbyte/issues/12059). |
| 0.2.5 | 2022-04-05 | [11727](https://github.com/airbytehq/airbyte/pull/11727) | Fixed the bug when state was not parsed correctly |
| 0.2.4 | 2022-04-04 | [11688](https://github.com/airbytehq/airbyte/pull/11688) | Small documentation corrections |
| 0.2.3 | 2022-03-23 | [11349](https://github.com/airbytehq/airbyte/pull/11349) | Fixed the bug when Tickets stream didn't return deleted records |
| 0.2.2 | 2022-03-17 | [11237](https://github.com/airbytehq/airbyte/pull/11237) | Fixed the bug when TicketComments stream didn't return all records |
| 0.2.1 | 2022-03-15 | [11162](https://github.com/airbytehq/airbyte/pull/11162) | Added support of OAuth2.0 authentication method |
| 0.2.0 | 2022-03-01 | [9456](https://github.com/airbytehq/airbyte/pull/9456) | Update source to use future requests |
| 0.1.12 | 2022-01-25 | [9785](https://github.com/airbytehq/airbyte/pull/9785) | Add additional log messages |
| 0.1.11 | 2021-12-21 | [8987](https://github.com/airbytehq/airbyte/pull/8987) | Update connector fields title/description |
| 0.1.9 | 2021-12-16 | [8616](https://github.com/airbytehq/airbyte/pull/8616) | Adds Brands, CustomRoles and Schedules streams |
| 0.1.8 | 2021-11-23 | [8050](https://github.com/airbytehq/airbyte/pull/8168) | Adds TicketMetricEvents stream |
| 0.1.7 | 2021-11-23 | [8058](https://github.com/airbytehq/airbyte/pull/8058) | Added support of AccessToken authentication |
| 0.1.6 | 2021-11-18 | [8050](https://github.com/airbytehq/airbyte/pull/8050) | Fix wrong types for schemas, add TypeTransformer |
| 0.1.5 | 2021-10-26 | [7679](https://github.com/airbytehq/airbyte/pull/7679) | Add ticket_id and ticket_comments |
| 0.1.4 | 2021-10-26 | [7377](https://github.com/airbytehq/airbyte/pull/7377) | Fix initially_assigned_at type in ticket metrics |
| 0.1.3 | 2021-10-17 | [7097](https://github.com/airbytehq/airbyte/pull/7097) | Corrected the connector's specification |
| 0.1.2 | 2021-10-16 | [6513](https://github.com/airbytehq/airbyte/pull/6513) | Fixed TicketComments stream |
| 0.1.1 | 2021-09-02 | [5787](https://github.com/airbytehq/airbyte/pull/5787) | Fixed incremental logic for the ticket_comments stream |
| 0.1.0 | 2021-07-21 | [4861](https://github.com/airbytehq/airbyte/pull/4861) | Created CDK native zendesk connector |
| Version | Date | Pull Request | Subject |
|:-----------|:-----------|:---------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 4.8.0 | 2025-03-25 | [56354](https://github.com/airbytehq/airbyte/pull/56354) | Migrate posts to low-code |
| 4.7.2 | 2025-03-11 | [55676](https://github.com/airbytehq/airbyte/pull/55676) | Prepare posts stream for low-code migration |
| 4.7.1 | 2025-02-26 | [54696](https://github.com/airbytehq/airbyte/pull/54696) | Update requests-mock dependency versionb |
| 4.7.0 | 2025-02-24 | [54656](https://github.com/airbytehq/airbyte/pull/54656) | Promoting release candidate 4.7.0-rc.1 to a main version. |
| 4.7.0-rc.1 | 2025-02-13 | [53620](https://github.com/airbytehq/airbyte/pull/53620) | Replace ZendeskSupportAuditLogsIncrementalSync with low-code DatetimeBasedCursor |
| 4.6.0 | 2024-12-09 | [47939](https://github.com/airbytehq/airbyte/pull/47939) | Add `User Identities` stream |
| 4.5.0 | 2024-12-02 | [48761](https://github.com/airbytehq/airbyte/pull/48761) | Add `Categories` and `Sections` stream |
| 4.4.4 | 2025-02-08 | [51943](https://github.com/airbytehq/airbyte/pull/51943) | Update dependencies |
| 4.4.3 | 2025-02-03 | [52625](https://github.com/airbytehq/airbyte/pull/52625) | Update error message during check for `organization_access_enabled` |
| 4.4.2 | 2025-01-11 | [48309](https://github.com/airbytehq/airbyte/pull/48309) | Starting with this version, the Docker image is now rootless. Please note that this and future versions will not be compatible with Airbyte versions earlier than 0.64 |
| 4.4.1 | 2024-12-13 | [48889](https://github.com/airbytehq/airbyte/pull/48889) | Check if `start_date` exist in check operation |
| 4.4.0 | 2024-11-11 | [48379](https://github.com/airbytehq/airbyte/pull/48379) | Make DatetimeBasedCursor syncs concurrent |
| 4.3.3 | 2024-10-28 | [47663](https://github.com/airbytehq/airbyte/pull/47663) | Update dependencies |
| 4.3.2 | 2024-10-21 | [47202](https://github.com/airbytehq/airbyte/pull/47202) | Update dependencies and expected records |
| 4.3.1 | 2024-10-12 | [46794](https://github.com/airbytehq/airbyte/pull/46794) | Update dependencies |
| 4.3.0 | 2024-10-09 | [46096](https://github.com/airbytehq/airbyte/pull/46096) | Updates `TicketMetrics` stream for improved reliability for long syncs, updates state cursor field to `_ab_updated_at`, automatically migrates legacy state |
| 4.2.3 | 2024-10-05 | [46408](https://github.com/airbytehq/airbyte/pull/46408) | Update dependencies |
| 4.2.2 | 2024-09-28 | [45784](https://github.com/airbytehq/airbyte/pull/45784) | Update dependencies |
| 4.2.1 | 2024-09-14 | [45561](https://github.com/airbytehq/airbyte/pull/45561) | Update dependencies |
| 4.2.0 | 2024-09-10 | [44610](https://github.com/airbytehq/airbyte/pull/44610) | Add `Automations` and `Triggers` stream |
| 4.1.1 | 2024-09-07 | [45215](https://github.com/airbytehq/airbyte/pull/45215) | Update dependencies |
| 4.1.0 | 2024-09-06 | [45187](https://github.com/airbytehq/airbyte/pull/45187) | Migrate to CDK v5 |
| 4.0.2 | 2024-08-31 | [44965](https://github.com/airbytehq/airbyte/pull/44965) | Update dependencies |
| 4.0.1 | 2024-08-24 | [44692](https://github.com/airbytehq/airbyte/pull/44692) | Update dependencies |
| 4.0.0 | 2024-08-19 | [44096](https://github.com/airbytehq/airbyte/pull/44096) | Stream `Tags`: use cursor based pagination |
| 3.0.1 | 2024-08-17 | [44324](https://github.com/airbytehq/airbyte/pull/44324) | Update dependencies |
| 3.0.0 | 2024-08-13 | [43446](https://github.com/airbytehq/airbyte/pull/43446) | `TicketMetrics` stream: updates cursor field to `generated_timestamp` |
| 2.7.3 | 2024-08-12 | [43900](https://github.com/airbytehq/airbyte/pull/43900) | Update dependencies |
| 2.7.2 | 2024-08-10 | [43614](https://github.com/airbytehq/airbyte/pull/43614) | Update dependencies |
| 2.7.1 | 2024-08-03 | [41799](https://github.com/airbytehq/airbyte/pull/41799) | Update dependencies |
| 2.7.0 | 2024-08-02 | [42975](https://github.com/airbytehq/airbyte/pull/42975) | Migrate to CDK v4.3.0 |
| 2.6.13 | 2024-07-31 | [42892](https://github.com/airbytehq/airbyte/pull/42892) | Update BackoffStrategy interface to be up-to-date with latest parent interface. |
| 2.6.12 | 2024-07-25 | [42519](https://github.com/airbytehq/airbyte/pull/42519) | Update error message for permission issue. |
| 2.6.11 | 2024-07-18 | [42100](https://github.com/airbytehq/airbyte/pull/42100) | Raise config error on 403/404 status code. |
| 2.6.10 | 2024-07-10 | [41436](https://github.com/airbytehq/airbyte/pull/41436) | Fix unit test |
| 2.6.9 | 2024-07-10 | [41390](https://github.com/airbytehq/airbyte/pull/41390) | Update dependencies |
| 2.6.8 | 2024-07-09 | [40025](https://github.com/airbytehq/airbyte/pull/40025) | Update dependencies |
| 2.6.7 | 2024-07-09 | [41032](https://github.com/airbytehq/airbyte/pull/41032) | Use latest `CDK`: 3.0.0 |
| 2.6.6 | 2024-06-27 | [40592](https://github.com/airbytehq/airbyte/pull/40592) | Updated to use latest `CDK` version, fixed `cursor pagination` logic |
| 2.6.5 | 2024-05-23 | [38607](https://github.com/airbytehq/airbyte/pull/38607) | Migrate to cursor based pagination in stream `Organization memberships` |
| 2.6.4 | 2024-05-20 | [38310](https://github.com/airbytehq/airbyte/pull/38310) | Fix record filter for `Ticket Metrics` stream |
| 2.6.3 | 2024-05-02 | [36669](https://github.com/airbytehq/airbyte/pull/36669) | Schema descriptions |
| 2.6.2 | 2024-02-05 | [37761](https://github.com/airbytehq/airbyte/pull/37761) | Add stop condition for `Ticket Audits` when recieved old records; Ignore 403 and 404 status codes. |
| 2.6.1 | 2024-04-30 | [37723](https://github.com/airbytehq/airbyte/pull/37723) | Add %Y-%m-%dT%H:%M:%S%z to cursor_datetime_formats |
| 2.6.0 | 2024-04-29 | [36823](https://github.com/airbytehq/airbyte/pull/36823) | Migrate to low code; Add new stream `Ticket Activities` |
| 2.5.0 | 2024-04-25 | [36388](https://github.com/airbytehq/airbyte/pull/36388) | Fix data type of field in `Tickets` stream schema stream. |
| 2.4.1 | 2024-04-20 | [37450](https://github.com/airbytehq/airbyte/pull/37450) | Fix parsing response for `Ticket Metrics` stream. |
| 2.4.0 | 2024-04-09 | [36897](https://github.com/airbytehq/airbyte/pull/36897) | Fix long-running syncs for `Ticket Metrics`, `Ticket Audits` and `Satisfaction Ratings` streams. |
| 2.3.0 | 2024-03-26 | [36403](https://github.com/airbytehq/airbyte/pull/36403) | Unpin CDK version, add record counts to state messages |
| 2.2.8 | 2024-02-09 | [35083](https://github.com/airbytehq/airbyte/pull/35083) | Manage dependencies with Poetry. |
| 2.2.7 | 2024-02-05 | [34840](https://github.com/airbytehq/airbyte/pull/34840) | Fix missing fields in schema |
| 2.2.6 | 2024-01-11 | [34064](https://github.com/airbytehq/airbyte/pull/34064) | Skip 504 Error for stream `Ticket Audits` |
| 2.2.5 | 2024-01-08 | [34010](https://github.com/airbytehq/airbyte/pull/34010) | Prepare for airbyte-lib |
| 2.2.4 | 2023-12-20 | [33680](https://github.com/airbytehq/airbyte/pull/33680) | Fix pagination issue for streams related to incremental export sync |
| 2.2.3 | 2023-12-14 | [33435](https://github.com/airbytehq/airbyte/pull/33435) | Fix 504 Error for stream Ticket Audits |
| 2.2.2 | 2023-12-01 | [33012](https://github.com/airbytehq/airbyte/pull/33012) | Increase number of retries for backoff policy to 10 |
| 2.2.1 | 2023-11-10 | [32440](https://github.com/airbytehq/airbyte/pull/32440) | Made refactoring to improve code maintainability |
| 2.2.0 | 2023-10-31 | [31999](https://github.com/airbytehq/airbyte/pull/31999) | Extended the `CustomRoles` stream schema |
| 2.1.1 | 2023-10-23 | [31702](https://github.com/airbytehq/airbyte/pull/31702) | Base image migration: remove Dockerfile and use the python-connector-base image |
| 2.1.0 | 2023-10-19 | [31606](https://github.com/airbytehq/airbyte/pull/31606) | Added new field `reply_time_in_seconds` to the `Ticket Metrics` stream schema |
| 2.0.0 | 2023-09-15 | [30440](https://github.com/airbytehq/airbyte/pull/30440) | Remove stream `Deleted Tickets` |
| 1.7.0 | 2023-09-11 | [30259](https://github.com/airbytehq/airbyte/pull/30259) | Add stream `Deleted Tickets` |
| 1.6.0 | 2023-09-09 | [30168](https://github.com/airbytehq/airbyte/pull/30168) | Make `start_date` field optional |
| 1.5.1 | 2023-09-05 | [30142](https://github.com/airbytehq/airbyte/pull/30142) | Handle non-JSON Response |
| 1.5.0 | 2023-09-04 | [30138](https://github.com/airbytehq/airbyte/pull/30138) | Add new Streams: `Article Votes`, `Article Comments`, `Article Comment Votes` |
| 1.4.0 | 2023-09-04 | [30134](https://github.com/airbytehq/airbyte/pull/30134) | Add incremental support for streams: `custom Roles`, `Schedules`, `SLA Policies` |
| 1.3.0 | 2023-08-30 | [30031](https://github.com/airbytehq/airbyte/pull/30031) | Add new streams: `Articles`, `Organization Fields` |
| 1.2.2 | 2023-08-30 | [29998](https://github.com/airbytehq/airbyte/pull/29998) | Fix typo in stream `AttributeDefinitions`: field condition |
| 1.2.1 | 2023-08-30 | [29991](https://github.com/airbytehq/airbyte/pull/29991) | Remove Custom availability strategy |
| 1.2.0 | 2023-08-29 | [29940](https://github.com/airbytehq/airbyte/pull/29940) | Add undeclared fields to schemas |
| 1.1.1 | 2023-08-29 | [29904](https://github.com/airbytehq/airbyte/pull/29904) | Make `Organizations` stream incremental |
| 1.1.0 | 2023-08-28 | [29891](https://github.com/airbytehq/airbyte/pull/29891) | Add stream `UserFields` |
| 1.0.0 | 2023-07-27 | [28774](https://github.com/airbytehq/airbyte/pull/28774) | Fix retry logic & update cursor for `Tickets` stream |
| 0.11.0 | 2023-08-10 | [27208](https://github.com/airbytehq/airbyte/pull/27208) | Add stream `Topics` |
| 0.10.7 | 2023-08-09 | [29256](https://github.com/airbytehq/airbyte/pull/29256) | Update tooltip descriptions in spec |
| 0.10.6 | 2023-08-04 | [29031](https://github.com/airbytehq/airbyte/pull/29031) | Reverted `advancedAuth` spec changes |
| 0.10.5 | 2023-08-01 | [28910](https://github.com/airbytehq/airbyte/pull/28910) | Updated `advancedAuth` broken references |
| 0.10.4 | 2023-07-25 | [28397](https://github.com/airbytehq/airbyte/pull/28397) | Handle 404 Error |
| 0.10.3 | 2023-07-24 | [28612](https://github.com/airbytehq/airbyte/pull/28612) | Fix pagination for stream `TicketMetricEvents` |
| 0.10.2 | 2023-07-19 | [28487](https://github.com/airbytehq/airbyte/pull/28487) | Remove extra page from params |
| 0.10.1 | 2023-07-10 | [28096](https://github.com/airbytehq/airbyte/pull/28096) | Replace `offset` pagination with `cursor` pagination |
| 0.10.0 | 2023-07-06 | [27991](https://github.com/airbytehq/airbyte/pull/27991) | Add streams: `PostVotes`, `PostCommentVotes` |
| 0.9.0 | 2023-07-05 | [27961](https://github.com/airbytehq/airbyte/pull/27961) | Add stream: `Post Comments` |
| 0.8.1 | 2023-06-27 | [27765](https://github.com/airbytehq/airbyte/pull/27765) | Bugfix: Nonetype error while syncing more then 100000 organizations |
| 0.8.0 | 2023-06-09 | [27156](https://github.com/airbytehq/airbyte/pull/27156) | Add stream `Posts` |
| 0.7.0 | 2023-06-27 | [27436](https://github.com/airbytehq/airbyte/pull/27436) | Add Ticket Skips stream |
| 0.6.0 | 2023-06-27 | [27450](https://github.com/airbytehq/airbyte/pull/27450) | Add Skill Based Routing streams |
| 0.5.0 | 2023-06-26 | [27735](https://github.com/airbytehq/airbyte/pull/27735) | License Update: Elv2 stream stream |
| 0.4.0 | 2023-06-16 | [27431](https://github.com/airbytehq/airbyte/pull/27431) | Add Organization Memberships stream |
| 0.3.1 | 2023-06-02 | [26945](https://github.com/airbytehq/airbyte/pull/26945) | Make `Ticket Metrics` stream to use cursor pagination |
| 0.3.0 | 2023-05-23 | [26347](https://github.com/airbytehq/airbyte/pull/26347) | Add stream `Audit Logs` logs` |
| 0.2.30 | 2023-05-23 | [26414](https://github.com/airbytehq/airbyte/pull/26414) | Added missing handlers when `empty json` or `JSONDecodeError` is received |
| 0.2.29 | 2023-04-18 | [25214](https://github.com/airbytehq/airbyte/pull/25214) | Add missing fields to `Tickets` stream |
| 0.2.28 | 2023-03-21 | [24053](https://github.com/airbytehq/airbyte/pull/24053) | Fix stream `sla_policies` schema data type error (events.value) |
| 0.2.27 | 2023-03-22 | [22817](https://github.com/airbytehq/airbyte/pull/22817) | Specified date formatting in specification |
| 0.2.26 | 2023-03-20 | [24252](https://github.com/airbytehq/airbyte/pull/24252) | Handle invalid `start_date` when checking connection |
| 0.2.25 | 2023-02-28 | [22308](https://github.com/airbytehq/airbyte/pull/22308) | Add `AvailabilityStrategy` for all streams |
| 0.2.24 | 2023-02-17 | [23246](https://github.com/airbytehq/airbyte/pull/23246) | Handle `StartTimeTooRecent` error for Tickets stream |
| 0.2.23 | 2023-02-15 | [23035](https://github.com/airbytehq/airbyte/pull/23035) | Handle 403 Error |
| 0.2.22 | 2023-02-14 | [22483](https://github.com/airbytehq/airbyte/pull/22483) | Fix test; handle 400 error |
| 0.2.21 | 2023-01-27 | [22027](https://github.com/airbytehq/airbyte/pull/22027) | Set `AvailabilityStrategy` for streams explicitly to `None` |
| 0.2.20 | 2022-12-28 | [20900](https://github.com/airbytehq/airbyte/pull/20900) | Remove synchronous time.sleep, add logging, reduce backoff time |
| 0.2.19 | 2022-12-09 | [19967](https://github.com/airbytehq/airbyte/pull/19967) | Fix reading response for more than 100k records |
| 0.2.18 | 2022-11-29 | [19432](https://github.com/airbytehq/airbyte/pull/19432) | Revert changes from version 0.2.15, use a test read instead |
| 0.2.17 | 2022-11-24 | [19792](https://github.com/airbytehq/airbyte/pull/19792) | Transform `ticket_comments.via` "-" to null |
| 0.2.16 | 2022-09-28 | [17326](https://github.com/airbytehq/airbyte/pull/17326) | Migrate to per-stream states. |
| 0.2.15 | 2022-08-03 | [15233](https://github.com/airbytehq/airbyte/pull/15233) | Added `subscription plan` check on `streams discovery` step to remove streams that are not accessible for fetch due to subscription plan restrictions |
| 0.2.14 | 2022-07-27 | [15036](https://github.com/airbytehq/airbyte/pull/15036) | Convert `ticket_audits.previous_value` values to string |
| 0.2.13 | 2022-07-21 | [14829](https://github.com/airbytehq/airbyte/pull/14829) | Convert `tickets.custom_fields` values to string |
| 0.2.12 | 2022-06-30 | [14304](https://github.com/airbytehq/airbyte/pull/14304) | Fixed Pagination for Group Membership stream |
| 0.2.11 | 2022-06-24 | [14112](https://github.com/airbytehq/airbyte/pull/14112) | Fixed "Retry-After" non integer value |
| 0.2.10 | 2022-06-14 | [13757](https://github.com/airbytehq/airbyte/pull/13757) | Fixed the bug with `TicketMetrics` stream, HTTP Error 429, caused by lots of API requests |
| 0.2.9 | 2022-05-27 | [13261](https://github.com/airbytehq/airbyte/pull/13261) | Bugfix for the unhandled [ChunkedEncodingError](https://github.com/airbytehq/airbyte/issues/12591) and [ConnectionError](https://github.com/airbytehq/airbyte/issues/12155) |
| 0.2.8 | 2022-05-20 | [13055](https://github.com/airbytehq/airbyte/pull/13055) | Fixed minor issue for stream `ticket_audits` schema |
| 0.2.7 | 2022-04-27 | [12335](https://github.com/airbytehq/airbyte/pull/12335) | Adding fixtures to mock time.sleep for connectors that explicitly sleep |
| 0.2.6 | 2022-04-19 | [12122](https://github.com/airbytehq/airbyte/pull/12122) | Fixed the bug when only 100,000 Users are synced [11895](https://github.com/airbytehq/airbyte/issues/11895) and fixed bug when `start_date` is not used on user stream [12059](https://github.com/airbytehq/airbyte/issues/12059). |
| 0.2.5 | 2022-04-05 | [11727](https://github.com/airbytehq/airbyte/pull/11727) | Fixed the bug when state was not parsed correctly |
| 0.2.4 | 2022-04-04 | [11688](https://github.com/airbytehq/airbyte/pull/11688) | Small documentation corrections |
| 0.2.3 | 2022-03-23 | [11349](https://github.com/airbytehq/airbyte/pull/11349) | Fixed the bug when Tickets stream didn't return deleted records |
| 0.2.2 | 2022-03-17 | [11237](https://github.com/airbytehq/airbyte/pull/11237) | Fixed the bug when TicketComments stream didn't return all records |
| 0.2.1 | 2022-03-15 | [11162](https://github.com/airbytehq/airbyte/pull/11162) | Added support of OAuth2.0 authentication method |
| 0.2.0 | 2022-03-01 | [9456](https://github.com/airbytehq/airbyte/pull/9456) | Update source to use future requests |
| 0.1.12 | 2022-01-25 | [9785](https://github.com/airbytehq/airbyte/pull/9785) | Add additional log messages |
| 0.1.11 | 2021-12-21 | [8987](https://github.com/airbytehq/airbyte/pull/8987) | Update connector fields title/description |
| 0.1.9 | 2021-12-16 | [8616](https://github.com/airbytehq/airbyte/pull/8616) | Adds Brands, CustomRoles and Schedules streams |
| 0.1.8 | 2021-11-23 | [8050](https://github.com/airbytehq/airbyte/pull/8168) | Adds TicketMetricEvents stream |
| 0.1.7 | 2021-11-23 | [8058](https://github.com/airbytehq/airbyte/pull/8058) | Added support of AccessToken authentication |
| 0.1.6 | 2021-11-18 | [8050](https://github.com/airbytehq/airbyte/pull/8050) | Fix wrong types for schemas, add TypeTransformer |
| 0.1.5 | 2021-10-26 | [7679](https://github.com/airbytehq/airbyte/pull/7679) | Add ticket_id and ticket_comments |
| 0.1.4 | 2021-10-26 | [7377](https://github.com/airbytehq/airbyte/pull/7377) | Fix initially_assigned_at type in ticket metrics |
| 0.1.3 | 2021-10-17 | [7097](https://github.com/airbytehq/airbyte/pull/7097) | Corrected the connector's specification |
| 0.1.2 | 2021-10-16 | [6513](https://github.com/airbytehq/airbyte/pull/6513) | Fixed TicketComments stream |
| 0.1.1 | 2021-09-02 | [5787](https://github.com/airbytehq/airbyte/pull/5787) | Fixed incremental logic for the ticket_comments stream |
| 0.1.0 | 2021-07-21 | [4861](https://github.com/airbytehq/airbyte/pull/4861) | Created CDK native zendesk connector |
</details>