8000 File upload should be mostly complete · python-gitlab/python-gitlab@62f71bc · GitHub
[go: up one dir, main page]

Skip to content

Commit 62f71bc

Browse files
committed
File upload should be mostly complete
see #56 * file uploads are not directly associated with issues/notes * should work with v3 and v4 apis * CLIs seem to be working
1 parent 1ce40a3 commit 62f71bc

File tree

12 files changed

+306
-72
lines changed

12 files changed

+306
-72
lines changed

docs/gl_objects/projects.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,29 @@
368368
# board lists delete
369369
b_list.delete()
370370
# end board lists delete
371+
372+
# project file upload by path
373+
# Or provide a full path to the uploaded file
374+
project.upload("filename.txt", filepath="/some/path/filename.txt")
375+
# end project file upload by path
376+
377+
# project file upload with data
378+
# Upload a file using its filename and filedata
379+
project.upload("filename.txt", filedata="Raw data")
380+
# end project file upload with data
381+
382+
# project file upload markdown
383+
uploaded_file = project.upload_file("filename.txt", filedata="data")
384+
issue = project.issues.get(issue_id)
385+
issue.notes.create({
386+
"body": "See the attached file: {}".format(uploaded_file)
387+
})
388+
# project file upload markdown
389+
390+
# project file upload markdown custom
391+
uploaded_file = project.upload_file("filename.txt", filedata="data")
392+
issue = project.issues.get(issue_id)
393+
issue.notes.create({
394+
"body": "See the [attached file]({})".format(uploaded_file.url)
395+
})
396+
# project file upload markdown

docs/gl_objects/projects.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,3 +779,51 @@ Delete a list:
779779
.. literalinclude:: projects.py
780780
:start-after: # board lists delete
781781
:end-before: # end board lists delete
782+
783+
784+
File Uploads
785+
============
786+
787+
Reference
788+
---------
789+
790+
* v4 API:
791+
792+
+ :attr:`gitlab.v4.objects.Project.upload`
793+
+ :class:`gitlab.v4.objects.ProjectUpload`
794+
795+
* v3 API:
796+
797+
+ :attr:`gitlab.v3.objects.Project.upload`
798+
+ :class:`gitlab.v3.objects.ProjectUpload`
799+
800+
* Gitlab API: https://docs.gitlab.com/ce/api/projects.html#upload-a-file
801+
802+
Examples
803+
--------
804+
805+
Upload a file into a project using a filesystem path:
806+
807+
.. literalinclude:: projects.py
808+
:start-after: # project file upload by path
809+
:end-before: # end project file upload by path
810+
811+
Upload a file into a project without a filesystem path:
812+
813+
.. literalinclude:: projects.py
814+
:start-after: # project file upload with data
815+
:end-before: # end project file upload with data
816+
817+
Upload a file and comment on an issue using the uploaded file's
818+
markdown:
819+
820+
.. literalinclude:: projects.py
821+
:start-after: # project file upload markdown
822+
:end-before: # end project file upload markdown
823+
824+
Upload a file and comment on an issue while using custom
825+
markdown to reference the uploaded file:
826+
827+
.. literalinclude:: projects.py
828+
:start-after: # project file upload markdown custom
829+
:end-before: # end project file upload markdown custom

gitlab/__init__.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ def _build_url(self, path):
628628
return '%s%s' % (self._url, path)
629629

630630
def http_request(self, verb, path, query_data={}, post_data={},
631-
streamed=False, **kwargs):
631+
streamed=False, files=None, **kwargs):
632632
"""Make an HTTP request to the Gitlab server.
633633
634634
Args:
@@ -658,6 +658,11 @@ def sanitized_url(url):
658658
params = query_data.copy()
659659
params.update(kwargs)
660660
opts = self._get_session_opts(content_type='application/json')
661+
662+
# don't set the content-type header when uploading files
663+
if files is not None:
664+
del opts["headers"]["Content-type"]
665+
661666
verify = opts.pop('verify')
662667
timeout = opts.pop('timeout')
663668

@@ -668,7 +673,7 @@ def sanitized_url(url):
668673
# always agree with this decision (this is the case with a default
669674
# gitlab installation)
670675
req = requests.Request(verb, url, json=post_data, params=params,
671-
**opts)
676+
files=files, **opts)
672677
prepped = self.session.prepare_request(req)
673678
prepped.url = sanitized_url(prepped.url)
674679
result = self.session.send(prepped, stream=streamed, verify=verify,
@@ -754,7 +759,7 @@ def http_list(self, path, query_data={}, as_list=None, **kwargs):
754759
# No pagination, generator requested
755760
return GitlabList(self, url, query_data, **kwargs)
756761

757-
def http_post(self, path, query_data={}, post_data={}, **kwargs):
762+
def http_post(self, path, query_data={}, post_data={}, files=None, **kwargs):
758763
"""Make a POST request to the Gitlab server.
759764
760765
Args:
@@ -774,7 +779,7 @@ def http_post(self, path, query_data={}, post_data={}, **kwargs):
774779
GitlabParsingError: If the json data could not be parsed
775780
"""
776781
result = self.http_request('post', path, query_data=query_data,
777-
post_data=post_data, **kwargs)
782+
post_data=post_data, files=files, **kwargs)
778783
try:
779784
if result.headers.get('Content-Type', None) == 'application/json':
780785
return result.json()

gitlab/base.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,10 +533,34 @@ def __ne__(self, other):
533533
return not self.__eq__(other)
534534

535535

536-
class RESTObject(object):
536+
class InformationalObject(object):
537+
"""A basic object that holds values, more like a struct
538+
than a class.
539+
540+
Inheriting classes should define which attributes are relevant/important
541+
when defining the class. E.g.:
542+
543+
.. code-block:: python
544+
545+
class ProjectUpload(InformationalObject):
546+
_attr_names = ["alt", "markdown", "id", "url"]
547+
"""
548+
_attr_names = []
549+
_id_attr = "id"
550+
_short_print_attr = "id"
551+
552+
@property
553+
def attributes(self):
554+
res = {}
555+
for attr_name in self._attr_names:
556+
res[attr_name] = getattr(self, attr_name)
557+
return res
558+
559+
560+
class RESTObject(InformationalObject):
537561
"""Represents an object built from server data.
538562
539-
It holds the attributes know from te server, and the updated attributes in
563+
It holds the attributes know from the server, and the updated attributes in
540564
another. This allows smart updates, if the object allows it.
541565
542566
You can redefine ``_id_attr`` in child classes to specify which attribute

gitlab/v3/cli.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
'unstar': {'required': ['id']},
6969
'archive': {'required': ['id']},
7070
'unarchive': {'required': ['id']},
71-
'share': {'required': ['id', 'group-id', 'group-access']}},
71+
'share': {'required': ['id', 'group-id', 'group-access']},
72+
'upload': {'required': ['id', 'filename', 'filepath']}},
7273
gitlab.v3.objects.User: {
7374
'block': {'required': ['id']},
7475
'unblock': {'required': ['id']},
@@ -348,6 +349,19 @@ def do_user_getbyusername(self, cls, gl, what, args):
348349
except Exception as e:
349350
cli.die("Impossible to get user %s" % args['query'], e)
350351

352+
def do_project_upload(self, cls, gl, what, args):
353+
try:
354+
project = gl.projects.get(args["id"])
355+
except Exception as e:
356+
cli.die("Could not load project '{!r}'".format(args["id"]), e)
357+
358+
try:
359+
res = project.upload(filename=args["filename"], filepath=args["filepath"])
360+
except Exception as e:
361+
cli.die("Could not upload file into project", e)
362+
363+
return res
364+
351365

352366
def _populate_sub_parser_by_class(cls, sub_parser):
353367
for action_name in ['list', 'get', 'create', 'update', 'delete']:
@@ -469,6 +483,7 @@ def run(gl, what, action, args, verbose, *fargs, **kwargs):
469483
cli.die("Unknown object: %s" % what)
470484

471485
g_cli = GitlabCLI()
486+
472487
method = None
473488
what = what.replace('-', '_')
474489
action = action.lower().replace('-', '')

gitlab/v3/objects.py

Lines changed: 59 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -66,55 +66,6 @@ def compound_metrics(self, **kwargs):
6666
return self._simple_get('/sidekiq/compound_metrics', **kwargs)
6767

6868

69-
class ProjectUploadable(object):
70-
"""A mixin for objects that allow files to be uploaded/attached.
71-
"""
72-
73-
description_attr = "description"
74-
"""The attribute of the object that newly uploaded files'
75-
markdown should be appended to.
76-
"""
77-
78-
project_id_attr = "project_id"
79-
"""The attribute that specifies the attribute that contains
80-
the project id into which files may be uploaded.
81-
"""
82-
83-
# see #56 - add file uploading/attachment features
84-
def attach_file(self, filename, filedata, **kwargs):
85-
"""Attach a file to the issue
86-
87-
Args:
88-
filename (str): The filename of the file being uploaded.
89-
filedata (str): The raw data of the file being uploaded.
90-
Raises:
91-
GitlabConnectionError: If the server cannot be reached.
92-
"""
93-
project_id = getattr(self, self.project_id_attr, None)
94-
if project_id is None:
95-
raise GitlabAttachFileError("{}'s project id could not be determined (tried using {!r})".format(
96-
self,
97-
self.project_id_attr,
98-
))
99-
100-
project = self.gitlab.projects.get(project_id)
101-
res = project.upload(
102-
filename = filename,
103-
filedata = filedata,
104-
)
105-
106-
orig_desc = getattr(self, self.description_attr, "")
107-
# files are "attached" to issues and comments by uploading the
108-
# the file into the project, and including the returned
109-
# markdown in the description
110-
setattr(self, self.description_attr, orig_desc + "\n\n" + res["markdown"])
111-
112-
# XXX:TODO: Is this correct? any changes to the current object instance
113-
# that have not yet been saved will be saved along with the
114-
# file upload, which *may not* be what the user intended.
115-
self.save()
116-
117-
11869
class UserEmail(GitlabObject):
11970
_url = '/users/%(user_id)s/emails'
12071
canUpdate = False
@@ -950,7 +901,7 @@ class ProjectHookManager(BaseManager):
950901
obj_cls = ProjectHook
951902

952903

953-
class ProjectIssueNote(GitlabObject, ProjectUploadable):
904+
class ProjectIssueNote(GitlabObject):
954905
_url = '/projects/%(project_id)s/issues/%(issue_id)s/notes'
955906
_constructorTypes = {'author': 'User'}
956907
canDelete = False
@@ -967,7 +918,7 @@ class ProjectIssueNoteManager(BaseManager):
967918
obj_cls = ProjectIssueNote
968919

969920

970-
class ProjectIssue(GitlabObject, ProjectUploadable):
921+
class ProjectIssue(GitlabObject):
971922
_url = '/projects/%(project_id)s/issues/'
972923
_constructorTypes = {'author': 'User', 'assignee': 'User',
973924
'milestone': 'ProjectMilestone'}
@@ -1819,6 +1770,33 @@ class ProjectRunnerManager(BaseManager):
18191770
obj_cls = ProjectRunner
18201771

18211772

1773+
class ProjectUpload(GitlabObject):
1774+
shortPrintAttr = "url"
1775+
1776+
def __init__(self, alt, url, markdown):
1777+
"""Create a new ProfileFileUpload instance that
1778+
holds the ``alt`` (uploaded filename without the extension),
1779+
``url``, and ``markdown`` data about the file upload.
1780+
1781+
Args:
1782+
alt (str): The alt of the upload
1783+
url (str): The url of to the uploaded file
1784+
markdown (str): The markdown text that creates a link to the uploaded file
1785+
"""
1786+
self.alt = alt
1787+
self.url = url
1788+
self.markdown = markdown
1789+
1790+
# url should be in this form: /uploads/ID/filename.txt
1791+
self.id = url.replace("/uploads/", "").split("/")[0]
1792+
1793+
def __str__(self):
1794+
"""Return the markdown representation of the uploaded
1795+
file.
1796+
"""
1797+
return self.markdown
1798+
1799+
18221800
class Project(GitlabObject):
18231801
_url = '/projects'
18241802
_constructorTypes = {'owner': 'User', 'namespace': 'Group'}
@@ -2155,27 +2133,38 @@ def trigger_build(self, ref, token, variables={}, **kwargs):
21552133
raise_error_from_response(r, GitlabCreateError, 201)
21562134

21572135
# see #56 - add file attachment features
2158-
def upload(self, filename, filedata, **kwargs):
2159-
"""Upload a file into the project. This will return the raw response
2160-
from the gitlab API in the form:
2161-
2162-
{
2163-
"alt": "dk",
2164-
"url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png",
2165-
"markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)"
2166-
}
2167-
2168-
See https://github.com/gitlabhq/gitlabhq/blob/master/doc/api/projects.md#upload-a-file
2169-
for more information.
2136+
def upload(self, filename, filedata=None, filepath=None, **kwargs):
2137+
"""Upload the specified file into the project.
2138+
2139+
.. note::
2140+
2141+
Either ``filedata`` or ``filepath`` *MUST* be specified.
21702142
21712143
Args:
21722144
filename (str): The name of the file being uploaded
21732145
filedata (bytes): The raw data of the file being uploaded
2146+
filepath (str): The path to a local file to upload (optional)
21742147
21752148
Raises:
21762149
GitlabConnectionError: If the server cannot be reached
21772150
GitlabUploadError: If the file upload fails
2151+
GitlabUploadError: If ``filedata`` and ``filepath`` are not specified
2152+
GitlabUploadError: If both ``filedata`` and ``filepath`` are specified
2153+
2154+
Returns:
2155+
ProjectUpload: A ``ProjectUpload`` instance containing
2156+
information about the uploaded file.
21782157
"""
2158+
if filepath is None and filedata is None:
2159+
raise GitlabUploadError("No file contents or path specified")
2160+
2161+
if filedata is not None and filepath is not None:
2162+
raise GitlabUploadError("File contents and file path specified")
2163+
2164+
if filepath is not None:
2165+
with open(filepath, "rb") as f:
2166+
filedata = f.read()
2167+
21792168
url = ("/projects/%(id)s/uploads" % {
21802169
"id": self.id,
21812170
})
@@ -2185,7 +2174,13 @@ def upload(self, filename, filedata, **kwargs):
21852174
)
21862175
# returns 201 status code (created)
21872176
raise_error_from_response(r, GitlabUploadError, expected_code=201)
2188-
return r.json()
2177+
data = r.json()
2178+
2179+
return ProjectUpload(
2180+
alt = data["alt"],
2181+
url = data["url"],
2182+
markdown = data["markdown"]
2183+
)
21892184

21902185

21912186
class Runner(GitlabObject):

gitlab/v4/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ def get_dict(obj):
321321
else:
322322
print(obj)
323323
print('')
324-
elif isinstance(ret_val, gitlab.base.RESTObject):
324+
elif isinstance(ret_val, gitlab.base.InformationalObject):
325325
printer.display(get_dict(ret_val), verbose=verbose, obj=ret_val)
326326
elif isinstance(ret_val, six.string_types):
327327
print(ret_val)

0 commit comments

Comments
 (0)
0