Skip to content

Commit

Permalink
Merge pull request #4 from menegazzo/load-lazy-information
Browse files Browse the repository at this point in the history
Adding support to load entities from lazy information.

Closes #2
  • Loading branch information
menegazzo committed Aug 5, 2014
2 parents 22b27a6 + 2b7aa40 commit aaa2c9c
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 1 deletion.
64 changes: 63 additions & 1 deletion travispy/_tests/test_travispy.py
@@ -1,5 +1,5 @@
from travispy import TravisPy
from travispy.entities import User
from travispy.entities import Build, Job, Log, Repo, User
import os
import pytest

Expand Down Expand Up @@ -97,6 +97,28 @@ def test_builds(self, python_version, repo_slug):
assert build.restart() == True
assert build.cancel() == True

repository = build.repository
assert isinstance(repository, Repo)
assert build.repository_id == repository.id
assert repository == build.repository

build.repository_id = -1
assert build.repository == None

jobs = build.jobs
assert isinstance(jobs, list)

job_ids = []
for job in jobs:
assert isinstance(job, Job)
job_ids.append(job.id)

assert build.job_ids == job_ids
assert jobs == build.jobs

build.job_ids = [-1]
assert build.jobs == []


def test_hooks(self):
hooks = self._travis.hooks()
Expand Down Expand Up @@ -137,13 +159,45 @@ def test_jobs(self, python_version, repo_slug):
assert job.restart() == True
assert job.cancel() == True

build = job.build
assert isinstance(build, Build)
assert job.build_id == build.id
assert build == job.build

job.build_id = -1
assert job.build == None

repository = job.repository
assert isinstance(repository, Repo)
assert job.repository_id == repository.id
assert repository == job.repository

job.repository_id = -1
assert job.repository == None

log = job.log
assert isinstance(log, Log)
assert job.log_id == log.id
assert log == job.log

job.log_id = -1
assert job.log == None


def test_log(self):
log = self._travis.log(15928905)
assert log.id == 15928905
assert log.job_id == 25718104
assert hasattr(log, 'body')

job = log.job
assert isinstance(job, Job)
assert log.job_id == job.id
assert job == log.job

log.job_id = -1
assert log.job == None


def test_repos(self, repo_slug):
repos = self._travis.repos()
Expand All @@ -168,6 +222,14 @@ def test_repos(self, repo_slug):
assert hasattr(repo, 'last_build_finished_at')
assert repo.state == repo.last_build_state

last_build = repo.last_build
assert isinstance(last_build, Build)
assert last_build.repository_id == repo.id
assert last_build == repo.last_build

repo.last_build_id = -1
assert repo.last_build == None


def test_user(self):
user = self._travis.user()
Expand Down
128 changes: 128 additions & 0 deletions travispy/entities/_entity.py
Expand Up @@ -16,11 +16,15 @@ class Entity(object):
__slots__ = [
'id',
'_session',
'__cache',
]

def __init__(self, session):
self._session = session

# A dictionary used to cache objects loaded from lazy information.
self.__cache = {}


@classmethod
def one(cls):
Expand All @@ -44,5 +48,129 @@ def many(cls):
return cls.one() + 's'


def _load_from_lazy_information(self, lazy_information, entity_class, cache_name, load_method, load_kwarg):
'''
Some |travisci| entities stores lazy information (or references) to other entities that they
have a relationship. Consider :class:`.Build`: it has a reference to its :class:`.Repo`
container through the attribute ``repository_id`` and other reference to the :class:`.Jobs`
within it (through ``job_ids``).
This method is responsible for loading a requested ``lazy_information`` and returning it as
objects of the given ``entity_class``. Also it creates an internal cache so if it is
requested more than once, the same result will be returned.
Cache will be updated whenever attribute related to the given ``lazy_information`` is
changed.
:param str lazy_information:
Attribute name where lazy information is stored.
:type entity_class: :class:`.Entity`
:param entity_class:
Class of entity which will be loaded from lazy information.
:param str cache_name:
Name that will be used to store loaded information.
.. note::
Cache will not be accessible from outside the object itself.
:param callable load_method:
Method or function that will be used to load lazy information. It must support two
parameters:
* given ``entity_class``
* ``load_kwarg`` which will receive the stored lazy information
:param str load_kwarg:
Name of keyword argument that will be used within ``load_method``.
:rtype: ``entity_class`` instance | list(``entity_class`` instance)
:returns:
The information loaded from stored lazy information. The return type will vary depending
on what ``load_method`` returns.
'''
cache = self.__cache

cached_property_name = 'cached_%s' % cache_name
cached_property_ref_name = 'cached_%s' % lazy_information

property_ref = getattr(self, lazy_information)
if cache.get(cached_property_ref_name) == property_ref:
return cache[cached_property_name]

