Last active
August 29, 2015 14:11
-
-
Save sephii/646307fe735afea53e64 to your computer and use it in GitHub Desktop.
xmodel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from urllib2 import urlopen | |
from lxml import etree | |
import six | |
class Options(object): | |
""" | |
_meta class for FeedModel. | |
""" | |
def __init__(self): | |
self.local_fields = [] | |
def add_field(self, field): | |
self.local_fields.append(field) | |
class BaseFeedModel(type): | |
""" | |
Metaclass for FeedModel. Register attributes to the class. | |
""" | |
def __new__(cls, name, bases, attrs): | |
new_class = super(BaseFeedModel, cls).__new__(cls, name, bases, attrs) | |
# Create internal _meta object | |
setattr(new_class, '_meta', Options()) | |
for attr_name, attr in attrs.items(): | |
if hasattr(attr, 'contribute_to_class'): | |
attr.contribute_to_class(new_class, attr_name) | |
return new_class | |
class FeedModel(six.with_metaclass(BaseFeedModel)): | |
pass | |
class BaseField(object): | |
def __init__(self, xpath): | |
self.xpath = xpath | |
def contribute_to_class(self, cls, name): | |
self.model = cls | |
self.name = name | |
cls._meta.add_field(self) | |
def to_python(self, value): | |
return value | |
class TextField(BaseField): | |
pass | |
class IntegerField(BaseField): | |
def to_python(self, value): | |
return int(value) | |
class CollectionField(BaseField): | |
""" | |
Allow to access collections of items through the use of the all() method. | |
""" | |
def __init__(self, rel, xpath): | |
super(CollectionField, self).__init__(xpath) | |
self.rel = rel | |
def to_python(self, value): | |
objects = self.rel.objects | |
objects.xml = value | |
objects.selector = self.xpath | |
return objects | |
class BaseManager(object): | |
""" | |
Here's the main logic. The all() method goes through the elements of the | |
given selector and then goes through the different fields defined for this | |
element and sets its attributes by running xpath expressions. | |
""" | |
def contribute_to_class(self, cls, name): | |
self.model = cls | |
def all(self): | |
results = [] | |
for element in self.get_elements(): | |
relative_element = etree.ElementTree(element) | |
instance = self.model() | |
for field in self.model._meta.local_fields: | |
field_value = relative_element.xpath(field.xpath) | |
# CollectionField is a special case, create a CollectionManager | |
# that will operate on a subset of the XML tree | |
if isinstance(field, CollectionField): | |
new_manager = CollectionManager(field_value) | |
new_manager.contribute_to_class(field.rel, None) | |
setattr(instance, field.name, new_manager) | |
else: | |
if field_value and isinstance(field_value, list): | |
field_value = field_value[0] | |
field_value = field.to_python(field_value) | |
setattr(instance, field.name, field_value) | |
results.append(instance) | |
return results | |
class CollectionManager(BaseManager): | |
def __init__(self, collection): | |
super(CollectionManager, self).__init__() | |
self.collection = collection | |
def get_elements(self): | |
return self.collection | |
class FeedManager(BaseManager): | |
def __init__(self, url, selector): | |
super(FeedManager, self).__init__() | |
self.url = url | |
self.selector = selector | |
self.model = None | |
def get_xml(self): | |
return etree.parse(urlopen(self.url)) | |
def get_elements(self): | |
return self.get_xml().xpath(self.selector) | |
########################## | |
# Test models definition # | |
########################## | |
class Season(FeedModel): | |
id = IntegerField('@id') | |
name = TextField('@name') | |
year_start = IntegerField('@start') | |
year_end = IntegerField('@end') | |
class Competition(FeedModel): | |
id = IntegerField('@id') | |
name = TextField('@name') | |
seasons = CollectionField(Season, '/competition/seasons/season') | |
objects = FeedManager( | |
url='https://gist.githubusercontent.com/sephii/46d6cf59873bac9eb553/raw/4c9ee0ef8ddafa68784f6c11b60fa65aeb84f0e4/gistfile1.xml', | |
selector='/competitions/competition' | |
) | |
# Access to objects like Competition.objects.all() and then to properties like Competition.objects.all()[0].name | |
# You can access to related collections like so: Competition.objects.all()[0].seasons.all() | |
import pdb; pdb.set_trace() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment