Skip to content

Commit

Permalink
Fix bug in find_model_id and improve docs
Browse files Browse the repository at this point in the history
  • Loading branch information
samamorgan committed Oct 27, 2023
1 parent 3e36785 commit c30f217
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 22 deletions.
13 changes: 12 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ classifiers = [
python = "^3.7"
requests = "^2.28.1"
portalocker = "^2.7.0"
inflection = "^0.5.1"

[tool.poetry.group.dev]
optional = true
Expand Down
4 changes: 3 additions & 1 deletion test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
clean_request_params,
clean_request_payload,
find_model_id,
to_camel_case,
to_snake_case,
)

Expand Down Expand Up @@ -129,7 +130,8 @@ def test_clean_datetime(dt, expected, request):
"FOOBar",
],
)
def test_to_snake_case(input):
def test_case_converters(input):
assert to_camel_case(input) == "fooBar"
assert to_snake_case(input) == "foo_bar"


Expand Down
86 changes: 66 additions & 20 deletions welkin/util.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import functools
import re
from datetime import date, datetime, timezone
from functools import cache, wraps
from types import FunctionType
from typing import Any
from uuid import UUID

from welkin.models.base import SchemaBase
import inflection

from welkin.models.base import Collection, Resource, SchemaBase

# NOTE: `clean_request_payload` and `clean_request_params` are intentionally DRY
# violations. The code may be the same, but they represent different knowledge.
Expand Down Expand Up @@ -111,21 +113,46 @@ def clean_datetime(dt: datetime) -> str:
)


def find_model_id(obj, model: str):
if obj.__class__.__name__ == model:
return obj.id
elif hasattr(obj, f"{to_snake_case(model)}Id"):
return obj.patientId
elif obj._parent:
return find_model_id(obj._parent, model)
def find_model_id(instance: Collection | Resource, model_name: str) -> str:
"""Recursively traverse the `_parent` chain searching for a model id.
raise AttributeError(f"Cannot find {model} id. Model._parent chain ends in {obj}")
Args:
instance (Collection | Resource): The instanceect instance to inspect.
model_name (str): The class name of the model to find.
Raises:
AttributeError: If recursion ends without finding the model id.
def model_id(*models):
def decorator(f):
@functools.wraps(f)
def wrapper(self, *args, **kwargs):
Returns:
str: The model id.
"""
body_id_key = f"{to_camel_case(model_name)}Id"

if instance.__class__.__name__ == model_name:
return instance.id
elif hasattr(instance, body_id_key):
return getattr(instance, body_id_key)
elif instance._parent:
return find_model_id(instance._parent, model_name)

raise AttributeError(
f"Cannot find {model_name} id. Model._parent chain ends in {instance}"
)


def model_id(*models: tuple[str]) -> FunctionType:
"""Insert values for `model_id` arguments if not provided.
Args:
*models (tuple[str]): The model names to search for.
Raises:
TypeError: If no ID is found and no arguments are provided.
"""

def decorator(f: FunctionType):
@wraps(f)
def wrapper(self, *args, **kwargs) -> FunctionType:
outer_exc = None
for model in models:
key = f"{to_snake_case(model)}_id"
Expand All @@ -139,16 +166,35 @@ def wrapper(self, *args, **kwargs):
try:
return f(self, *args, **kwargs)
except TypeError as exc:
# Raise from the outer `AttributeError` so we don't lose context.
raise exc from outer_exc

return wrapper

return decorator


def to_snake_case(s):
first = re.compile(r"(.)([A-Z][a-z]+)")
second = re.compile(r"([a-z0-9])([A-Z])")
repl = r"\1_\2"
@cache
def to_camel_case(s: str) -> str:
"""Convert a string to camelCase.
Args:
s (str): The string to convert.
Returns:
str: The converted camelCase string.
"""
return inflection.camelize(to_snake_case(s), uppercase_first_letter=False)


@cache
def to_snake_case(s: str) -> str:
"""Convert a string to snake_case.
Args:
s (str): The string to convert.
return second.sub(repl, first.sub(repl, s)).lower()
Returns:
str: The converted snake_case string.
"""
return inflection.underscore(s)

0 comments on commit c30f217

Please sign in to comment.