Skip to content

Commit

Permalink
Merge pull request #297 from recurly/schema_aware_resources
Browse files Browse the repository at this point in the history
Documentation and casting improvements
  • Loading branch information
Aaron Suarez authored Jun 18, 2019
2 parents f7fc948 + f41d660 commit e8e9e31
Show file tree
Hide file tree
Showing 9 changed files with 832 additions and 271 deletions.
8 changes: 8 additions & 0 deletions recurly/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
name = "recurly"

import os
from os.path import dirname, basename, isfile
import glob

# Running in strict mode will throw exceptions
# when API responses don't line up with the client's expectations.
# The client's default behavior is to try to recover from these
# errors. This is used to catch bugs in testing.
# You do not want to enable this for production.
STRICT_MODE = os.getenv("RECURLY_STRICT_MODE", "FALSE").upper() == "TRUE"


class RecurlyError(Exception):
pass
Expand Down
106 changes: 77 additions & 29 deletions recurly/resource.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,95 @@
from pydoc import locate
import datetime
import recurly

# TODO - more resilient parsing
DT_FORMAT = "%Y-%m-%dT%H:%M:%SZ"


class Resource:
"""Class representing a server-side object in Recurly"""

# Allows us to override resource location
# Allows us to override resource location for testing
locator = lambda class_name: locate("recurly.resources.%s" % class_name)

@classmethod
def cast(cls, properties):
def cast(cls, properties, class_name=None):
"""Casts a dict of properties into a Recurly Resource"""

if "object" not in properties:
return properties

if (
properties["object"] == "list"
and "data" in properties
and "has_more" in properties
):
properties["data"] = [Resource.cast(i) for i in properties["data"]]
return Page(properties)

name_parts = properties["object"].split("_")
class_name = "".join(x.title() for x in name_parts)
if class_name is None and "object" in properties:
# If it's a Page, let's return that now
if (
properties["object"] == "list"
and "data" in properties
and "has_more" in properties
):
properties["data"] = [Resource.cast(i) for i in properties["data"]]
return Page(properties)

# If it's not a Page, we need to derive the class name
# from the "object" property. The class_name passed in should
# take precedence.
name_parts = properties["object"].split("_")
class_name = "".join(x.title() for x in name_parts)

klass = cls.locator(class_name)

# If we can't find a resource class, we should return
# the untyped properties dict. If in strict-mode, explode.
if klass is None:
return properties
if recurly.STRICT_MODE:
raise ValueError("Class could not be found for json: %s" % properties)
else:
return properties

del properties["object"]
resource = klass()
for k, v in properties.items():
if isinstance(v, dict):
properties[k] = Resource.cast(v)
elif isinstance(v, list):
for i in range(len(v)):
if isinstance(v[i], dict):
v[i] = Resource.cast(v[i])

return klass(properties)

def __init__(self, properties):
vars(self).update(properties)
# Skip "object" attributes
if k == "object":
continue

attr = None
attr_type = klass.schema.get(k)
if attr_type:
# if the value is None, let's set to none
# and skip the casting
if v is None:
attr = None

# if it's a plain type, use the type to cast it
elif type(attr_type) == type:
attr = attr_type(v)

# if it's a datetime, parse it
elif attr_type == datetime:
attr = datetime.datetime.strptime(v, DT_FORMAT)

# If the schema type a string, it's a reference
# to another resource
elif isinstance(attr_type, str) and isinstance(v, dict):
attr = Resource.cast(v, class_name=attr_type)

# If the schema type is a list of strings, it's a reference
# to a list of resources
elif (
isinstance(attr_type, list)
and isinstance(attr_type[0], str)
and isinstance(v, list)
):
attr = [Resource.cast(r, class_name=attr_type[0]) for r in v]

# We want to explode in strict mode because
# the schema doesn't know about this attribute. In production
# we will just set the attr to it's value or None
if recurly.STRICT_MODE and attr_type is None:
raise ValueError(
"%s could not find property %s in schema %s given value %s"
% (klass.__name__, k, klass.schema, v)
)
else:
setattr(resource, k, attr)

return resource


class Page(Resource):
Expand All @@ -59,4 +106,5 @@ class Page(Resource):
The list of data for this page. The data will be the requested type of Resource.
"""

pass
def __init__(self, properties):
vars(self).update(properties)
Loading

0 comments on commit e8e9e31

Please sign in to comment.