[copr] master: Merge branch 'python-client' (2d3742c)
by vgologuz@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
>---------------------------------------------------------------
commit 2d3742ccddcdbc73e84a8aa3dd5bdbe406e791b2
Merge: 2fe9742 6e04d7e
Author: Valentin Gologuzov <vgologuz(a)redhat.com>
Date: Mon Aug 18 09:53:30 2014 +0200
Merge branch 'python-client'
>---------------------------------------------------------------
.gitignore | 1 +
cli/copr-cli.spec | 8 +-
cli/copr_cli/main.py | 111 ++++++--
cli/copr_cli/subcommands.py | 307 -------------------
cli/{coprcli-setup.py => setup.py} | 2 +-
{backend => python}/LICENSE | 0
{cli/copr_cli => python/copr_client}/README.rst | 30 +-
python/copr_client/__init__.py | 4 +
python/copr_client/client.py | 314 ++++++++++++++++++++
.../copr_client/exceptions.py | 14 +-
python/copr_client/responses.py | 308 +++++++++++++++++++
python/dist/copr-client.spec | 41 +++
{backend => python}/documentation/Makefile | 4 +-
.../how-to-generate-documentation.txt | 0
.../python-copr-client.spec | 26 +--
python/requirements.txt | 4 +
cli/coprcli-setup.py => python/setup.py | 18 +-
17 files changed, 805 insertions(+), 387 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1be2b2f..8200cb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
data/
cli/dist/
build/
+python-doc/
*.egg-info
documentation/build
documentation/python-doc
diff --git a/cli/copr-cli.spec b/cli/copr-cli.spec
index d38fe6c..eda40dd 100644
--- a/cli/copr-cli.spec
+++ b/cli/copr-cli.spec
@@ -22,8 +22,8 @@ BuildRequires: asciidoc
BuildRequires: libxslt
BuildRequires: util-linux
BuildRequires: python-setuptools
-BuildRequires: python-requests
BuildRequires: python2-devel
+BuildRequires: python-copr-client
%if 0%{?rhel} < 7 && 0%{?rhel} > 0
BuildRequires: python-argparse
%endif
@@ -31,8 +31,8 @@ BuildRequires: python-argparse
BuildRequires: epydoc
BuildRequires: graphviz
-Requires: python-requests
Requires: python-setuptools
+Requires: python-copr-client
%if 0%{?rhel} < 7 && 0%{?rhel} > 0
Requires: python-argparse
%endif
@@ -60,6 +60,8 @@ only.
%build
+%{__python2} setup.py build
+
mv copr_cli/README.rst ./
# convert manages
@@ -74,7 +76,7 @@ popd
%install
install -d %{buildroot}%{_pkgdocdir}/
-%{__python2} coprcli-setup.py install --root %{buildroot}
+%{__python2} setup.py install --root %{buildroot}
install -d %{buildroot}%{_mandir}/man1
install -p -m 644 man/copr-cli.1 %{buildroot}/%{_mandir}/man1/
diff --git a/cli/copr_cli/main.py b/cli/copr_cli/main.py
index 597b57a..33244f9 100644
--- a/cli/copr_cli/main.py
+++ b/cli/copr_cli/main.py
@@ -1,14 +1,63 @@
#-*- coding: UTF-8 -*-
+__version__ = "0.3.0"
+__description__ = "CLI tool to run copr"
+
import argparse
import sys
import ConfigParser
+import datetime
+import time
+from collections import defaultdict
-import subcommands
-import copr_exceptions
+from copr_client import CoprClient
+import copr_client.exceptions as copr_exceptions
+
+client = CoprClient.create_from_file_config()
-__version__ = "0.2.0"
-__description__ = "CLI tool to run copr"
+
+def _watch_builds(build_ids):
+ print("Watching build(s): (this may be safely interrupted)")
+ prevstatus = defaultdict(lambda: None)
+ failed_ids = []
+ watched_ids = build_ids[:]
+
+ try:
+ while True:
+ for build_id in watched_ids:
+ build_status = client.get_build_status(build_id)
+ if build_status.output != "ok":
+ errmsg = " Build {1}: Unable to get build status: {0}".format(
+ build_status.error, build_id)
+ raise copr_exceptions.CoprRequestException(errmsg)
+
+ now = datetime.datetime.now()
+ if prevstatus[build_id] != build_status.status:
+ prevstatus[build_id] = build_status.status
+ print(" {0} Build {2}: {1}".format(
+ now.strftime("%H:%M:%S"),
+ build_status.status, build_id))
+
+ if build_status.status in ["failed"]:
+ failed_ids.append(build_id)
+ if build_status.status in ["succeeded", "skipped",
+ "failed", "canceled"]:
+ watched_ids.remove(build_id)
+ if build_status.status == "unknown":
+ raise copr_exceptions.CoprBuildException(
+ "Unknown status.")
+
+ if not watched_ids:
+ break
+ time.sleep(60)
+
+ if failed_ids:
+ raise copr_exceptions.CoprBuildException(
+ "Build(s) {0} failed.".format(
+ ", ".join(str(x) for x in failed_ids)))
+
+ except KeyboardInterrupt:
+ pass
def action_build(args):
@@ -18,8 +67,17 @@ def action_build(args):
:param args: argparse arguments provided by the user
"""
- subcommands.build(args.copr, args.pkgs,
- args.memory, args.timeout, not args.nowait, chroots=args.chroots)
+ result = client.create_new_build(
+ projectname=args.copr, chroots=args.chroots, pkgs=args.pkgs,
+ memory=args.memory, timeout=args.timeout)
+ if result.output != "ok":
+ print(result.error)
+ return
+ print(result.message)
+ print("Created builds: {0}".format(" ".join(map(str, result.ids))))
+
+ if not args.nowait:
+ _watch_builds(result.ids)
def action_create(args):
@@ -29,9 +87,12 @@ def action_create(args):
:param args: argparse arguments provided by the user
"""
- subcommands.create(args.name, args.chroots, args.description,
- args.instructions, args.repos,
- args.initial_pkgs)
+ result = client.create_project(
+ projectname=args.name, description=args.description,
+ instructions=args.instructions, chroots=args.chroots,
+ repos=args.repos, initial_pkgs=args.initial_pkgs)
+ print(result)
+
def action_delete(args):
""" Method called when the 'delete' action has been selected by the
@@ -39,7 +100,8 @@ def action_delete(args):
:param args: argparse arguments provided by the user
"""
- subcommands.delete(args.copr)
+ result = client.delete_project(projectname=args.copr)
+ print(result)
def action_list(args):
@@ -49,20 +111,31 @@ def action_list(args):
:param args: argparse arguments provided by the user
"""
- subcommands.listcoprs(args.username)
+ username = args.username or client.username
+ result = client.get_projects_list(username)
+ if result.response["output"] != "ok":
+ print(result.error)
+ print("Un-expected data returned, please report this issue")
+ elif not result.projects:
+ print("No copr retrieved for user: '{0}'".format(username))
+ return
+
+ for prj in result.projects:
+ print(prj)
def action_status(args):
- subcommands.status(args.build_id)
+ result = client.get_build_status(args.build_id)
+ print(result)
+
def action_cancel(args):
""" Method called when the 'cancel' action has been selected by the
user.
-
:param args: argparse arguments provided by the user
-
"""
- subcommands.cancel(args.build_id)
+ result = client.cancel_build(args.build_id)
+ print(result)
def setup_parser():
"""
@@ -159,24 +232,24 @@ def main(argv=sys.argv[1:]):
except KeyboardInterrupt:
sys.stderr.write("\nInterrupted by user.")
sys.exit(1)
- except copr_exceptions.CoprCliRequestException, e:
+ except copr_exceptions.CoprRequestException, e:
sys.stderr.write("\nSomething went wrong:")
sys.stderr.write("\nError: {0}\n".format(e))
sys.exit(1)
except argparse.ArgumentTypeError, e:
sys.stderr.write("\nError: {0}".format(e))
sys.exit(2)
- except copr_exceptions.CoprCliException, e:
+ except copr_exceptions.CoprException, e:
sys.stderr.write("\nError: {0}\n".format(e))
sys.exit(3)
except ConfigParser.ParsingError, e:
sys.stderr.write("\nError: {0}\n".format(e))
sys.stderr.write("Lines in INI file should not be indented.\n")
sys.exit(3)
- except copr_exceptions.CoprCliBuildException, e:
+ except copr_exceptions.CoprBuildException, e:
sys.stderr.write("\nBuild error: {0}\n".format(e))
sys.exit(4)
- except copr_exceptions.CoprCliUnknownResponseException, e:
+ except copr_exceptions.CoprUnknownResponseException, e:
sys.stderr.write("\nError: {0}\n".format(e))
sys.exit(5)
# except Exception as e:
diff --git a/cli/copr_cli/subcommands.py b/cli/copr_cli/subcommands.py
deleted file mode 100644
index ab72f59..0000000
--- a/cli/copr_cli/subcommands.py
+++ /dev/null
@@ -1,307 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-"""
-Function actually doing the work of calling the API and handling the
-output.
-"""
-
-import ConfigParser
-import datetime
-import json
-import os
-import re
-import requests
-import sys
-import time
-
-import copr_exceptions
-
-
-def _get_data(req, user, copr=None):
- """ Wrapper around response from server
-
- checks data and raises a CoprCliRequestException with nice error message
- or CoprCliUnknownResponseException in case of some some error.
- Otherwise return json object.
- """
- if "<title>Sign in Coprs</title>" in req.text:
- raise copr_exceptions.CoprCliRequestException("Invalid API token\n")
- return
-
- if req.status_code == 404:
- if copr is None:
- raise copr_exceptions.CoprCliRequestException(
- "User {0} is unknown.\n".format(user["username"]))
- else:
- raise copr_exceptions.CoprCliRequestException(
- "Project {0}/{1} not found.\n".format(
- user["username"], copr))
- try:
- output = json.loads(req.text)
- except ValueError:
- raise copr_exceptions.CoprCliUnknownResponseException(
- "Unknown response from the server.")
- if req.status_code != 200:
- raise copr_exceptions.CoprCliRequestException(output["error"])
-
- if output is None:
- raise copr_exceptions.CoprCliUnknownResponseException(
- "No response from the server.")
- return output
-
-
-def get_user():
- """ Retrieve the user information from the config file. """
- config = ConfigParser.ConfigParser()
- if not config.read(
- os.path.join(os.path.expanduser("~"), ".config", "copr")):
- raise copr_exceptions.CoprCliNoConfException(
- "No configuration file '~/.config/copr' found. "
- "See man copr-cli for more information")
- try:
- username = config.get("copr-cli", "username", None)
- login = config.get("copr-cli", "login", None)
- token = config.get("copr-cli", "token", None)
- except ConfigParser.Error as err:
- raise copr_exceptions.CoprCliConfigException(
- "Bad configuration file: {0}".format(err))
- return {"username": username, "token": token, "login": login}
-
-
-def get_api_url():
- """ Retrieve the user information from the config file. """
- config = ConfigParser.ConfigParser()
- config.read(
- os.path.join(os.path.expanduser("~"), ".config", "copr")
- )
-
- # Default copr_url:
- copr_url = "http://copr.fedoraproject.org/"
- if (config.has_section("copr-cli") and
- config.has_option("copr-cli", "copr_url")):
-
- copr_url = config.get("copr-cli", "copr_url")
- return "{0}/api".format(copr_url)
-
-
-def listcoprs(username=None):
- """ List all the copr of a user. """
- user = {}
- if not username:
- user = get_user()
- del(user["token"])
-
- if username:
- user["username"] = username
-
- copr_api_url = get_api_url()
- url = "{0}/coprs/{1}/".format(copr_api_url, user["username"])
-
- req = requests.get(url)
- output = _get_data(req, user)
- if output is None:
- return
- elif "repos" in output:
- PAD = " " * 2
- if output["repos"]:
- for repo in output["repos"]:
- print("Name: {0}".format(repo["name"]))
-
- if "description" in repo:
- desc = repo["description"]
- print(PAD + "Description: {0}".format(desc))
-
- if "yum_repos" in repo:
- yum_repos = repo["yum_repos"]
- print(PAD + "Yum repo(s):")
- for k in sorted(yum_repos.keys()):
- print(PAD * 2 + "{0}: {1}".format(k, yum_repos[k]))
-
- if "additional_repos" in repo:
- add_repos = repo["additional_repos"]
- print(PAD + "Additional repos: {0}".format(add_repos))
-
- if "instructions" in repo:
- instructions = repo["instructions"]
- print(PAD + "Instructions: {0}".format(instructions))
- else:
- print("No copr retrieved for user: '{0}'".format(
- user["username"]))
- else:
- print("Un-expected data returned, please report this issue")
-
-
-def create(name, chroots=[], description=None, instructions=None,
- repos=None, initial_pkgs=None):
- """ Create a new copr. """
- if chroots is None:
- raise copr_exceptions.CoprCliRequestException(
- "At least one chroot must be selected")
-
- user = get_user()
- copr_api_url = get_api_url()
- URL = "{0}/coprs/{1}/new/".format(copr_api_url, user["username"])
-
- if type(repos) == list():
- repos = " ".join(repos)
-
- if type(initial_pkgs) == list():
- initial_pkgs = " ".join(initial_pkgs)
-
- data = {"name": name,
- "repos": repos,
- "initial_pkgs": initial_pkgs,
- "description": description,
- "instructions": instructions
- }
- for chroot in chroots:
- data[chroot] = "y"
-
- req = requests.post(URL,
- auth=(user["login"], user["token"]),
- data=data)
- output = _get_data(req, user)
- if output is not None:
- print(output["message"])
-
-
-def delete(copr):
- """ Delete the entire project.
- """
- user = get_user()
- username = user["username"]
-
- copr_api_url = get_api_url()
- URL = "{0}/coprs/{1}/{2}/delete/".format(
- copr_api_url,
- username,
- copr)
-
- data = {"verify": "yes"}
-
- req = requests.post(URL,
- auth=(user["login"], user["token"]),
- data=data)
- output = _get_data(req, user, copr)
-
-
-def _fetch_status(build_id):
- user = get_user()
- copr_api_url = get_api_url()
- URL = "{0}/coprs/build_status/{1}/".format(
- copr_api_url,
- build_id)
-
- req = requests.get(URL, auth=(user["login"], user["token"]))
- output = _get_data(req, user)
- if output is None:
- return (False, "Error occurred.")
- elif "status" in output:
- return (True, output["status"])
- else:
- return (False, output["error"])
-
-
-def status(build_id):
- """ Return status of build """
- (ret, value) = _fetch_status(build_id)
- print(value)
-
-def cancel(build_id):
- """ Cancel specified build_id """
- user = get_user()
- copr_api_url = get_api_url()
- #/coprs/rhscl/nodejs010/cancel_build/4060/
- URL = "{0}/coprs/cancel_build/{1}/".format(
- copr_api_url,
- build_id)
- req = requests.post(URL, auth=(user["login"], user["token"]))
- output = _get_data(req, user)
- if output is None:
- return (False, "Error occurred.")
- elif "status" in output:
- return (True, output["status"])
- else:
- return (False, output["error"])
-
-def build(copr, pkgs, memory, timeout, wait=True, result=None, chroots=None):
- """ Build a new package into a given copr.
-
- Result is dictionary where is returned "errmsg" in case of error.
- And "id" and "status" otherwise.
- """
- user = get_user()
- username = user["username"]
-
- # if you specify copr as username/foo, retrieve and cut username
- m = re.match(r"(.+)/(.+)", copr)
- if m:
- username = m.group(1)
- copr = m.group(2)
-
- copr_api_url = get_api_url()
- URL = "{0}/coprs/{1}/{2}/new_build/".format(
- copr_api_url,
- username,
- copr)
-
- data = {"pkgs": " ".join(pkgs),
- "memory_reqs": memory,
- "timeout": timeout
- }
-
- if chroots is not None:
- for chroot in chroots:
- data[chroot] = "y"
-
- req = requests.post(URL,
- auth=(user["login"], user["token"]),
- data=data)
- output = _get_data(req, user, copr)
- print("{1}:\n {0} build(s) were added successfully\n".format(len(output["ids"]), copr))
-
- if wait:
- print("Watching build(s): (this may be safely interrupted)")
- prevstatus = {}
- failed_ids = []
- for id in output["ids"]:
- prevstatus[id] = None
- if result is not None:
- result[id] = {}
- try:
- while True:
- for id in output["ids"]:
- (ret, status) = _fetch_status(id)
- if not ret:
- errmsg = " Build {1}: Unable to get build status: {0}".format(status,id)
- if result is not None:
- result[id]['errmsg'] = errmsg
- raise copr_exceptions.CoprCliRequestException(errmsg)
-
- now = datetime.datetime.now()
- if prevstatus[id] != status:
- print(" {0} Build {2}: {1}".format(now.strftime("%H:%M:%S"), status, id))
- prevstatus[id] = status
-
- if status in ["succeeded", "skipped", "failed", "canceled"]:
- if result is not None:
- result[id]['status'] = status
- if status in ["failed"]:
- failed_ids.append(id)
- output["ids"].remove(id)
-
- if status == "unknown":
- raise copr_exceptions.CoprCliBuildException(
- "Unknown status.")
-
- if not output["ids"]:
- break
- time.sleep(60)
-
- if failed_ids:
- raise copr_exceptions.CoprCliBuildException(
- "Build(s) {0} failed.".format(
- ", ".join(str(x) for x in failed_ids)))
-
- except KeyboardInterrupt:
- pass
diff --git a/cli/coprcli-setup.py b/cli/setup.py
similarity index 98%
copy from cli/coprcli-setup.py
copy to cli/setup.py
index fa67387..68369f7 100644
--- a/cli/coprcli-setup.py
+++ b/cli/setup.py
@@ -14,7 +14,7 @@ This part is a command line interface to use copr."""
from copr_cli.main import __description__, __version__
requires = [
- 'requests',
+ 'copr-client'
]
diff --git a/backend/LICENSE b/python/LICENSE
similarity index 100%
copy from backend/LICENSE
copy to python/LICENSE
diff --git a/cli/copr_cli/README.rst b/python/copr_client/README.rst
similarity index 62%
copy from cli/copr_cli/README.rst
copy to python/copr_client/README.rst
index e46f5ff..fedfca0 100644
--- a/cli/copr_cli/README.rst
+++ b/python/copr_client/README.rst
@@ -1,4 +1,4 @@
-Copr CLI
+Copr python client
========
Copr is designed to be a lightweight buildsystem that allows contributors
@@ -6,7 +6,7 @@ to create packages, put them in repositories, and make it easy for users
to install the packages onto their system. Within the Fedora Project it
is used to allow packagers to create third party repositories.
-This part is a command line interface to use copr.
+This part is a python client to access copr service.
About this project:
-------------------
@@ -14,29 +14,29 @@ About this project:
- Git: http://git.fedorahosted.org/cgit/copr.git
- Test instance: http://copr-fe.cloud.fedoraproject.org/
-
Dependencies:
-------------
-.. _python-requests: http://docs.python-requests.org/en/latest/
-.. _python-argparse: https://pypi.python.org/pypi/argparse
-
-The CLI depends on:
+The client depends on:
-- python (should work on 2.5, not tested)
-- `python-argparse`_ (for python < 2.7)
-- `python-requests`_
+.. _python2.6 +
+.. _python-requests: http://docs.python-requests.org/en/latest/
Usage:
------
-.. _test instance: http://copr-fe.cloud.fedoraproject.org/
-
- Create an account on copr `test instance`_
- Go to the API page: http://copr-fe.cloud.fedoraproject.org/api
- Retrieve your API token
+
+::
+
+ from python_copr.main import CoprClient
+
+
+Alternatively you could use configuration file:
- Create the file ``~/.config/copr``
- In this file add the following content
-
+(which is also provided by /api page of the copr service)
::
[copr-cli]
@@ -45,7 +45,3 @@ Usage:
You should then be able to use copr-cli to list, create and build on copr.
-
-.. note:: You can use directly copr-cli to list someone's copr repo but to create
- a copr or build packages into an existing repo you need to authenticate
- via the API token.
diff --git a/python/copr_client/__init__.py b/python/copr_client/__init__.py
new file mode 100644
index 0000000..7b901d6
--- /dev/null
+++ b/python/copr_client/__init__.py
@@ -0,0 +1,4 @@
+#!/usr/bin/python -tt
+#-*- coding: UTF-8 -*-
+
+from client import CoprClient
diff --git a/python/copr_client/client.py b/python/copr_client/client.py
new file mode 100644
index 0000000..082b200
--- /dev/null
+++ b/python/copr_client/client.py
@@ -0,0 +1,314 @@
+#-*- coding: UTF-8 -*-
+
+import json
+from pprint import pprint
+import os
+import sys
+import ConfigParser
+import logging
+
+logging.basicConfig(level=logging.WARN)
+log = logging.getLogger(__name__)
+
+
+import requests
+import exceptions
+
+from .responses import BuildStatusResponse, BuildRequestResponse, \
+ CreateProjectResponse, BaseResponse, \
+ DeleteProjectResponse, CancelBuildResponse, \
+ ProjectDetailsResponse, BuildDetailsResponse, GetProjectsListResponse, ModifyProjectResponse, SearchResponse
+
+__version__ = "0.0.1"
+__description__ = "Python client for copr service"
+
+
+class CoprClient(object):
+ def __init__(self, config=None):
+ """ Main interface to the copr service
+ :param config: Configuration dictionary.
+ Fields:
+ copr_url - copr service location
+ login - user login, used for identification
+ token - copr api token
+ username - used as copr projects root
+
+ """
+ self.token = config.get("token")
+ self.login = config.get("login")
+ self.username = config.get("username")
+ self.copr_url = config.get("copr_url", "http://copr.fedoraproject.org/")
+
+ def __str__(self):
+ return "<Copr client. username: {0}, api url: {1}>".format(
+ self.username, self.api_url
+ )
+
+ @property
+ def api_url(self):
+ return "{0}/api".format(self.copr_url)
+
+ @staticmethod
+ def create_from_file_config(filepath=None):
+ """
+ Retrieve copr client information from the config file.
+ :param filepath: specifies config location, default: "~/.config/copr"
+ """
+ raw_config = ConfigParser.ConfigParser()
+ if not filepath:
+ filepath = os.path.join(os.path.expanduser("~"), ".config", "copr")
+ config = {}
+ if not raw_config.read(filepath):
+ raise exceptions.CoprNoConfException(
+ "No configuration file '~/.config/copr' found. "
+ "See man copr-cli for more information")
+ try:
+ for field in ["username", "login", "token", "copr_url"]:
+ config[field] = raw_config.get("copr-cli", field, None)
+ except ConfigParser.Error as err:
+ raise exceptions.CoprConfigException(
+ "Bad configuration file: {0}".format(err))
+ return CoprClient(config=config)
+
+ def _fetch(self, url, data=None, projectname=None, method=None,
+ skip_auth=False, on_error_response=None):
+ """ Fetch data from server,
+ checks response and raises a CoprCliRequestException with nice error message
+ or CoprCliUnknownResponseException in case of some some error.
+ Otherwise return json object.
+
+ :param url: formed url to fetch
+ :param data: serialised data to send
+ :param skip_auth: don't send auth credentials
+ :param projectname: name of the copr project
+ :param on_error_response: function to handle responses with bad status code
+ """
+ if method is None:
+ method = "get"
+
+ log.debug("Fetching url: {0}, for login: {1}".format(url, self.login))
+
+ kwargs = {}
+ if not skip_auth:
+ kwargs["auth"] = (self.login, self.token)
+ if data is not None:
+ kwargs["data"] = data
+
+ if method not in ["get", "post", "head", "delete", "put"]:
+ raise Exception("Method {0} not allowed".format(method))
+
+ #print("Url: {0}".format(url))
+ #print(kwargs)
+ response = requests.request(
+ method=method.upper(),
+ url=url,
+ **kwargs
+ )
+ log.debug("raw response: {0}".format(response.text))
+
+ if "<title>Sign in Coprs</title>" in response.text:
+ raise exceptions.CoprRequestException("Invalid API token\n")
+
+ if response.status_code > 299 and on_error_response is not None:
+ return on_error_response(response)
+
+ #TODO: better status code handling
+ if response.status_code == 404:
+ if projectname is None:
+ raise exceptions.CoprRequestException(
+ "User {0} is unknown.\n".format(self.username))
+ else:
+ raise exceptions.CoprRequestException(
+ "Project {0}/{1} not found.\n".format(
+ self.username, projectname))
+
+ if 400 <= response.status_code < 500:
+ log.error("Bad request, raw response body: {0}".format(response.text))
+ elif response.status_code >= 500:
+ #import ipdb; ipdb.set_trace()
+ log.error("Server error, raw response body: {0}".format(response.text))
+
+ try:
+ output = json.loads(response.text)
+ except ValueError:
+ #import ipdb; ipdb.set_trace()
+ raise exceptions.CoprUnknownResponseException(
+ "Unknown response from the server.")
+ if response.status_code != 200:
+ raise exceptions.CoprRequestException(output["error"])
+
+ if output is None:
+ raise exceptions.CoprUnknownResponseException(
+ "No response from the server.")
+ return output
+
+ def get_build_status(self, build_id):
+ url = "{0}/coprs/build_status/{1}/".format(
+ self.api_url, build_id)
+
+ response = self._fetch(url)
+ return BuildStatusResponse(
+ client=self, build_id=build_id, response=response)
+
+ def get_build_details(self, build_id):
+ url = "{0}/coprs/build/{1}/".format(
+ self.api_url, build_id)
+
+ response = self._fetch(url, skip_auth=True)
+ return BuildDetailsResponse(self, response, build_id)
+
+ def cancel_build(self, build_id):
+ url = "{0}/coprs/cancel_build/{1}/".format(
+ self.api_url, build_id)
+ response = self._fetch(url, method="post")
+ return CancelBuildResponse(self, response, build_id)
+
+ def delete_project(self, projectname):
+ """
+ Delete the entire project
+ """
+ url = "{0}/coprs/{1}/{2}/delete/".format(
+ self.api_url, self.username, projectname
+ )
+ response = self._fetch(
+ url, data={"verify": "yes"}, method="post")
+ return DeleteProjectResponse(self, response, projectname)
+
+ def get_project_details(self, projectname):
+ """
+ Get project details
+ """
+ url = "{0}/coprs/{1}/{2}/detail/".format(
+ self.api_url, self.username, projectname
+ )
+
+ response = self._fetch(url, skip_auth=True)
+ return ProjectDetailsResponse(self, response, projectname)
+
+ def create_project(
+ self, projectname,
+ description=None, instructions=None,
+ chroots=None, repos=None, initial_pkgs=None
+ ):
+ """
+ Create a new copr project
+ """
+
+ url = "{0}/coprs/{1}/new/".format(
+ self.api_url, self.username)
+
+ if type(repos) == list():
+ repos = " ".join(repos)
+
+ if type(initial_pkgs) == list():
+ initial_pkgs = " ".join(initial_pkgs)
+
+ data = {"name": projectname,
+ "repos": repos,
+ "initial_pkgs": initial_pkgs,
+ "description": description,
+ "instructions": instructions
+ }
+ for chroot in chroots:
+ data[chroot] = "y"
+
+ #def on bad_response()
+ response = self._fetch(url, data=data, method="post")
+ return CreateProjectResponse(
+ self, response,
+ name=projectname, description=description, instructions=instructions,
+ repos=repos, chroots=chroots, initial_pkgs=initial_pkgs
+ )
+
+ def get_projects_list(self, username=None):
+ """
+ Get list of projects created by the user
+ """
+ url = "{0}/coprs/{1}/".format(
+ self.api_url, username or self.username)
+ response = self._fetch(url)
+ return GetProjectsListResponse(self, response)
+
+ def get_project_chroot_details(self, name, chroot):
+ url = "{0}/coprs/{1}/{2}/detail/{3}/".format(
+ self.api_url, self.username, name, chroot
+ )
+ response = self._fetch(url, skip_auth=True)
+ return BaseResponse(self, response)
+
+ def modify_project_chroot_details(self, projectname, chroot, pkgs=None):
+ if pkgs is None:
+ pkgs = []
+
+ url = "{0}/coprs/{1}/{2}/modify/{3}/".format(
+ self.api_url, self.username, projectname, chroot
+ )
+ data = {
+ "buildroot_pkgs": " ".join(pkgs)
+ }
+ response = self._fetch(url, data=data, method="post")
+ return BaseResponse(self, response)
+
+ def modify_project(self, projectname, description=None,
+ instructions=None, repos=None):
+
+ """
+ Modifies main project settings.
+
+ :param projectname:
+ :param description:
+ :param instructions:
+ :param repos:
+ :return:
+ """
+ url = "{0}/coprs/{1}/{2}/modify/".format(
+ self.api_url, self.username, projectname
+ )
+ data = {}
+ if description:
+ data["description"] = description
+ if instructions:
+ data["instructions"] = instructions
+ if repos:
+ data["repos"] = repos
+
+ response = self._fetch(url, data=data, method="post")
+ return ModifyProjectResponse(self, response, projectname, description,
+ instructions, repos)
+
+ def create_new_build(self, projectname, pkgs,
+ timeout=None, memory=None, chroots=None):
+ """
+ Creates new build in `projectname` copr.
+
+ :param projectname: name of copr project (without user namespace)
+ :param pkgs: list of packages to include in build
+ :param timeout: ?build timeout
+ :param memory: amount of required memory for build process
+ :param wait: if True function wait for packages to be build
+ :param chroots: build only with given chroots
+
+ """
+
+ url = "{0}/coprs/{1}/{2}/new_build/".format(
+ self.api_url, self.username, projectname
+ )
+ data = {
+ "pkgs": " ".join(pkgs),
+ "memory_reqs": memory,
+ "timeout": timeout
+ }
+ for chroot in chroots or []:
+ data[chroot] = "y"
+
+ response = self._fetch(url, data, method="post")
+ return BuildRequestResponse(
+ self, response,
+ projectname, pkgs, memory, timeout, chroots)
+
+ def search_projects(self, query):
+ url = "{0}/coprs/search/{1}/".format(
+ self.api_url, query
+ )
+ response = self._fetch(url, skip_auth=True)
+ return SearchResponse(self, response, query)
diff --git a/cli/copr_cli/copr_exceptions.py b/python/copr_client/exceptions.py
similarity index 69%
rename from cli/copr_cli/copr_exceptions.py
rename to python/copr_client/exceptions.py
index 1e10678..6528061 100644
--- a/cli/copr_cli/copr_exceptions.py
+++ b/python/copr_client/exceptions.py
@@ -5,41 +5,41 @@ Exceptions for copr-cli
"""
-class CoprCliException(Exception):
+class CoprException(Exception):
""" Basic exception class for copr-cli. """
pass
-class CoprCliNoConfException(CoprCliException):
+class CoprNoConfException(CoprException):
""" Exception thrown when no config file is found. """
pass
-class CoprCliConfigException(CoprCliException):
+class CoprConfigException(CoprException):
""" Exception thrown when the config file is incomplete or
- malformated.
+ malformed.
"""
pass
-class CoprCliRequestException(Exception):
+class CoprRequestException(Exception):
""" Exception thrown when the request is bad. For example,
the user provided wrong project name or build ID.
"""
pass
-class CoprCliBuildException(Exception):
+class CoprBuildException(Exception):
""" Exception thrown when one or more builds fail and Cli is waiting
for the result.
"""
pass
-class CoprCliUnknownResponseException(Exception):
+class CoprUnknownResponseException(Exception):
""" Exception thrown when the response is unknown to cli.
It usualy means that something is broken.
"""
diff --git a/python/copr_client/responses.py b/python/copr_client/responses.py
new file mode 100644
index 0000000..45012ce
--- /dev/null
+++ b/python/copr_client/responses.py
@@ -0,0 +1,308 @@
+import weakref
+
+
+class BaseResponse(object):
+ """
+ Base class for API responses.
+ Some response provides further actions based on response content.
+ """
+ _simple_fields = []
+
+ def __init__(self, client, response):
+ """
+ :param client: copr_client.main.Client
+ :param response:
+ """
+ self.response = response
+ self.client = weakref.proxy(client)
+
+ def __str__(self):
+ return str(self.response)
+
+ def __unicode__(self):
+ return unicode(self.response)
+
+ def __getattr__(self, item):
+ if item not in self._simple_fields:
+ raise KeyError(item)
+ if self.response is None:
+ raise RuntimeError()
+
+ return self.response[item]
+
+ def repeat_action(self):
+ raise NotImplementedError()
+
+
+class ProjectMixin(object):
+ """
+ Provides methods related to copr project.
+ Object must have fields "client" and "project_name"
+ """
+ def get_project_details(self):
+ return self.client.get_project_details(self.project_name)
+
+ def delete_project(self):
+ return self.client.delete_project(self.project_name)
+
+
+class ProjectWrapper(object):
+ def __init__(self, username, projectname,
+ description=None, instructions=None,
+ yum_repos=None, additional_repos=None):
+
+ self.username = username
+ self.projectname = projectname
+ self.description = description
+ self.instructions = instructions
+ self.yum_repos = yum_repos or {}
+ self.additional_repos = additional_repos or {}
+
+ def __str__(self):
+ out = list()
+ out.append("Name: {0}".format(self.projectname))
+ out.append(" Description: {0}".format(self.description))
+
+ if self.yum_repos:
+ out.append(" Yum repo(s):")
+ for k in sorted(self.yum_repos.keys()):
+ out.append(" {0}: {1}".format(k, self.yum_repos[k]))
+ if self.additional_repos:
+ out.append(" Additional repo: {0}".format(self.additional_repos))
+ if self.instructions:
+ out.append(" Instructions: {0}".format(self.instructions))
+
+ out.append("")
+ return "\n".join(out)
+
+
+class GetProjectsListResponse(BaseResponse):
+ _simple_fields = ["output", "repos"]
+
+ def __init__(self, client, response, username=None):
+ super(GetProjectsListResponse, self).__init__(client, response)
+ self.username = username or self.client.username
+
+ @property
+ def projects(self):
+ if not self.response or \
+ self.response.get("output", None) != "ok" or \
+ not self.response.get("repos"):
+ return None
+ else:
+ return [
+ ProjectWrapper(
+ username=self.username,
+ projectname=prj.get("name"),
+ description=prj.get("description"),
+ yum_repos=prj.get("yum_repos"),
+ additional_repos=prj.get("additional_repos"),
+ ) for prj in self.response["repos"]
+ ]
+
+
+class CreateProjectResponse(ProjectMixin, BaseResponse):
+ _simple_fields = ["message", "output"]
+
+ def __init__(self, client, response,
+ name, description, instructions,
+ chroots, repos, initial_pkgs):
+ super(CreateProjectResponse, self).__init__(client, response)
+ self.project_name = name
+ self.description = description
+ self.instructions = instructions
+ self.chroots = chroots
+ self.repos = repos
+ self.initial_pkgs = initial_pkgs
+
+ def __str__(self):
+ if hasattr(self, "message"):
+ return str(self.message)
+
+ def __unicode__(self):
+ if hasattr(self, "message"):
+ return unicode(self.message)
+
+
+class ModifyProjectResponse(ProjectMixin, BaseResponse):
+ def __init__(self, client, response,
+ name, description=None,
+ instructions=None, repos=None):
+ super(ModifyProjectResponse, self).__init__(client, response)
+ self.project_name = name
+ self.description = description
+ self.instructions = instructions
+ self.repos = repos
+
+
+class ProjectDetailsResponse(ProjectMixin, BaseResponse):
+ _simple_fields = ["detail", "output"]
+
+ def __init__(self, client, response, name):
+ super(ProjectDetailsResponse, self).__init__(client, response)
+ self.project_name = name
+
+ def __str__(self):
+ if hasattr(self, "detail"):
+ return str(self.detail)
+
+ def __unicode__(self):
+ if hasattr(self, "detail."):
+ return unicode(self.detail)
+
+ def repeat_action(self):
+ return self.get_project_details()
+
+
+class DeleteProjectResponse(ProjectMixin, BaseResponse):
+ _simple_fields = ["message", "output"]
+
+ def __init__(self, client, response, name):
+ super(DeleteProjectResponse, self).__init__(client, response)
+ self.project_name = name
+
+ def __str__(self):
+ if hasattr(self, "message"):
+ return str(self.message)
+
+ def __unicode__(self):
+ if hasattr(self, "message"):
+ return unicode(self.message)
+
+ def repeat_action(self):
+ return self.delete_project()
+
+
+class BuildMixin(object):
+ """
+ Provides methods related to individual builds
+ Object must have fields "client" and "build_id"
+ """
+ def get_build_status(self):
+ return self.client.get_build_status(self.build_id)
+
+ def get_build_details(self):
+ return self.client.get_build_details(self.build_id)
+
+ def cancel_build(self):
+ return self.client.cancel_build(self.build_id)
+
+ def __str__(self):
+ if not self.response:
+ return super(BuildMixin, self).__str__()
+ elif self.response["output"] == "ok":
+ return self.response["status"]
+ else:
+ return self.response["error"]
+
+
+class BuildStatusResponse(BuildMixin, BaseResponse):
+ _simple_fields = ["status", "output", "error"]
+
+ def __init__(self, client, response, build_id):
+ super(BuildStatusResponse, self).__init__(client, response)
+ self.build_id = build_id
+
+ def repeat_action(self):
+ return self.get_build_status()
+
+
+class BuildDetailsResponse(BuildMixin, BaseResponse):
+ _simple_fields = [
+ 'status', 'error', 'submitted_by', 'results', 'src_pkg', 'started_on',
+ 'submitted_on', 'owner', 'chroots', 'project', 'built_pkgs',
+ 'ended_on', 'output', 'src_version'
+ ]
+
+ def __init__(self, client, response, build_id):
+ super(BuildDetailsResponse, self).__init__(client, response)
+ self.build_id = build_id
+
+ def repeat_action(self):
+ return self.get_build_details()
+
+
+class CancelBuildResponse(BuildMixin, BaseResponse):
+ _simple_fields = ["message", "output", "error"]
+
+ def __init__(self, client, response, build_id):
+ super(CancelBuildResponse, self).__init__(client, response)
+ self.build_id = build_id
+
+ def repeat_action(self):
+ return self.client.cancel_build(self.build_id)
+
+ # def __str__(self):
+ # if not self.response:
+ # return super(CancelBuildResponse, self).__str__()
+ # elif self.response["output"] == "ok":
+ # return self.response["status"]
+ # else:
+ # return self.response["error"]
+
+
+class BuildRequestResponse(ProjectMixin, BaseResponse):
+ _simple_fields = ["message", "output", "error", "ids"]
+
+ def __init__(self, client, response,
+ copr_project, pkgs, memory, timeout, chroots):
+ super(BuildRequestResponse, self).__init__(client, response)
+ self.copr_project = copr_project
+ self.pkgs = pkgs
+ self.memory = memory
+ self.timeout = timeout
+ self.chroots = chroots
+
+ def repeat_action(self):
+ return self.client.send_new_build(
+ copr_project=self.copr_project,
+ pkgs=self.pkgs,
+ memory=self.memory,
+ timeout=self.timeout,
+ chroots=self.chroots
+ )
+
+
+class ProjectChrootMixin(ProjectMixin):
+ """
+ Provides methods related to project chroots
+ Object must have fields "client", "project_name", "chroot"
+ """
+ def get_project_chroot_details(self):
+ return self.client.get_project_chroot_details(self.project_name, self.chroot)
+
+ def modify_project_chroot_details(self, pkgs):
+ return self.client.modify_project_chroot_details(
+ self.project_name, self.chroot, pkgs=pkgs)
+
+
+class ProjectChrootDetailsResponse(ProjectChrootMixin, BaseResponse):
+ def __init__(self, client, response, name, chroot):
+ super(ProjectChrootDetailsResponse, self).__init__(client, response)
+ self.project_name = name
+ self.chroot = chroot
+
+ def repeat_action(self):
+ return self.get_project_details()
+
+
+class ModifyProjectChrootResponse(ProjectChrootMixin, BaseResponse):
+ def __init__(self, client, response, name, chroot, pkgs):
+ super(ModifyProjectChrootResponse, self).__init__(client, response)
+ self.project_name = name
+ self.chroot = chroot
+ self.pkgs = pkgs
+
+ def repeat_action(self):
+ return self.modify_project_chroot_details(self.pkgs)
+
+
+class SearchResponse(BaseResponse):
+ _simple_fields = ["output", "repos"]
+
+ def __init__(self, client, response, query):
+ super(SearchResponse, self).__init__(client, response)
+ self.query = query
+
+ def repeat_action(self):
+ return self.client.search_projects(self.query)
diff --git a/python/dist/copr-client.spec b/python/dist/copr-client.spec
new file mode 100644
index 0000000..a5a7fde
--- /dev/null
+++ b/python/dist/copr-client.spec
@@ -0,0 +1,41 @@
+%define name copr-client
+%define version 0.0.1
+%define unmangled_version 0.0.1
+%define unmangled_version 0.0.1
+%define release 1
+
+Summary: Python client for copr service
+Name: %{name}
+Version: %{version}
+Release: %{release}
+Source0: %{name}-%{unmangled_version}.tar.gz
+License: GPLv2+
+Group: Development/Libraries
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
+Prefix: %{_prefix}
+BuildArch: noarch
+Vendor: Valentin Gologuzov <vgologuz(a)redhat.com>
+Url: http://fedorahosted.org/copr/
+
+%description
+Copr is designed to be a lightweight buildsystem that allows contributors
+to create packages, put them in repositories, and make it easy for users
+to install the packages onto their system. Within the Fedora Project it
+is used to allow packagers to create third party repositories.
+
+This part is a python client to the copr service.
+
+%prep
+%setup -n %{name}-%{unmangled_version} -n %{name}-%{unmangled_version}
+
+%build
+python setup.py build
+
+%install
+python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+%files -f INSTALLED_FILES
+%defattr(-,root,root)
diff --git a/backend/documentation/Makefile b/python/documentation/Makefile
similarity index 91%
copy from backend/documentation/Makefile
copy to python/documentation/Makefile
index 06216f0..11be589 100644
--- a/backend/documentation/Makefile
+++ b/python/documentation/Makefile
@@ -11,9 +11,9 @@ BUILD_DIR := build
# Python doc related
EPYDOC_BIN := `which epydoc`
-EPYDOC_SWITCHES := --inheritance listed --graph all --html
+EPYDOC_SWITCHES := --inheritance listed --graph all --html -v
PYTHON_DOC_DIR := python-doc
-PYTHON_DIRS := ../
+PYTHON_DIRS := ../copr_client
# TARGETS
# schema is not incldued here, since you need a live db
diff --git a/backend/documentation/how-to-generate-documentation.txt b/python/documentation/how-to-generate-documentation.txt
similarity index 100%
copy from backend/documentation/how-to-generate-documentation.txt
copy to python/documentation/how-to-generate-documentation.txt
diff --git a/cli/copr-cli.spec b/python/python-copr-client.spec
similarity index 97%
copy from cli/copr-cli.spec
copy to python/python-copr-client.spec
index d38fe6c..6420c11 100644
--- a/cli/copr-cli.spec
+++ b/python/python-copr-client.spec
@@ -3,39 +3,32 @@
%global __python2 %{__python}
%endif
-Name: copr-cli
-Version: 1.36
+Name: python-copr-client
+Version: 1.42
Release: 1%{?dist}
-Summary: Command line interface for COPR
+Summary: Python interface for COPR
Group: Applications/Productivity
License: GPLv2+
URL: https://fedorahosted.org/copr/
# Source is created by
# git clone https://git.fedorahosted.org/git/copr.git
-# cd copr/cli
+# cd copr/python
# tito build --tgz
Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
-BuildRequires: asciidoc
BuildRequires: libxslt
BuildRequires: util-linux
BuildRequires: python-setuptools
BuildRequires: python-requests
BuildRequires: python2-devel
-%if 0%{?rhel} < 7 && 0%{?rhel} > 0
-BuildRequires: python-argparse
-%endif
#for doc package
BuildRequires: epydoc
BuildRequires: graphviz
Requires: python-requests
Requires: python-setuptools
-%if 0%{?rhel} < 7 && 0%{?rhel} > 0
-Requires: python-argparse
-%endif
%description
COPR is lightweight build system. It allows you to create new project in WebUI,
@@ -60,10 +53,9 @@ only.
%build
-mv copr_cli/README.rst ./
+%{__python2} setup.py build
-# convert manages
-a2x -d manpage -f manpage man/copr-cli.1.asciidoc
+mv copr_client/README.rst ./
%if 0%{?fedora}
# build documentation
@@ -74,9 +66,7 @@ popd
%install
install -d %{buildroot}%{_pkgdocdir}/
-%{__python2} coprcli-setup.py install --root %{buildroot}
-install -d %{buildroot}%{_mandir}/man1
-install -p -m 644 man/copr-cli.1 %{buildroot}/%{_mandir}/man1/
+%{__python2} setup.py install --root %{buildroot}
#doc
%if 0%{?fedora}
@@ -85,9 +75,7 @@ cp -a documentation/python-doc %{buildroot}%{_pkgdocdir}/
%files
%doc LICENSE README.rst
-%{_bindir}/copr-cli
%{python_sitelib}/*
-%{_mandir}/man1/copr-cli.1*
%if 0%{?fedora}
%files doc
diff --git a/python/requirements.txt b/python/requirements.txt
new file mode 100644
index 0000000..80e788a
--- /dev/null
+++ b/python/requirements.txt
@@ -0,0 +1,4 @@
+# Used for when working from a virtualenv.
+# Use this file by running "$ pip install -r requirements.txt"
+
+requests
diff --git a/cli/coprcli-setup.py b/python/setup.py
similarity index 74%
rename from cli/coprcli-setup.py
rename to python/setup.py
index fa67387..eccb299 100644
--- a/cli/coprcli-setup.py
+++ b/python/setup.py
@@ -9,20 +9,20 @@ to create packages, put them in repositories, and make it easy for users
to install the packages onto their system. Within the Fedora Project it
is used to allow packagers to create third party repositories.
-This part is a command line interface to use copr."""
+This part is a python client to the copr service."""
-from copr_cli.main import __description__, __version__
+from copr_client.client import __description__, __version__
requires = [
'requests',
]
-__name__ = 'copr-cli'
+__name__ = 'copr-client'
__version__ = __version__
__description__ = __description__
-__author__ = "Pierre-Yves Chibon"
-__author_email__ = "pingou(a)pingoured.fr"
+__author__ = "Valentin Gologuzov"
+__author_email__ = "vgologuz(a)redhat.com"
__url__ = "http://fedorahosted.org/copr/"
@@ -43,13 +43,7 @@ setup(
"Development Status :: 1 - Alpha",
],
install_requires=requires,
- packages=['copr_cli'],
- namespace_packages=['copr_cli'],
+ packages=['copr_client'],
include_package_data=True,
zip_safe=False,
- entry_points={
- 'console_scripts': [
- 'copr-cli = copr_cli.main:main'
- ]
- },
)
9 years, 9 months
[copr] python-client: [python-copr, copr-cli] fixing, cleanup (6e04d7e)
by vgologuz@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : python-client
>---------------------------------------------------------------
commit 6e04d7eceaa038f93344af5d0649ae7486cd3faa
Author: Valentin Gologuzov <vgologuz(a)redhat.com>
Date: Fri Aug 15 17:24:55 2014 +0200
[python-copr,copr-cli] fixing, cleanup
>---------------------------------------------------------------
cli/copr_cli/main.py | 10 ++--------
python/copr_client/client.py | 11 +++++------
python/copr_client/responses.py | 34 +++++++++++++++++++++-------------
3 files changed, 28 insertions(+), 27 deletions(-)
diff --git a/cli/copr_cli/main.py b/cli/copr_cli/main.py
index a390ceb..33244f9 100644
--- a/cli/copr_cli/main.py
+++ b/cli/copr_cli/main.py
@@ -13,18 +13,12 @@ from collections import defaultdict
from copr_client import CoprClient
import copr_client.exceptions as copr_exceptions
-# client = CoprClient.create_from_file_config()
-client = CoprClient(dict(
- login="Y29wcg==##fragphakffjdnlehuegu",
- username="vgologuz",
- token="srkqukvupebdipjfqyncohhvftiksi",
- copr_url="http://copr-fe-dev.cloud.fedoraproject.org"
-))
+client = CoprClient.create_from_file_config()
def _watch_builds(build_ids):
print("Watching build(s): (this may be safely interrupted)")
- prevstatus = defaultdict()
+ prevstatus = defaultdict(lambda: None)
failed_ids = []
watched_ids = build_ids[:]
diff --git a/python/copr_client/client.py b/python/copr_client/client.py
index 56d34a2..082b200 100644
--- a/python/copr_client/client.py
+++ b/python/copr_client/client.py
@@ -253,7 +253,8 @@ class CoprClient(object):
instructions=None, repos=None):
"""
- Modifies main project settings
+ Modifies main project settings.
+
:param projectname:
:param description:
:param instructions:
@@ -278,16 +279,14 @@ class CoprClient(object):
def create_new_build(self, projectname, pkgs,
timeout=None, memory=None, chroots=None):
"""
+ Creates new build in `projectname` copr.
:param projectname: name of copr project (without user namespace)
:param pkgs: list of packages to include in build
-
- :param timeout: ?? build timeout
-
+ :param timeout: ?build timeout
:param memory: amount of required memory for build process
:param wait: if True function wait for packages to be build
- :param result:
- :param chroots:
+ :param chroots: build only with given chroots
"""
diff --git a/python/copr_client/responses.py b/python/copr_client/responses.py
index 0dea83d..45012ce 100644
--- a/python/copr_client/responses.py
+++ b/python/copr_client/responses.py
@@ -53,7 +53,7 @@ class ProjectWrapper(object):
self.username = username
self.projectname = projectname
- self.description = description or "<No description provided>"
+ self.description = description
self.instructions = instructions
self.yum_repos = yum_repos or {}
self.additional_repos = additional_repos or {}
@@ -64,7 +64,7 @@ class ProjectWrapper(object):
out.append(" Description: {0}".format(self.description))
if self.yum_repos:
- out.append(" Yum repo(s)")
+ out.append(" Yum repo(s):")
for k in sorted(self.yum_repos.keys()):
out.append(" {0}: {1}".format(k, self.yum_repos[k]))
if self.additional_repos:
@@ -101,7 +101,7 @@ class GetProjectsListResponse(BaseResponse):
]
-class CreateProjectResponse(BaseResponse, ProjectMixin):
+class CreateProjectResponse(ProjectMixin, BaseResponse):
_simple_fields = ["message", "output"]
def __init__(self, client, response,
@@ -124,7 +124,7 @@ class CreateProjectResponse(BaseResponse, ProjectMixin):
return unicode(self.message)
-class ModifyProjectResponse(BaseResponse, ProjectMixin):
+class ModifyProjectResponse(ProjectMixin, BaseResponse):
def __init__(self, client, response,
name, description=None,
instructions=None, repos=None):
@@ -135,7 +135,7 @@ class ModifyProjectResponse(BaseResponse, ProjectMixin):
self.repos = repos
-class ProjectDetailsResponse(BaseResponse, ProjectMixin):
+class ProjectDetailsResponse(ProjectMixin, BaseResponse):
_simple_fields = ["detail", "output"]
def __init__(self, client, response, name):
@@ -154,7 +154,7 @@ class ProjectDetailsResponse(BaseResponse, ProjectMixin):
return self.get_project_details()
-class DeleteProjectResponse(BaseResponse, ProjectMixin):
+class DeleteProjectResponse(ProjectMixin, BaseResponse):
_simple_fields = ["message", "output"]
def __init__(self, client, response, name):
@@ -189,14 +189,14 @@ class BuildMixin(object):
def __str__(self):
if not self.response:
- return super(BuildStatusResponse, self).__str__()
+ return super(BuildMixin, self).__str__()
elif self.response["output"] == "ok":
return self.response["status"]
else:
return self.response["error"]
-class BuildStatusResponse(BaseResponse, BuildMixin):
+class BuildStatusResponse(BuildMixin, BaseResponse):
_simple_fields = ["status", "output", "error"]
def __init__(self, client, response, build_id):
@@ -207,7 +207,7 @@ class BuildStatusResponse(BaseResponse, BuildMixin):
return self.get_build_status()
-class BuildDetailsResponse(BaseResponse, BuildMixin):
+class BuildDetailsResponse(BuildMixin, BaseResponse):
_simple_fields = [
'status', 'error', 'submitted_by', 'results', 'src_pkg', 'started_on',
'submitted_on', 'owner', 'chroots', 'project', 'built_pkgs',
@@ -222,7 +222,7 @@ class BuildDetailsResponse(BaseResponse, BuildMixin):
return self.get_build_details()
-class CancelBuildResponse(BaseResponse, BuildMixin):
+class CancelBuildResponse(BuildMixin, BaseResponse):
_simple_fields = ["message", "output", "error"]
def __init__(self, client, response, build_id):
@@ -232,8 +232,16 @@ class CancelBuildResponse(BaseResponse, BuildMixin):
def repeat_action(self):
return self.client.cancel_build(self.build_id)
+ # def __str__(self):
+ # if not self.response:
+ # return super(CancelBuildResponse, self).__str__()
+ # elif self.response["output"] == "ok":
+ # return self.response["status"]
+ # else:
+ # return self.response["error"]
+
-class BuildRequestResponse(BaseResponse, ProjectMixin):
+class BuildRequestResponse(ProjectMixin, BaseResponse):
_simple_fields = ["message", "output", "error", "ids"]
def __init__(self, client, response,
@@ -268,7 +276,7 @@ class ProjectChrootMixin(ProjectMixin):
self.project_name, self.chroot, pkgs=pkgs)
-class ProjectChrootDetailsResponse(BaseResponse, ProjectChrootMixin):
+class ProjectChrootDetailsResponse(ProjectChrootMixin, BaseResponse):
def __init__(self, client, response, name, chroot):
super(ProjectChrootDetailsResponse, self).__init__(client, response)
self.project_name = name
@@ -278,7 +286,7 @@ class ProjectChrootDetailsResponse(BaseResponse, ProjectChrootMixin):
return self.get_project_details()
-class ModifyProjectChrootResponse(BaseResponse, ProjectChrootMixin):
+class ModifyProjectChrootResponse(ProjectChrootMixin, BaseResponse):
def __init__(self, client, response, name, chroot, pkgs):
super(ModifyProjectChrootResponse, self).__init__(client, response)
self.project_name = name
9 years, 9 months
[copr] master: [frontend] paginator fix (2fe9742)
by asamalik@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
>---------------------------------------------------------------
commit 2fe97427c4e1d64876b841a7b41caaff04bf7ae6
Author: Adam Samalik <asamalik(a)redhat.com>
Date: Fri Aug 15 16:08:50 2014 +0200
[frontend] paginator fix
>---------------------------------------------------------------
frontend/coprs_frontend/coprs/helpers.py | 4 ++--
.../coprs/views/coprs_ns/coprs_builds.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/frontend/coprs_frontend/coprs/helpers.py b/frontend/coprs_frontend/coprs/helpers.py
index b1e62b9..d6eed2c 100644
--- a/frontend/coprs_frontend/coprs/helpers.py
+++ b/frontend/coprs_frontend/coprs/helpers.py
@@ -80,8 +80,8 @@ class Paginator(object):
self._sliced_query = None
def page_slice(self, page):
- return (constants.ITEMS_PER_PAGE * (page - 1),
- constants.ITEMS_PER_PAGE * page)
+ return (self.per_page * (page - 1),
+ self.per_page * page)
@property
def sliced_query(self):
diff --git a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py
index 04813bd..e63b222 100644
--- a/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py
+++ b/frontend/coprs_frontend/coprs/views/coprs_ns/coprs_builds.py
@@ -62,7 +62,7 @@ def copr_builds(username, coprname, page=1):
flask.g.user, copr=copr)
paginator = helpers.Paginator(
- builds_query, copr.build_count, page, per_page_override=10)
+ builds_query, copr.build_count, page, per_page_override=20)
return flask.render_template("coprs/detail/builds.html",
copr=copr,
9 years, 9 months
[copr] master: [frontend] build states list (326cefa)
by asamalik@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
>---------------------------------------------------------------
commit 326cefa011e69333f088fd85f64c07ce06b81087
Author: Adam Samalik <asamalik(a)redhat.com>
Date: Fri Aug 15 16:00:03 2014 +0200
[frontend] build states list
>---------------------------------------------------------------
.../templates/coprs/detail/_build_states.html | 35 ++++++++++++++++++++
.../coprs/templates/coprs/detail/builds.html | 2 +
.../coprs/templates/coprs/detail/monitor.html | 2 +
3 files changed, 39 insertions(+), 0 deletions(-)
diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/_build_states.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/_build_states.html
new file mode 100644
index 0000000..adbc743
--- /dev/null
+++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/_build_states.html
@@ -0,0 +1,35 @@
+{% macro build_states() %}
+ <dl class="aligned">
+ <div>
+ <dt> Possible build states: </dt>
+ <dd>
+ <span class="build-pending"> Pending </span>
+ - Your build is waiting for a builder.
+ </dd>
+ <dd>
+ <span class="build-starting"> Starting </span>
+ - A builder is being spawned.
+ </dd>
+ <dd>
+ <span class="build-running"> Running </span>
+ - Build in progress.
+ </dd>
+ <dd>
+ <span class="build-succeeded"> Succeeded </span>
+ - Succesfully built.
+ </dd>
+ <dd>
+ <span class="build-skipped"> Skipped </span>
+ - Package has been skipped because it has been successfully built before.
+ </dd>
+ <dd>
+ <span class="build-failed"> Failed </span>
+ - Build failed. The reason could be timeout or the package itself. See logs for more details.
+ </dd>
+ <dd>
+ <span class="build-cancelled"> Cancelled </span>
+ - The build has been cancelled manually.
+ </dd>
+ </div>
+ </dl>
+{% endmacro %}
diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html
index 3aca8bf..0eabe19 100644
--- a/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html
+++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/builds.html
@@ -3,6 +3,7 @@
{% block builds_selected %}selected{% endblock %}
{% from "_helpers.html" import render_pagination %}
{% from "coprs/detail/_builds_table.html" import builds_table with context %}
+{% from "coprs/detail/_build_states.html" import build_states %}
{% block detail_body %}
{% if builds %}
@@ -13,4 +14,5 @@
{% else %}
<h2>No builds so far.</h2>
{% endif %}
+ {{ build_states() }}
{% endblock %}
diff --git a/frontend/coprs_frontend/coprs/templates/coprs/detail/monitor.html b/frontend/coprs_frontend/coprs/templates/coprs/detail/monitor.html
index 238e257..197b0ac 100644
--- a/frontend/coprs_frontend/coprs/templates/coprs/detail/monitor.html
+++ b/frontend/coprs_frontend/coprs/templates/coprs/detail/monitor.html
@@ -1,4 +1,5 @@
{% extends "coprs/detail.html" %}
+{% from "coprs/detail/_build_states.html" import build_states %}
{% block title %}Monitor {{ copr.owner.name }}/{{ copr.name }}{% endblock %}
{% block monitor_selected %}selected{% endblock %}
@@ -54,4 +55,5 @@
{% else %}
<h2>No builds so far.</h2>
{% endif %}
+ {{ build_states() }}
{% endblock %}
9 years, 9 months
[copr] master: [backend] async build playbooks (2b3e031)
by asamalik@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
>---------------------------------------------------------------
commit 2b3e031079a3cde259c1f46bd6f2e874bbbfa564
Author: Adam Samalik <asamalik(a)redhat.com>
Date: Fri Aug 15 13:25:18 2014 +0200
[backend] async build playbooks
>---------------------------------------------------------------
backend/backend/.mockremote.py.swo | Bin 0 -> 16384 bytes
backend/backend/mockremote.py | 19 +++++++++++++++++--
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/backend/backend/.mockremote.py.swo b/backend/backend/.mockremote.py.swo
new file mode 100644
index 0000000..a21c555
Binary files /dev/null and b/backend/backend/.mockremote.py.swo differ
diff --git a/backend/backend/mockremote.py b/backend/backend/mockremote.py
index b21076b..ae21aeb 100755
--- a/backend/backend/mockremote.py
+++ b/backend/backend/mockremote.py
@@ -426,11 +426,25 @@ class Builder(object):
buildcmd += dest
# run the mockchain command async
- # this runs it sync - FIXME
self.mockremote.callback.log("executing: {0}".format(buildcmd))
self.conn.module_name = "shell"
self.conn.module_args = buildcmd
- results = self.conn.run()
+
+ _, poller = self.conn.run_async(self.timeout)
+
+ waited = 0
+ while True:
+ results = poller.poll()
+
+ if results["contacted"] or results["dark"]:
+ break
+
+ if waited >= self.timeout:
+ self.mockremote.callback.log("Build timeout expired.")
+ return False, "", "Timeout expired", build_details
+
+ time.sleep(10)
+ waited += 10
is_err, err_results = check_for_ans_error(
results, self.hostname, success_codes=[0],
@@ -448,6 +462,7 @@ class Builder(object):
successfile = os.path.join(self._get_remote_pkg_dir(pkg), "success")
testcmd = "/usr/bin/test -f {0}".format(successfile)
+ self.conn.module_name = "shell"
self.conn.module_args = testcmd
results = self.conn.run()
is_err, err_results = check_for_ans_error(
9 years, 9 months
[copr] master: [java] Remove unneeded import (fe3e94f)
by Mikolaj Izdebski
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
>---------------------------------------------------------------
commit fe3e94fdc35c9725fdbc79bbd8884de00138c96d
Author: Mikolaj Izdebski <mizdebsk(a)redhat.com>
Date: Fri Aug 15 07:03:17 2014 +0200
[java] Remove unneeded import
>---------------------------------------------------------------
.../fedoraproject/copr/client/impl/RpcTest.java | 1 -
1 files changed, 0 insertions(+), 1 deletions(-)
diff --git a/java/copr-impl/src/test/java/org/fedoraproject/copr/client/impl/RpcTest.java b/java/copr-impl/src/test/java/org/fedoraproject/copr/client/impl/RpcTest.java
index 2c4463f..ebbfcd2 100644
--- a/java/copr-impl/src/test/java/org/fedoraproject/copr/client/impl/RpcTest.java
+++ b/java/copr-impl/src/test/java/org/fedoraproject/copr/client/impl/RpcTest.java
@@ -23,7 +23,6 @@ import org.easymock.MockType;
import org.fedoraproject.copr.client.CoprConfiguration;
import org.fedoraproject.copr.client.CoprService;
import org.fedoraproject.copr.client.CoprSession;
-import org.fedoraproject.copr.client.impl.DefaultCoprService;
import org.junit.After;
import org.junit.Before;
9 years, 9 months
[copr] python-client: [python-copr, copr-cli] Copr-cli now uses python-copr-cli package. [copr-cli] updated .spec to reflect usage of python-copr-client (b1215c9)
by vgologuz@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : python-client
>---------------------------------------------------------------
commit b1215c976b39ba2ac6c83276239d4aa1ad8b38ed
Author: Valentin Gologuzov <vgologuz(a)redhat.com>
Date: Thu Aug 14 23:07:26 2014 +0200
[python-copr,copr-cli] Copr-cli now uses python-copr-cli package.
[copr-cli] updated .spec to reflect usage of python-copr-client
>---------------------------------------------------------------
cli/copr-cli.spec | 8 +-
cli/copr_cli/copr_exceptions.py | 46 ------
cli/copr_cli/main.py | 117 ++++++++++++---
cli/copr_cli/subcommands.py | 307 ------------------------------------
cli/{coprcli-setup.py => setup.py} | 2 +-
python/copr_client/client.py | 61 ++++----
python/copr_client/responses.py | 53 ++++++
7 files changed, 188 insertions(+), 406 deletions(-)
diff --git a/cli/copr-cli.spec b/cli/copr-cli.spec
index d38fe6c..eda40dd 100644
--- a/cli/copr-cli.spec
+++ b/cli/copr-cli.spec
@@ -22,8 +22,8 @@ BuildRequires: asciidoc
BuildRequires: libxslt
BuildRequires: util-linux
BuildRequires: python-setuptools
-BuildRequires: python-requests
BuildRequires: python2-devel
+BuildRequires: python-copr-client
%if 0%{?rhel} < 7 && 0%{?rhel} > 0
BuildRequires: python-argparse
%endif
@@ -31,8 +31,8 @@ BuildRequires: python-argparse
BuildRequires: epydoc
BuildRequires: graphviz
-Requires: python-requests
Requires: python-setuptools
+Requires: python-copr-client
%if 0%{?rhel} < 7 && 0%{?rhel} > 0
Requires: python-argparse
%endif
@@ -60,6 +60,8 @@ only.
%build
+%{__python2} setup.py build
+
mv copr_cli/README.rst ./
# convert manages
@@ -74,7 +76,7 @@ popd
%install
install -d %{buildroot}%{_pkgdocdir}/
-%{__python2} coprcli-setup.py install --root %{buildroot}
+%{__python2} setup.py install --root %{buildroot}
install -d %{buildroot}%{_mandir}/man1
install -p -m 644 man/copr-cli.1 %{buildroot}/%{_mandir}/man1/
diff --git a/cli/copr_cli/copr_exceptions.py b/cli/copr_cli/copr_exceptions.py
deleted file mode 100644
index 1e10678..0000000
--- a/cli/copr_cli/copr_exceptions.py
+++ /dev/null
@@ -1,46 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-"""
-Exceptions for copr-cli
-"""
-
-
-class CoprCliException(Exception):
-
- """ Basic exception class for copr-cli. """
- pass
-
-
-class CoprCliNoConfException(CoprCliException):
-
- """ Exception thrown when no config file is found. """
- pass
-
-
-class CoprCliConfigException(CoprCliException):
-
- """ Exception thrown when the config file is incomplete or
- malformated.
- """
- pass
-
-
-class CoprCliRequestException(Exception):
- """ Exception thrown when the request is bad. For example,
- the user provided wrong project name or build ID.
- """
- pass
-
-
-class CoprCliBuildException(Exception):
- """ Exception thrown when one or more builds fail and Cli is waiting
- for the result.
- """
- pass
-
-
-class CoprCliUnknownResponseException(Exception):
- """ Exception thrown when the response is unknown to cli.
- It usualy means that something is broken.
- """
- pass
diff --git a/cli/copr_cli/main.py b/cli/copr_cli/main.py
index 597b57a..a390ceb 100644
--- a/cli/copr_cli/main.py
+++ b/cli/copr_cli/main.py
@@ -1,14 +1,69 @@
#-*- coding: UTF-8 -*-
+__version__ = "0.3.0"
+__description__ = "CLI tool to run copr"
+
import argparse
import sys
import ConfigParser
+import datetime
+import time
+from collections import defaultdict
-import subcommands
-import copr_exceptions
+from copr_client import CoprClient
+import copr_client.exceptions as copr_exceptions
+
+# client = CoprClient.create_from_file_config()
+client = CoprClient(dict(
+ login="Y29wcg==##fragphakffjdnlehuegu",
+ username="vgologuz",
+ token="srkqukvupebdipjfqyncohhvftiksi",
+ copr_url="http://copr-fe-dev.cloud.fedoraproject.org"
+))
-__version__ = "0.2.0"
-__description__ = "CLI tool to run copr"
+
+def _watch_builds(build_ids):
+ print("Watching build(s): (this may be safely interrupted)")
+ prevstatus = defaultdict()
+ failed_ids = []
+ watched_ids = build_ids[:]
+
+ try:
+ while True:
+ for build_id in watched_ids:
+ build_status = client.get_build_status(build_id)
+ if build_status.output != "ok":
+ errmsg = " Build {1}: Unable to get build status: {0}".format(
+ build_status.error, build_id)
+ raise copr_exceptions.CoprRequestException(errmsg)
+
+ now = datetime.datetime.now()
+ if prevstatus[build_id] != build_status.status:
+ prevstatus[build_id] = build_status.status
+ print(" {0} Build {2}: {1}".format(
+ now.strftime("%H:%M:%S"),
+ build_status.status, build_id))
+
+ if build_status.status in ["failed"]:
+ failed_ids.append(build_id)
+ if build_status.status in ["succeeded", "skipped",
+ "failed", "canceled"]:
+ watched_ids.remove(build_id)
+ if build_status.status == "unknown":
+ raise copr_exceptions.CoprBuildException(
+ "Unknown status.")
+
+ if not watched_ids:
+ break
+ time.sleep(60)
+
+ if failed_ids:
+ raise copr_exceptions.CoprBuildException(
+ "Build(s) {0} failed.".format(
+ ", ".join(str(x) for x in failed_ids)))
+
+ except KeyboardInterrupt:
+ pass
def action_build(args):
@@ -18,8 +73,17 @@ def action_build(args):
:param args: argparse arguments provided by the user
"""
- subcommands.build(args.copr, args.pkgs,
- args.memory, args.timeout, not args.nowait, chroots=args.chroots)
+ result = client.create_new_build(
+ projectname=args.copr, chroots=args.chroots, pkgs=args.pkgs,
+ memory=args.memory, timeout=args.timeout)
+ if result.output != "ok":
+ print(result.error)
+ return
+ print(result.message)
+ print("Created builds: {0}".format(" ".join(map(str, result.ids))))
+
+ if not args.nowait:
+ _watch_builds(result.ids)
def action_create(args):
@@ -29,9 +93,12 @@ def action_create(args):
:param args: argparse arguments provided by the user
"""
- subcommands.create(args.name, args.chroots, args.description,
- args.instructions, args.repos,
- args.initial_pkgs)
+ result = client.create_project(
+ projectname=args.name, description=args.description,
+ instructions=args.instructions, chroots=args.chroots,
+ repos=args.repos, initial_pkgs=args.initial_pkgs)
+ print(result)
+
def action_delete(args):
""" Method called when the 'delete' action has been selected by the
@@ -39,7 +106,8 @@ def action_delete(args):
:param args: argparse arguments provided by the user
"""
- subcommands.delete(args.copr)
+ result = client.delete_project(projectname=args.copr)
+ print(result)
def action_list(args):
@@ -49,20 +117,31 @@ def action_list(args):
:param args: argparse arguments provided by the user
"""
- subcommands.listcoprs(args.username)
+ username = args.username or client.username
+ result = client.get_projects_list(username)
+ if result.response["output"] != "ok":
+ print(result.error)
+ print("Un-expected data returned, please report this issue")
+ elif not result.projects:
+ print("No copr retrieved for user: '{0}'".format(username))
+ return
+
+ for prj in result.projects:
+ print(prj)
def action_status(args):
- subcommands.status(args.build_id)
+ result = client.get_build_status(args.build_id)
+ print(result)
+
def action_cancel(args):
""" Method called when the 'cancel' action has been selected by the
user.
-
:param args: argparse arguments provided by the user
-
"""
- subcommands.cancel(args.build_id)
+ result = client.cancel_build(args.build_id)
+ print(result)
def setup_parser():
"""
@@ -159,24 +238,24 @@ def main(argv=sys.argv[1:]):
except KeyboardInterrupt:
sys.stderr.write("\nInterrupted by user.")
sys.exit(1)
- except copr_exceptions.CoprCliRequestException, e:
+ except copr_exceptions.CoprRequestException, e:
sys.stderr.write("\nSomething went wrong:")
sys.stderr.write("\nError: {0}\n".format(e))
sys.exit(1)
except argparse.ArgumentTypeError, e:
sys.stderr.write("\nError: {0}".format(e))
sys.exit(2)
- except copr_exceptions.CoprCliException, e:
+ except copr_exceptions.CoprException, e:
sys.stderr.write("\nError: {0}\n".format(e))
sys.exit(3)
except ConfigParser.ParsingError, e:
sys.stderr.write("\nError: {0}\n".format(e))
sys.stderr.write("Lines in INI file should not be indented.\n")
sys.exit(3)
- except copr_exceptions.CoprCliBuildException, e:
+ except copr_exceptions.CoprBuildException, e:
sys.stderr.write("\nBuild error: {0}\n".format(e))
sys.exit(4)
- except copr_exceptions.CoprCliUnknownResponseException, e:
+ except copr_exceptions.CoprUnknownResponseException, e:
sys.stderr.write("\nError: {0}\n".format(e))
sys.exit(5)
# except Exception as e:
diff --git a/cli/copr_cli/subcommands.py b/cli/copr_cli/subcommands.py
deleted file mode 100644
index ab72f59..0000000
--- a/cli/copr_cli/subcommands.py
+++ /dev/null
@@ -1,307 +0,0 @@
-#-*- coding: UTF-8 -*-
-
-"""
-Function actually doing the work of calling the API and handling the
-output.
-"""
-
-import ConfigParser
-import datetime
-import json
-import os
-import re
-import requests
-import sys
-import time
-
-import copr_exceptions
-
-
-def _get_data(req, user, copr=None):
- """ Wrapper around response from server
-
- checks data and raises a CoprCliRequestException with nice error message
- or CoprCliUnknownResponseException in case of some some error.
- Otherwise return json object.
- """
- if "<title>Sign in Coprs</title>" in req.text:
- raise copr_exceptions.CoprCliRequestException("Invalid API token\n")
- return
-
- if req.status_code == 404:
- if copr is None:
- raise copr_exceptions.CoprCliRequestException(
- "User {0} is unknown.\n".format(user["username"]))
- else:
- raise copr_exceptions.CoprCliRequestException(
- "Project {0}/{1} not found.\n".format(
- user["username"], copr))
- try:
- output = json.loads(req.text)
- except ValueError:
- raise copr_exceptions.CoprCliUnknownResponseException(
- "Unknown response from the server.")
- if req.status_code != 200:
- raise copr_exceptions.CoprCliRequestException(output["error"])
-
- if output is None:
- raise copr_exceptions.CoprCliUnknownResponseException(
- "No response from the server.")
- return output
-
-
-def get_user():
- """ Retrieve the user information from the config file. """
- config = ConfigParser.ConfigParser()
- if not config.read(
- os.path.join(os.path.expanduser("~"), ".config", "copr")):
- raise copr_exceptions.CoprCliNoConfException(
- "No configuration file '~/.config/copr' found. "
- "See man copr-cli for more information")
- try:
- username = config.get("copr-cli", "username", None)
- login = config.get("copr-cli", "login", None)
- token = config.get("copr-cli", "token", None)
- except ConfigParser.Error as err:
- raise copr_exceptions.CoprCliConfigException(
- "Bad configuration file: {0}".format(err))
- return {"username": username, "token": token, "login": login}
-
-
-def get_api_url():
- """ Retrieve the user information from the config file. """
- config = ConfigParser.ConfigParser()
- config.read(
- os.path.join(os.path.expanduser("~"), ".config", "copr")
- )
-
- # Default copr_url:
- copr_url = "http://copr.fedoraproject.org/"
- if (config.has_section("copr-cli") and
- config.has_option("copr-cli", "copr_url")):
-
- copr_url = config.get("copr-cli", "copr_url")
- return "{0}/api".format(copr_url)
-
-
-def listcoprs(username=None):
- """ List all the copr of a user. """
- user = {}
- if not username:
- user = get_user()
- del(user["token"])
-
- if username:
- user["username"] = username
-
- copr_api_url = get_api_url()
- url = "{0}/coprs/{1}/".format(copr_api_url, user["username"])
-
- req = requests.get(url)
- output = _get_data(req, user)
- if output is None:
- return
- elif "repos" in output:
- PAD = " " * 2
- if output["repos"]:
- for repo in output["repos"]:
- print("Name: {0}".format(repo["name"]))
-
- if "description" in repo:
- desc = repo["description"]
- print(PAD + "Description: {0}".format(desc))
-
- if "yum_repos" in repo:
- yum_repos = repo["yum_repos"]
- print(PAD + "Yum repo(s):")
- for k in sorted(yum_repos.keys()):
- print(PAD * 2 + "{0}: {1}".format(k, yum_repos[k]))
-
- if "additional_repos" in repo:
- add_repos = repo["additional_repos"]
- print(PAD + "Additional repos: {0}".format(add_repos))
-
- if "instructions" in repo:
- instructions = repo["instructions"]
- print(PAD + "Instructions: {0}".format(instructions))
- else:
- print("No copr retrieved for user: '{0}'".format(
- user["username"]))
- else:
- print("Un-expected data returned, please report this issue")
-
-
-def create(name, chroots=[], description=None, instructions=None,
- repos=None, initial_pkgs=None):
- """ Create a new copr. """
- if chroots is None:
- raise copr_exceptions.CoprCliRequestException(
- "At least one chroot must be selected")
-
- user = get_user()
- copr_api_url = get_api_url()
- URL = "{0}/coprs/{1}/new/".format(copr_api_url, user["username"])
-
- if type(repos) == list():
- repos = " ".join(repos)
-
- if type(initial_pkgs) == list():
- initial_pkgs = " ".join(initial_pkgs)
-
- data = {"name": name,
- "repos": repos,
- "initial_pkgs": initial_pkgs,
- "description": description,
- "instructions": instructions
- }
- for chroot in chroots:
- data[chroot] = "y"
-
- req = requests.post(URL,
- auth=(user["login"], user["token"]),
- data=data)
- output = _get_data(req, user)
- if output is not None:
- print(output["message"])
-
-
-def delete(copr):
- """ Delete the entire project.
- """
- user = get_user()
- username = user["username"]
-
- copr_api_url = get_api_url()
- URL = "{0}/coprs/{1}/{2}/delete/".format(
- copr_api_url,
- username,
- copr)
-
- data = {"verify": "yes"}
-
- req = requests.post(URL,
- auth=(user["login"], user["token"]),
- data=data)
- output = _get_data(req, user, copr)
-
-
-def _fetch_status(build_id):
- user = get_user()
- copr_api_url = get_api_url()
- URL = "{0}/coprs/build_status/{1}/".format(
- copr_api_url,
- build_id)
-
- req = requests.get(URL, auth=(user["login"], user["token"]))
- output = _get_data(req, user)
- if output is None:
- return (False, "Error occurred.")
- elif "status" in output:
- return (True, output["status"])
- else:
- return (False, output["error"])
-
-
-def status(build_id):
- """ Return status of build """
- (ret, value) = _fetch_status(build_id)
- print(value)
-
-def cancel(build_id):
- """ Cancel specified build_id """
- user = get_user()
- copr_api_url = get_api_url()
- #/coprs/rhscl/nodejs010/cancel_build/4060/
- URL = "{0}/coprs/cancel_build/{1}/".format(
- copr_api_url,
- build_id)
- req = requests.post(URL, auth=(user["login"], user["token"]))
- output = _get_data(req, user)
- if output is None:
- return (False, "Error occurred.")
- elif "status" in output:
- return (True, output["status"])
- else:
- return (False, output["error"])
-
-def build(copr, pkgs, memory, timeout, wait=True, result=None, chroots=None):
- """ Build a new package into a given copr.
-
- Result is dictionary where is returned "errmsg" in case of error.
- And "id" and "status" otherwise.
- """
- user = get_user()
- username = user["username"]
-
- # if you specify copr as username/foo, retrieve and cut username
- m = re.match(r"(.+)/(.+)", copr)
- if m:
- username = m.group(1)
- copr = m.group(2)
-
- copr_api_url = get_api_url()
- URL = "{0}/coprs/{1}/{2}/new_build/".format(
- copr_api_url,
- username,
- copr)
-
- data = {"pkgs": " ".join(pkgs),
- "memory_reqs": memory,
- "timeout": timeout
- }
-
- if chroots is not None:
- for chroot in chroots:
- data[chroot] = "y"
-
- req = requests.post(URL,
- auth=(user["login"], user["token"]),
- data=data)
- output = _get_data(req, user, copr)
- print("{1}:\n {0} build(s) were added successfully\n".format(len(output["ids"]), copr))
-
- if wait:
- print("Watching build(s): (this may be safely interrupted)")
- prevstatus = {}
- failed_ids = []
- for id in output["ids"]:
- prevstatus[id] = None
- if result is not None:
- result[id] = {}
- try:
- while True:
- for id in output["ids"]:
- (ret, status) = _fetch_status(id)
- if not ret:
- errmsg = " Build {1}: Unable to get build status: {0}".format(status,id)
- if result is not None:
- result[id]['errmsg'] = errmsg
- raise copr_exceptions.CoprCliRequestException(errmsg)
-
- now = datetime.datetime.now()
- if prevstatus[id] != status:
- print(" {0} Build {2}: {1}".format(now.strftime("%H:%M:%S"), status, id))
- prevstatus[id] = status
-
- if status in ["succeeded", "skipped", "failed", "canceled"]:
- if result is not None:
- result[id]['status'] = status
- if status in ["failed"]:
- failed_ids.append(id)
- output["ids"].remove(id)
-
- if status == "unknown":
- raise copr_exceptions.CoprCliBuildException(
- "Unknown status.")
-
- if not output["ids"]:
- break
- time.sleep(60)
-
- if failed_ids:
- raise copr_exceptions.CoprCliBuildException(
- "Build(s) {0} failed.".format(
- ", ".join(str(x) for x in failed_ids)))
-
- except KeyboardInterrupt:
- pass
diff --git a/cli/coprcli-setup.py b/cli/setup.py
similarity index 98%
rename from cli/coprcli-setup.py
rename to cli/setup.py
index fa67387..68369f7 100644
--- a/cli/coprcli-setup.py
+++ b/cli/setup.py
@@ -14,7 +14,7 @@ This part is a command line interface to use copr."""
from copr_cli.main import __description__, __version__
requires = [
- 'requests',
+ 'copr-client'
]
diff --git a/python/copr_client/client.py b/python/copr_client/client.py
index 33082c3..56d34a2 100644
--- a/python/copr_client/client.py
+++ b/python/copr_client/client.py
@@ -1,6 +1,7 @@
#-*- coding: UTF-8 -*-
import json
+from pprint import pprint
import os
import sys
import ConfigParser
@@ -69,7 +70,7 @@ class CoprClient(object):
"Bad configuration file: {0}".format(err))
return CoprClient(config=config)
- def _fetch(self, url, data=None, copr=None, method=None,
+ def _fetch(self, url, data=None, projectname=None, method=None,
skip_auth=False, on_error_response=None):
""" Fetch data from server,
checks response and raises a CoprCliRequestException with nice error message
@@ -79,7 +80,7 @@ class CoprClient(object):
:param url: formed url to fetch
:param data: serialised data to send
:param skip_auth: don't send auth credentials
- :param copr: name of the copr project
+ :param projectname: name of the copr project
:param on_error_response: function to handle responses with bad status code
"""
if method is None:
@@ -113,24 +114,24 @@ class CoprClient(object):
#TODO: better status code handling
if response.status_code == 404:
- if copr is None:
+ if projectname is None:
raise exceptions.CoprRequestException(
"User {0} is unknown.\n".format(self.username))
else:
raise exceptions.CoprRequestException(
"Project {0}/{1} not found.\n".format(
- self.username, copr))
+ self.username, projectname))
if 400 <= response.status_code < 500:
- raise Exception("Bad request, raw response body: {0}".format(response.text))
+ log.error("Bad request, raw response body: {0}".format(response.text))
elif response.status_code >= 500:
#import ipdb; ipdb.set_trace()
- raise Exception("Server error, raw response body: {0}".format(response.text))
+ log.error("Server error, raw response body: {0}".format(response.text))
try:
output = json.loads(response.text)
except ValueError:
- import ipdb; ipdb.set_trace()
+ #import ipdb; ipdb.set_trace()
raise exceptions.CoprUnknownResponseException(
"Unknown response from the server.")
if response.status_code != 200:
@@ -162,30 +163,30 @@ class CoprClient(object):
response = self._fetch(url, method="post")
return CancelBuildResponse(self, response, build_id)
- def delete_project(self, name):
+ def delete_project(self, projectname):
"""
Delete the entire project
"""
url = "{0}/coprs/{1}/{2}/delete/".format(
- self.api_url, self.username, name
+ self.api_url, self.username, projectname
)
response = self._fetch(
url, data={"verify": "yes"}, method="post")
- return DeleteProjectResponse(self, response, name)
+ return DeleteProjectResponse(self, response, projectname)
- def get_project_details(self, name):
+ def get_project_details(self, projectname):
"""
Get project details
"""
url = "{0}/coprs/{1}/{2}/detail/".format(
- self.api_url, self.username, name
+ self.api_url, self.username, projectname
)
response = self._fetch(url, skip_auth=True)
- return ProjectDetailsResponse(self, response, name)
+ return ProjectDetailsResponse(self, response, projectname)
def create_project(
- self, name,
+ self, projectname,
description=None, instructions=None,
chroots=None, repos=None, initial_pkgs=None
):
@@ -202,7 +203,7 @@ class CoprClient(object):
if type(initial_pkgs) == list():
initial_pkgs = " ".join(initial_pkgs)
- data = {"name": name,
+ data = {"name": projectname,
"repos": repos,
"initial_pkgs": initial_pkgs,
"description": description,
@@ -215,15 +216,16 @@ class CoprClient(object):
response = self._fetch(url, data=data, method="post")
return CreateProjectResponse(
self, response,
- name=name, description=description, instructions=instructions,
+ name=projectname, description=description, instructions=instructions,
repos=repos, chroots=chroots, initial_pkgs=initial_pkgs
)
- def get_projects_list(self):
+ def get_projects_list(self, username=None):
"""
Get list of projects created by the user
"""
- url = "{0}/coprs/{1}/".format(self.api_url, self.username)
+ url = "{0}/coprs/{1}/".format(
+ self.api_url, username or self.username)
response = self._fetch(url)
return GetProjectsListResponse(self, response)
@@ -234,12 +236,12 @@ class CoprClient(object):
response = self._fetch(url, skip_auth=True)
return BaseResponse(self, response)
- def modify_project_chroot_details(self, name, chroot, pkgs=None):
+ def modify_project_chroot_details(self, projectname, chroot, pkgs=None):
if pkgs is None:
pkgs = []
url = "{0}/coprs/{1}/{2}/modify/{3}/".format(
- self.api_url, self.username, name, chroot
+ self.api_url, self.username, projectname, chroot
)
data = {
"buildroot_pkgs": " ".join(pkgs)
@@ -247,19 +249,19 @@ class CoprClient(object):
response = self._fetch(url, data=data, method="post")
return BaseResponse(self, response)
- def modify_project(self, name, description=None,
+ def modify_project(self, projectname, description=None,
instructions=None, repos=None):
"""
Modifies main project settings
- :param name:
+ :param projectname:
:param description:
:param instructions:
:param repos:
:return:
"""
url = "{0}/coprs/{1}/{2}/modify/".format(
- self.api_url, self.username, name
+ self.api_url, self.username, projectname
)
data = {}
if description:
@@ -270,15 +272,14 @@ class CoprClient(object):
data["repos"] = repos
response = self._fetch(url, data=data, method="post")
- return ModifyProjectResponse(self, response, name, description,
+ return ModifyProjectResponse(self, response, projectname, description,
instructions, repos)
- def create_new_build(self, copr_project,
- pkgs, timeout, memory,
- wait=True, result=None, chroots=None):
+ def create_new_build(self, projectname, pkgs,
+ timeout=None, memory=None, chroots=None):
"""
- :param copr_project: name of copr project (without user namespace)
+ :param projectname: name of copr project (without user namespace)
:param pkgs: list of packages to include in build
:param timeout: ?? build timeout
@@ -291,7 +292,7 @@ class CoprClient(object):
"""
url = "{0}/coprs/{1}/{2}/new_build/".format(
- self.api_url, self.username, copr_project
+ self.api_url, self.username, projectname
)
data = {
"pkgs": " ".join(pkgs),
@@ -304,7 +305,7 @@ class CoprClient(object):
response = self._fetch(url, data, method="post")
return BuildRequestResponse(
self, response,
- copr_project, pkgs, memory, timeout, chroots)
+ projectname, pkgs, memory, timeout, chroots)
def search_projects(self, query):
url = "{0}/coprs/search/{1}/".format(
diff --git a/python/copr_client/responses.py b/python/copr_client/responses.py
index a4e6e2a..0dea83d 100644
--- a/python/copr_client/responses.py
+++ b/python/copr_client/responses.py
@@ -46,9 +46,60 @@ class ProjectMixin(object):
return self.client.delete_project(self.project_name)
+class ProjectWrapper(object):
+ def __init__(self, username, projectname,
+ description=None, instructions=None,
+ yum_repos=None, additional_repos=None):
+
+ self.username = username
+ self.projectname = projectname
+ self.description = description or "<No description provided>"
+ self.instructions = instructions
+ self.yum_repos = yum_repos or {}
+ self.additional_repos = additional_repos or {}
+
+ def __str__(self):
+ out = list()
+ out.append("Name: {0}".format(self.projectname))
+ out.append(" Description: {0}".format(self.description))
+
+ if self.yum_repos:
+ out.append(" Yum repo(s)")
+ for k in sorted(self.yum_repos.keys()):
+ out.append(" {0}: {1}".format(k, self.yum_repos[k]))
+ if self.additional_repos:
+ out.append(" Additional repo: {0}".format(self.additional_repos))
+ if self.instructions:
+ out.append(" Instructions: {0}".format(self.instructions))
+
+ out.append("")
+ return "\n".join(out)
+
+
class GetProjectsListResponse(BaseResponse):
_simple_fields = ["output", "repos"]
+ def __init__(self, client, response, username=None):
+ super(GetProjectsListResponse, self).__init__(client, response)
+ self.username = username or self.client.username
+
+ @property
+ def projects(self):
+ if not self.response or \
+ self.response.get("output", None) != "ok" or \
+ not self.response.get("repos"):
+ return None
+ else:
+ return [
+ ProjectWrapper(
+ username=self.username,
+ projectname=prj.get("name"),
+ description=prj.get("description"),
+ yum_repos=prj.get("yum_repos"),
+ additional_repos=prj.get("additional_repos"),
+ ) for prj in self.response["repos"]
+ ]
+
class CreateProjectResponse(BaseResponse, ProjectMixin):
_simple_fields = ["message", "output"]
@@ -183,6 +234,8 @@ class CancelBuildResponse(BaseResponse, BuildMixin):
class BuildRequestResponse(BaseResponse, ProjectMixin):
+ _simple_fields = ["message", "output", "error", "ids"]
+
def __init__(self, client, response,
copr_project, pkgs, memory, timeout, chroots):
super(BuildRequestResponse, self).__init__(client, response)
9 years, 9 months
[copr] python-client: [python-copr] Introduced python-copr client. Added setup.py, .spec, documentation. (71831a7)
by vgologuz@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : python-client
>---------------------------------------------------------------
commit 71831a75eb19b6ed4362e2e0416114f6bd51b150
Author: Valentin Gologuzov <vgologuz(a)redhat.com>
Date: Tue Aug 12 12:49:55 2014 +0200
[python-copr]
Introduced python-copr client.
Added setup.py, .spec, documentation.
>---------------------------------------------------------------
.gitignore | 1 +
{backend => python}/LICENSE | 0
{cli/copr_cli => python/copr_client}/README.rst | 30 +-
python/copr_client/__init__.py | 4 +
python/copr_client/client.py | 314 ++++++++++++++++++++
.../copr_client/exceptions.py | 14 +-
python/copr_client/responses.py | 247 +++++++++++++++
python/dist/copr-client.spec | 41 +++
{backend => python}/documentation/Makefile | 4 +-
.../how-to-generate-documentation.txt | 0
.../python-copr-client.spec | 26 +--
python/requirements.txt | 4 +
cli/coprcli-setup.py => python/setup.py | 18 +-
13 files changed, 646 insertions(+), 57 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1be2b2f..8200cb9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
data/
cli/dist/
build/
+python-doc/
*.egg-info
documentation/build
documentation/python-doc
diff --git a/backend/LICENSE b/python/LICENSE
similarity index 100%
copy from backend/LICENSE
copy to python/LICENSE
diff --git a/cli/copr_cli/README.rst b/python/copr_client/README.rst
similarity index 62%
copy from cli/copr_cli/README.rst
copy to python/copr_client/README.rst
index e46f5ff..fedfca0 100644
--- a/cli/copr_cli/README.rst
+++ b/python/copr_client/README.rst
@@ -1,4 +1,4 @@
-Copr CLI
+Copr python client
========
Copr is designed to be a lightweight buildsystem that allows contributors
@@ -6,7 +6,7 @@ to create packages, put them in repositories, and make it easy for users
to install the packages onto their system. Within the Fedora Project it
is used to allow packagers to create third party repositories.
-This part is a command line interface to use copr.
+This part is a python client to access copr service.
About this project:
-------------------
@@ -14,29 +14,29 @@ About this project:
- Git: http://git.fedorahosted.org/cgit/copr.git
- Test instance: http://copr-fe.cloud.fedoraproject.org/
-
Dependencies:
-------------
-.. _python-requests: http://docs.python-requests.org/en/latest/
-.. _python-argparse: https://pypi.python.org/pypi/argparse
-
-The CLI depends on:
+The client depends on:
-- python (should work on 2.5, not tested)
-- `python-argparse`_ (for python < 2.7)
-- `python-requests`_
+.. _python2.6 +
+.. _python-requests: http://docs.python-requests.org/en/latest/
Usage:
------
-.. _test instance: http://copr-fe.cloud.fedoraproject.org/
-
- Create an account on copr `test instance`_
- Go to the API page: http://copr-fe.cloud.fedoraproject.org/api
- Retrieve your API token
+
+::
+
+ from python_copr.main import CoprClient
+
+
+Alternatively you could use configuration file:
- Create the file ``~/.config/copr``
- In this file add the following content
-
+(which is also provided by /api page of the copr service)
::
[copr-cli]
@@ -45,7 +45,3 @@ Usage:
You should then be able to use copr-cli to list, create and build on copr.
-
-.. note:: You can use directly copr-cli to list someone's copr repo but to create
- a copr or build packages into an existing repo you need to authenticate
- via the API token.
diff --git a/python/copr_client/__init__.py b/python/copr_client/__init__.py
new file mode 100644
index 0000000..7b901d6
--- /dev/null
+++ b/python/copr_client/__init__.py
@@ -0,0 +1,4 @@
+#!/usr/bin/python -tt
+#-*- coding: UTF-8 -*-
+
+from client import CoprClient
diff --git a/python/copr_client/client.py b/python/copr_client/client.py
new file mode 100644
index 0000000..33082c3
--- /dev/null
+++ b/python/copr_client/client.py
@@ -0,0 +1,314 @@
+#-*- coding: UTF-8 -*-
+
+import json
+import os
+import sys
+import ConfigParser
+import logging
+
+logging.basicConfig(level=logging.WARN)
+log = logging.getLogger(__name__)
+
+
+import requests
+import exceptions
+
+from .responses import BuildStatusResponse, BuildRequestResponse, \
+ CreateProjectResponse, BaseResponse, \
+ DeleteProjectResponse, CancelBuildResponse, \
+ ProjectDetailsResponse, BuildDetailsResponse, GetProjectsListResponse, ModifyProjectResponse, SearchResponse
+
+__version__ = "0.0.1"
+__description__ = "Python client for copr service"
+
+
+class CoprClient(object):
+ def __init__(self, config=None):
+ """ Main interface to the copr service
+ :param config: Configuration dictionary.
+ Fields:
+ copr_url - copr service location
+ login - user login, used for identification
+ token - copr api token
+ username - used as copr projects root
+
+ """
+ self.token = config.get("token")
+ self.login = config.get("login")
+ self.username = config.get("username")
+ self.copr_url = config.get("copr_url", "http://copr.fedoraproject.org/")
+
+ def __str__(self):
+ return "<Copr client. username: {0}, api url: {1}>".format(
+ self.username, self.api_url
+ )
+
+ @property
+ def api_url(self):
+ return "{0}/api".format(self.copr_url)
+
+ @staticmethod
+ def create_from_file_config(filepath=None):
+ """
+ Retrieve copr client information from the config file.
+ :param filepath: specifies config location, default: "~/.config/copr"
+ """
+ raw_config = ConfigParser.ConfigParser()
+ if not filepath:
+ filepath = os.path.join(os.path.expanduser("~"), ".config", "copr")
+ config = {}
+ if not raw_config.read(filepath):
+ raise exceptions.CoprNoConfException(
+ "No configuration file '~/.config/copr' found. "
+ "See man copr-cli for more information")
+ try:
+ for field in ["username", "login", "token", "copr_url"]:
+ config[field] = raw_config.get("copr-cli", field, None)
+ except ConfigParser.Error as err:
+ raise exceptions.CoprConfigException(
+ "Bad configuration file: {0}".format(err))
+ return CoprClient(config=config)
+
+ def _fetch(self, url, data=None, copr=None, method=None,
+ skip_auth=False, on_error_response=None):
+ """ Fetch data from server,
+ checks response and raises a CoprCliRequestException with nice error message
+ or CoprCliUnknownResponseException in case of some some error.
+ Otherwise return json object.
+
+ :param url: formed url to fetch
+ :param data: serialised data to send
+ :param skip_auth: don't send auth credentials
+ :param copr: name of the copr project
+ :param on_error_response: function to handle responses with bad status code
+ """
+ if method is None:
+ method = "get"
+
+ log.debug("Fetching url: {0}, for login: {1}".format(url, self.login))
+
+ kwargs = {}
+ if not skip_auth:
+ kwargs["auth"] = (self.login, self.token)
+ if data is not None:
+ kwargs["data"] = data
+
+ if method not in ["get", "post", "head", "delete", "put"]:
+ raise Exception("Method {0} not allowed".format(method))
+
+ #print("Url: {0}".format(url))
+ #print(kwargs)
+ response = requests.request(
+ method=method.upper(),
+ url=url,
+ **kwargs
+ )
+ log.debug("raw response: {0}".format(response.text))
+
+ if "<title>Sign in Coprs</title>" in response.text:
+ raise exceptions.CoprRequestException("Invalid API token\n")
+
+ if response.status_code > 299 and on_error_response is not None:
+ return on_error_response(response)
+
+ #TODO: better status code handling
+ if response.status_code == 404:
+ if copr is None:
+ raise exceptions.CoprRequestException(
+ "User {0} is unknown.\n".format(self.username))
+ else:
+ raise exceptions.CoprRequestException(
+ "Project {0}/{1} not found.\n".format(
+ self.username, copr))
+
+ if 400 <= response.status_code < 500:
+ raise Exception("Bad request, raw response body: {0}".format(response.text))
+ elif response.status_code >= 500:
+ #import ipdb; ipdb.set_trace()
+ raise Exception("Server error, raw response body: {0}".format(response.text))
+
+ try:
+ output = json.loads(response.text)
+ except ValueError:
+ import ipdb; ipdb.set_trace()
+ raise exceptions.CoprUnknownResponseException(
+ "Unknown response from the server.")
+ if response.status_code != 200:
+ raise exceptions.CoprRequestException(output["error"])
+
+ if output is None:
+ raise exceptions.CoprUnknownResponseException(
+ "No response from the server.")
+ return output
+
+ def get_build_status(self, build_id):
+ url = "{0}/coprs/build_status/{1}/".format(
+ self.api_url, build_id)
+
+ response = self._fetch(url)
+ return BuildStatusResponse(
+ client=self, build_id=build_id, response=response)
+
+ def get_build_details(self, build_id):
+ url = "{0}/coprs/build/{1}/".format(
+ self.api_url, build_id)
+
+ response = self._fetch(url, skip_auth=True)
+ return BuildDetailsResponse(self, response, build_id)
+
+ def cancel_build(self, build_id):
+ url = "{0}/coprs/cancel_build/{1}/".format(
+ self.api_url, build_id)
+ response = self._fetch(url, method="post")
+ return CancelBuildResponse(self, response, build_id)
+
+ def delete_project(self, name):
+ """
+ Delete the entire project
+ """
+ url = "{0}/coprs/{1}/{2}/delete/".format(
+ self.api_url, self.username, name
+ )
+ response = self._fetch(
+ url, data={"verify": "yes"}, method="post")
+ return DeleteProjectResponse(self, response, name)
+
+ def get_project_details(self, name):
+ """
+ Get project details
+ """
+ url = "{0}/coprs/{1}/{2}/detail/".format(
+ self.api_url, self.username, name
+ )
+
+ response = self._fetch(url, skip_auth=True)
+ return ProjectDetailsResponse(self, response, name)
+
+ def create_project(
+ self, name,
+ description=None, instructions=None,
+ chroots=None, repos=None, initial_pkgs=None
+ ):
+ """
+ Create a new copr project
+ """
+
+ url = "{0}/coprs/{1}/new/".format(
+ self.api_url, self.username)
+
+ if type(repos) == list():
+ repos = " ".join(repos)
+
+ if type(initial_pkgs) == list():
+ initial_pkgs = " ".join(initial_pkgs)
+
+ data = {"name": name,
+ "repos": repos,
+ "initial_pkgs": initial_pkgs,
+ "description": description,
+ "instructions": instructions
+ }
+ for chroot in chroots:
+ data[chroot] = "y"
+
+ #def on bad_response()
+ response = self._fetch(url, data=data, method="post")
+ return CreateProjectResponse(
+ self, response,
+ name=name, description=description, instructions=instructions,
+ repos=repos, chroots=chroots, initial_pkgs=initial_pkgs
+ )
+
+ def get_projects_list(self):
+ """
+ Get list of projects created by the user
+ """
+ url = "{0}/coprs/{1}/".format(self.api_url, self.username)
+ response = self._fetch(url)
+ return GetProjectsListResponse(self, response)
+
+ def get_project_chroot_details(self, name, chroot):
+ url = "{0}/coprs/{1}/{2}/detail/{3}/".format(
+ self.api_url, self.username, name, chroot
+ )
+ response = self._fetch(url, skip_auth=True)
+ return BaseResponse(self, response)
+
+ def modify_project_chroot_details(self, name, chroot, pkgs=None):
+ if pkgs is None:
+ pkgs = []
+
+ url = "{0}/coprs/{1}/{2}/modify/{3}/".format(
+ self.api_url, self.username, name, chroot
+ )
+ data = {
+ "buildroot_pkgs": " ".join(pkgs)
+ }
+ response = self._fetch(url, data=data, method="post")
+ return BaseResponse(self, response)
+
+ def modify_project(self, name, description=None,
+ instructions=None, repos=None):
+
+ """
+ Modifies main project settings
+ :param name:
+ :param description:
+ :param instructions:
+ :param repos:
+ :return:
+ """
+ url = "{0}/coprs/{1}/{2}/modify/".format(
+ self.api_url, self.username, name
+ )
+ data = {}
+ if description:
+ data["description"] = description
+ if instructions:
+ data["instructions"] = instructions
+ if repos:
+ data["repos"] = repos
+
+ response = self._fetch(url, data=data, method="post")
+ return ModifyProjectResponse(self, response, name, description,
+ instructions, repos)
+
+ def create_new_build(self, copr_project,
+ pkgs, timeout, memory,
+ wait=True, result=None, chroots=None):
+ """
+
+ :param copr_project: name of copr project (without user namespace)
+ :param pkgs: list of packages to include in build
+
+ :param timeout: ?? build timeout
+
+ :param memory: amount of required memory for build process
+ :param wait: if True function wait for packages to be build
+ :param result:
+ :param chroots:
+
+ """
+
+ url = "{0}/coprs/{1}/{2}/new_build/".format(
+ self.api_url, self.username, copr_project
+ )
+ data = {
+ "pkgs": " ".join(pkgs),
+ "memory_reqs": memory,
+ "timeout": timeout
+ }
+ for chroot in chroots or []:
+ data[chroot] = "y"
+
+ response = self._fetch(url, data, method="post")
+ return BuildRequestResponse(
+ self, response,
+ copr_project, pkgs, memory, timeout, chroots)
+
+ def search_projects(self, query):
+ url = "{0}/coprs/search/{1}/".format(
+ self.api_url, query
+ )
+ response = self._fetch(url, skip_auth=True)
+ return SearchResponse(self, response, query)
diff --git a/cli/copr_cli/copr_exceptions.py b/python/copr_client/exceptions.py
similarity index 69%
copy from cli/copr_cli/copr_exceptions.py
copy to python/copr_client/exceptions.py
index 1e10678..6528061 100644
--- a/cli/copr_cli/copr_exceptions.py
+++ b/python/copr_client/exceptions.py
@@ -5,41 +5,41 @@ Exceptions for copr-cli
"""
-class CoprCliException(Exception):
+class CoprException(Exception):
""" Basic exception class for copr-cli. """
pass
-class CoprCliNoConfException(CoprCliException):
+class CoprNoConfException(CoprException):
""" Exception thrown when no config file is found. """
pass
-class CoprCliConfigException(CoprCliException):
+class CoprConfigException(CoprException):
""" Exception thrown when the config file is incomplete or
- malformated.
+ malformed.
"""
pass
-class CoprCliRequestException(Exception):
+class CoprRequestException(Exception):
""" Exception thrown when the request is bad. For example,
the user provided wrong project name or build ID.
"""
pass
-class CoprCliBuildException(Exception):
+class CoprBuildException(Exception):
""" Exception thrown when one or more builds fail and Cli is waiting
for the result.
"""
pass
-class CoprCliUnknownResponseException(Exception):
+class CoprUnknownResponseException(Exception):
""" Exception thrown when the response is unknown to cli.
It usualy means that something is broken.
"""
diff --git a/python/copr_client/responses.py b/python/copr_client/responses.py
new file mode 100644
index 0000000..a4e6e2a
--- /dev/null
+++ b/python/copr_client/responses.py
@@ -0,0 +1,247 @@
+import weakref
+
+
+class BaseResponse(object):
+ """
+ Base class for API responses.
+ Some response provides further actions based on response content.
+ """
+ _simple_fields = []
+
+ def __init__(self, client, response):
+ """
+ :param client: copr_client.main.Client
+ :param response:
+ """
+ self.response = response
+ self.client = weakref.proxy(client)
+
+ def __str__(self):
+ return str(self.response)
+
+ def __unicode__(self):
+ return unicode(self.response)
+
+ def __getattr__(self, item):
+ if item not in self._simple_fields:
+ raise KeyError(item)
+ if self.response is None:
+ raise RuntimeError()
+
+ return self.response[item]
+
+ def repeat_action(self):
+ raise NotImplementedError()
+
+
+class ProjectMixin(object):
+ """
+ Provides methods related to copr project.
+ Object must have fields "client" and "project_name"
+ """
+ def get_project_details(self):
+ return self.client.get_project_details(self.project_name)
+
+ def delete_project(self):
+ return self.client.delete_project(self.project_name)
+
+
+class GetProjectsListResponse(BaseResponse):
+ _simple_fields = ["output", "repos"]
+
+
+class CreateProjectResponse(BaseResponse, ProjectMixin):
+ _simple_fields = ["message", "output"]
+
+ def __init__(self, client, response,
+ name, description, instructions,
+ chroots, repos, initial_pkgs):
+ super(CreateProjectResponse, self).__init__(client, response)
+ self.project_name = name
+ self.description = description
+ self.instructions = instructions
+ self.chroots = chroots
+ self.repos = repos
+ self.initial_pkgs = initial_pkgs
+
+ def __str__(self):
+ if hasattr(self, "message"):
+ return str(self.message)
+
+ def __unicode__(self):
+ if hasattr(self, "message"):
+ return unicode(self.message)
+
+
+class ModifyProjectResponse(BaseResponse, ProjectMixin):
+ def __init__(self, client, response,
+ name, description=None,
+ instructions=None, repos=None):
+ super(ModifyProjectResponse, self).__init__(client, response)
+ self.project_name = name
+ self.description = description
+ self.instructions = instructions
+ self.repos = repos
+
+
+class ProjectDetailsResponse(BaseResponse, ProjectMixin):
+ _simple_fields = ["detail", "output"]
+
+ def __init__(self, client, response, name):
+ super(ProjectDetailsResponse, self).__init__(client, response)
+ self.project_name = name
+
+ def __str__(self):
+ if hasattr(self, "detail"):
+ return str(self.detail)
+
+ def __unicode__(self):
+ if hasattr(self, "detail."):
+ return unicode(self.detail)
+
+ def repeat_action(self):
+ return self.get_project_details()
+
+
+class DeleteProjectResponse(BaseResponse, ProjectMixin):
+ _simple_fields = ["message", "output"]
+
+ def __init__(self, client, response, name):
+ super(DeleteProjectResponse, self).__init__(client, response)
+ self.project_name = name
+
+ def __str__(self):
+ if hasattr(self, "message"):
+ return str(self.message)
+
+ def __unicode__(self):
+ if hasattr(self, "message"):
+ return unicode(self.message)
+
+ def repeat_action(self):
+ return self.delete_project()
+
+
+class BuildMixin(object):
+ """
+ Provides methods related to individual builds
+ Object must have fields "client" and "build_id"
+ """
+ def get_build_status(self):
+ return self.client.get_build_status(self.build_id)
+
+ def get_build_details(self):
+ return self.client.get_build_details(self.build_id)
+
+ def cancel_build(self):
+ return self.client.cancel_build(self.build_id)
+
+ def __str__(self):
+ if not self.response:
+ return super(BuildStatusResponse, self).__str__()
+ elif self.response["output"] == "ok":
+ return self.response["status"]
+ else:
+ return self.response["error"]
+
+
+class BuildStatusResponse(BaseResponse, BuildMixin):
+ _simple_fields = ["status", "output", "error"]
+
+ def __init__(self, client, response, build_id):
+ super(BuildStatusResponse, self).__init__(client, response)
+ self.build_id = build_id
+
+ def repeat_action(self):
+ return self.get_build_status()
+
+
+class BuildDetailsResponse(BaseResponse, BuildMixin):
+ _simple_fields = [
+ 'status', 'error', 'submitted_by', 'results', 'src_pkg', 'started_on',
+ 'submitted_on', 'owner', 'chroots', 'project', 'built_pkgs',
+ 'ended_on', 'output', 'src_version'
+ ]
+
+ def __init__(self, client, response, build_id):
+ super(BuildDetailsResponse, self).__init__(client, response)
+ self.build_id = build_id
+
+ def repeat_action(self):
+ return self.get_build_details()
+
+
+class CancelBuildResponse(BaseResponse, BuildMixin):
+ _simple_fields = ["message", "output", "error"]
+
+ def __init__(self, client, response, build_id):
+ super(CancelBuildResponse, self).__init__(client, response)
+ self.build_id = build_id
+
+ def repeat_action(self):
+ return self.client.cancel_build(self.build_id)
+
+
+class BuildRequestResponse(BaseResponse, ProjectMixin):
+ def __init__(self, client, response,
+ copr_project, pkgs, memory, timeout, chroots):
+ super(BuildRequestResponse, self).__init__(client, response)
+ self.copr_project = copr_project
+ self.pkgs = pkgs
+ self.memory = memory
+ self.timeout = timeout
+ self.chroots = chroots
+
+ def repeat_action(self):
+ return self.client.send_new_build(
+ copr_project=self.copr_project,
+ pkgs=self.pkgs,
+ memory=self.memory,
+ timeout=self.timeout,
+ chroots=self.chroots
+ )
+
+
+class ProjectChrootMixin(ProjectMixin):
+ """
+ Provides methods related to project chroots
+ Object must have fields "client", "project_name", "chroot"
+ """
+ def get_project_chroot_details(self):
+ return self.client.get_project_chroot_details(self.project_name, self.chroot)
+
+ def modify_project_chroot_details(self, pkgs):
+ return self.client.modify_project_chroot_details(
+ self.project_name, self.chroot, pkgs=pkgs)
+
+
+class ProjectChrootDetailsResponse(BaseResponse, ProjectChrootMixin):
+ def __init__(self, client, response, name, chroot):
+ super(ProjectChrootDetailsResponse, self).__init__(client, response)
+ self.project_name = name
+ self.chroot = chroot
+
+ def repeat_action(self):
+ return self.get_project_details()
+
+
+class ModifyProjectChrootResponse(BaseResponse, ProjectChrootMixin):
+ def __init__(self, client, response, name, chroot, pkgs):
+ super(ModifyProjectChrootResponse, self).__init__(client, response)
+ self.project_name = name
+ self.chroot = chroot
+ self.pkgs = pkgs
+
+ def repeat_action(self):
+ return self.modify_project_chroot_details(self.pkgs)
+
+
+class SearchResponse(BaseResponse):
+ _simple_fields = ["output", "repos"]
+
+ def __init__(self, client, response, query):
+ super(SearchResponse, self).__init__(client, response)
+ self.query = query
+
+ def repeat_action(self):
+ return self.client.search_projects(self.query)
diff --git a/python/dist/copr-client.spec b/python/dist/copr-client.spec
new file mode 100644
index 0000000..a5a7fde
--- /dev/null
+++ b/python/dist/copr-client.spec
@@ -0,0 +1,41 @@
+%define name copr-client
+%define version 0.0.1
+%define unmangled_version 0.0.1
+%define unmangled_version 0.0.1
+%define release 1
+
+Summary: Python client for copr service
+Name: %{name}
+Version: %{version}
+Release: %{release}
+Source0: %{name}-%{unmangled_version}.tar.gz
+License: GPLv2+
+Group: Development/Libraries
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-buildroot
+Prefix: %{_prefix}
+BuildArch: noarch
+Vendor: Valentin Gologuzov <vgologuz(a)redhat.com>
+Url: http://fedorahosted.org/copr/
+
+%description
+Copr is designed to be a lightweight buildsystem that allows contributors
+to create packages, put them in repositories, and make it easy for users
+to install the packages onto their system. Within the Fedora Project it
+is used to allow packagers to create third party repositories.
+
+This part is a python client to the copr service.
+
+%prep
+%setup -n %{name}-%{unmangled_version} -n %{name}-%{unmangled_version}
+
+%build
+python setup.py build
+
+%install
+python setup.py install --single-version-externally-managed -O1 --root=$RPM_BUILD_ROOT --record=INSTALLED_FILES
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+%files -f INSTALLED_FILES
+%defattr(-,root,root)
diff --git a/backend/documentation/Makefile b/python/documentation/Makefile
similarity index 91%
copy from backend/documentation/Makefile
copy to python/documentation/Makefile
index 06216f0..11be589 100644
--- a/backend/documentation/Makefile
+++ b/python/documentation/Makefile
@@ -11,9 +11,9 @@ BUILD_DIR := build
# Python doc related
EPYDOC_BIN := `which epydoc`
-EPYDOC_SWITCHES := --inheritance listed --graph all --html
+EPYDOC_SWITCHES := --inheritance listed --graph all --html -v
PYTHON_DOC_DIR := python-doc
-PYTHON_DIRS := ../
+PYTHON_DIRS := ../copr_client
# TARGETS
# schema is not incldued here, since you need a live db
diff --git a/backend/documentation/how-to-generate-documentation.txt b/python/documentation/how-to-generate-documentation.txt
similarity index 100%
copy from backend/documentation/how-to-generate-documentation.txt
copy to python/documentation/how-to-generate-documentation.txt
diff --git a/cli/copr-cli.spec b/python/python-copr-client.spec
similarity index 97%
copy from cli/copr-cli.spec
copy to python/python-copr-client.spec
index d38fe6c..6420c11 100644
--- a/cli/copr-cli.spec
+++ b/python/python-copr-client.spec
@@ -3,39 +3,32 @@
%global __python2 %{__python}
%endif
-Name: copr-cli
-Version: 1.36
+Name: python-copr-client
+Version: 1.42
Release: 1%{?dist}
-Summary: Command line interface for COPR
+Summary: Python interface for COPR
Group: Applications/Productivity
License: GPLv2+
URL: https://fedorahosted.org/copr/
# Source is created by
# git clone https://git.fedorahosted.org/git/copr.git
-# cd copr/cli
+# cd copr/python
# tito build --tgz
Source0: %{name}-%{version}.tar.gz
BuildArch: noarch
-BuildRequires: asciidoc
BuildRequires: libxslt
BuildRequires: util-linux
BuildRequires: python-setuptools
BuildRequires: python-requests
BuildRequires: python2-devel
-%if 0%{?rhel} < 7 && 0%{?rhel} > 0
-BuildRequires: python-argparse
-%endif
#for doc package
BuildRequires: epydoc
BuildRequires: graphviz
Requires: python-requests
Requires: python-setuptools
-%if 0%{?rhel} < 7 && 0%{?rhel} > 0
-Requires: python-argparse
-%endif
%description
COPR is lightweight build system. It allows you to create new project in WebUI,
@@ -60,10 +53,9 @@ only.
%build
-mv copr_cli/README.rst ./
+%{__python2} setup.py build
-# convert manages
-a2x -d manpage -f manpage man/copr-cli.1.asciidoc
+mv copr_client/README.rst ./
%if 0%{?fedora}
# build documentation
@@ -74,9 +66,7 @@ popd
%install
install -d %{buildroot}%{_pkgdocdir}/
-%{__python2} coprcli-setup.py install --root %{buildroot}
-install -d %{buildroot}%{_mandir}/man1
-install -p -m 644 man/copr-cli.1 %{buildroot}/%{_mandir}/man1/
+%{__python2} setup.py install --root %{buildroot}
#doc
%if 0%{?fedora}
@@ -85,9 +75,7 @@ cp -a documentation/python-doc %{buildroot}%{_pkgdocdir}/
%files
%doc LICENSE README.rst
-%{_bindir}/copr-cli
%{python_sitelib}/*
-%{_mandir}/man1/copr-cli.1*
%if 0%{?fedora}
%files doc
diff --git a/python/requirements.txt b/python/requirements.txt
new file mode 100644
index 0000000..80e788a
--- /dev/null
+++ b/python/requirements.txt
@@ -0,0 +1,4 @@
+# Used for when working from a virtualenv.
+# Use this file by running "$ pip install -r requirements.txt"
+
+requests
diff --git a/cli/coprcli-setup.py b/python/setup.py
similarity index 74%
copy from cli/coprcli-setup.py
copy to python/setup.py
index fa67387..eccb299 100644
--- a/cli/coprcli-setup.py
+++ b/python/setup.py
@@ -9,20 +9,20 @@ to create packages, put them in repositories, and make it easy for users
to install the packages onto their system. Within the Fedora Project it
is used to allow packagers to create third party repositories.
-This part is a command line interface to use copr."""
+This part is a python client to the copr service."""
-from copr_cli.main import __description__, __version__
+from copr_client.client import __description__, __version__
requires = [
'requests',
]
-__name__ = 'copr-cli'
+__name__ = 'copr-client'
__version__ = __version__
__description__ = __description__
-__author__ = "Pierre-Yves Chibon"
-__author_email__ = "pingou(a)pingoured.fr"
+__author__ = "Valentin Gologuzov"
+__author_email__ = "vgologuz(a)redhat.com"
__url__ = "http://fedorahosted.org/copr/"
@@ -43,13 +43,7 @@ setup(
"Development Status :: 1 - Alpha",
],
install_requires=requires,
- packages=['copr_cli'],
- namespace_packages=['copr_cli'],
+ packages=['copr_client'],
include_package_data=True,
zip_safe=False,
- entry_points={
- 'console_scripts': [
- 'copr-cli = copr_cli.main:main'
- ]
- },
)
9 years, 9 months
[copr] master: [frontend] minor bugfix: fixed api method `cancel build`. (7c7c936)
by vgologuz@fedoraproject.org
Repository : http://git.fedorahosted.org/cgit/copr.git
On branch : master
>---------------------------------------------------------------
commit 7c7c936ede4e10c57745846d2edca054c64278f9
Author: Valentin Gologuzov <vgologuz(a)redhat.com>
Date: Thu Aug 14 19:36:53 2014 +0200
[frontend] minor bugfix: fixed api method `cancel build`.
>---------------------------------------------------------------
.../coprs/views/api_ns/api_general.py | 2 +-
1 files changed, 1 insertions(+), 1 deletions(-)
diff --git a/frontend/coprs_frontend/coprs/views/api_ns/api_general.py b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py
index a665332..f002c6c 100644
--- a/frontend/coprs_frontend/coprs/views/api_ns/api_general.py
+++ b/frontend/coprs_frontend/coprs/views/api_ns/api_general.py
@@ -366,7 +366,7 @@ def cancel_build(build_id):
if build:
try:
builds_logic.BuildsLogic.cancel_build(flask.g.user, build)
- except InsufficientRightsException as e:
+ except exceptions.InsufficientRightsException as e:
output = {'output': 'notok', 'error': str(e)}
httpcode = 500
else:
9 years, 9 months