diff --git a/AUTHORS b/AUTHORS index 1ac8933ad..81c476f01 100644 --- a/AUTHORS +++ b/AUTHORS @@ -17,7 +17,7 @@ Andrew Austin Armin Weihbold Aron Pammer Asher256 -Asher256@users.noreply.github.com +Carlo Mion Christian Christian Wenk Colin D Bennett @@ -54,6 +54,7 @@ Matej Zerovnik Matt Odden Maura Hausman Michal Galet +Mike Kobit Mikhail Lopotkov Missionrulz Mond WAN @@ -67,6 +68,7 @@ Peter Mosmans Philipp Busch Rafael Eyng Richard Hansen +Robert Lu samcday savenger Stefan K. Dunkler diff --git a/ChangeLog.rst b/ChangeLog.rst index 969d9ef39..2ecb7cb02 100644 --- a/ChangeLog.rst +++ b/ChangeLog.rst @@ -1,6 +1,21 @@ ChangeLog ========= +Version 1.0.1_ - 2017-09-21 +--------------------------- + +* Tags can be retrieved by ID +* Add the server response in GitlabError exceptions +* Add support for project file upload +* Minor typo fix in "Switching to v4" documentation +* Fix password authentication for v4 +* Fix the labels attrs on MR and issues +* Exceptions: use a proper error message +* Fix http_get method in get artifacts and job trace +* CommitStatus: `sha` is parent attribute +* Fix a couple listing calls to allow proper pagination +* Add missing doc file + Version 1.0.0_ - 2017-09-08 --------------------------- @@ -447,6 +462,7 @@ Version 0.1 - 2013-07-08 * Initial release +.. _1.0.1: https://github.com/python-gitlab/python-gitlab/compare/1.0.0...1.0.1 .. _1.0.0: https://github.com/python-gitlab/python-gitlab/compare/0.21.2...1.0.0 .. _0.21.2: https://github.com/python-gitlab/python-gitlab/compare/0.21.1...0.21.2 .. _0.21.1: https://github.com/python-gitlab/python-gitlab/compare/0.21...0.21.1 diff --git a/docs/gl_objects/projects.py b/docs/gl_objects/projects.py index 131f43c66..8fbcf2b88 100644 --- a/docs/gl_objects/projects.py +++ b/docs/gl_objects/projects.py @@ -368,3 +368,29 @@ # board lists delete b_list.delete() # end board lists delete + +# project file upload by path +# Or provide a full path to the uploaded file +project.upload("filename.txt", filepath="/some/path/filename.txt") +# end project file upload by path + +# project file upload with data +# Upload a file using its filename and filedata +project.upload("filename.txt", filedata="Raw data") +# end project file upload with data + +# project file upload markdown +uploaded_file = project.upload_file("filename.txt", filedata="data") +issue = project.issues.get(issue_id) +issue.notes.create({ + "body": "See the attached file: {}".format(uploaded_file["markdown"]) +}) +# project file upload markdown + +# project file upload markdown custom +uploaded_file = project.upload_file("filename.txt", filedata="data") +issue = project.issues.get(issue_id) +issue.notes.create({ + "body": "See the [attached file]({})".format(uploaded_file["url"]) +}) +# project file upload markdown diff --git a/docs/gl_objects/projects.rst b/docs/gl_objects/projects.rst index 4a8a0ad27..b6cf311c5 100644 --- a/docs/gl_objects/projects.rst +++ b/docs/gl_objects/projects.rst @@ -779,3 +779,51 @@ Delete a list: .. literalinclude:: projects.py :start-after: # board lists delete :end-before: # end board lists delete + + +File Uploads +============ + +Reference +--------- + +* v4 API: + + + :attr:`gitlab.v4.objects.Project.upload` + + :class:`gitlab.v4.objects.ProjectUpload` + +* v3 API: + + + :attr:`gitlab.v3.objects.Project.upload` + + :class:`gitlab.v3.objects.ProjectUpload` + +* Gitlab API: https://docs.gitlab.com/ce/api/projects.html#upload-a-file + +Examples +-------- + +Upload a file into a project using a filesystem path: + +.. literalinclude:: projects.py + :start-after: # project file upload by path + :end-before: # end project file upload by path + +Upload a file into a project without a filesystem path: + +.. literalinclude:: projects.py + :start-after: # project file upload with data + :end-before: # end project file upload with data + +Upload a file and comment on an issue using the uploaded file's +markdown: + +.. literalinclude:: projects.py + :start-after: # project file upload markdown + :end-before: # end project file upload markdown + +Upload a file and comment on an issue while using custom +markdown to reference the uploaded file: + +.. literalinclude:: projects.py + :start-after: # project file upload markdown custom + :end-before: # end project file upload markdown custom diff --git a/docs/gl_objects/protected_branches.rst b/docs/gl_objects/protected_branches.rst new file mode 100644 index 000000000..4a6c8374b --- /dev/null +++ b/docs/gl_objects/protected_branches.rst @@ -0,0 +1,44 @@ +################## +Protected branches +################## + +You can define a list of protected branch names on a repository. Names can use +wildcards (``*``). + +References +---------- + +* v4 API: + + + :class:`gitlab.v4.objects.ProjectProtectedBranch` + + :class:`gitlab.v4.objects.ProjectProtectedBranchManager` + + :attr:`gitlab.v4.objects.Project.protectedbranches` + +* GitLab API: https://docs.gitlab.com/ce/api/protected_branches.html#protected-branches-api + +Examples +-------- + +Get the list of protected branches for a project: + +.. literalinclude:: branches.py + :start-after: # p_branch list + :end-before: # end p_branch list + +Get a single protected branch: + +.. literalinclude:: branches.py + :start-after: # p_branch get + :end-before: # end p_branch get + +Create a protected branch: + +.. literalinclude:: branches.py + :start-after: # p_branch create + :end-before: # end p_branch create + +Delete a protected branch: + +.. literalinclude:: branches.py + :start-after: # p_branch delete + :end-before: # end p_branch delete diff --git a/docs/switching-to-v4.rst b/docs/switching-to-v4.rst index 3415bc432..fff9573b8 100644 --- a/docs/switching-to-v4.rst +++ b/docs/switching-to-v4.rst @@ -1,7 +1,7 @@ .. _switching_to_v4: ########################## -Switching to GtiLab API v4 +Switching to GitLab API v4 ########################## GitLab provides a new API version (v4) since its 9.0 release. ``python-gitlab`` diff --git a/gitlab/__init__.py b/gitlab/__init__.py index e94c6b25a..36a93ed9f 100644 --- a/gitlab/__init__.py +++ b/gitlab/__init__.py @@ -34,7 +34,7 @@ from gitlab.v3.objects import * # noqa __title__ = 'python-gitlab' -__version__ = '1.0.0' +__version__ = '1.0.1' __author__ = 'Gauvain Pocentek' __email__ = 'gauvain@pocentek.net' __license__ = 'LGPL3' @@ -193,15 +193,16 @@ def _credentials_auth(self): if not self.email or not self.password: raise GitlabAuthenticationError("Missing email/password") + data = {'email': self.email, 'password': self.password} if self.api_version == '3': - data = json.dumps({'email': self.email, 'password': self.password}) - r = self._raw_post('/session', data, + r = self._raw_post('/session', json.dumps(data), content_type='application/json') raise_error_from_response(r, GitlabAuthenticationError, 201) self.user = self._objects.CurrentUser(self, r.json()) else: - manager = self._objects.CurrentUserManager() - self.user = manager.get(self.email, self.password) + r = self.http_post('/session', data) + manager = self._objects.CurrentUserManager(self) + self.user = self._objects.CurrentUser(manager, r) self._set_token(self.user.private_token) @@ -396,11 +397,13 @@ def _raw_list(self, path_, cls, **kwargs): 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 +631,7 @@ def _build_url(self, path): 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 +661,11 @@ def sanitized_url(url): 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 +676,7 @@ def sanitized_url(url): # 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, @@ -677,12 +685,19 @@ def sanitized_url(url): if 200 <= result.status_code < 300: return result + try: + error_message = result.json()['message'] + except (KeyError, ValueError, TypeError): + error_message = result.content + if result.status_code == 401: raise GitlabAuthenticationError(response_code=result.status_code, - error_message=result.content) + error_message=error_message, + response_body=result.content) raise GitlabHttpError(response_code=result.status_code, - error_message=result.content) + error_message=error_message, + response_body=result.content) def http_get(self, path, query_data={}, streamed=False, **kwargs): """Make a GET request to the Gitlab server. @@ -754,7 +769,8 @@ def http_list(self, path, query_data={}, as_list=None, **kwargs): # 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: @@ -774,7 +790,7 @@ def http_post(self, path, query_data={}, post_data={}, **kwargs): 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 a9521eb1d..ccc9e4a24 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -536,7 +536,7 @@ def __ne__(self, other): 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 @@ -564,7 +564,24 @@ def __getattr__(self, name): return self.__dict__['_updated_attrs'][name] except KeyError: try: - return self.__dict__['_attrs'][name] + value = self.__dict__['_attrs'][name] + + # If the value is a list, we copy it in the _updated_attrs dict + # because we are not able to detect changes made on the object + # (append, insert, pop, ...). Without forcing the attr + # creation __setattr__ is never called, the list never ends up + # in the _updated_attrs dict, and the update() and save() + # method never push the new data to the server. + # See https://github.com/python-gitlab/python-gitlab/issues/306 + # + # note: _parent_attrs will only store simple values (int) so we + # don't make this check in the next except block. + if isinstance(value, list): + self.__dict__['_updated_attrs'][name] = value[:] + return self.__dict__['_updated_attrs'][name] + + return value + except KeyError: try: return self.__dict__['_parent_attrs'][name] diff --git a/gitlab/exceptions.py b/gitlab/exceptions.py index fc2c16247..a10039551 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 @@ -230,6 +238,6 @@ def wrapped_f(*args, **kwargs): try: return f(*args, **kwargs) except GitlabHttpError as e: - raise error(e.response_code, e.error_message) + raise error(e.error_message, e.response_code, e.response_body) return wrapped_f return wrap diff --git a/gitlab/v3/cli.py b/gitlab/v3/cli.py index ae16cf7d7..a8e3a5fae 100644 --- a/gitlab/v3/cli.py +++ b/gitlab/v3/cli.py @@ -68,7 +68,8 @@ '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 @@ def do_user_getbyusername(self, cls, gl, what, args): 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 94c3873e4..338d2190c 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 @@ def trigger_build(self, ref, token, variables={}, **kwargs): 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 637adfc96..6e664b392 100644 --- a/gitlab/v4/cli.py +++ b/gitlab/v4/cli.py @@ -324,6 +324,8 @@ def get_dict(obj): 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 07a1940d6..9e0256080 100644 --- a/gitlab/v4/objects.py +++ b/gitlab/v4/objects.py @@ -689,7 +689,7 @@ def artifacts(self, streamed=False, action=None, chunk_size=1024, str: The artifacts if `streamed` is False, None otherwise. """ path = '%s/%s/artifacts' % (self.manager.path, self.get_id()) - result = self.manager.gitlab.get_http(path, streamed=streamed, + result = self.manager.gitlab.http_get(path, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) @@ -715,7 +715,7 @@ def trace(self, streamed=False, action=None, chunk_size=1024, **kwargs): str: The trace """ path = '%s/%s/trace' % (self.manager.path, self.get_id()) - result = self.manager.gitlab.get_http(path, streamed=streamed, + result = self.manager.gitlab.http_get(path, streamed=streamed, **kwargs) return utils.response_content(result, streamed, action, chunk_size) @@ -735,7 +735,7 @@ class ProjectCommitStatusManager(GetFromListMixin, CreateMixin, RESTManager): '/statuses') _obj_cls = ProjectCommitStatus _from_parent_attrs = {'project_id': 'project_id', 'commit_id': 'id'} - _create_attrs = (('state', 'sha'), + _create_attrs = (('state', ), ('description', 'name', 'context', 'ref', 'target_url', 'coverage')) @@ -960,6 +960,12 @@ class ProjectIssueManager(CRUDMixin, RESTManager): 'milestone_id', 'labels', 'created_at', 'updated_at', 'state_event', 'due_date')) + def _sanitize_data(self, data, action): + new_data = data.copy() + if 'labels' in data: + new_data['labels'] = ','.join(data['labels']) + return new_data + class ProjectMember(SaveMixin, ObjectDeleteMixin, RESTObject): _short_print_attr = 'username' @@ -1035,8 +1041,7 @@ def set_release_description(self, description, **kwargs): self.release = server_data -class ProjectTagManager(GetFromListMixin, CreateMixin, DeleteMixin, - RESTManager): +class ProjectTagManager(NoUpdateMixin, RESTManager): _path = '/projects/%(project_id)s/repository/tags' _obj_cls = ProjectTag _from_parent_attrs = {'project_id': 'id'} @@ -1810,8 +1815,8 @@ def repository_tree(self, path='', ref='', **kwargs): query_data['path'] = path if ref: query_data['ref'] = ref - return self.manager.gitlab.http_get(gl_path, query_data=query_data, - **kwargs) + return self.manager.gitlab.http_list(gl_path, query_data=query_data, + **kwargs) @cli.register_custom_action('Project', ('sha', )) @exc.on_http_error(exc.GitlabGetError) @@ -1899,7 +1904,7 @@ def repository_contributors(self, **kwargs): list: The contributors """ path = '/projects/%s/repository/contributors' % self.get_id() - return self.manager.gitlab.http_get(path, **kwargs) + return self.manager.gitlab.http_list(path, **kwargs) @cli.register_custom_action('Project', tuple(), ('sha', )) @exc.on_http_error(exc.GitlabListError) @@ -2072,6 +2077,59 @@ def trigger_pipeline(self, ref, token, variables={}, **kwargs): 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 diff --git a/tools/cli_test_v3.sh b/tools/cli_test_v3.sh index d71f4378b..ed433ceef 100644 --- a/tools/cli_test_v3.sh +++ b/tools/cli_test_v3.sh @@ -98,6 +98,10 @@ testcase "branch deletion" ' --name branch1 >/dev/null 2>&1 ' +testcase "project upload" ' + GITLAB project upload --id "$PROJECT_ID" --filename '$(basename $0)' --filepath '$0' +' + testcase "project deletion" ' GITLAB project delete --id "$PROJECT_ID" ' diff --git a/tools/cli_test_v4.sh b/tools/cli_test_v4.sh index 8399bd855..813d85b06 100644 --- a/tools/cli_test_v4.sh +++ b/tools/cli_test_v4.sh @@ -94,6 +94,10 @@ testcase "branch deletion" ' --name branch1 >/dev/null 2>&1 ' +testcase "project upload" ' + GITLAB project upload --id "$PROJECT_ID" --filename '$(basename $0)' --filepath '$0' +' + testcase "project deletion" ' GITLAB project delete --id "$PROJECT_ID" ' diff --git a/tools/python_test_v3.py b/tools/python_test_v3.py index a730f77fe..00faccc87 100644 --- a/tools/python_test_v3.py +++ b/tools/python_test_v3.py @@ -1,4 +1,5 @@ import base64 +import re import time import gitlab @@ -194,6 +195,18 @@ archive2 = admin_project.repository_archive('master') assert(archive1 == archive2) +# project file uploads +filename = "test.txt" +file_contents = "testing contents" +uploaded_file = admin_project.upload(filename, file_contents) +assert(uploaded_file["alt"] == filename) +assert(uploaded_file["url"].startswith("/uploads/")) +assert(uploaded_file["url"].endswith("/" + filename)) +assert(uploaded_file["markdown"] == "[{}]({})".format( + uploaded_file["alt"], + uploaded_file["url"], +)) + # deploy keys deploy_key = admin_project.keys.create({'title': 'foo@bar', 'key': DEPLOY_KEY}) project_keys = admin_project.keys.list() diff --git a/tools/python_test_v4.py b/tools/python_test_v4.py index 2113830d0..386b59b7d 100644 --- a/tools/python_test_v4.py +++ b/tools/python_test_v4.py @@ -258,6 +258,18 @@ archive2 = admin_project.repository_archive('master') assert(archive1 == archive2) +# project file uploads +filename = "test.txt" +file_contents = "testing contents" +uploaded_file = admin_project.upload(filename, file_contents) +assert(uploaded_file["alt"] == filename) +assert(uploaded_file["url"].startswith("/uploads/")) +assert(uploaded_file["url"].endswith("/" + filename)) +assert(uploaded_file["markdown"] == "[{}]({})".format( + uploaded_file["alt"], + uploaded_file["url"], +)) + # environments admin_project.environments.create({'name': 'env1', 'external_url': 'http://fake.env/whatever'})