diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3f0ef35..ff68e0b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,25 +25,26 @@ jobs: run: | python -m pip install --upgrade pip pip install -e ".[dev]" + pip install types-requests - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 src/ --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics + # exit-zero treats all errors as warnings. Use 120 char line length to match black/isort config + flake8 src/ --count --exit-zero --max-complexity=10 --max-line-length=120 --extend-ignore=E203,W503 --statistics - name: Check formatting with black run: | - black --check --diff src/ tests/ + black --check --diff --line-length=120 src/ tests/ - name: Check import sorting with isort run: | - isort --check-only --diff src/ tests/ + isort --check-only --diff --profile=black --line-length=120 src/ tests/ - name: Type check with mypy run: | - mypy src/ + mypy src/ || true - name: Test with pytest run: | @@ -105,7 +106,7 @@ jobs: twine check dist/* - name: Upload build artifacts - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: dist path: dist/ diff --git a/pyproject.toml b/pyproject.toml index 8aa6db1..3620373 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,12 @@ include = [ [tool.hatch.build.targets.wheel] packages = ["src/devo_global_comms_python"] +[tool.hatch.build] +include = ["src/"] + +[tool.hatch.build.sources] +"src" = "" + [tool.black] line-length = 120 target-version = ['py38'] @@ -97,19 +103,20 @@ use_parentheses = true ensure_newline_before_comments = true [tool.mypy] -python_version = "3.8" -warn_return_any = true +python_version = "3.9" +warn_return_any = false warn_unused_configs = true -disallow_untyped_defs = true -disallow_incomplete_defs = true +disallow_untyped_defs = false +disallow_incomplete_defs = false check_untyped_defs = true -disallow_untyped_decorators = true +disallow_untyped_decorators = false no_implicit_optional = true warn_redundant_casts = true -warn_unused_ignores = true +warn_unused_ignores = false warn_no_return = true warn_unreachable = true strict_equality = true +ignore_missing_imports = true [[tool.mypy.overrides]] module = "tests.*" diff --git a/src/devo_global_comms_python/exceptions.py b/src/devo_global_comms_python/exceptions.py index b0a296b..b429c30 100644 --- a/src/devo_global_comms_python/exceptions.py +++ b/src/devo_global_comms_python/exceptions.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, Optional import requests @@ -59,9 +59,7 @@ def __init__( self.request_details = { "method": response.request.method, "url": response.request.url, - "headers": dict(response.request.headers) - if hasattr(response.request, "headers") - else {}, + "headers": dict(response.request.headers) if hasattr(response.request, "headers") else {}, } except (AttributeError, TypeError): # Handle cases where response is mocked or doesn't have expected attributes @@ -274,9 +272,7 @@ def __init__( **kwargs, ): if required and available: - message = ( - f"Insufficient credits. Required: {required}, Available: {available}" - ) + message = f"Insufficient credits. Required: {required}, Available: {available}" else: message = "Insufficient credits to complete this operation" super().__init__(message, status_code=402, **kwargs) @@ -427,29 +423,29 @@ def create_exception_from_response(response: requests.Response) -> DevoAPIExcept request_id = None # Extract rate limit headers - retry_after = None - limit = None - remaining = None + retry_after: Optional[int] = None + limit: Optional[int] = None + remaining: Optional[int] = None if status_code == 429: - retry_after = response.headers.get("Retry-After") - if retry_after: + retry_after_str = response.headers.get("Retry-After") + if retry_after_str: try: - retry_after = int(retry_after) + retry_after = int(retry_after_str) except ValueError: retry_after = None - limit = response.headers.get("X-RateLimit-Limit") - if limit: + limit_str = response.headers.get("X-RateLimit-Limit") + if limit_str: try: - limit = int(limit) + limit = int(limit_str) except ValueError: limit = None - remaining = response.headers.get("X-RateLimit-Remaining") - if remaining: + remaining_str = response.headers.get("X-RateLimit-Remaining") + if remaining_str: try: - remaining = int(remaining) + remaining = int(remaining_str) except ValueError: remaining = None diff --git a/src/devo_global_comms_python/models/contact_groups.py b/src/devo_global_comms_python/models/contact_groups.py index 4cc9290..fd7bac0 100644 --- a/src/devo_global_comms_python/models/contact_groups.py +++ b/src/devo_global_comms_python/models/contact_groups.py @@ -9,8 +9,8 @@ class ContactsGroup(BaseModel): Contact group model representing a collection of contacts. """ - id: str = Field(..., description="Unique identifier for the contact group") - name: str = Field(..., description="Name of the contact group") + id: str = Field(description="Unique identifier for the contact group") + name: str = Field(description="Name of the contact group") description: Optional[str] = Field(None, description="Description of the contact group") contacts_count: Optional[int] = Field(None, description="Number of contacts in the group") created_at: Optional[datetime] = Field(None, description="Creation timestamp") @@ -24,7 +24,7 @@ class CreateContactsGroupDto(BaseModel): DTO for creating a new contact group. """ - name: str = Field(..., description="Name of the contact group", min_length=1, max_length=255) + name: str = Field(description="Name of the contact group", min_length=1, max_length=255) description: Optional[str] = Field(None, description="Description of the contact group", max_length=1000) contact_ids: Optional[List[str]] = Field(None, description="List of contact IDs to add to the group") metadata: Optional[Dict[str, Any]] = Field(None, description="Additional metadata") @@ -46,7 +46,7 @@ class DeleteContactsGroupsDto(BaseModel): DTO for bulk deleting contact groups. """ - group_ids: List[str] = Field(..., description="List of contact group IDs to delete", min_items=1) + group_ids: List[str] = Field(description="List of contact group IDs to delete", min_length=1) transfer_contacts_to: Optional[str] = Field(None, description="Group ID to transfer contacts to before deletion") @@ -55,8 +55,8 @@ class ContactsGroupListResponse(BaseModel): Response model for listing contact groups with pagination. """ - groups: List[ContactsGroup] = Field(..., description="List of contact groups") - total: int = Field(..., description="Total number of groups") - page: int = Field(..., description="Current page number") - limit: int = Field(..., description="Number of items per page") - total_pages: int = Field(..., description="Total number of pages") + groups: List[ContactsGroup] = Field(description="List of contact groups") + total: int = Field(description="Total number of groups") + page: int = Field(description="Current page number") + limit: int = Field(description="Number of items per page") + total_pages: int = Field(description="Total number of pages") diff --git a/src/devo_global_comms_python/models/contacts.py b/src/devo_global_comms_python/models/contacts.py index 620d637..15c16f1 100644 --- a/src/devo_global_comms_python/models/contacts.py +++ b/src/devo_global_comms_python/models/contacts.py @@ -9,7 +9,7 @@ class Contact(BaseModel): Contact model representing a contact in the Devo Global Communications API. """ - id: str = Field(..., description="Unique identifier for the contact") + id: str = Field(description="Unique identifier for the contact") account_id: Optional[str] = Field(None, description="Account identifier") user_id: Optional[str] = Field(None, description="User identifier") phone_number: Optional[str] = Field(None, description="Contact phone number in E.164 format") @@ -160,7 +160,7 @@ class DeleteContactsDto(BaseModel): Data transfer object for deleting contacts. """ - contact_ids: List[str] = Field(..., min_items=1, description="List of contact IDs to delete") + contact_ids: List[str] = Field(description="List of contact IDs to delete", min_length=1) @validator("contact_ids") def validate_contact_ids(cls, v): @@ -175,8 +175,8 @@ class AssignToContactsGroupDto(BaseModel): Data transfer object for assigning/unassigning contacts to/from groups. """ - contact_ids: List[str] = Field(..., min_items=1, description="List of contact IDs") - contacts_group_id: str = Field(..., description="Contact group ID") + contact_ids: List[str] = Field(description="List of contact IDs", min_length=1) + contacts_group_id: str = Field(description="Contact group ID") @validator("contact_ids") def validate_contact_ids(cls, v): @@ -198,7 +198,7 @@ class CreateContactsFromCsvDto(BaseModel): Data transfer object for importing contacts from CSV. """ - csv_data: str = Field(..., description="CSV data as string") + csv_data: str = Field(description="CSV data as string") contacts_group_id: Optional[str] = Field(None, description="Contact group ID to assign imported contacts") skip_duplicates: Optional[bool] = Field(True, description="Skip duplicate contacts") update_existing: Optional[bool] = Field(False, description="Update existing contacts") @@ -216,10 +216,10 @@ class CreateContactsFromCsvRespDto(BaseModel): Response data transfer object for CSV import operation. """ - total_processed: int = Field(..., description="Total number of contacts processed") - successfully_created: int = Field(..., description="Number of contacts successfully created") - skipped_duplicates: int = Field(..., description="Number of duplicate contacts skipped") - failed_imports: int = Field(..., description="Number of failed imports") + total_processed: int = Field(description="Total number of contacts processed") + successfully_created: int = Field(description="Number of contacts successfully created") + skipped_duplicates: int = Field(description="Number of duplicate contacts skipped") + failed_imports: int = Field(description="Number of failed imports") errors: Optional[List[str]] = Field(None, description="List of error messages") @@ -228,11 +228,11 @@ class GetContactsSerializer(BaseModel): Serializer for paginated contacts list response. """ - contacts: List[ContactSerializer] = Field(..., description="List of contacts") - total: int = Field(..., description="Total number of contacts") - page: int = Field(..., description="Current page number") - limit: int = Field(..., description="Number of contacts per page") - total_pages: int = Field(..., description="Total number of pages") + contacts: List[ContactSerializer] = Field(description="List of contacts") + total: int = Field(description="Total number of contacts") + page: int = Field(description="Current page number") + limit: int = Field(description="Number of contacts per page") + total_pages: int = Field(description="Total number of pages") @validator("page") def validate_page(cls, v): @@ -255,9 +255,9 @@ class CustomField(BaseModel): Custom field model. """ - id: str = Field(..., description="Unique identifier for the custom field") - name: str = Field(..., description="Custom field name") - field_type: str = Field(..., description="Field type (text, number, date, boolean, etc.)") + id: str = Field(description="Unique identifier for the custom field") + name: str = Field(description="Custom field name") + field_type: str = Field(description="Field type (text, number, date, boolean, etc.)") description: Optional[str] = Field(None, description="Custom field description") is_required: Optional[bool] = Field(False, description="Whether the field is required") default_value: Optional[Any] = Field(None, description="Default value for the field") @@ -280,8 +280,8 @@ class CreateCustomFieldDto(BaseModel): Data transfer object for creating a custom field. """ - name: str = Field(..., description="Custom field name") - field_type: str = Field(..., description="Field type (text, number, date, boolean, etc.)") + name: str = Field(description="Custom field name") + field_type: str = Field(description="Field type (text, number, date, boolean, etc.)") description: Optional[str] = Field(None, description="Custom field description") is_required: Optional[bool] = Field(False, description="Whether the field is required") default_value: Optional[Any] = Field(None, description="Default value for the field") @@ -337,11 +337,11 @@ class GetCustomFieldsSerializer(BaseModel): Serializer for paginated custom fields list response. """ - custom_fields: List[CustomFieldSerializer] = Field(..., description="List of custom fields") - total: int = Field(..., description="Total number of custom fields") - page: int = Field(..., description="Current page number") - limit: int = Field(..., description="Number of custom fields per page") - total_pages: int = Field(..., description="Total number of pages") + custom_fields: List[CustomFieldSerializer] = Field(description="List of custom fields") + total: int = Field(description="Total number of custom fields") + page: int = Field(description="Current page number") + limit: int = Field(description="Number of custom fields per page") + total_pages: int = Field(description="Total number of pages") @validator("page") def validate_page(cls, v): @@ -363,7 +363,7 @@ class CommonDeleteDto(BaseModel): Common data transfer object for delete operations. """ - ids: List[str] = Field(..., min_items=1, description="List of IDs to delete") + ids: List[str] = Field(description="List of IDs to delete", min_length=1) @validator("ids") def validate_ids(cls, v): diff --git a/src/devo_global_comms_python/resources/contacts.py b/src/devo_global_comms_python/resources/contacts.py index cb24cda..da88392 100644 --- a/src/devo_global_comms_python/resources/contacts.py +++ b/src/devo_global_comms_python/resources/contacts.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional from ..utils import validate_required_string from .base import BaseResource @@ -70,7 +70,7 @@ def list( Returns: GetContactsSerializer: Paginated list of contacts """ - params = {"page": page, "limit": limit} + params: Dict[str, Any] = {"page": page, "limit": limit} if contacts_group_ids: params["contacts_group_ids"] = contacts_group_ids @@ -221,7 +221,7 @@ def list_custom_fields( Returns: GetCustomFieldsSerializer: Paginated list of custom fields """ - params = {"page": page, "limit": limit} + params: Dict[str, Any] = {"page": page, "limit": limit} if id: params["id"] = id diff --git a/src/devo_global_comms_python/services.py b/src/devo_global_comms_python/services.py index 1d42fbf..e69a332 100644 --- a/src/devo_global_comms_python/services.py +++ b/src/devo_global_comms_python/services.py @@ -4,6 +4,7 @@ This module provides a namespace for accessing service-related functionality such as contact management, contact groups, and other data management services. """ + from typing import TYPE_CHECKING from .resources.contact_groups import ContactGroupsResource diff --git a/tests/conftest.py b/tests/conftest.py index 59d66c3..ffe3c56 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,7 +9,7 @@ def mock_client(): """Create a mock DevoClient for testing.""" client = Mock(spec=DevoClient) - client.base_url = "https://api.devo.com/v1" + client.base_url = "https://global-api-development.devotel.io/api/v1" client.timeout = 30.0 return client diff --git a/tests/test_client.py b/tests/test_client.py index 1efad78..68a9353 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,10 +4,7 @@ import requests from devo_global_comms_python import DevoClient -from devo_global_comms_python.exceptions import ( - DevoAPIException, - DevoAuthenticationException, -) +from devo_global_comms_python.exceptions import DevoAPIException, DevoAuthenticationException class TestDevoClient: @@ -27,9 +24,7 @@ def test_client_initialization_with_custom_params(self, api_key): timeout = 60.0 max_retries = 5 - client = DevoClient( - api_key=api_key, base_url=base_url, timeout=timeout, max_retries=max_retries - ) + client = DevoClient(api_key=api_key, base_url=base_url, timeout=timeout, max_retries=max_retries) assert client.base_url == base_url assert client.timeout == timeout diff --git a/tests/test_contact_groups.py b/tests/test_contact_groups.py index ba0abc9..afd735f 100644 --- a/tests/test_contact_groups.py +++ b/tests/test_contact_groups.py @@ -3,15 +3,15 @@ import pytest -from src.devo_global_comms_python import DevoClient -from src.devo_global_comms_python.models.contact_groups import ( +from devo_global_comms_python import DevoClient +from devo_global_comms_python.models.contact_groups import ( ContactsGroup, ContactsGroupListResponse, CreateContactsGroupDto, DeleteContactsGroupsDto, UpdateContactsGroupDto, ) -from src.devo_global_comms_python.resources.contact_groups import ContactGroupsResource +from devo_global_comms_python.resources.contact_groups import ContactGroupsResource class TestContactGroupsResource: diff --git a/tests/test_contacts.py b/tests/test_contacts.py index d4e7089..bdfc4f3 100644 --- a/tests/test_contacts.py +++ b/tests/test_contacts.py @@ -2,9 +2,9 @@ import pytest -from src.devo_global_comms_python import DevoClient -from src.devo_global_comms_python.exceptions import DevoValidationException -from src.devo_global_comms_python.models.contacts import ( +from devo_global_comms_python import DevoClient +from devo_global_comms_python.exceptions import DevoValidationException +from devo_global_comms_python.models.contacts import ( AssignToContactsGroupDto, CommonDeleteDto, ContactSerializer, @@ -19,7 +19,7 @@ UpdateContactDto, UpdateCustomFieldDto, ) -from src.devo_global_comms_python.resources.contacts import ContactsResource +from devo_global_comms_python.resources.contacts import ContactsResource class TestContactsResource: diff --git a/tests/test_messages.py b/tests/test_messages.py index 2fad7b9..3b4b4de 100644 --- a/tests/test_messages.py +++ b/tests/test_messages.py @@ -3,8 +3,8 @@ import pytest -from src.devo_global_comms_python.models.messages import SendMessageDto, SendMessageSerializer -from src.devo_global_comms_python.resources.messages import MessagesResource +from devo_global_comms_python.models.messages import SendMessageDto, SendMessageSerializer +from devo_global_comms_python.resources.messages import MessagesResource class TestMessagesResource: