diff --git a/config.ci.toml b/config.ci.toml index 2bedb8ac..949bba44 100644 --- a/config.ci.toml +++ b/config.ci.toml @@ -11,12 +11,6 @@ host = "localhost" user = "postgres" password = "postgres" -[jira] -domain = "" -account = "" -api_key = "" -severity_field = "" - [slack] bot_token = "test-bot-token" team_id = "test-bot-id" diff --git a/config.example.toml b/config.example.toml index c981fa0a..5bec945c 100644 --- a/config.example.toml +++ b/config.example.toml @@ -10,12 +10,6 @@ host = "localhost" user = "postgres" password = "dummy_dev_password" -[jira] -domain = "https://.atlassian.net" -account = "" -api_key = "" -severity_field = "customfield_11023" - [slack] bot_token = "" team_id = "" diff --git a/pyproject.toml b/pyproject.toml index a0865b49..058293c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "django-kubernetes>=1.1.0", "djangorestframework>=3.15.2", "google-auth>=2.37.0", - "jira>=3.5.0", "psycopg[binary]>=3.2.11", "pyserde[toml]>=0.28.0", "slack-bolt>=1.27.0", diff --git a/src/firetower/config.py b/src/firetower/config.py index 7f8234f7..937159e1 100644 --- a/src/firetower/config.py +++ b/src/firetower/config.py @@ -26,14 +26,6 @@ class DatadogConfig: app_key: str -@deserialize -class JIRAConfig: - domain: str - account: str - api_key: str - severity_field: str - - @deserialize class SlackConfig: bot_token: str @@ -59,7 +51,6 @@ class ConfigFile: postgres: PostgresConfig datadog: DatadogConfig | None - jira: JIRAConfig slack: SlackConfig auth: AuthConfig @@ -113,12 +104,6 @@ def __init__(self) -> None: user="postgres", password="dummy_dev_password", ) - self.jira = JIRAConfig( - domain="", - account="", - api_key="", - severity_field="", - ) self.slack = SlackConfig( bot_token="", team_id="", diff --git a/src/firetower/incidents/serializers.py b/src/firetower/incidents/serializers.py index 92ce3c12..8909a6d8 100644 --- a/src/firetower/incidents/serializers.py +++ b/src/firetower/incidents/serializers.py @@ -322,7 +322,7 @@ class IncidentWriteSerializer(serializers.ModelSerializer): root_cause_tags, impact_type_tags captain/reporter: Email address of the user - external_links format: {"slack": "url", "jira": "url", ...} + external_links format: {"slack": "url", "linear": "url", ...} - Merges with existing links (only updates provided links) - Use null to delete a specific link: {"slack": null} - Omit external_links field to leave existing links unchanged diff --git a/src/firetower/incidents/tests/test_models.py b/src/firetower/incidents/tests/test_models.py index 59706dd7..616bbdae 100644 --- a/src/firetower/incidents/tests/test_models.py +++ b/src/firetower/incidents/tests/test_models.py @@ -394,13 +394,13 @@ def test_external_link_multiple_types(self): incident=incident, type=ExternalLinkType.SLACK, url="https://slack.com" ) - jira = ExternalLink.objects.create( - incident=incident, type=ExternalLinkType.JIRA, url="https://jira.com" + linear = ExternalLink.objects.create( + incident=incident, type=ExternalLinkType.LINEAR, url="https://linear.app" ) assert incident.external_links.count() == 2 assert slack in incident.external_links.all() - assert jira in incident.external_links.all() + assert linear in incident.external_links.all() def test_external_link_str(self): """Test external link string representation""" diff --git a/src/firetower/incidents/tests/test_serializers.py b/src/firetower/incidents/tests/test_serializers.py index 1707e690..85a2ba9b 100644 --- a/src/firetower/incidents/tests/test_serializers.py +++ b/src/firetower/incidents/tests/test_serializers.py @@ -144,7 +144,7 @@ def test_incident_detail_serialization(self): assert ( data["external_links"]["slack"] == "https://slack.com/channels/incident-123" ) - assert "jira" not in data["external_links"] # Not set, so not included + assert "linear" not in data["external_links"] # Not set, so not included assert len(data["external_links"]) == 1 diff --git a/src/firetower/incidents/tests/test_views.py b/src/firetower/incidents/tests/test_views.py index 97f52d51..e103e574 100644 --- a/src/firetower/incidents/tests/test_views.py +++ b/src/firetower/incidents/tests/test_views.py @@ -829,7 +829,7 @@ def test_create_incident_with_external_links(self): "reporter": self.reporter.email, "external_links": { "slack": "https://slack.com/channel/123", - "jira": "https://jira.company.com/browse/INC-1", + "linear": "https://linear.app/team/issue/ENG-1", }, } @@ -843,7 +843,7 @@ def test_create_incident_with_external_links(self): data = response.json() assert data["external_links"]["slack"] == "https://slack.com/channel/123" - assert data["external_links"]["jira"] == "https://jira.company.com/browse/INC-1" + assert data["external_links"]["linear"] == "https://linear.app/team/issue/ENG-1" assert "datadog" not in data["external_links"] def test_create_incident_with_tags(self): @@ -896,8 +896,8 @@ def test_add_external_link_via_patch(self): self.client.force_authenticate(user=self.captain) - # Add jira link, should keep slack - payload = {"external_links": {"jira": "https://jira.com/new"}} + # Add linear link, should keep slack + payload = {"external_links": {"linear": "https://linear.app/new"}} response = self.client.patch( f"/api/incidents/{incident.incident_number}/", payload, format="json" ) @@ -909,7 +909,7 @@ def test_add_external_link_via_patch(self): data = response.json() assert data["external_links"]["slack"] == "https://slack.com/original" - assert data["external_links"]["jira"] == "https://jira.com/new" + assert data["external_links"]["linear"] == "https://linear.app/new" def test_update_existing_external_link(self): """Test updating an existing external link via PATCH""" @@ -958,13 +958,13 @@ def test_delete_external_link_with_null(self): ) ExternalLink.objects.create( incident=incident, - type=ExternalLinkType.JIRA, - url="https://jira.com/test", + type=ExternalLinkType.LINEAR, + url="https://linear.app/test", ) self.client.force_authenticate(user=self.captain) - # Delete slack link, keep jira + # Delete slack link, keep linear payload = {"external_links": {"slack": None}} response = self.client.patch( f"/api/incidents/{incident.incident_number}/", payload, format="json" @@ -972,12 +972,12 @@ def test_delete_external_link_with_null(self): assert response.status_code == 200 - # Verify slack deleted, jira remains + # Verify slack deleted, linear remains response = self.client.get(f"/api/incidents/{incident.incident_number}/") data = response.json() assert "slack" not in data["external_links"] - assert data["external_links"]["jira"] == "https://jira.com/test" + assert data["external_links"]["linear"] == "https://linear.app/test" def test_invalid_external_link_type(self): """Test that invalid link types are rejected""" @@ -1047,7 +1047,7 @@ def test_patch_without_external_links_preserves_existing(self): "reporter": self.reporter.email, "external_links": { "slack": "https://slack.com/channel", - "jira": "https://jira.example.com/issue", + "linear": "https://linear.app/issue", }, } response = self.client.post("/api/incidents/", payload, format="json") @@ -1070,7 +1070,7 @@ def test_patch_without_external_links_preserves_existing(self): data = response.json() assert len(data["external_links"]) == 2 assert data["external_links"]["slack"] == "https://slack.com/channel" - assert data["external_links"]["jira"] == "https://jira.example.com/issue" + assert data["external_links"]["linear"] == "https://linear.app/issue" def test_update_affected_service_tags_via_patch(self): """Test setting affected_service_tags via PATCH""" diff --git a/src/firetower/integrations/services/__init__.py b/src/firetower/integrations/services/__init__.py index 568cfcb7..847856a5 100644 --- a/src/firetower/integrations/services/__init__.py +++ b/src/firetower/integrations/services/__init__.py @@ -1,6 +1,5 @@ """Services package for external integrations.""" -from .jira import JiraService from .slack import SlackService -__all__ = ["JiraService", "SlackService"] +__all__ = ["SlackService"] diff --git a/src/firetower/integrations/services/jira.py b/src/firetower/integrations/services/jira.py deleted file mode 100644 index 5aa58104..00000000 --- a/src/firetower/integrations/services/jira.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -Jira integration service for fetching incident data. - -This service provides a simple interface to interact with Jira's REST API -and transform Jira issues into our incident data format. -""" - -import re -from typing import Any - -from django.conf import settings -from jira import JIRA - - -class JiraService: - """ - Service class for interacting with Jira API. - - Provides methods to fetch incident data from Jira and transform it - into a format suitable for the Firetower application. - """ - - def __init__(self) -> None: - """Initialize the Jira service.""" - # Get Jira configuration from Django settings - jira_config = settings.JIRA - - # Validate required settings - if not jira_config["ACCOUNT"] or not jira_config["API_KEY"]: - raise ValueError("Jira credentials not configured in settings.JIRA") - - # Store config for later use - self.domain = jira_config["DOMAIN"] - self.project_key = settings.PROJECT_KEY - self.severity_field_id = jira_config["SEVERITY_FIELD"] - - # Initialize Jira client with basic auth - self.client = JIRA( - self.domain, basic_auth=(jira_config["ACCOUNT"], jira_config["API_KEY"]) - ) - - def _extract_severity(self, issue: Any) -> str | None: - """Extract severity from Jira custom field.""" - severity_field = getattr(issue.fields, self.severity_field_id, None) - return getattr(severity_field, "value", None) if severity_field else None - - def get_incident(self, incident_key: str) -> dict[str, Any]: - """ - Fetch a single incident by its Jira key. - - Args: - incident_key (str): Jira issue key (e.g., 'INC-1247') - - Returns: - dict: Incident data or None if not found - """ - issue = self.client.issue(incident_key) - - return { - "id": issue.key, - "title": issue.fields.summary, - "description": getattr(issue.fields, "description", "") or "", - "status": issue.fields.status.name, - "severity": self._extract_severity(issue), - "assignee": issue.fields.assignee.displayName - if issue.fields.assignee - else None, - "assignee_email": issue.fields.assignee.emailAddress - if issue.fields.assignee - else None, - "reporter": issue.fields.reporter.displayName - if issue.fields.reporter - else None, - "reporter_email": issue.fields.reporter.emailAddress - if issue.fields.reporter - else None, - "created_at": issue.fields.created, - "updated_at": issue.fields.updated, - } - - def get_incidents( - self, statuses: list[str] | None = None, max_results: int = 50 - ) -> list[dict[str, Any]]: - """ - Fetch a list of incidents from the Jira project. - - Args: - statuses (list[str], optional): Filter by status values (e.g., ['Active', 'Mitigated']) - max_results (int): Maximum number of incidents to return (default: 50) - - Returns: - list: List of incident data dictionaries - """ - jql_parts = [f'project = "{self.project_key}"'] - - if statuses: - # Validate each status - for status in statuses: - if not re.match(r"^[A-Za-z\s]+$", status): - raise ValueError( - f"Invalid status format: {status}. Only alphabetical characters and spaces allowed." - ) - - # Build IN clause for multiple statuses - status_list = ", ".join(f'"{s}"' for s in statuses) - jql_parts.append(f"status IN ({status_list})") - - jql_query = " AND ".join(jql_parts) - jql_query += " ORDER BY created DESC" - - issues = self.client.search_issues( - jql_query, maxResults=max_results, expand="changelog" - ) - - incidents = [] - for issue in issues: - incident_data = { - "id": issue.key, - "title": issue.fields.summary, - "description": getattr(issue.fields, "description", "") or "", - "status": issue.fields.status.name, - "severity": self._extract_severity(issue), - "assignee": issue.fields.assignee.displayName - if issue.fields.assignee - else None, - "assignee_email": issue.fields.assignee.emailAddress - if issue.fields.assignee - else None, - "reporter": issue.fields.reporter.displayName - if issue.fields.reporter - else None, - "reporter_email": issue.fields.reporter.emailAddress - if issue.fields.reporter - else None, - "created_at": issue.fields.created, - "updated_at": issue.fields.updated, - } - incidents.append(incident_data) - - return incidents diff --git a/src/firetower/integrations/test_jira.py b/src/firetower/integrations/test_jira.py deleted file mode 100644 index 855a053d..00000000 --- a/src/firetower/integrations/test_jira.py +++ /dev/null @@ -1,166 +0,0 @@ -""" -Basic pytest tests for Jira integration service. -""" - -import os -from unittest.mock import patch - -import pytest - -from .services.jira import JiraService - -# Set up Django settings -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "firetower.settings") - -import django -from django.conf import settings - -# Setup Django -django.setup() - - -class TestJiraService: - """Test suite for JiraService""" - - def test_initialization_requires_credentials(self): - """Test that JiraService initialization validates required credentials.""" - # Mock settings to have empty credentials - mock_jira_config = { - "ACCOUNT": "", - "API_KEY": "", - "DOMAIN": "https://test.atlassian.net", - "PROJECT_KEY": "INC", - "SEVERITY_FIELD": "customfield_10001", - } - - with patch.object(settings, "JIRA", mock_jira_config): - with pytest.raises(ValueError, match="Jira credentials not configured"): - JiraService() - - def test_initialization_success_with_valid_credentials(self): - """Test that JiraService initializes successfully with valid credentials.""" - mock_jira_config = { - "ACCOUNT": "test@example.com", - "API_KEY": "test-api-key", - "DOMAIN": "https://test.atlassian.net", - "SEVERITY_FIELD": "customfield_10001", - } - - with patch.object(settings, "JIRA", mock_jira_config): - with patch.object(settings, "PROJECT_KEY", "INC"): - with patch( - "firetower.integrations.services.jira.JIRA" - ) as mock_jira_client: - service = JiraService() - - # Verify the service was created and JIRA client was initialized - assert service.domain == "https://test.atlassian.net" - assert service.project_key == "INC" - assert service.severity_field_id == "customfield_10001" - mock_jira_client.assert_called_once() - - def test_extract_severity_with_valid_field(self): - """Test severity extraction from Jira issue with valid severity field.""" - mock_jira_config = { - "ACCOUNT": "test@example.com", - "API_KEY": "test-api-key", - "DOMAIN": "https://test.atlassian.net", - "PROJECT_KEY": "INC", - "SEVERITY_FIELD": "customfield_10001", - } - - with patch.object(settings, "JIRA", mock_jira_config): - with patch("firetower.integrations.services.jira.JIRA"): - service = JiraService() - - # Mock issue with severity field - mock_issue = type("MockIssue", (), {})() - mock_issue.fields = type("MockFields", (), {})() - mock_severity = type("MockSeverity", (), {"value": "P1"})() - setattr(mock_issue.fields, "customfield_10001", mock_severity) - - severity = service._extract_severity(mock_issue) - assert severity == "P1" - - def test_extract_severity_with_missing_field(self): - """Test severity extraction when severity field is missing.""" - mock_jira_config = { - "ACCOUNT": "test@example.com", - "API_KEY": "test-api-key", - "DOMAIN": "https://test.atlassian.net", - "PROJECT_KEY": "INC", - "SEVERITY_FIELD": "customfield_10001", - } - - with patch.object(settings, "JIRA", mock_jira_config): - with patch("firetower.integrations.services.jira.JIRA"): - service = JiraService() - - # Mock issue without severity field - mock_issue = type("MockIssue", (), {})() - mock_issue.fields = type("MockFields", (), {})() - - severity = service._extract_severity(mock_issue) - assert severity is None - - def test_get_incidents_validates_status_format(self): - """Test that get_incidents validates status parameter format.""" - mock_jira_config = { - "ACCOUNT": "test@example.com", - "API_KEY": "test-api-key", - "DOMAIN": "https://test.atlassian.net", - "PROJECT_KEY": "INC", - "SEVERITY_FIELD": "customfield_10001", - } - - with patch.object(settings, "JIRA", mock_jira_config): - with patch("firetower.integrations.services.jira.JIRA"): - service = JiraService() - - # Test with invalid characters in status - with pytest.raises(ValueError, match="Invalid status format"): - service.get_incidents(statuses=["Active; DROP TABLE incidents;"]) - - # Test with numbers in status - with pytest.raises(ValueError, match="Invalid status format"): - service.get_incidents(statuses=["Status123"]) - - def test_get_incidents_builds_correct_jql_query(self): - """Test that get_incidents builds the correct JQL query.""" - mock_jira_config = { - "ACCOUNT": "test@example.com", - "API_KEY": "test-api-key", - "DOMAIN": "https://test.atlassian.net", - "SEVERITY_FIELD": "customfield_10001", - } - - with patch.object(settings, "JIRA", mock_jira_config): - with patch.object(settings, "PROJECT_KEY", "TESTINC"): - with patch( - "firetower.integrations.services.jira.JIRA" - ) as mock_jira_client: - mock_client_instance = mock_jira_client.return_value - mock_client_instance.search_issues.return_value = [] - - service = JiraService() - - # Test without status filter - service.get_incidents() - expected_jql = 'project = "TESTINC" ORDER BY created DESC' - mock_client_instance.search_issues.assert_called_with( - expected_jql, maxResults=50, expand="changelog" - ) - - # Test with single status filter - service.get_incidents(statuses=["Active"]) - expected_jql_single = 'project = "TESTINC" AND status IN ("Active") ORDER BY created DESC' - mock_client_instance.search_issues.assert_called_with( - expected_jql_single, maxResults=50, expand="changelog" - ) - - # Test with multiple status filters - service.get_incidents(statuses=["Active", "Mitigated"]) - expected_jql_multiple = 'project = "TESTINC" AND status IN ("Active", "Mitigated") ORDER BY created DESC' - mock_client_instance.search_issues.assert_called_with( - expected_jql_multiple, maxResults=50, expand="changelog" - ) diff --git a/src/firetower/settings.py b/src/firetower/settings.py index b224befc..710e8bb9 100644 --- a/src/firetower/settings.py +++ b/src/firetower/settings.py @@ -215,14 +215,6 @@ def _coerce_region_grouping(raw: list[Any]) -> list[list[str]]: DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -# Jira Integration Configuration -JIRA = { - "DOMAIN": config.jira.domain, - "ACCOUNT": config.jira.account, - "API_KEY": config.jira.api_key, - "SEVERITY_FIELD": config.jira.severity_field, -} - class SlackSettings(TypedDict): BOT_TOKEN: str diff --git a/uv.lock b/uv.lock index 911cdd4d..c8af16db 100644 --- a/uv.lock +++ b/uv.lock @@ -394,15 +394,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8b/9d/6b95ba537a2dc8068142d6d89bf6ed973e32c632bd3f35aa402f5b8149fc/ddtrace-3.18.1-cp314-cp314-win_arm64.whl", hash = "sha256:8ff71b1f1490310ef4409317e2850662370fe14e48ff9b784531ce46b17f0e1c", size = 5208658, upload-time = "2025-11-07T22:55:08.169Z" }, ] -[[package]] -name = "defusedxml" -version = "0.7.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, -] - [[package]] name = "distlib" version = "0.4.0" @@ -522,7 +513,6 @@ dependencies = [ { name = "django-kubernetes" }, { name = "djangorestframework" }, { name = "google-auth" }, - { name = "jira" }, { name = "psycopg", extra = ["binary"] }, { name = "pyserde", extra = ["toml"] }, { name = "slack-bolt" }, @@ -557,7 +547,6 @@ requires-dist = [ { name = "django-kubernetes", specifier = ">=1.1.0" }, { name = "djangorestframework", specifier = ">=3.15.2" }, { name = "google-auth", specifier = ">=2.37.0" }, - { name = "jira", specifier = ">=3.5.0" }, { name = "psycopg", extras = ["binary"], specifier = ">=3.2.11" }, { name = "pyserde", extras = ["toml"], specifier = ">=0.28.0" }, { name = "slack-bolt", specifier = ">=1.27.0" }, @@ -708,23 +697,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "jira" -version = "3.10.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "defusedxml" }, - { name = "packaging" }, - { name = "requests" }, - { name = "requests-oauthlib" }, - { name = "requests-toolbelt" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/65/73/ee4daa7cf4eea457180de0ea78b730b44bb5ad2829dae49cf708a1460819/jira-3.10.5.tar.gz", hash = "sha256:2d09ae3bf4741a2787dd889dfea5926a5d509aac3b28ab3b98c098709e6ee72d", size = 105870, upload-time = "2025-07-28T12:18:22.796Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/89/57/ad078d7379e390798559446607e413fc953c7510711462ab34194dba5924/jira-3.10.5-py3-none-any.whl", hash = "sha256:d4da1385c924ee693d6cc9838e56a34e31d74f0d6899934ef35bbd0d2d33997f", size = 79250, upload-time = "2025-07-28T12:18:21.368Z" }, -] - [[package]] name = "legacy-cgi" version = "2.6.4" @@ -868,15 +840,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, ] -[[package]] -name = "oauthlib" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, -] - [[package]] name = "opentelemetry-api" version = "1.38.0" @@ -1199,31 +1162,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, ] -[[package]] -name = "requests-oauthlib" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "oauthlib" }, - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, -] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "requests" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, -] - [[package]] name = "rich" version = "14.2.0"