Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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/
19 changes: 13 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down Expand Up @@ -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.*"
Expand Down
34 changes: 15 additions & 19 deletions src/devo_global_comms_python/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, Optional, Union
from typing import Any, Dict, Optional

import requests

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
18 changes: 9 additions & 9 deletions src/devo_global_comms_python/models/contact_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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")
Expand All @@ -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")


Expand All @@ -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")
50 changes: 25 additions & 25 deletions src/devo_global_comms_python/models/contacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand All @@ -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")
Expand All @@ -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")


Expand All @@ -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):
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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):
Expand All @@ -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):
Expand Down
6 changes: 3 additions & 3 deletions src/devo_global_comms_python/resources/contacts.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/devo_global_comms_python/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 2 additions & 7 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tests/test_contact_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading