Skip to content

Commit

Permalink
WiP
Browse files Browse the repository at this point in the history
  • Loading branch information
matsk-sinch committed Jan 30, 2025
1 parent a070660 commit 0a32c12
Show file tree
Hide file tree
Showing 31 changed files with 1,465 additions and 362 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,7 @@ cython_debug/
.idea/

# Poetry
poetry.lock
poetry.lock

# DS-Store files
.DS_Store
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ keywords = ["sinch", "sdk"]
python = ">=3.9"
requests = "*"
httpx = "*"
pydantic = ">=2.0.0"

[build-system]
requires = ["poetry-core"]
Expand Down
6 changes: 5 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Testing
pytest
pytest-asyncio
pytest-mock
coverage

# Code Quality
flake8

# HTTP Libraries
httpx
requests
requests

# Data Validation
pydantic >= 2.0.0
82 changes: 82 additions & 0 deletions sinch/core/models/base_model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import json
import re
from datetime import datetime
from dataclasses import asdict, dataclass
from typing import Any

from pydantic import BaseModel, ConfigDict


@dataclass
Expand All @@ -15,3 +20,80 @@ def as_json(self):
class SinchRequestBaseModel(SinchBaseModel):
def as_dict(self):
return {k: v for k, v in asdict(self).items() if v is not None}


class BaseModelConfigRequest(BaseModel):
"""
A base model that allows extra fields and converts snake_case to camelCase.
"""

@staticmethod
def _to_camel_case(snake_str: str) -> str:
"""Converts snake_case to camelCase."""
components = snake_str.split('_')
return components[0] + ''.join(x.title() for x in components[1:])

model_config = ConfigDict(
# Allows using both alias (camelCase) and field name (snake_case)
populate_by_name=True,
# Allows extra values in input
extra="allow"
)

def model_dump(self, **kwargs) -> dict:
"""
Converts extra fields from snake_case to camelCase when dumping the model in endpoint.
"""
# Get the standard model dump
data = super().model_dump(**kwargs)

# Get extra fields
extra_data = self.__pydantic_extra__ or {}

# Convert extra fields to camelCase and collect the original snake_case keys
converted_extra = {}
for key, value in extra_data.items():
camel_case_key = self._to_camel_case(key)
converted_extra[camel_case_key] = value

# Remove snake_case keys from `data` before merging converted extras
for key in extra_data.keys():
data.pop(key, None) # Ensure snake_case fields are removed from final output

# Merge the cleaned base data with the converted extra fields
return {**data, **converted_extra}


class BaseModelConfigResponse(BaseModel):
"""
A base model that allows extra fields and converts camelCase to snake_case,
and serializes datetime fields to ISO format.
"""

@staticmethod
def datetime_encoder(v: datetime) -> str:
"""" Converts a datetime object to a string in ISO 8601 format """
return v.strftime("%Y-%m-%dT%H:%M:%S.%fZ")[:-3] + "Z"

model_config = ConfigDict(
# Allows using both alias (camelCase) and field name (snake_case)
populate_by_name=True,
# Allows extra values in input
extra="allow",
# Custom datetime serialization
ser_json_typed={datetime: datetime_encoder}
)

def model_post_init(self, __context: Any) -> None:
""" Converts unknown fields from camelCase to snake_case."""
if self.__pydantic_extra__:
converted_extra = {
self._to_snake_case(key): value for key, value in self.__pydantic_extra__.items()
}
self.__pydantic_extra__.clear()
self.__pydantic_extra__.update(converted_extra)

@staticmethod
def _to_snake_case(camel_str: str) -> str:
"""Helper to convert camelCase string to snake_case."""
return re.sub(r'(?<!^)(?=[A-Z])', '_', camel_str).lower()
106 changes: 1 addition & 105 deletions sinch/domains/numbers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
from sinch.core.pagination import TokenBasedPaginator, AsyncTokenBasedPaginator
from sinch.domains.numbers.endpoints.available.search_for_number import SearchForNumberEndpoint
from sinch.domains.numbers.endpoints.available.list_available_numbers import AvailableNumbersEndpoint
from sinch.domains.numbers.endpoints.available.activate_number import ActivateNumberEndpoint
from sinch.domains.numbers.endpoints.available.rent_any_number import RentAnyNumberEndpoint
from sinch.domains.numbers.available_numbers import AvailableNumbers
from sinch.domains.numbers.endpoints.callbacks.get_configuration import GetNumbersCallbackConfigurationEndpoint
from sinch.domains.numbers.endpoints.callbacks.update_configuration import UpdateNumbersCallbackConfigurationEndpoint

from sinch.domains.numbers.endpoints.active.list_active_numbers_for_project import ListActiveNumbersEndpoint
from sinch.domains.numbers.endpoints.active.update_number_configuration import UpdateNumberConfigurationEndpoint
from sinch.domains.numbers.endpoints.active.get_number_configuration import GetNumberConfigurationEndpoint
Expand All @@ -17,15 +13,7 @@
ListActiveNumbersRequest, GetNumberConfigurationRequest,
UpdateNumberConfigurationRequest, ReleaseNumberFromProjectRequest
)
from sinch.domains.numbers.models.available.requests import (
ListAvailableNumbersRequest, ActivateNumberRequest,
CheckNumberAvailabilityRequest, RentAnyNumberRequest
)
from sinch.domains.numbers.models.regions.responses import ListAvailableRegionsResponse
from sinch.domains.numbers.models.available.responses import (
ListAvailableNumbersResponse, ActivateNumberResponse,
CheckNumberAvailabilityResponse
)
from sinch.domains.numbers.models.active.responses import (
ListActiveNumbersResponse, UpdateNumberConfigurationResponse,
GetNumberConfigurationResponse, ReleaseNumberFromProjectResponse
Expand All @@ -39,98 +27,6 @@
)


class AvailableNumbers:
def __init__(self, sinch):
self._sinch = sinch

def list(
self,
region_code: str,
number_type: str,
number_pattern: str = None,
number_search_pattern: str = None,
capabilities: list = None,
page_size: int = None
) -> ListAvailableNumbersResponse:
"""
Search for available virtual numbers using a variety of parameters to filter results.
For additional documentation, see https://www.sinch.com and visit our developer portal.
"""
return self._sinch.configuration.transport.request(
AvailableNumbersEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=ListAvailableNumbersRequest(
region_code=region_code,
number_type=number_type,
page_size=page_size,
capabilities=capabilities,
number_search_pattern=number_search_pattern,
number_pattern=number_pattern
)
)
)

def activate(
self,
phone_number: str,
sms_configuration: dict = None,
voice_configuration: dict = None
) -> ActivateNumberResponse:
"""
Activate a virtual number to use with SMS products, Voice products, or both.
For additional documentation, see https://www.sinch.com and visit our developer portal.
"""
return self._sinch.configuration.transport.request(
ActivateNumberEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=ActivateNumberRequest(
phone_number=phone_number,
sms_configuration=sms_configuration,
voice_configuration=voice_configuration
)
)
)

def rent_any(
self,
region_code: str,
type_: str,
number_pattern: str = None,
capabilities: list = None,
sms_configuration: dict = None,
voice_configuration: dict = None,
callback_url: str = None
) -> RentAnyNumberRequest:
return self._sinch.configuration.transport.request(
RentAnyNumberEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=RentAnyNumberRequest(
region_code=region_code,
type_=type_,
number_pattern=number_pattern,
capabilities=capabilities,
sms_configuration=sms_configuration,
voice_configuration=voice_configuration,
callback_url=callback_url
)
)
)

def check_availability(self, phone_number: str) -> CheckNumberAvailabilityResponse:
"""
Enter a specific phone number to check availability.
For additional documentation, see https://www.sinch.com and visit our developer portal.
"""
return self._sinch.configuration.transport.request(
SearchForNumberEndpoint(
project_id=self._sinch.configuration.project_id,
request_data=CheckNumberAvailabilityRequest(
phone_number=phone_number
)
)
)


class ActiveNumbers:
def __init__(self, sinch):
self._sinch = sinch
Expand Down
Loading

0 comments on commit 0a32c12

Please sign in to comment.