diff options
author | James Johnson <d0c.s4vage@gmail.com> | 2017-09-11 23:20:08 -0500 |
---|---|---|
committer | Gauvain Pocentek <gauvain@pocentek.net> | 2017-09-12 06:20:08 +0200 |
commit | 29879d61d117ff7909302ed845a6a1eb13814365 (patch) | |
tree | fafcb7f7003cc4b5a4146ea6090a76d9bf82082e /gitlab | |
parent | fd40fce913fbb3cd0e3aa2fd042e20bf1d51e9d6 (diff) | |
download | gitlab-29879d61d117ff7909302ed845a6a1eb13814365.tar.gz |
adds project upload feature (#239)
Diffstat (limited to 'gitlab')
-rw-r--r-- | gitlab/__init__.py | 20 | ||||
-rw-r--r-- | gitlab/base.py | 2 | ||||
-rw-r--r-- | gitlab/exceptions.py | 8 | ||||
-rw-r--r-- | gitlab/v3/cli.py | 21 | ||||
-rw-r--r-- | gitlab/v3/objects.py | 63 | ||||
-rw-r--r-- | gitlab/v4/cli.py | 2 | ||||
-rw-r--r-- | gitlab/v4/objects.py | 53 |
7 files changed, 161 insertions, 8 deletions
diff --git a/gitlab/__init__.py b/gitlab/__init__.py index 0768abb..4a56175 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -396,11 +396,13 @@ class Gitlab(object): return results - def _raw_post(self, path_, data=None, content_type=None, **kwargs): + def _raw_post(self, path_, data=None, content_type=None, + files=None, **kwargs): url = '%s%s' % (self._url, path_) opts = self._get_session_opts(content_type) try: - return self.session.post(url, params=kwargs, data=data, **opts) + return self.session.post(url, params=kwargs, data=data, + files=files, **opts) except Exception as e: raise GitlabConnectionError( "Can't connect to GitLab server (%s)" % e) @@ -628,7 +630,7 @@ class Gitlab(object): return '%s%s' % (self._url, path) def http_request(self, verb, path, query_data={}, post_data={}, - streamed=False, **kwargs): + streamed=False, files=None, **kwargs): """Make an HTTP request to the Gitlab server. Args: @@ -658,6 +660,11 @@ class Gitlab(object): params = query_data.copy() params.update(kwargs) opts = self._get_session_opts(content_type='application/json') + + # don't set the content-type header when uploading files + if files is not None: + del opts["headers"]["Content-type"] + verify = opts.pop('verify') timeout = opts.pop('timeout') @@ -668,7 +675,7 @@ class Gitlab(object): # always agree with this decision (this is the case with a default # gitlab installation) req = requests.Request(verb, url, json=post_data, params=params, - **opts) + files=files, **opts) prepped = self.session.prepare_request(req) prepped.url = sanitized_url(prepped.url) result = self.session.send(prepped, stream=streamed, verify=verify, @@ -756,7 +763,8 @@ class Gitlab(object): # No pagination, generator requested return GitlabList(self, url, query_data, **kwargs) - def http_post(self, path, query_data={}, post_data={}, **kwargs): + def http_post(self, path, query_data={}, post_data={}, files=None, + **kwargs): """Make a POST request to the Gitlab server. Args: @@ -776,7 +784,7 @@ class Gitlab(object): GitlabParsingError: If the json data could not be parsed """ result = self.http_request('post', path, query_data=query_data, - post_data=post_data, **kwargs) + post_data=post_data, files=files, **kwargs) try: if result.headers.get('Content-Type', None) == 'application/json': return result.json() diff --git a/gitlab/base.py b/gitlab/base.py index a9521eb..01f6903 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -536,7 +536,7 @@ class GitlabObject(object): class RESTObject(object): """Represents an object built from server data. - It holds the attributes know from te server, and the updated attributes in + It holds the attributes know from the server, and the updated attributes in another. This allows smart updates, if the object allows it. You can redefine ``_id_attr`` in child classes to specify which attribute diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index 6aad810..a100395 100644 --- a/gitlab/exceptions.py +++ b/gitlab/exceptions.py @@ -173,6 +173,14 @@ class GitlabTimeTrackingError(GitlabOperationError): pass +class GitlabUploadError(GitlabOperationError): + pass + + +class GitlabAttachFileError(GitlabOperationError): + pass + + class GitlabCherryPickError(GitlabOperationError): pass diff --git a/gitlab/v3/cli.py b/gitlab/v3/cli.py index ae16cf7..a8e3a5f 100644 --- a/gitlab/v3/cli.py +++ b/gitlab/v3/cli.py @@ -68,7 +68,8 @@ EXTRA_ACTIONS = { 'unstar': {'required': ['id']}, 'archive': {'required': ['id']}, 'unarchive': {'required': ['id']}, - 'share': {'required': ['id', 'group-id', 'group-access']}}, + 'share': {'required': ['id', 'group-id', 'group-access']}, + 'upload': {'required': ['id', 'filename', 'filepath']}}, gitlab.v3.objects.User: { 'block': {'required': ['id']}, 'unblock': {'required': ['id']}, @@ -348,6 +349,20 @@ class GitlabCLI(object): except Exception as e: cli.die("Impossible to get user %s" % args['query'], e) + def do_project_upload(self, cls, gl, what, args): + try: + project = gl.projects.get(args["id"]) + except Exception as e: + cli.die("Could not load project '{!r}'".format(args["id"]), e) + + try: + res = project.upload(filename=args["filename"], + filepath=args["filepath"]) + except Exception as e: + cli.die("Could not upload file into project", e) + + return res + def _populate_sub_parser_by_class(cls, sub_parser): for action_name in ['list', 'get', 'create', 'update', 'delete']: @@ -469,6 +484,7 @@ def run(gl, what, action, args, verbose, *fargs, **kwargs): cli.die("Unknown object: %s" % what) g_cli = GitlabCLI() + method = None what = what.replace('-', '_') action = action.lower().replace('-', '') @@ -491,6 +507,9 @@ def run(gl, what, action, args, verbose, *fargs, **kwargs): print("") else: print(o) + elif isinstance(ret_val, dict): + for k, v in six.iteritems(ret_val): + print("{} = {}".format(k, v)) elif isinstance(ret_val, gitlab.base.GitlabObject): ret_val.display(verbose) elif isinstance(ret_val, six.string_types): diff --git a/gitlab/v3/objects.py b/gitlab/v3/objects.py index 94c3873..338d219 100644 --- a/gitlab/v3/objects.py +++ b/gitlab/v3/objects.py @@ -909,6 +909,10 @@ class ProjectIssueNote(GitlabObject): requiredCreateAttrs = ['body'] optionalCreateAttrs = ['created_at'] + # file attachment settings (see #56) + description_attr = "body" + project_id_attr = "project_id" + class ProjectIssueNoteManager(BaseManager): obj_cls = ProjectIssueNote @@ -933,6 +937,10 @@ class ProjectIssue(GitlabObject): [('project_id', 'project_id'), ('issue_id', 'id')]), ) + # file attachment settings (see #56) + description_attr = "description" + project_id_attr = "project_id" + def subscribe(self, **kwargs): """Subscribe to an issue. @@ -1057,6 +1065,7 @@ class ProjectIssueManager(BaseManager): class ProjectMember(GitlabObject): _url = '/projects/%(project_id)s/members' + requiredUrlAttrs = ['project_id'] requiredCreateAttrs = ['access_level', 'user_id'] optionalCreateAttrs = ['expires_at'] @@ -2096,6 +2105,60 @@ class Project(GitlabObject): r = self.gitlab._raw_post(url, data=data, **kwargs) raise_error_from_response(r, GitlabCreateError, 201) + # see #56 - add file attachment features + def upload(self, filename, filedata=None, filepath=None, **kwargs): + """Upload the specified file into the project. + + .. note:: + + Either ``filedata`` or ``filepath`` *MUST* be specified. + + Args: + filename (str): The name of the file being uploaded + filedata (bytes): The raw data of the file being uploaded + filepath (str): The path to a local file to upload (optional) + + Raises: + GitlabConnectionError: If the server cannot be reached + GitlabUploadError: If the file upload fails + GitlabUploadError: If ``filedata`` and ``filepath`` are not + specified + GitlabUploadError: If both ``filedata`` and ``filepath`` are + specified + + Returns: + dict: A ``dict`` with the keys: + * ``alt`` - The alternate text for the upload + * ``url`` - The direct url to the uploaded file + * ``markdown`` - Markdown for the uploaded file + """ + if filepath is None and filedata is None: + raise GitlabUploadError("No file contents or path specified") + + if filedata is not None and filepath is not None: + raise GitlabUploadError("File contents and file path specified") + + if filepath is not None: + with open(filepath, "rb") as f: + filedata = f.read() + + url = ("/projects/%(id)s/uploads" % { + "id": self.id, + }) + r = self.gitlab._raw_post( + url, + files={"file": (filename, filedata)}, + ) + # returns 201 status code (created) + raise_error_from_response(r, GitlabUploadError, expected_code=201) + data = r.json() + + return { + "alt": data['alt'], + "url": data['url'], + "markdown": data['markdown'] + } + class Runner(GitlabObject): _url = '/runners' diff --git a/gitlab/v4/cli.py b/gitlab/v4/cli.py index 637adfc..6e664b3 100644 --- a/gitlab/v4/cli.py +++ b/gitlab/v4/cli.py @@ -324,6 +324,8 @@ def run(gl, what, action, args, verbose, output, fields): else: print(obj) print('') + elif isinstance(ret_val, dict): + printer.display(ret_val, verbose=verbose, obj=ret_val) elif isinstance(ret_val, gitlab.base.RESTObject): printer.display(get_dict(ret_val), verbose=verbose, obj=ret_val) elif isinstance(ret_val, six.string_types): diff --git a/gitlab/v4/objects.py b/gitlab/v4/objects.py index 353f854..f1ec007 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -2071,6 +2071,59 @@ class Project(SaveMixin, ObjectDeleteMixin, RESTObject): post_data.update(form) self.manager.gitlab.http_post(path, post_data=post_data, **kwargs) + # see #56 - add file attachment features + @cli.register_custom_action('Project', ('filename', 'filepath')) + @exc.on_http_error(exc.GitlabUploadError) + def upload(self, filename, filedata=None, filepath=None, **kwargs): + """Upload the specified file into the project. + + .. note:: + + Either ``filedata`` or ``filepath`` *MUST* be specified. + + Args: + filename (str): The name of the file being uploaded + filedata (bytes): The raw data of the file being uploaded + filepath (str): The path to a local file to upload (optional) + + Raises: + GitlabConnectionError: If the server cannot be reached + GitlabUploadError: If the file upload fails + GitlabUploadError: If ``filedata`` and ``filepath`` are not + specified + GitlabUploadError: If both ``filedata`` and ``filepath`` are + specified + + Returns: + dict: A ``dict`` with the keys: + * ``alt`` - The alternate text for the upload + * ``url`` - The direct url to the uploaded file + * ``markdown`` - Markdown for the uploaded file + """ + if filepath is None and filedata is None: + raise GitlabUploadError("No file contents or path specified") + + if filedata is not None and filepath is not None: + raise GitlabUploadError("File contents and file path specified") + + if filepath is not None: + with open(filepath, "rb") as f: + filedata = f.read() + + url = ('/projects/%(id)s/uploads' % { + 'id': self.id, + }) + file_info = { + 'file': (filename, filedata), + } + data = self.manager.gitlab.http_post(url, files=file_info) + + return { + "alt": data['alt'], + "url": data['url'], + "markdown": data['markdown'] + } + class Runner(SaveMixin, ObjectDeleteMixin, RESTObject): pass |