Repository :
http://git.fedorahosted.org/cgit/copr.git
On branch : python_copr_apiv2
---------------------------------------------------------------
commit d55ba16218373c0aefd6b15307fd14224d0c686a
Author: Valentin Gologuzov <vgologuz(a)redhat.com>
Date: Tue Sep 22 16:09:28 2015 +0200
[python][WIP] support APIv2 -- client architecture seems fine, going to check some
corner cases, i.e.: file upload, work with embedded
---------------------------------------------------------------
python/copr/__init__.py | 5 +
python/copr/client_v2/client.py | 16 +--
python/copr/client_v2/entities.py | 260 ++++--------------------------------
python/copr/client_v2/handlers.py | 87 ++++++++++++
python/copr/client_v2/resources.py | 174 ++++++++++++++++++++++++
python/copr/client_v2/schemas.py | 34 +++++
python/run_tmp.py | 63 ++++++---
7 files changed, 374 insertions(+), 265 deletions(-)
diff --git a/python/copr/__init__.py b/python/copr/__init__.py
index 4f929cd..f2c03b3 100644
--- a/python/copr/__init__.py
+++ b/python/copr/__init__.py
@@ -1 +1,6 @@
from .client import CoprClient
+
+from .client_v2.client import CoprClient as ClientV2
+
+create_client2_from_params = ClientV2.create_from_params
+create_client2_from_file_config = ClientV2.create_from_file_config
diff --git a/python/copr/client_v2/client.py b/python/copr/client_v2/client.py
index e57496b..5b680a0 100644
--- a/python/copr/client_v2/client.py
+++ b/python/copr/client_v2/client.py
@@ -17,8 +17,9 @@ import six
from six.moves import configparser
# from requests_toolbelt.multipart.encoder import MultipartEncoder
-from .entities import parse_root, ProjectHandle, RootEntity
-from client_v2.common import EntityTypes
+from .resources import Root
+from .handlers import ProjectHandle
+from .common import EntityTypes
from .net_client import NetClient
if sys.version_info < (2, 7):
@@ -32,15 +33,6 @@ from ..exceptions import CoprConfigException, CoprNoConfException, \
CoprRequestException, \
CoprUnknownResponseException
-
-
-# from .responses import ProjectHandle, \
-# CoprResponse, BuildHandle, BaseHandle, ProjectChrootHandle
-#
-# from .parsers import fabric_simple_fields_parser, ProjectListParser, \
-# CommonMsgErrorOutParser, NewBuildListParser, ProjectChrootsParser, \
-# ProjectDetailsFieldsParser
-
from ..util import UnicodeMixin
log = logging.getLogger(__name__)
@@ -149,7 +141,7 @@ class CoprClient(UnicodeMixin):
log.debug("Getting root resources")
response = self.nc.request(self.api_root)
- self.root = parse_root(response, self.root_url)
+ self.root = Root.from_response(response, self.root_url)
self.projects = ProjectHandle(self.nc,
self.root.get_resource_base_url(u"projects"))
# import ipdb; ipdb.set_trace()
diff --git a/python/copr/client_v2/entities.py b/python/copr/client_v2/entities.py
index b9ef8e7..bc422e6 100644
--- a/python/copr/client_v2/entities.py
+++ b/python/copr/client_v2/entities.py
@@ -1,26 +1,21 @@
# coding: utf-8
-from collections import Iterable
-import json
+from ..util import UnicodeMixin
-from client_v2.common import EntityTypes
-from .net_client import NetClient, ResponseWrapper
+from .schemas import ProjectSchema, EmptySchema
-class Link(object):
+class Link(UnicodeMixin):
def __init__(self, role, href, target_type):
self.role = role
self.href = href
- self.target_type = target_type
+ self.target_type = target_type # not sure if we need this
def __unicode__(self):
return u"<Link: role: {}, target type: {}, href: {}".format(
self.role, self.target_type, self.href)
- def __str__(self):
- return self.__unicode__()
-
@classmethod
def parse_from_dict(cls, data_dict, map_name_to_role):
"""
@@ -49,242 +44,41 @@ class Link(object):
}
-class Individual(object):
- """
- :type handle: AbstractHandle or None
- :type response: ResponseWrapper
- :type links: (dict of (str, Link)) or None
- """
+class Entity(UnicodeMixin):
- def __init__(self, handle=None, response=None, links=None, data=None, embedded=None,
options=None):
- self._handle = handle
- self._response = response
- self._links = links
- self._data = data
- self._embedded = embedded or dict()
- self._options = options or dict()
+ _schema = EmptySchema()
- def get_href_by_name(self, name):
- """
- :type name: str
- """
- return self._links[name].href
+ def to_dict(self):
+ return self._schema.dump(self)
def to_json(self):
- return "null"
-
-
-class Collection(Iterable):
- """
- :type handle: AbstractHandle or None
- :type response: ResponseWrapper
- :type links: (dict of (str, Link)) or None
- """
-
- def __init__(self, handle=None, response=None, links=None, individuals=None,
options=None):
- self._handle = handle
- self._response = response
- self._links = links
- self._options = options or dict()
- self._individuals = individuals
-
- def get_href_by_name(self, name):
- """
- :type name: str
- """
- return self._links[name].href
-
- def __iter__(self):
- """
- :rtype: Iterable[Individual]
- """
- return iter(self._individuals)
-
+ return self._schema.dumps(self)
-class ProjectsList(Collection):
- """
- :type handle: ProjectHandle
- """
-
- # def __iter__(self):
- # """
- # :rtype: Iterable[ProjectEntity]
- # """
- # return super(ProjectsList, self).__iter__()
-
- def __init__(self, handle, **kwargs):
- super(ProjectsList, self).__init__(**kwargs)
- self._handle = handle
-
- @property
- def projects(self):
- return self._individuals
-
- def next_page(self):
- limit = self._options.get("limit", 100)
- offset = self._options.get("offset", 0)
-
- offset += limit
- params = {}
- params.update(self._options)
- params["limit"] = limit
- params["offset"] = offset
-
- return self._handle.get_list(self, **params)
-
-
-class OperationResult(Individual):
-
- def __init__(self, handle, response=None, data=None, new_location=None,
options=None):
- super(OperationResult, self).__init__(handle, response=response, data=data,
options=options)
- self.new_location = new_location
-
-
-class RootEntity(Individual):
-
- def __init__(self, response, links, root_url):
- super(RootEntity, self).__init__(response=response, links=links)
- self.root_url = root_url
-
- def get_resource_base_url(self, resource_name):
- """
- :type entity_type: client_v2.common.EntityTypes
- """
- return "{}{}".format(self.root_url,
self.get_href_by_name(resource_name))
+ @classmethod
+ def from_dict(cls, raw_dict):
+ parsed = cls._schema.load(raw_dict)
+ return cls(**parsed.data)
-# TODO: replace with marshmallow
-class ProjectEntity(Individual):
+class ProjectEntity(Entity):
"""
- :type handle: ProjectHandle
+ :type handle: client_v2.handlers.ProjectHandle
"""
- def __init__(self, handle, **kwargs):
- super(ProjectEntity, self).__init__( **kwargs)
- self._handle = handle
- self.id = self._data.get("id")
- self.name = self._data.get("name")
- self.owner = self._data.get("owner")
- self.description = self._data.get("description")
- self.instructions = self._data.get("instructions")
- self.homepage = self._data.get("homepage")
- self.contact = self._data.get("contact")
- self.disable_createrepo = self._data.get("disable_creatrepo")
- self.build_enable_net = self._data.get("build_enable_net")
- self.repos = self._data.get("repos")
-
- def to_json(self):
- # todo: replace with marshmallow
- to_dump = {}
- for key in self._data.keys():
- to_dump[key] = getattr(self, key)
- return json.dumps(to_dump)
+ _schema = ProjectSchema(strict=True)
- def update(self):
- self._handle.update(self)
+ def __init__(self, **kwargs):
- def delete(self):
- self._handle.delete(self.id)
+ self.id = kwargs.get("id")
+ self.name = kwargs.get("name")
+ self.owner = kwargs.get("owner")
+ self.description = kwargs.get("description")
+ self.instructions = kwargs.get("instructions")
+ self.homepage = kwargs.get("homepage")
+ self.contact = kwargs.get("contact")
+ self.disable_createrepo = kwargs.get("disable_creatrepo")
+ self.build_enable_net = kwargs.get("build_enable_net")
+ self.repos = kwargs.get("repos")
def __unicode__(self):
return "<Project #{}: {}/{}>".format(self.id, self.owner,
self.name)
-
- def __str__(self):
- return self.__unicode__()
-
-
-def parse_root(response, root_url):
- """
- :type response: ResponseWrapper
- :type root_url: unicode
- """
- data_dict = response.json
- links = Link.parse_from_dict(data_dict["_links"], {
- "self": EntityTypes.ROOT,
- "projects": EntityTypes.PROJECT,
- "builds": EntityTypes.BUILD,
- "build_tasks": EntityTypes.BUILD_TASK,
- "mock_chroots": EntityTypes.MOCK_CHROOT,
- })
- return RootEntity(response=response, links=links, root_url=root_url)
-
-
-def parse_project(handle, response, data_dict, options=None):
- links = Link.parse_from_dict(data_dict["_links"], {
- "self": EntityTypes.PROJECT,
- "builds": EntityTypes.BUILD,
- })
- return ProjectEntity(handle, response=response, links=links,
data=data_dict["project"], options=options)
-
-
-class AbstractHandle(object):
- pass
-
-
-class ProjectHandle(AbstractHandle):
- """
- :type nc: NetClient
- """
- def __init__(self, nc, base_url):
-
- self.nc = nc
- self.base_url = base_url
-
- def get_list(self, search_query=None, owner=None, name=None, limit=None,
offset=None):
- """
- :param search_query:
- :param owner:
- :param name:
- :param limit:
- :param offset:
- :rtype: ProjectsList
- """
- options = {}
- if limit:
- options["limit"] = limit
- # todo: add other
-
- response = self.nc.request(self.base_url, query_params=options)
- data_dict = response.json
- result = ProjectsList(
- self,
- response=response,
- links=Link.parse_from_dict(data_dict["_links"], {
- "self": EntityTypes.PROJECT,
- "builds": EntityTypes.BUILD,
- }),
- individuals=[
- parse_project(self, response, data_dict=dict_part)
- for dict_part in data_dict["projects"]
- ],
- options=None
- )
- return result
-
- def get_one(self, project_id, show_builds=False, show_chroots=False):
- """
- :type project_id: int
- """
- query_params = {
- "show_builds": show_builds,
- "show_chroots": show_chroots
- }
-
- url = "{}/{}".format(self.base_url, project_id)
- response = self.nc.request(url, query_params=query_params)
- return parse_project(self, response, data_dict=response.json,
options=query_params)
-
- def update(self, project):
- """
- :type project: ProjectEntity
- """
- url = "{}/{}".format(self.base_url, project.id)
- data = project.to_json()
-
- response = self.nc.request(url, method="put", data=data, do_auth=True)
- return OperationResult(self, response)
-
- def delete(self, project_id):
- url = "{}/{}".format(self.base_url, project_id)
- response = self.nc.request(url, method="delete", do_auth=True)
- return OperationResult(self, response)
diff --git a/python/copr/client_v2/handlers.py b/python/copr/client_v2/handlers.py
new file mode 100644
index 0000000..f343722
--- /dev/null
+++ b/python/copr/client_v2/handlers.py
@@ -0,0 +1,87 @@
+# coding: utf-8
+
+from .common import EntityTypes
+from .entities import Link
+from .resources import Project, OperationResult, ProjectsList
+
+
+class AbstractHandle(object):
+ pass
+
+
+class ProjectHandle(AbstractHandle):
+ """
+ :type nc: NetClient
+ """
+ def __init__(self, nc, base_url):
+
+ self.nc = nc
+ self.base_url = base_url
+
+ def get_list(self, search_query=None, owner=None, name=None, limit=None,
offset=None):
+ """
+ :param search_query:
+ :param owner:
+ :param name:
+ :param limit:
+ :param offset:
+ :rtype: ProjectsList
+ """
+ options = {}
+ if limit:
+ options["limit"] = limit
+ # todo: add other
+
+ response = self.nc.request(self.base_url, query_params=options)
+ data_dict = response.json
+ result = ProjectsList(
+ self,
+ response=response,
+ links=Link.parse_from_dict(data_dict["_links"], {
+ "self": EntityTypes.PROJECT,
+ "builds": EntityTypes.BUILD,
+ }),
+ individuals=[
+ Project.from_response(
+ handle=self,
+ response=None,
+ data_dict=dict_part,
+ )
+ for dict_part in data_dict["projects"]
+ ],
+ options=None
+ )
+ return result
+
+ def get_one(self, project_id, show_builds=False, show_chroots=False):
+ """
+ :type project_id: int
+ """
+ query_params = {
+ "show_builds": show_builds,
+ "show_chroots": show_chroots
+ }
+
+ url = "{}/{}".format(self.base_url, project_id)
+ response = self.nc.request(url, query_params=query_params)
+ return Project.from_response(
+ handle=self,
+ response=response,
+ data_dict=response.json,
+ options=query_params
+ )
+
+ def update(self, project):
+ """
+ :type project: ProjectEntity
+ """
+ url = "{}/{}".format(self.base_url, project.id)
+ data = project.to_json()
+
+ response = self.nc.request(url, method="put", data=data, do_auth=True)
+ return OperationResult(self, response)
+
+ def delete(self, project_id):
+ url = "{}/{}".format(self.base_url, project_id)
+ response = self.nc.request(url, method="delete", do_auth=True)
+ return OperationResult(self, response)
diff --git a/python/copr/client_v2/resources.py b/python/copr/client_v2/resources.py
new file mode 100644
index 0000000..32bc9a3
--- /dev/null
+++ b/python/copr/client_v2/resources.py
@@ -0,0 +1,174 @@
+# coding: utf-8
+from collections import Iterable
+
+from ..util import UnicodeMixin
+
+from .common import EntityTypes
+from .entities import Link, ProjectEntity
+from .schemas import EmptySchema
+
+
+class IndividualResource(UnicodeMixin):
+ """
+ :type handle: client_v2.handlers.AbstractHandle or None
+ :type response: ResponseWrapper or None
+ :type links: (dict of (str, Link)) or None
+ """
+ _schema = EmptySchema(strict=True)
+
+ def __init__(self, entity, handle=None, response=None, links=None, embedded=None,
options=None):
+ self._entity = entity
+ self._handle = handle
+ self._response = response
+ self._links = links
+ self._embedded = embedded or dict()
+ self._options = options or dict()
+
+ def __dir__(self):
+ res = list(set(
+ dir(self.__class__) + list(self.__dict__.keys())
+ ))
+ if self._entity:
+ res.extend([x for x in dir(self._entity) if not
x.startswith("_")])
+ return res
+
+ def __getattr__(self, item):
+ # import ipdb; ipdb.set_trace()
+ if hasattr(self._entity, item):
+ return getattr(self._entity, item)
+ else:
+ raise KeyError(item)
+
+ def get_href_by_name(self, name):
+ """
+ :type name: str
+ """
+ return self._links[name].href
+
+
+class Root(IndividualResource):
+
+ def __init__(self, response, links, root_url):
+ super(Root, self).__init__(entity=None, response=response, links=links)
+ self.root_url = root_url
+
+ def get_resource_base_url(self, resource_name):
+ """
+ :type entity_type: client_v2.common.EntityTypes
+ """
+ return "{}{}".format(self.root_url,
self.get_href_by_name(resource_name))
+
+ @classmethod
+ def from_response(cls, response, root_url):
+ """
+ :type response: ResponseWrapper
+ :type root_url: unicode
+ """
+ data_dict = response.json
+ links = Link.parse_from_dict(data_dict["_links"], {
+ "self": EntityTypes.ROOT,
+ "projects": EntityTypes.PROJECT,
+ "builds": EntityTypes.BUILD,
+ "build_tasks": EntityTypes.BUILD_TASK,
+ "mock_chroots": EntityTypes.MOCK_CHROOT,
+ })
+ return Root(response=response, links=links, root_url=root_url)
+
+
+class Project(IndividualResource):
+ """
+ :type entity: ProjectEntity
+ :type handle: client_v2.handlers.ProjectHandle
+ """
+ def __init__(self, entity, handle, **kwargs):
+ super(Project, self).__init__(entity=entity, handle=handle, **kwargs)
+ self._entity = entity
+ self._handle = handle
+
+ def update(self):
+ return self._handle.update(self._entity)
+
+ def delete(self):
+ return self._handle.delete(self.id)
+
+ def get_self(self):
+ return self._handle.get_one(self.id, **self._options)
+
+ def __unicode__(self):
+ return self._entity.__unicode__()
+
+ @classmethod
+ def from_response(cls, handle, response, data_dict, options=None):
+ links = Link.parse_from_dict(data_dict["_links"], {
+ "self": EntityTypes.PROJECT,
+ "builds": EntityTypes.BUILD,
+ })
+ entity = ProjectEntity.from_dict(data_dict["project"])
+ return cls(entity=entity, handle=handle, response=response, links=links,
options=options)
+
+
+class OperationResult(IndividualResource):
+
+ def __init__(self, handle, response=None, entity=None, new_location=None,
options=None):
+ super(OperationResult, self).__init__(handle=handle, response=response,
entity=entity, options=options)
+ self.new_location = new_location
+
+
+class CollectionResource(Iterable, UnicodeMixin):
+ """
+ :type handle: client_v2.handlers.AbstractHandle or None
+ :type response: ResponseWrapper
+ :type links: (dict of (str, Link)) or None
+ """
+
+ def __init__(self, handle=None, response=None, links=None, individuals=None,
options=None):
+ self._handle = handle
+ self._response = response
+ self._links = links
+ self._options = options or dict()
+ self._individuals = individuals
+
+ def get_href_by_name(self, name):
+ """
+ :type name: str
+ """
+ return self._links[name].href
+
+ def __iter__(self):
+ """
+ :rtype: Iterable[IndividualResource]
+ """
+ return iter(self._individuals)
+
+
+class ProjectsList(CollectionResource):
+ """
+ :type handle: client_v2.handlers.ProjectHandle
+ """
+
+ # def __iter__(self):
+ # """
+ # :rtype: Iterable[ProjectEntity]
+ # """
+ # return super(ProjectsList, self).__iter__()
+
+ def __init__(self, handle, **kwargs):
+ super(ProjectsList, self).__init__(**kwargs)
+ self._handle = handle
+
+ @property
+ def projects(self):
+ return self._individuals
+
+ def next_page(self):
+ limit = self._options.get("limit", 100)
+ offset = self._options.get("offset", 0)
+
+ offset += limit
+ params = {}
+ params.update(self._options)
+ params["limit"] = limit
+ params["offset"] = offset
+
+ return self._handle.get_list(self, **params)
+
diff --git a/python/copr/client_v2/schemas.py b/python/copr/client_v2/schemas.py
new file mode 100644
index 0000000..8280fe9
--- /dev/null
+++ b/python/copr/client_v2/schemas.py
@@ -0,0 +1,34 @@
+# coding: utf-8
+
+from collections import Iterable
+from marshmallow import Schema, fields
+from marshmallow import Schema, fields, validates_schema, ValidationError, validate
+
+# todo: maybe define schemas for Link sets in each indvidual/collection
+# class LinkSchema(Schema):
+# href = fields.Str()
+#
+
+
+class EmptySchema(Schema):
+ pass
+
+
+class ProjectSchema(Schema):
+
+ id = fields.Int()
+ name = fields.Str()
+
+ owner = fields.Str()
+ description = fields.Str()
+ instructions = fields.Str()
+ homepage = fields.Url(allow_none=True)
+ contact = fields.Str(allow_none=True)
+
+ disable_createrepo = fields.Bool(allow_none=True)
+ build_enable_net = fields.Bool(allow_none=True)
+ last_modified = fields.DateTime(dump_only=True)
+
+ repos = fields.List(fields.Str())
+
+
diff --git a/python/run_tmp.py b/python/run_tmp.py
index 04b6771..134e49e 100644
--- a/python/run_tmp.py
+++ b/python/run_tmp.py
@@ -1,32 +1,55 @@
# coding: utf-8
-from copr.client_v2.client import CoprClient
-from copr.client_v2.entities import ProjectEntity
+#from copr.client_v2.client import CoprClient
+# from copr.client_v2.entities import ProjectEntity
+
+from copr import create_client2_from_params
def main():
copr_url = "http://copr-fe-dev.cloud.fedoraproject.org"
- client = CoprClient.create_from_params(
+ client = create_client2_from_params(
root_url=copr_url,
login="wczictdaerhmwfxolham",
token="kxcnwfmleulpnkckzspxxgwhxjolhc"
-
)
- client.post_init()
-
- project = client.projects.get_one(3260)
-
- print("Project: {} id {}, {}\n {}".format(
- project.name,
- project.id,
- project.description,
- [(x, y.href) for x, y in project._links.items()]
- ))
- # res = project._handle.update(project)
-
- plist = client.projects.get_list(limit=7)
- for p in plist:
- print(p)
- print(p.get_href_by_name("builds"))
+
+ def pp(project):
+ print("Project: {} id {}, {}\n {}\n{}".format(
+ project.name,
+ project.id,
+ project.description,
+ project.repos,
+ [(x, y.href) for x, y in project._links.items()]
+ ))
+
+ def t1():
+ project = client.projects.get_one(2262)
+ #
+ pp(project)
+
+ # res = project.update()
+ # print(res)
+ #
+ def t3(project):
+ p = project.get_self()
+ pp(p)
+
+ def t2():
+ plist = client.projects.get_list(limit=20)
+ for p in plist:
+ #print(p)
+ #print(p.get_href_by_name("builds"))
+ pp(p)
+ print("==")
+ print("==")
+ print("==")
+
+
+
+ # t1()
+ t2()
+
+
if __name__ == "__main__":