result = load_method(entity_class, **{load_kwarg: property_ref})

# If no result was found, current cache will be deleted.
if not result:

if cached_property_ref_name in cache:
del cache[cached_property_ref_name]

if cached_property_name in cache:
del cache[cached_property_name]

# Valid result should be stored in cache.
else:
cache[cached_property_ref_name] = property_ref
cache[cached_property_name] = result

return result


def _load_one_lazy_information(self, entity_class, lazy_information=None):
'''
Method responsible for searching one ``entity_class`` based on related ``lazy_information``.
:type lazy_information: str | None
:param lazy_information:
When lazy information is not provided it will be built automatically based on given
`` entity_class``. See more at :meth:`._load_from_lazy_information`.
:rtype: ``entity_class`` instance
:returns:
The information loaded from stored lazy information.
.. seealso:: :meth:`._load_from_lazy_information`
'''
if lazy_information is None:
lazy_information = '%s_id' % entity_class.one()

return self._load_from_lazy_information(
lazy_information,
entity_class,
entity_class.one(),
self._session.find_one,
'entity_id',
)


def _load_many_lazy_information(self, entity_class, lazy_information=None):
'''
Method responsible for searching many ``entity_class`` based on related ``lazy_information``.
:type lazy_information: str | None
:param lazy_information:
When lazy information is not provided it will be built automatically based on given
`` entity_class``. See more at :meth:`._load_from_lazy_information`.
:rtype: list(``entity_class`` instance)
:returns:
The information loaded from stored lazy information.
.. seealso:: :meth:`._load_from_lazy_information`
'''
if lazy_information is None:
lazy_information = '%s_ids' % entity_class.one()

return self._load_from_lazy_information(
lazy_information,
entity_class,
entity_class.many(),
self._session.find_many,
'ids',
)


def __getitem__(self, key):
return getattr(self, key)
21 changes: 21 additions & 0 deletions travispy/entities/build.py
Expand Up @@ -55,3 +55,24 @@ class Build(Restartable):
'duration',
'job_ids',
]

@property
def repository(self):
'''
:rtype: :class:`.Repo`
:returns:
A :class:`.Repo` object with information related to current ``repository_id``.
'''
from .repo import Repo
return self._load_one_lazy_information(Repo, 'repository_id')


@property
def jobs(self):
'''
:rtype: list(:class:`.Job`)
:returns:
A list of :class:`.Job` objects with information related to current ``job_ids``.
'''
from .job import Job
return self._load_many_lazy_information(Job)
32 changes: 32 additions & 0 deletions travispy/entities/job.py
Expand Up @@ -59,3 +59,35 @@ class Job(Restartable):
'allow_failure',
'annotation_ids',
]

@property
def build(self):
'''
:rtype: :class:`.Build`
:returns:
A :class:`.Build` object with information related to current ``build_id``.
'''
from .build import Build
return self._load_one_lazy_information(Build)


@property
def repository(self):
'''
:rtype: :class:`.Repo`
:returns:
A :class:`.Repo` object with information related to current ``repository_id``.
'''
from .repo import Repo
return self._load_one_lazy_information(Repo, 'repository_id')


@property
def log(self):
'''
:rtype: :class:`.Log`
:returns:
A :class:`.Log` object with information related to current ``log_id``.
'''
from .log import Log
return self._load_one_lazy_information(Log)
10 changes: 10 additions & 0 deletions travispy/entities/log.py
Expand Up @@ -21,3 +21,13 @@ class Log(Entity):
'body',
'type',
]

@property
def job(self):
'''
:rtype: :class:`.Job`
:returns:
A :class:`.Job` object with information related to current ``job_id``.
'''
from .job import Job
return self._load_one_lazy_information(Job)
11 changes: 11 additions & 0 deletions travispy/entities/repo.py
Expand Up @@ -60,3 +60,14 @@ def state(self):
.. seealso:: :class:`.Stateful` for ``state`` full documentation.
'''
return self.last_build_state


@property
def last_build(self):
'''
:rtype: :class:`.Build`
:returns:
A :class:`.Build` object with information related to current ``last_build_id``.
'''
from .build import Build
return self._load_one_lazy_information(Build, 'last_build_id')
4 changes: 4 additions & 0 deletions travispy/entities/session.py
Expand Up @@ -36,9 +36,13 @@ def find_one(self, entity_class, entity_id, **kwargs):
if response.status_code == 200:
contents = response.json()
info = contents.get(entity_class.one(), {})
if not info:
return

entity = entity_class(self)
for key, value in info.items():
setattr(entity, key, value)

return entity


Expand Down

0 comments on commit aaa2c9c

Please sign in to comment.