├── README ├── MANIFEST.in ├── bitbucket ├── tests │ ├── private │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── ssh.py │ │ ├── issue.py │ │ ├── service.py │ │ ├── issue_comment.py │ │ ├── private.py │ │ └── repository.py │ ├── __init__.py │ └── public.py ├── ssh.py ├── deploy_key.py ├── __init__.py ├── service.py ├── issue_comment.py ├── issue.py ├── repository.py └── bitbucket.py ├── requirements.txt ├── .travis.yml ├── .gitignore ├── docs ├── bitbucket.tests.rst ├── index.rst ├── installation.rst ├── bitbucket.rst ├── bitbucket.tests.private.rst ├── usage.rst ├── Makefile ├── make.bat └── conf.py ├── LICENSE ├── setup.py └── README.md /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | -------------------------------------------------------------------------------- /bitbucket/tests/private/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.2.1 2 | requests-oauthlib==0.3.0 3 | sh==1.08 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.7 4 | install: pip install -r requirements.txt 5 | script: python -m bitbucket.tests.public 6 | -------------------------------------------------------------------------------- /bitbucket/tests/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from bitbucket.tests.public import * 3 | 4 | if __name__ == "__main__": 5 | unittest.main() 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | env/ 2 | dist/ 3 | *.pyc 4 | MANIFEST 5 | bitbucket/tests/private/settings.py 6 | 7 | # Ignore Sublime Text project files 8 | *.sublime-project 9 | *.sublime-workspace 10 | 11 | # Ignore aptana and eclipse files 12 | .project 13 | .pydevproject 14 | .settings/ 15 | build/ 16 | 17 | # Ignore doc generated files 18 | docs/_build/ 19 | -------------------------------------------------------------------------------- /docs/bitbucket.tests.rst: -------------------------------------------------------------------------------- 1 | bitbucket.tests Package 2 | ======================= 3 | 4 | :mod:`tests` Package 5 | -------------------- 6 | 7 | .. automodule:: bitbucket.tests 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`public` Module 13 | -------------------- 14 | 15 | .. automodule:: bitbucket.tests.public 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | Subpackages 21 | ----------- 22 | 23 | .. toctree:: 24 | 25 | bitbucket.tests.private 26 | 27 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Bitbucket-API documentation master file, created by 2 | sphinx-quickstart on Thu May 23 14:55:30 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Bitbucket-API's documentation! 7 | ========================================= 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | 12 | installation.rst 13 | usage.rst 14 | 15 | bitbucket.rst 16 | bitbucket.tests.rst 17 | bitbucket.tests.private.rst 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /bitbucket/tests/private/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # run with `site-packages$> python -m bitbucket.tests.private` 3 | import unittest 4 | 5 | from bitbucket.tests.private.private import BitbucketAuthenticatedMethodsTest 6 | from bitbucket.tests.private.repository import RepositoryAuthenticatedMethodsTest, ArchiveRepositoryAuthenticatedMethodsTest 7 | from bitbucket.tests.private.issue import IssueAuthenticatedMethodsTest 8 | from bitbucket.tests.private.issue_comment import IssueCommentAuthenticatedMethodsTest 9 | from bitbucket.tests.private.service import ServiceAuthenticatedMethodsTest 10 | from bitbucket.tests.private.ssh import SSHAuthenticatedMethodsTest 11 | 12 | if __name__ == '__main__': 13 | unittest.main() 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Millou Baptiste 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | 4 | Pip 5 | ^^^ 6 | Installing Bitbucket-API is simple with pip: :: 7 | 8 | pip install Bitbucket-API 9 | 10 | Get the Code & contribute 11 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 12 | Bitbucket-API is hosted on GitHub, where the code is always available. 13 | 14 | You can either clone the public repository: :: 15 | 16 | git clone git@github.com:Sheeprider/BitBucket-api.git 17 | 18 | Download the tarball: :: 19 | 20 | curl -OL https://github.com/Sheeprider/BitBucket-api/tarball/master 21 | 22 | Or, download the zipball: :: 23 | 24 | curl -OL https://github.com/Sheeprider/Bitbucket-API/zipball/master 25 | 26 | Test 27 | ^^^^ 28 | Run public tests:: 29 | 30 | site-packages$> python -m bitbucket.tests.public 31 | 32 | Run private tests. Require **USERNAME** and **PASSWORD** or **USERNAME**, **CONSUMER_KEY** and **CONSUMER_SECRET** in *bitbucket/tests/private/settings.py*:: 33 | 34 | site-packages$> python -m bitbucket.tests.private 35 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from distutils.core import setup 3 | 4 | import bitbucket 5 | 6 | if __name__ == "__main__": 7 | setup( 8 | name='bitbucket-api', 9 | version=bitbucket.__version__, 10 | description='Bitbucket API', 11 | long_description=open('README').read(), 12 | author='Baptiste Millou', 13 | author_email='baptiste@smoothie-creative.com', 14 | url='https://github.com/Sheeprider/BitBucket-api', 15 | packages=[ 16 | 'bitbucket', 17 | 'bitbucket.tests', 18 | ], 19 | license=open('LICENSE').read(), 20 | install_requires=['requests', 'sh', 'requests-oauthlib'], 21 | classifiers=[ 22 | 'Development Status :: 3 - Alpha', 23 | 'Intended Audience :: Developers', 24 | 'License :: OSI Approved :: ISC License (ISCL)', 25 | 'Programming Language :: Python', 26 | 'Natural Language :: English', 27 | 'Operating System :: OS Independent', 28 | 'Topic :: Software Development :: Libraries :: Python Modules', 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /docs/bitbucket.rst: -------------------------------------------------------------------------------- 1 | bitbucket Package 2 | ================= 3 | 4 | :mod:`bitbucket` Package 5 | ------------------------ 6 | 7 | .. automodule:: bitbucket 8 | 9 | :mod:`Bitbucket` Module 10 | ----------------------- 11 | 12 | .. automodule:: bitbucket.bitbucket 13 | :members: 14 | :undoc-members: 15 | 16 | 17 | :mod:`issue` Module 18 | ------------------- 19 | 20 | .. automodule:: bitbucket.issue 21 | :members: 22 | :undoc-members: 23 | 24 | :mod:`issue_comment` Module 25 | --------------------------- 26 | 27 | .. automodule:: bitbucket.issue_comment 28 | :members: 29 | :undoc-members: 30 | 31 | :mod:`repository` Module 32 | ------------------------ 33 | 34 | .. automodule:: bitbucket.repository 35 | :members: 36 | :undoc-members: 37 | 38 | :mod:`service` Module 39 | --------------------- 40 | 41 | .. automodule:: bitbucket.service 42 | :members: 43 | :undoc-members: 44 | 45 | :mod:`ssh` Module 46 | ----------------- 47 | 48 | .. automodule:: bitbucket.ssh 49 | :members: 50 | :undoc-members: 51 | 52 | Subpackages 53 | ----------- 54 | 55 | .. toctree:: 56 | 57 | bitbucket.tests 58 | 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #BitBucket-API 2 | 3 | [![Build Status](https://secure.travis-ci.org/Sheeprider/BitBucket-api.png)](http://travis-ci.org/Sheeprider/BitBucket-api) 4 | 5 | BitBucket-api is an ISC Licensed library, written in Python. 6 | 7 | Bitbucket has a REST API publicly available, this package provide methods to interact with it. 8 | It allows you to access most repositories, services (hooks) and ssh keys related functionalities. 9 | 10 | ##Features 11 | 12 | * Access public user informations 13 | * Access public or private repositories, tags or branches 14 | * Create, update or delete one of your repository 15 | * Access, create, update or delete a service (hook) 16 | * Access, create or delete an SSH key 17 | * Download a repository as an archive 18 | * Access, create, update or delete an issue 19 | * Access, create, update or delete an issue comment 20 | 21 | ##Installation 22 | 23 | To install bitbucket-api, simply: 24 | 25 | $ pip install bitbucket-api 26 | 27 | 28 | ##Requirements 29 | 30 | Bitbucket-api require [requests](https://github.com/kennethreitz/requests), [sh](https://github.com/amoffat/sh) and [requests-oauthlib](https://github.com/requests/requests-oauthlib)to work, but dependencies should be handled by pip. 31 | 32 | ##Documentation 33 | Documentation is available on [Read The Docs](https://bitbucket-api.readthedocs.org/en/latest/index.html). 34 | -------------------------------------------------------------------------------- /docs/bitbucket.tests.private.rst: -------------------------------------------------------------------------------- 1 | bitbucket.tests.private Package 2 | =============================== 3 | 4 | :mod:`private` Package 5 | ---------------------- 6 | 7 | .. automodule:: bitbucket.tests.private 8 | :members: 9 | :undoc-members: 10 | :show-inheritance: 11 | 12 | :mod:`issue` Module 13 | ------------------- 14 | 15 | .. automodule:: bitbucket.tests.private.issue 16 | :members: 17 | :undoc-members: 18 | :show-inheritance: 19 | 20 | :mod:`issue_comment` Module 21 | --------------------------- 22 | 23 | .. automodule:: bitbucket.tests.private.issue_comment 24 | :members: 25 | :undoc-members: 26 | :show-inheritance: 27 | 28 | :mod:`private` Module 29 | --------------------- 30 | 31 | .. automodule:: bitbucket.tests.private.private 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | :mod:`repository` Module 37 | ------------------------ 38 | 39 | .. automodule:: bitbucket.tests.private.repository 40 | :members: 41 | :undoc-members: 42 | :show-inheritance: 43 | 44 | :mod:`service` Module 45 | --------------------- 46 | 47 | .. automodule:: bitbucket.tests.private.service 48 | :members: 49 | :undoc-members: 50 | :show-inheritance: 51 | 52 | :mod:`settings` Module 53 | ---------------------- 54 | 55 | Set **USERNAME** and **PASSWORD** or **USERNAME**, **CONSUMER_KEY** and 56 | **CONSUMER_SECRET** in *bitbucket/tests/private/settings.py* 57 | if you want to run tests for private methods. 58 | 59 | 60 | :mod:`ssh` Module 61 | ----------------- 62 | 63 | .. automodule:: bitbucket.tests.private.ssh 64 | :members: 65 | :undoc-members: 66 | :show-inheritance: 67 | 68 | -------------------------------------------------------------------------------- /bitbucket/ssh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | URLS = { 3 | # SSH keys 4 | 'GET_SSH_KEYS': 'ssh-keys/', 5 | 'GET_SSH_KEY': 'ssh-keys/%(key_id)s', 6 | 'SET_SSH_KEY': 'ssh-keys/', 7 | 'DELETE_SSH_KEY': 'ssh-keys/%(key_id)s', 8 | } 9 | 10 | 11 | class SSH(object): 12 | """ This class provide ssh-related methods to Bitbucket objects.""" 13 | 14 | def __init__(self, bitbucket): 15 | self.bitbucket = bitbucket 16 | self.bitbucket.URLS.update(URLS) 17 | 18 | def all(self): 19 | """ Get all ssh keys associated with your account. 20 | """ 21 | url = self.bitbucket.url('GET_SSH_KEYS') 22 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 23 | 24 | def get(self, key_id=None): 25 | """ Get one of the ssh keys associated with your account. 26 | """ 27 | url = self.bitbucket.url('GET_SSH_KEY', key_id=key_id) 28 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 29 | 30 | def create(self, key=None, label=None): 31 | """ Associate an ssh key with your account and return it. 32 | """ 33 | key = '%s' % key 34 | url = self.bitbucket.url('SET_SSH_KEY') 35 | return self.bitbucket.dispatch('POST', url, auth=self.bitbucket.auth, key=key, label=label) 36 | 37 | def delete(self, key_id=None): 38 | """ Delete one of the ssh keys associated with your account. 39 | Please use with caution as there is NO confimation and NO undo. 40 | """ 41 | url = self.bitbucket.url('DELETE_SSH_KEY', key_id=key_id) 42 | return self.bitbucket.dispatch('DELETE', url, auth=self.bitbucket.auth) 43 | -------------------------------------------------------------------------------- /bitbucket/tests/private/ssh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from bitbucket.tests.private.private import AuthenticatedBitbucketTest 3 | 4 | 5 | class SSHAuthenticatedMethodsTest(AuthenticatedBitbucketTest): 6 | """ Testing bitbucket.ssh methods.""" 7 | 8 | def test_all(self): 9 | """ Test get all sshs.""" 10 | success, result = self.bb.ssh.all() 11 | self.assertTrue(success) 12 | self.assertIsInstance(result, list) 13 | 14 | def _create_ssh(self): 15 | # Test create an invalid ssh 16 | success, result = self.bb.ssh.create() 17 | self.assertFalse(success) 18 | # Test create an ssh 19 | success, result = self.bb.ssh.create( 20 | key=r'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDftGyHMtFSVkIxESnJQQDGy+uXT3LuUqkaLKKjb+WeXxJEabVyur8z4urzRQyHSenrrAyd9GTLtx1zmxCn7LP626ztrIYqjdK2WBhx+wjBsF39+DNvokwLAzHpQVZnywZZXaf8aeKdwiLmUpBSpfk7dSYsjQvfkmsjBpDJz9z9NsOzVK3fIVjEdnu7nPgINiJ/DlqB9zfdXpu0o98tH/WfhDo+PvkIWYrkH/cms9LIsc4zNZIKJF6i0hDAAnC0V27GQKRXpXcnj32PZvk2eXF8TxiO0rGjkEBSd1J638GHvgLI9d8iQUAVIOm69x/trQhUKcGlcHcbU0VzaFaIYawr baptiste@smoothie-creative.com\ 21 | ', 22 | label=u'test key',) 23 | self.assertTrue(success) 24 | self.assertIsInstance(result, dict) 25 | # Save latest ssh's id 26 | self.ssh_id = result[u'pk'] 27 | 28 | def _get_ssh(self): 29 | # Test get an ssh. 30 | success, result = self.bb.ssh.get(key_id=self.ssh_id) 31 | self.assertTrue(success) 32 | self.assertIsInstance(result, dict) 33 | # Test get an invalid ssh. 34 | success, result = self.bb.ssh.get(key_id=99999999999) 35 | self.assertFalse(success) 36 | 37 | def _delete_ssh(self): 38 | # Test ssh delete. 39 | success, result = self.bb.ssh.delete(key_id=self.ssh_id) 40 | self.assertTrue(success) 41 | self.assertEqual(result, '') 42 | 43 | success, result = self.bb.ssh.get(key_id=self.ssh_id) 44 | self.assertFalse(success) 45 | 46 | def test_CRUD(self): 47 | """ Test ssh create/read/delete.""" 48 | self._create_ssh() 49 | self._get_ssh() 50 | self._delete_ssh() 51 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ----- 3 | 4 | Public 5 | ^^^^^^ 6 | You can access any public repository on Bitbucket, but some actions won't be available without credentials. :: 7 | 8 | >>> from bitbucket.bitbucket import Bitbucket 9 | >>> bb = Bitbucket(USERNAME, repo_name_or_slug='public_slug') 10 | >>> success, result = bb.repository.delete() 11 | >>> print success 12 | False 13 | 14 | 15 | Private 16 | ^^^^^^^ 17 | With the correct credentials you can access private repositories on Bitbucket. :: 18 | 19 | >>> from bitbucket.bitbucket import Bitbucket 20 | >>> bb = Bitbucket(USERNAME, PASSWORD, 'private_slug') 21 | >>> success, result = bb.repository.get() 22 | >>> print success, result 23 | True {...} 24 | 25 | Examples 26 | ^^^^^^^^ 27 | Connect using Oauth :: 28 | 29 | >>> import webbrowser 30 | >>> from bitbucket.bitbucket import Bitbucket 31 | >>> bb = Bitbucket(USERNAME) 32 | >>> # First time we need to open up a browser to enter the verifier 33 | >>> if not OAUTH_ACCESS_TOKEN and not OAUTH_ACCESS_TOKEN_SECRET: 34 | >>> bb.authorize(CONSUMER_KEY, CONSUMER_SECRET, 'http://localhost/') 35 | >>> # open a webbrowser and get the token 36 | >>> webbrowser.open(bb.url('AUTHENTICATE', token=bb.access_token)) 37 | >>> # Copy the verifier field from the URL in the browser into the console 38 | >>> oauth_verifier = raw_input('Enter verifier from url [oauth_verifier]') 39 | >>> bb.verify(oauth_verifier) 40 | >>> OAUTH_ACCESS_TOKEN = bb.access_token 41 | >>> OAUTH_ACCESS_TOKEN_SECRET = bb.access_token_secret 42 | >>> else: 43 | >>> bb.authorize(CONSUMER_KEY, CONSUMER_SECRET, 'http://localhost/', OAUTH_ACCESS_TOKEN, OAUTH_ACCESS_TOKEN_SECRET) 44 | 45 | List all repositories for a user (from `@matthew-campbell`_):: 46 | 47 | >>> from bitbucket.bitbucket import Bitbucket 48 | >>> bb = Bitbucket(USERNAME, PASSWORD) 49 | >>> success, repositories = bb.repository.all() 50 | >>> for repo in sorted(repositories): 51 | >>> p = '+' 52 | >>> if repo['is_private']: 53 | >>> p ='-' 54 | >>> print('({}){}, {}, {}'.format(p, repo['name'], repo['last_updated'], repo['scm'])) 55 | >>> print('Total {}'.format(len(repositories))) 56 | 57 | .. _@matthew-campbell: https://gist.github.com/matthew-campbell/5471630 58 | -------------------------------------------------------------------------------- /bitbucket/tests/private/issue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from bitbucket.tests.private.private import AuthenticatedBitbucketTest 3 | 4 | 5 | class IssueAuthenticatedMethodsTest(AuthenticatedBitbucketTest): 6 | """ Testing bitbucket.issue methods.""" 7 | 8 | def test_all(self): 9 | """ Test get all issues.""" 10 | success, result = self.bb.issue.all() 11 | self.assertTrue(success) 12 | self.assertIsInstance(result, dict) 13 | 14 | def _create_issue(self): 15 | # Test create an invalid issue 16 | success, result = self.bb.issue.create() 17 | self.assertFalse(success) 18 | # Test create an issue 19 | success, result = self.bb.issue.create( 20 | title=u'Test Issue Bitbucket API', 21 | content=u'Test Issue Bitbucket API', 22 | responsible=self.bb.username, 23 | status=u'new', 24 | kind=u'bug',) 25 | self.assertTrue(success) 26 | self.assertIsInstance(result, dict) 27 | # Save latest issue's id 28 | self.issue_id = result[u'local_id'] 29 | 30 | def _get_issue(self): 31 | # Test get an issue. 32 | success, result = self.bb.issue.get(issue_id=self.issue_id) 33 | self.assertTrue(success) 34 | self.assertIsInstance(result, dict) 35 | # Test get an invalid issue. 36 | success, result = self.bb.issue.get(issue_id=99999999999) 37 | self.assertFalse(success) 38 | 39 | def _update_issue(self): 40 | # Test issue update. 41 | test_content = 'Test content' 42 | success, result = self.bb.issue.update(issue_id=self.issue_id, 43 | content=test_content) 44 | self.assertTrue(success) 45 | self.assertIsInstance(result, dict) 46 | self.assertEqual(test_content, result[u'content']) 47 | 48 | def _delete_issue(self): 49 | # Test issue delete. 50 | success, result = self.bb.issue.delete(issue_id=self.issue_id) 51 | self.assertTrue(success) 52 | self.assertEqual(result, '') 53 | 54 | success, result = self.bb.issue.get(issue_id=self.issue_id) 55 | self.assertFalse(success) 56 | 57 | def test_CRUD(self): 58 | """ Test issue create/read/update/delete.""" 59 | self._create_issue() 60 | self._get_issue() 61 | self._update_issue() 62 | self._delete_issue() 63 | -------------------------------------------------------------------------------- /bitbucket/tests/private/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from bitbucket.tests.private.private import AuthenticatedBitbucketTest 3 | from bitbucket.tests.public import httpbin 4 | 5 | 6 | class ServiceAuthenticatedMethodsTest(AuthenticatedBitbucketTest): 7 | """ Testing bitbucket.service methods.""" 8 | 9 | def test_all(self): 10 | """ Test get all services.""" 11 | success, result = self.bb.service.all() 12 | self.assertTrue(success) 13 | self.assertIsInstance(result, list) 14 | 15 | def _create_service(self): 16 | # Test create an invalid service 17 | with self.assertRaises(TypeError): 18 | self.bb.service.create() 19 | # Test create an service 20 | success, result = self.bb.service.create( 21 | service=u'POST', 22 | URL=httpbin + 'post',) 23 | self.assertTrue(success) 24 | self.assertIsInstance(result, dict) 25 | # Save latest service's id 26 | self.service_id = result[u'id'] 27 | 28 | def _get_service(self): 29 | # Test get an service. 30 | success, result = self.bb.service.get(service_id=self.service_id) 31 | self.assertTrue(success) 32 | self.assertIsInstance(result, list) 33 | # Test get an invalid service. 34 | success, result = self.bb.service.get(service_id=99999999999) 35 | self.assertTrue(success) 36 | self.assertEqual(result, []) 37 | 38 | def _update_service(self): 39 | # Test service update. 40 | test_url = httpbin + 'get' 41 | success, result = self.bb.service.update(service_id=self.service_id, 42 | URL=test_url) 43 | self.assertTrue(success) 44 | self.assertIsInstance(result, dict) 45 | self.assertEqual(test_url, result[u'service'][u'fields'][0][u'value']) 46 | 47 | def _delete_service(self): 48 | # Test service delete. 49 | success, result = self.bb.service.delete(service_id=self.service_id) 50 | self.assertTrue(success) 51 | self.assertEqual(result, '') 52 | 53 | success, result = self.bb.service.get(service_id=self.service_id) 54 | self.assertTrue(success) 55 | self.assertEqual(result, []) 56 | 57 | def test_CRUD(self): 58 | """ Test service create/read/update/delete.""" 59 | self._create_service() 60 | self._get_service() 61 | self._update_service() 62 | self._delete_service() 63 | -------------------------------------------------------------------------------- /bitbucket/deploy_key.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | URLS = { 3 | # deploy keys 4 | 'GET_DEPLOY_KEYS': 'repositories/%(username)s/%(repo_slug)s/deploy-keys', 5 | 'SET_DEPLOY_KEY': 'repositories/%(username)s/%(repo_slug)s/deploy-keys', 6 | 'GET_DEPLOY_KEY': 'repositories/%(username)s/%(repo_slug)s/deploy-keys/%(key_id)s', 7 | 'DELETE_DEPLOY_KEY': 'repositories/%(username)s/%(repo_slug)s/deploy-keys/%(key_id)s', 8 | } 9 | 10 | 11 | class DeployKey(object): 12 | """ This class provide services-related methods to Bitbucket objects.""" 13 | 14 | def __init__(self, bitbucket): 15 | self.bitbucket = bitbucket 16 | self.bitbucket.URLS.update(URLS) 17 | 18 | def all(self, repo_slug=None): 19 | """ Get all ssh keys associated with a repo 20 | """ 21 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 22 | url = self.bitbucket.url('GET_DEPLOY_KEYS', 23 | username=self.bitbucket.username, 24 | repo_slug=repo_slug) 25 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 26 | 27 | def get(self, repo_slug=None, key_id=None): 28 | """ Get one of the ssh keys associated with this repo 29 | """ 30 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 31 | url = self.bitbucket.url('GET_DEPLOY_KEY', 32 | key_id=key_id, 33 | username=self.bitbucket.username, 34 | repo_slug=repo_slug) 35 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 36 | 37 | def create(self, repo_slug=None, key=None, label=None): 38 | """ Associate an ssh key with your repo and return it. 39 | """ 40 | key = '%s' % key 41 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 42 | url = self.bitbucket.url('SET_DEPLOY_KEY', 43 | username=self.bitbucket.username, 44 | repo_slug=repo_slug) 45 | return self.bitbucket.dispatch('POST', 46 | url, 47 | auth=self.bitbucket.auth, 48 | key=key, 49 | label=label) 50 | 51 | def delete(self, repo_slug=None, key_id=None): 52 | """ Delete one of the ssh keys associated with your repo. 53 | Please use with caution as there is NO confimation and NO undo. 54 | """ 55 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 56 | url = self.bitbucket.url('DELETE_DEPLOY_KEY', 57 | key_id=key_id, 58 | username=self.bitbucket.username, 59 | repo_slug=repo_slug) 60 | return self.bitbucket.dispatch('DELETE', url, auth=self.bitbucket.auth) 61 | -------------------------------------------------------------------------------- /bitbucket/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | __version__ = '0.5.0' 3 | 4 | __doc__ = """ 5 | Bitbucket has a REST API publicly available, this package provide methods to interact with it. 6 | It allows you to access repositories and perform various actions on them. 7 | 8 | Various usages : :: 9 | 10 | from bitbucket.bitbucket import Bitbucket 11 | 12 | # Access a public repository 13 | bb = Bitbucket(USERNAME, repo_name_or_slug="public_repository") 14 | 15 | # Access a private repository 16 | bb = Bitbucket(USERNAME, PASSWORD, repo_name_or_slug="private_repository") 17 | 18 | # Access a private repository through oauth 19 | bb = Bitbucket(USERNAME, repo_name_or_slug="public_repository") 20 | bb.authorize(CONSUMER_KEY, CONSUMER_SECRET, 'http://localhost/') 21 | 22 | 23 | # Access your working repository 24 | success, result = bb.repository.get() 25 | 26 | # Create a repository, and define it as your working repository 27 | success, result = bb.repository.create("repository_slug") 28 | bb.repo_slug = "repository_slug" 29 | 30 | # Update your working repository 31 | success, result = bb.repository.update(description='new description') 32 | 33 | # Delete a repository 34 | success, result = bb.repository.delete("repository_slug") 35 | 36 | # Download a repository as an archive 37 | success, archive_path = bb.repository.archive() 38 | 39 | # Access user informations 40 | success, result = bb.get_user(username=USERNAME) 41 | 42 | # Access tags and branches 43 | success, result = bb.get_tags() 44 | success, result = bb.get_branches() 45 | 46 | # Access, create, update or delete a service (hook) 47 | success, result = bb.service.get(service_id=SERVICE_ID) 48 | success, result = bb.service.create(service=u'POST', URL='http://httpbin.org/') 49 | success, result = bb.service.update(service_id=SERVICE_ID, URL='http://google.com') 50 | success, result = bb.service.delete(service_id=SERVICE_ID) 51 | 52 | # Access, create or delete an SSH key 53 | success, result = bb.ssh.get(key_id=SSH_ID) 54 | success, result = bb.ssh.create(key=r'ssh-rsa a1b2c3d4e5', label=u'my key') 55 | success, result = bb.ssh.delete(key_id=SSH_ID) 56 | 57 | # Access, create, update or delete an issue 58 | success, result = bb.issue.get(issue_id=ISSUE_ID) 59 | success, result = bb.issue.create( 60 | title=u'Issue title', 61 | content=u'Issue content', 62 | responsible=bb.username, 63 | status=u'new', 64 | kind=u'bug') 65 | success, result = bb.issue.update(issue_id=ISSUE_ID, content='New content') 66 | success, result = bb.issue.delete(issue_id=ISSUE_ID) 67 | 68 | # Access, create, update or delete an issue comment 69 | success, result = bb.issue.comment.get(comment_id=COMMENT_ID) 70 | success, result = bb.issue.comment.create(content='Content') 71 | success, result = bb.issue.comment.update( 72 | comment_id=COMMENT_ID, 73 | content='New content') 74 | success, result = bb.issue.comment.delete(comment_id=COMMENT_ID) 75 | 76 | """ 77 | -------------------------------------------------------------------------------- /bitbucket/tests/private/issue_comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from bitbucket.tests.private.private import AuthenticatedBitbucketTest 3 | 4 | 5 | class IssueCommentAuthenticatedMethodsTest(AuthenticatedBitbucketTest): 6 | """ Testing bitbucket.issue.comments methods.""" 7 | 8 | def setUp(self): 9 | """ Add an issue to the test repository and save it's id.""" 10 | super(IssueCommentAuthenticatedMethodsTest, self).setUp() 11 | # Create an issue. 12 | success, result = self.bb.issue.create( 13 | title=u'Test Issue Bitbucket API', 14 | content=u'Test Issue Bitbucket API', 15 | responsible=self.bb.username, 16 | status=u'new', 17 | kind=u'bug',) 18 | # Save latest issue's id 19 | assert success 20 | self.bb.issue.comment.issue_id = result[u'local_id'] 21 | 22 | def tearDown(self): 23 | """ Delete the issue.""" 24 | self.bb.issue.delete(issue_id=self.bb.issue.comment.issue_id) 25 | super(IssueCommentAuthenticatedMethodsTest, self).tearDown() 26 | 27 | def test_all(self): 28 | """ Test get all issue comments.""" 29 | success, result = self.bb.issue.comment.all() 30 | self.assertTrue(success) 31 | self.assertIsInstance(result, list) 32 | 33 | def _create_issue_comment(self): 34 | content = u'Test Issue comment Bitbucket API' 35 | # Test create an issue comment 36 | success, result = self.bb.issue.comment.create( 37 | content=content) 38 | self.assertTrue(success) 39 | self.assertIsInstance(result, dict) 40 | self.assertEqual(result[u'content'], content) 41 | # Save latest issue comment's id 42 | self.comment_id = result[u'comment_id'] 43 | 44 | def _get_issue_comment(self): 45 | # Test get an issue comment. 46 | success, result = self.bb.issue.comment.get(comment_id=self.comment_id) 47 | self.assertTrue(success) 48 | self.assertIsInstance(result, dict) 49 | # Test get an invalid issue comment. 50 | success, result = self.bb.issue.comment.get(comment_id=99999999999) 51 | self.assertFalse(success) 52 | 53 | def _update_issue_comment(self): 54 | # Test issue comment update. 55 | test_content = 'Test content' 56 | success, result = self.bb.issue.comment.update( 57 | comment_id=self.comment_id, 58 | content=test_content) 59 | self.assertTrue(success) 60 | self.assertIsInstance(result, dict) 61 | self.assertEqual(test_content, result[u'content']) 62 | 63 | def _delete_issue_comment(self): 64 | # Test issue comment delete. 65 | success, result = self.bb.issue.comment.delete( 66 | comment_id=self.comment_id) 67 | self.assertTrue(success) 68 | self.assertEqual(result, '') 69 | 70 | success, result = self.bb.issue.comment.get(comment_id=self.comment_id) 71 | self.assertFalse(success) 72 | 73 | def test_CRUD(self): 74 | """ Test issue comment create/read/update/delete.""" 75 | self._create_issue_comment() 76 | self._get_issue_comment() 77 | self._update_issue_comment() 78 | self._delete_issue_comment() 79 | -------------------------------------------------------------------------------- /bitbucket/service.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | URLS = { 3 | # Get services (hooks) 4 | 'GET_SERVICE': 'repositories/%(username)s/%(repo_slug)s/services/%(service_id)s/', 5 | 'GET_SERVICES': 'repositories/%(username)s/%(repo_slug)s/services/', 6 | # Set services (hooks) 7 | 'SET_SERVICE': 'repositories/%(username)s/%(repo_slug)s/services/', 8 | 'UPDATE_SERVICE': 'repositories/%(username)s/%(repo_slug)s/services/%(service_id)s/', 9 | 'DELETE_SERVICE': 'repositories/%(username)s/%(repo_slug)s/services/%(service_id)s/', 10 | } 11 | 12 | 13 | class Service(object): 14 | """ This class provide services-related methods to Bitbucket objects.""" 15 | 16 | def __init__(self, bitbucket): 17 | self.bitbucket = bitbucket 18 | self.bitbucket.URLS.update(URLS) 19 | 20 | def create(self, service, repo_slug=None, **kwargs): 21 | """ Add a service (hook) to one of your repositories. 22 | Each type of service require a different set of additionnal fields, 23 | you can pass them as keyword arguments (fieldname='fieldvalue'). 24 | """ 25 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 26 | url = self.bitbucket.url('SET_SERVICE', username=self.bitbucket.username, repo_slug=repo_slug) 27 | return self.bitbucket.dispatch('POST', url, auth=self.bitbucket.auth, type=service, **kwargs) 28 | 29 | def get(self, service_id, repo_slug=None): 30 | """ Get a service (hook) from one of your repositories. 31 | """ 32 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 33 | url = self.bitbucket.url('GET_SERVICE', username=self.bitbucket.username, repo_slug=repo_slug, service_id=service_id) 34 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 35 | 36 | def update(self, service_id, repo_slug=None, **kwargs): 37 | """ Update a service (hook) from one of your repositories. 38 | """ 39 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 40 | url = self.bitbucket.url('UPDATE_SERVICE', username=self.bitbucket.username, repo_slug=repo_slug, service_id=service_id) 41 | return self.bitbucket.dispatch('PUT', url, auth=self.bitbucket.auth, **kwargs) 42 | 43 | def delete(self, service_id, repo_slug=None): 44 | """ Delete a service (hook) from one of your repositories. 45 | Please use with caution as there is NO confimation and NO undo. 46 | """ 47 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 48 | url = self.bitbucket.url('DELETE_SERVICE', username=self.bitbucket.username, repo_slug=repo_slug, service_id=service_id) 49 | return self.bitbucket.dispatch('DELETE', url, auth=self.bitbucket.auth) 50 | 51 | def all(self, repo_slug=None): 52 | """ Get all services (hook) from one of your repositories. 53 | """ 54 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 55 | url = self.bitbucket.url('GET_SERVICES', username=self.bitbucket.username, repo_slug=repo_slug) 56 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 57 | 58 | # ============ 59 | # = Services = 60 | # ============ 61 | # SERVICES = { 62 | # 'Basecamp': ('Username', 'Password', 'Discussion URL',), 63 | # 'CIA.vc': ('Module', 'Project',), 64 | # 'Email Diff': ('Email',), 65 | # 'Email': ('Email',), 66 | # 'FogBugz': ('Repository ID', 'CVSSubmit URL',), 67 | # 'FriendFeed': ('Username', 'Remote Key', 'Format',), 68 | # 'Geocommit': (None,), 69 | # 'Issues': (None,), 70 | # 'Lighthouse': ('Project ID', 'API Key', 'Subdomain',), 71 | # 'Pivotal Tracker': ('Token',), 72 | # 'POST': ('URL',), 73 | # 'Rietveld': ('Email', 'Password', 'URL',), 74 | # 'Superfeedr': (None,), 75 | # } 76 | -------------------------------------------------------------------------------- /bitbucket/tests/private/private.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import unittest 3 | import webbrowser 4 | 5 | from bitbucket.bitbucket import Bitbucket 6 | 7 | TEST_REPO_SLUG = 'test_bitbucket_api' 8 | 9 | # Store oauth credentials between tests 10 | OAUTH_ACCESS_TOKEN = '' 11 | OAUTH_ACCESS_TOKEN_SECRET = '' 12 | 13 | 14 | class AuthenticatedBitbucketTest(unittest.TestCase): 15 | """ Bitbucket test base class for authenticated methods.""" 16 | def setUp(self): 17 | """Creating a new authenticated Bitbucket...""" 18 | try: 19 | # Try and get OAuth credentials first, if that fails try basic auth 20 | from settings import USERNAME, CONSUMER_KEY, CONSUMER_SECRET 21 | PASSWORD = None 22 | except ImportError: 23 | try: 24 | # TODO : check validity of credentials ? 25 | from settings import USERNAME, PASSWORD 26 | CONSUMER_KEY = None 27 | CONSUMER_SECRET = None 28 | except ImportError: 29 | # Private tests require username and password of an existing user. 30 | raise ImportError('Please provide either USERNAME and PASSWORD or USERNAME, CONSUMER_KEY and CONSUMER_SECRET in bitbucket/tests/private/settings.py.') 31 | 32 | if USERNAME and PASSWORD: 33 | self.bb = Bitbucket(USERNAME, PASSWORD) 34 | elif USERNAME and CONSUMER_KEY and CONSUMER_SECRET: 35 | # Try Oauth authentication 36 | global OAUTH_ACCESS_TOKEN, OAUTH_ACCESS_TOKEN_SECRET 37 | self.bb = Bitbucket(USERNAME) 38 | 39 | # First time we need to open up a browser to enter the verifier 40 | if not OAUTH_ACCESS_TOKEN and not OAUTH_ACCESS_TOKEN_SECRET: 41 | self.bb.authorize(CONSUMER_KEY, CONSUMER_SECRET, 'http://localhost/') 42 | # open a webbrowser and get the token 43 | webbrowser.open(self.bb.url('AUTHENTICATE', token=self.bb.access_token)) 44 | # Copy the verifier field from the URL in the browser into the console 45 | token_is_valid = False 46 | while not token_is_valid: 47 | # Ensure a valid oauth_verifier before starting tests 48 | oauth_verifier = raw_input('Enter verifier from url [oauth_verifier]') 49 | token_is_valid = bool(oauth_verifier and self.bb.verify(oauth_verifier)[0]) 50 | if not token_is_valid: 51 | print('Invalid oauth_verifier, please try again or quit with CONTROL-C.') 52 | OAUTH_ACCESS_TOKEN = self.bb.access_token 53 | OAUTH_ACCESS_TOKEN_SECRET = self.bb.access_token_secret 54 | else: 55 | self.bb.authorize(CONSUMER_KEY, CONSUMER_SECRET, 'http://localhost/', OAUTH_ACCESS_TOKEN, OAUTH_ACCESS_TOKEN_SECRET) 56 | 57 | # Create a repository. 58 | success, result = self.bb.repository.create(TEST_REPO_SLUG, has_issues=True) 59 | # Save repository's id 60 | assert success 61 | self.bb.repo_slug = result[u'slug'] 62 | 63 | def tearDown(self): 64 | """Destroying the Bitbucket...""" 65 | # Delete the repository. 66 | self.bb.repository.delete() 67 | self.bb = None 68 | 69 | 70 | class BitbucketAuthenticatedMethodsTest(AuthenticatedBitbucketTest): 71 | """ Testing Bitbucket annonymous methods.""" 72 | 73 | def test_get_tags(self): 74 | """ Test get_tags.""" 75 | success, result = self.bb.get_tags() 76 | self.assertTrue(success) 77 | self.assertIsInstance(result, dict) 78 | # test with invalid repository name 79 | success, result = self.bb.get_tags(repo_slug='azertyuiop') 80 | self.assertFalse(success) 81 | 82 | def test_get_branches(self): 83 | """ Test get_branches.""" 84 | success, result = self.bb.get_branches() 85 | self.assertTrue(success) 86 | self.assertIsInstance(result, dict) 87 | # test with invalid repository name 88 | success, result = self.bb.get_branches(repo_slug='azertyuiop') 89 | self.assertFalse(success) 90 | -------------------------------------------------------------------------------- /bitbucket/issue_comment.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | URLS = { 3 | # Issue comments 4 | 'GET_COMMENTS': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/comments/', 5 | 'GET_COMMENT': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/comments/%(comment_id)s/', 6 | 'CREATE_COMMENT': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/comments/', 7 | 'UPDATE_COMMENT': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/comments/%(comment_id)s/', 8 | 'DELETE_COMMENT': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/comments/%(comment_id)s/', 9 | } 10 | 11 | 12 | class IssueComment(object): 13 | """ This class provide issue's comments related methods to Bitbucket objects.""" 14 | 15 | def __init__(self, issue): 16 | self.issue = issue 17 | self.bitbucket = self.issue.bitbucket 18 | self.bitbucket.URLS.update(URLS) 19 | self.issue_id = issue.issue_id 20 | 21 | def all(self, issue_id=None, repo_slug=None): 22 | """ Get issue comments from one of your repositories. 23 | """ 24 | issue_id = issue_id or self.issue_id 25 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 26 | url = self.bitbucket.url('GET_COMMENTS', 27 | username=self.bitbucket.username, 28 | repo_slug=repo_slug, 29 | issue_id=issue_id) 30 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 31 | 32 | def get(self, comment_id, issue_id=None, repo_slug=None): 33 | """ Get an issue from one of your repositories. 34 | """ 35 | issue_id = issue_id or self.issue_id 36 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 37 | url = self.bitbucket.url('GET_COMMENT', 38 | username=self.bitbucket.username, 39 | repo_slug=repo_slug, 40 | issue_id=issue_id, 41 | comment_id=comment_id) 42 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 43 | 44 | def create(self, issue_id=None, repo_slug=None, **kwargs): 45 | """ Add an issue comment to one of your repositories. 46 | Each issue comment require only the content data field 47 | the system autopopulate the rest. 48 | """ 49 | issue_id = issue_id or self.issue_id 50 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 51 | url = self.bitbucket.url('CREATE_COMMENT', 52 | username=self.bitbucket.username, 53 | repo_slug=repo_slug, 54 | issue_id=issue_id) 55 | return self.bitbucket.dispatch('POST', url, auth=self.bitbucket.auth, **kwargs) 56 | 57 | def update(self, comment_id, issue_id=None, repo_slug=None, **kwargs): 58 | """ Update an issue comment in one of your repositories. 59 | Each issue comment require only the content data field 60 | the system autopopulate the rest. 61 | """ 62 | issue_id = issue_id or self.issue_id 63 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 64 | url = self.bitbucket.url('UPDATE_COMMENT', 65 | username=self.bitbucket.username, 66 | repo_slug=repo_slug, 67 | issue_id=issue_id, 68 | comment_id=comment_id) 69 | return self.bitbucket.dispatch('PUT', url, auth=self.bitbucket.auth, **kwargs) 70 | 71 | def delete(self, comment_id, issue_id=None, repo_slug=None): 72 | """ Delete an issue from one of your repositories. 73 | """ 74 | issue_id = issue_id or self.issue_id 75 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 76 | url = self.bitbucket.url('DELETE_COMMENT', 77 | username=self.bitbucket.username, 78 | repo_slug=repo_slug, 79 | issue_id=issue_id, 80 | comment_id=comment_id) 81 | return self.bitbucket.dispatch('DELETE', url, auth=self.bitbucket.auth) 82 | -------------------------------------------------------------------------------- /bitbucket/tests/private/repository.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from functools import wraps 3 | from zipfile import is_zipfile 4 | import os 5 | import random 6 | import sh 7 | import string 8 | import unittest 9 | 10 | from bitbucket.tests.private.private import AuthenticatedBitbucketTest 11 | 12 | TEST_REPO_SLUG = "test_repository_creation" 13 | 14 | 15 | def skipUnlessHasGit(f): 16 | """ This decorator pass the test if git is not found.""" 17 | @wraps(f) 18 | def _decorator(): 19 | try: 20 | sh.git(version=True, _out='/dev/null') 21 | return f() 22 | except sh.CommandNotFound: 23 | return unittest.skip("Git is not installed") 24 | return _decorator 25 | 26 | 27 | class RepositoryAuthenticatedMethodsTest(AuthenticatedBitbucketTest): 28 | """ Testing bitbucket.repository methods.""" 29 | 30 | def test_all(self): 31 | """ Test get all repositories.""" 32 | success, result = self.bb.repository.all() 33 | self.assertTrue(success) 34 | self.assertIsInstance(result, list) 35 | 36 | def test_get(self): 37 | """ Test get a repository.""" 38 | success, result = self.bb.repository.get() 39 | self.assertTrue(success) 40 | self.assertIsInstance(result, dict) 41 | 42 | def test_create(self): 43 | """ Test repository creation.""" 44 | # TODO : test private/public repository creation 45 | success, result = self.bb.repository.create(TEST_REPO_SLUG) 46 | self.assertTrue(success) 47 | self.assertIsInstance(result, dict) 48 | # Delete repo 49 | success, result = self.bb.repository.delete(repo_slug=TEST_REPO_SLUG) 50 | assert success 51 | 52 | def test_update(self): 53 | """ Test repository update.""" 54 | # Try to change description 55 | test_description = 'Test Description' 56 | success, result = self.bb.repository.update(description=test_description) 57 | self.assertTrue(success) 58 | self.assertIsInstance(result, dict) 59 | self.assertEqual(test_description, result[u'description']) 60 | 61 | def test_delete(self): 62 | """ Test repository deletion.""" 63 | # Create repo 64 | success, result = self.bb.repository.create(TEST_REPO_SLUG) 65 | assert success 66 | # Delete it 67 | success, result = self.bb.repository.delete(repo_slug=TEST_REPO_SLUG) 68 | self.assertTrue(success) 69 | self.assertEqual(result, '') 70 | 71 | success, result = self.bb.repository.get(repo_slug=TEST_REPO_SLUG) 72 | self.assertFalse(success) 73 | 74 | 75 | class ArchiveRepositoryAuthenticatedMethodsTest(AuthenticatedBitbucketTest): 76 | """ 77 | Testing bitbucket.repository.archive method, which require 78 | custom setUp and tearDown methods. 79 | 80 | test_archive require a commit to download the repository. 81 | """ 82 | 83 | def setUp(self): 84 | """ Clone the test repo locally, then add and push a commit.""" 85 | super(ArchiveRepositoryAuthenticatedMethodsTest, self).setUp() 86 | # Clone test repository localy. 87 | repo_origin = 'git@bitbucket.org:%s/%s.git' % (self.bb.username, self.bb.repo_slug) 88 | # TODO : Put the temp folder on the right place for windows. 89 | repo_folder = os.path.join( 90 | '/tmp', 91 | 'bitbucket-' + ''.join(random.choice(string.digits + string.letters) for x in range(10))) 92 | sh.mkdir(repo_folder) 93 | sh.cd(repo_folder) 94 | self.pwd = sh.pwd().strip() 95 | sh.git.init() 96 | sh.git.remote('add', 'origin', repo_origin) 97 | # Add commit with empty file. 98 | sh.touch('file') 99 | sh.git.add('.') 100 | sh.git.commit('-m', '"Add empty file."') 101 | sh.git.push('origin', 'master') 102 | 103 | def tearDown(self): 104 | """ Delete the git folder.""" 105 | super(ArchiveRepositoryAuthenticatedMethodsTest, self).tearDown() 106 | sh.rm('-rf', self.pwd) 107 | 108 | @skipUnlessHasGit 109 | def test_archive(self): 110 | """ Test repository download as archive.""" 111 | success, archive_path = self.bb.repository.archive() 112 | self.assertTrue(success) 113 | self.assertTrue(os.path.exists(archive_path)) 114 | self.assertTrue(is_zipfile(archive_path)) 115 | # delete temporary file 116 | os.unlink(archive_path) 117 | -------------------------------------------------------------------------------- /bitbucket/issue.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from .issue_comment import IssueComment 3 | 4 | 5 | URLS = { 6 | # Issues 7 | 'GET_ISSUES': 'repositories/%(username)s/%(repo_slug)s/issues/', 8 | 'GET_ISSUE': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/', 9 | 'CREATE_ISSUE': 'repositories/%(username)s/%(repo_slug)s/issues/', 10 | 'UPDATE_ISSUE': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/', 11 | 'DELETE_ISSUE': 'repositories/%(username)s/%(repo_slug)s/issues/%(issue_id)s/', 12 | } 13 | 14 | 15 | class Issue(object): 16 | """ This class provide issue-related methods to Bitbucket objects.""" 17 | 18 | def __init__(self, bitbucket, issue_id=None): 19 | self.bitbucket = bitbucket 20 | self.bitbucket.URLS.update(URLS) 21 | self.issue_id = issue_id 22 | self.comment = IssueComment(self) 23 | 24 | @property 25 | def issue_id(self): 26 | """Your repository slug name.""" 27 | return self._issue_id 28 | 29 | @issue_id.setter 30 | def issue_id(self, value): 31 | if value: 32 | self._issue_id = int(value) 33 | elif value is None: 34 | self._issue_id = None 35 | 36 | @issue_id.deleter 37 | def issue_id(self): 38 | del self._issue_id 39 | 40 | def all(self, repo_slug=None, params=None): 41 | """ Get issues from one of your repositories. 42 | """ 43 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 44 | url = self.bitbucket.url('GET_ISSUES', username=self.bitbucket.username, repo_slug=repo_slug) 45 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth, params=params) 46 | 47 | def get(self, issue_id, repo_slug=None): 48 | """ Get an issue from one of your repositories. 49 | """ 50 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 51 | url = self.bitbucket.url('GET_ISSUE', username=self.bitbucket.username, repo_slug=repo_slug, issue_id=issue_id) 52 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 53 | 54 | def create(self, repo_slug=None, **kwargs): 55 | """ 56 | Add an issue to one of your repositories. 57 | Each issue require a different set of attributes, 58 | you can pass them as keyword arguments (attributename='attributevalue'). 59 | Attributes are: 60 | 61 | * title: The title of the new issue. 62 | * content: The content of the new issue. 63 | * component: The component associated with the issue. 64 | * milestone: The milestone associated with the issue. 65 | * version: The version associated with the issue. 66 | * responsible: The username of the person responsible for the issue. 67 | * status: The status of the issue (new, open, resolved, on hold, invalid, duplicate, or wontfix). 68 | * kind: The kind of issue (bug, enhancement, or proposal). 69 | """ 70 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 71 | url = self.bitbucket.url('CREATE_ISSUE', username=self.bitbucket.username, repo_slug=repo_slug) 72 | return self.bitbucket.dispatch('POST', url, auth=self.bitbucket.auth, **kwargs) 73 | 74 | def update(self, issue_id, repo_slug=None, **kwargs): 75 | """ 76 | Update an issue to one of your repositories. 77 | Each issue require a different set of attributes, 78 | you can pass them as keyword arguments (attributename='attributevalue'). 79 | Attributes are: 80 | 81 | * title: The title of the new issue. 82 | * content: The content of the new issue. 83 | * component: The component associated with the issue. 84 | * milestone: The milestone associated with the issue. 85 | * version: The version associated with the issue. 86 | * responsible: The username of the person responsible for the issue. 87 | * status: The status of the issue (new, open, resolved, on hold, invalid, duplicate, or wontfix). 88 | * kind: The kind of issue (bug, enhancement, or proposal). 89 | """ 90 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 91 | url = self.bitbucket.url('UPDATE_ISSUE', username=self.bitbucket.username, repo_slug=repo_slug, issue_id=issue_id) 92 | return self.bitbucket.dispatch('PUT', url, auth=self.bitbucket.auth, **kwargs) 93 | 94 | def delete(self, issue_id, repo_slug=None): 95 | """ Delete an issue from one of your repositories. 96 | """ 97 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 98 | url = self.bitbucket.url('DELETE_ISSUE', username=self.bitbucket.username, repo_slug=repo_slug, issue_id=issue_id) 99 | return self.bitbucket.dispatch('DELETE', url, auth=self.bitbucket.auth) 100 | -------------------------------------------------------------------------------- /bitbucket/tests/public.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # run with `site-packages$> python -m bitbucket.tests.public` 3 | import unittest 4 | from bitbucket.bitbucket import Bitbucket 5 | 6 | 7 | httpbin = 'http://httpbin.org/' 8 | foo = u'foo' 9 | bar = u'bar' 10 | username = 'baptistemillou' 11 | 12 | 13 | class AnonymousBitbucketTest(unittest.TestCase): 14 | """ Bitbucket test base class.""" 15 | def setUp(self): 16 | """Create a new annonymous Bitbucket...""" 17 | self.bb = Bitbucket() 18 | 19 | def tearDown(self): 20 | """Destroy the Bitbucket...""" 21 | self.bb = None 22 | 23 | 24 | class BitbucketUtilitiesTest(AnonymousBitbucketTest): 25 | """ Test Bitbucket utilities functions.""" 26 | 27 | def test_default_credential(self): 28 | self.assertEqual(self.bb.username, '') 29 | self.assertEqual(self.bb.password, '') 30 | self.assertEqual(self.bb.repo_slug, '') 31 | 32 | def test_dispatch_get(self): 33 | success, result = self.bb.dispatch('GET', httpbin + 'get') 34 | self.assertTrue(success) 35 | self.assertIsInstance(result, dict) 36 | 37 | def test_dispatch_post(self): 38 | success, result = self.bb.dispatch('POST', httpbin + 'post', foo='bar') 39 | self.assertTrue(success) 40 | self.assertIsInstance(result, dict) 41 | self.assertEqual(result['form'], {foo: bar}) 42 | 43 | def test_dispatch_put(self): 44 | success, result = self.bb.dispatch('PUT', httpbin + 'put', foo='bar') 45 | self.assertTrue(success) 46 | self.assertIsInstance(result, dict) 47 | self.assertEqual(result['form'], {foo: bar}) 48 | 49 | def test_dispatch_delete(self): 50 | success, result = self.bb.dispatch('DELETE', httpbin + 'delete') 51 | self.assertTrue(success) 52 | self.assertIsInstance(result, dict) 53 | 54 | def test_url_simple(self): 55 | base = self.bb.URLS['BASE'] 56 | create_repo = self.bb.URLS['CREATE_REPO'] 57 | self.assertEqual(self.bb.url('CREATE_REPO'), base % create_repo) 58 | 59 | def test_url_complex(self): 60 | base = self.bb.URLS['BASE'] 61 | get_branches = self.bb.URLS['GET_BRANCHES'] 62 | self.assertEqual( 63 | self.bb.url('GET_BRANCHES', 64 | username=self.bb.username, 65 | repo_slug=self.bb.repo_slug), 66 | base % get_branches % {'username': '', 'repo_slug': ''}) 67 | 68 | def test_auth(self): 69 | self.assertEqual(self.bb.auth, (self.bb.username, self.bb.password)) 70 | 71 | def test_username(self): 72 | self.bb.username = foo 73 | self.assertEqual(self.bb.username, foo) 74 | 75 | del self.bb.username 76 | with self.assertRaises(AttributeError): 77 | self.bb.username 78 | 79 | def test_password(self): 80 | self.bb.password = foo 81 | self.assertEqual(self.bb.password, foo) 82 | 83 | del self.bb.password 84 | with self.assertRaises(AttributeError): 85 | self.bb.password 86 | 87 | def test_repo_slug(self): 88 | self.bb.repo_slug = foo 89 | self.assertEqual(self.bb.repo_slug, foo) 90 | 91 | del self.bb.repo_slug 92 | with self.assertRaises(AttributeError): 93 | self.bb.repo_slug 94 | 95 | 96 | class BitbucketAnnonymousMethodsTest(AnonymousBitbucketTest): 97 | """ Test Bitbucket annonymous methods.""" 98 | 99 | def test_get_user(self): 100 | """ Test get_user on specific user.""" 101 | success, result = self.bb.get_user(username=username) 102 | self.assertTrue(success) 103 | self.assertIsInstance(result, dict) 104 | 105 | def test_get_self_user(self): 106 | """ Test get_user on self username.""" 107 | self.bb.username = username 108 | success, result = self.bb.get_user() 109 | self.assertTrue(success) 110 | self.assertIsInstance(result, dict) 111 | 112 | def test_get_none_user(self): 113 | """ Test get_user with no username.""" 114 | self.bb.username = None 115 | success, result = self.bb.get_user() 116 | self.assertFalse(success) 117 | self.assertEqual(result, 'Service not found.') 118 | 119 | def test_get_public_repos(self): 120 | """ Test public_repos on specific user.""" 121 | success, result = self.bb.repository.public(username=username) 122 | self.assertTrue(success) 123 | self.assertIsInstance(result, (dict, list)) 124 | 125 | def test_get_self_public_repos(self): 126 | """ Test public_repos on specific user.""" 127 | self.bb.username = username 128 | success, result = self.bb.repository.public() 129 | self.assertTrue(success) 130 | self.assertIsInstance(result, (dict, list)) 131 | 132 | def test_get_none_public_repos(self): 133 | """ Test public_repos on specific user.""" 134 | self.bb.username = None 135 | success, result = self.bb.repository.public() 136 | self.assertFalse(success) 137 | self.assertEqual(result, 'Service not found.') 138 | 139 | if __name__ == "__main__": 140 | unittest.main() 141 | -------------------------------------------------------------------------------- /bitbucket/repository.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from tempfile import NamedTemporaryFile 3 | from zipfile import ZipFile 4 | 5 | 6 | URLS = { 7 | 'CREATE_REPO': 'repositories/', 8 | 'GET_REPO': 'repositories/%(username)s/%(repo_slug)s/', 9 | 'UPDATE_REPO': 'repositories/%(username)s/%(repo_slug)s/', 10 | 'DELETE_REPO': 'repositories/%(username)s/%(repo_slug)s/', 11 | # Get archive 12 | 'GET_ARCHIVE': 'repositories/%(username)s/%(repo_slug)s/%(format)s/master/', 13 | } 14 | 15 | 16 | class Repository(object): 17 | """ This class provide repository-related methods to Bitbucket objects.""" 18 | 19 | def __init__(self, bitbucket): 20 | self.bitbucket = bitbucket 21 | self.bitbucket.URLS.update(URLS) 22 | 23 | def _get_files_in_dir(self, repo_slug=None, dir='/'): 24 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 25 | dir = dir.lstrip('/') 26 | url = self.bitbucket.url( 27 | 'GET_ARCHIVE', 28 | username=self.bitbucket.username, 29 | repo_slug=repo_slug, 30 | format='src') 31 | dir_url = url + dir 32 | response = self.bitbucket.dispatch('GET', dir_url, auth=self.bitbucket.auth) 33 | if response[0] and isinstance(response[1], dict): 34 | repo_tree = response[1] 35 | url = self.bitbucket.url( 36 | 'GET_ARCHIVE', 37 | username=self.bitbucket.username, 38 | repo_slug=repo_slug, 39 | format='raw') 40 | # Download all files in dir 41 | for file in repo_tree['files']: 42 | file_url = url + '/'.join((file['path'],)) 43 | response = self.bitbucket.dispatch('GET', file_url, auth=self.bitbucket.auth) 44 | self.bitbucket.repo_tree[file['path']] = response[1] 45 | # recursively download in dirs 46 | for directory in repo_tree['directories']: 47 | dir_path = '/'.join((dir, directory)) 48 | self._get_files_in_dir(repo_slug=repo_slug, dir=dir_path) 49 | 50 | def public(self, username=None): 51 | """ Returns all public repositories from an user. 52 | If username is not defined, tries to return own public repos. 53 | """ 54 | username = username or self.bitbucket.username or '' 55 | url = self.bitbucket.url('GET_USER', username=username) 56 | response = self.bitbucket.dispatch('GET', url) 57 | try: 58 | return (response[0], response[1]['repositories']) 59 | except TypeError: 60 | pass 61 | return response 62 | 63 | def all(self): 64 | """ Return own repositories.""" 65 | url = self.bitbucket.url('GET_USER', username=self.bitbucket.username) 66 | response = self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 67 | try: 68 | return (response[0], response[1]['repositories']) 69 | except TypeError: 70 | pass 71 | return response 72 | 73 | def get(self, repo_slug=None): 74 | """ Get a single repository on Bitbucket and return it.""" 75 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 76 | url = self.bitbucket.url('GET_REPO', username=self.bitbucket.username, repo_slug=repo_slug) 77 | return self.bitbucket.dispatch('GET', url, auth=self.bitbucket.auth) 78 | 79 | def create(self, repo_name, scm='git', private=True, **kwargs): 80 | """ Creates a new repository on own Bitbucket account and return it.""" 81 | url = self.bitbucket.url('CREATE_REPO') 82 | return self.bitbucket.dispatch('POST', url, auth=self.bitbucket.auth, name=repo_name, scm=scm, is_private=private, **kwargs) 83 | 84 | def update(self, repo_slug=None, **kwargs): 85 | """ Updates repository on own Bitbucket account and return it.""" 86 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 87 | url = self.bitbucket.url('UPDATE_REPO', username=self.bitbucket.username, repo_slug=repo_slug) 88 | return self.bitbucket.dispatch('PUT', url, auth=self.bitbucket.auth, **kwargs) 89 | 90 | def delete(self, repo_slug=None): 91 | """ Delete a repository on own Bitbucket account. 92 | Please use with caution as there is NO confimation and NO undo. 93 | """ 94 | repo_slug = repo_slug or self.bitbucket.repo_slug or '' 95 | url = self.bitbucket.url('DELETE_REPO', username=self.bitbucket.username, repo_slug=repo_slug) 96 | return self.bitbucket.dispatch('DELETE', url, auth=self.bitbucket.auth) 97 | 98 | def archive(self, repo_slug=None, format='zip', prefix=''): 99 | """ Get one of your repositories and compress it as an archive. 100 | Return the path of the archive. 101 | 102 | format parameter is curently not supported. 103 | """ 104 | prefix = '%s'.lstrip('/') % prefix 105 | self._get_files_in_dir(repo_slug=repo_slug, dir='/') 106 | if self.bitbucket.repo_tree: 107 | with NamedTemporaryFile(delete=False) as archive: 108 | with ZipFile(archive, 'w') as zip_archive: 109 | for name, file in self.bitbucket.repo_tree.items(): 110 | with NamedTemporaryFile(delete=False) as temp_file: 111 | temp_file.write(file.encode('utf-8')) 112 | zip_archive.write(temp_file.name, prefix + name) 113 | return (True, archive.name) 114 | return (False, 'Could not archive your project.') 115 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Bitbucket-API.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Bitbucket-API.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Bitbucket-API" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Bitbucket-API" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | goto end 41 | ) 42 | 43 | if "%1" == "clean" ( 44 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 45 | del /q /s %BUILDDIR%\* 46 | goto end 47 | ) 48 | 49 | 50 | %SPHINXBUILD% 2> nul 51 | if errorlevel 9009 ( 52 | echo. 53 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 54 | echo.installed, then set the SPHINXBUILD environment variable to point 55 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 56 | echo.may add the Sphinx directory to PATH. 57 | echo. 58 | echo.If you don't have Sphinx installed, grab it from 59 | echo.http://sphinx-doc.org/ 60 | exit /b 1 61 | ) 62 | 63 | if "%1" == "html" ( 64 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 68 | goto end 69 | ) 70 | 71 | if "%1" == "dirhtml" ( 72 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 76 | goto end 77 | ) 78 | 79 | if "%1" == "singlehtml" ( 80 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 84 | goto end 85 | ) 86 | 87 | if "%1" == "pickle" ( 88 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can process the pickle files. 92 | goto end 93 | ) 94 | 95 | if "%1" == "json" ( 96 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 97 | if errorlevel 1 exit /b 1 98 | echo. 99 | echo.Build finished; now you can process the JSON files. 100 | goto end 101 | ) 102 | 103 | if "%1" == "htmlhelp" ( 104 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 105 | if errorlevel 1 exit /b 1 106 | echo. 107 | echo.Build finished; now you can run HTML Help Workshop with the ^ 108 | .hhp project file in %BUILDDIR%/htmlhelp. 109 | goto end 110 | ) 111 | 112 | if "%1" == "qthelp" ( 113 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 114 | if errorlevel 1 exit /b 1 115 | echo. 116 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 117 | .qhcp project file in %BUILDDIR%/qthelp, like this: 118 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Bitbucket-API.qhcp 119 | echo.To view the help file: 120 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Bitbucket-API.ghc 121 | goto end 122 | ) 123 | 124 | if "%1" == "devhelp" ( 125 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished. 129 | goto end 130 | ) 131 | 132 | if "%1" == "epub" ( 133 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 137 | goto end 138 | ) 139 | 140 | if "%1" == "latex" ( 141 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 145 | goto end 146 | ) 147 | 148 | if "%1" == "latexpdf" ( 149 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 150 | cd %BUILDDIR%/latex 151 | make all-pdf 152 | cd %BUILDDIR%/.. 153 | echo. 154 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 155 | goto end 156 | ) 157 | 158 | if "%1" == "latexpdfja" ( 159 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 160 | cd %BUILDDIR%/latex 161 | make all-pdf-ja 162 | cd %BUILDDIR%/.. 163 | echo. 164 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 165 | goto end 166 | ) 167 | 168 | if "%1" == "text" ( 169 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 170 | if errorlevel 1 exit /b 1 171 | echo. 172 | echo.Build finished. The text files are in %BUILDDIR%/text. 173 | goto end 174 | ) 175 | 176 | if "%1" == "man" ( 177 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 178 | if errorlevel 1 exit /b 1 179 | echo. 180 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 181 | goto end 182 | ) 183 | 184 | if "%1" == "texinfo" ( 185 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 186 | if errorlevel 1 exit /b 1 187 | echo. 188 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 189 | goto end 190 | ) 191 | 192 | if "%1" == "gettext" ( 193 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 194 | if errorlevel 1 exit /b 1 195 | echo. 196 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 197 | goto end 198 | ) 199 | 200 | if "%1" == "changes" ( 201 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 202 | if errorlevel 1 exit /b 1 203 | echo. 204 | echo.The overview file is in %BUILDDIR%/changes. 205 | goto end 206 | ) 207 | 208 | if "%1" == "linkcheck" ( 209 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 210 | if errorlevel 1 exit /b 1 211 | echo. 212 | echo.Link check complete; look for any errors in the above output ^ 213 | or in %BUILDDIR%/linkcheck/output.txt. 214 | goto end 215 | ) 216 | 217 | if "%1" == "doctest" ( 218 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 219 | if errorlevel 1 exit /b 1 220 | echo. 221 | echo.Testing of doctests in the sources finished, look at the ^ 222 | results in %BUILDDIR%/doctest/output.txt. 223 | goto end 224 | ) 225 | 226 | if "%1" == "xml" ( 227 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 228 | if errorlevel 1 exit /b 1 229 | echo. 230 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 231 | goto end 232 | ) 233 | 234 | if "%1" == "pseudoxml" ( 235 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 236 | if errorlevel 1 exit /b 1 237 | echo. 238 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 239 | goto end 240 | ) 241 | 242 | :end 243 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Bitbucket-API documentation build configuration file, created by 4 | # sphinx-quickstart on Thu May 23 14:55:30 2013. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | sys.path.insert(0, os.path.abspath('..')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'Bitbucket-API' 44 | copyright = u'2013, Baptiste Millou' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.4' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.4.4dev' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build', ] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | # If true, keep warnings as "system message" paragraphs in the built documents. 90 | #keep_warnings = False 91 | 92 | 93 | # -- Options for HTML output --------------------------------------------------- 94 | 95 | # The theme to use for HTML and HTML Help pages. See the documentation for 96 | # a list of builtin themes. 97 | html_theme = 'default' 98 | 99 | # Theme options are theme-specific and customize the look and feel of a theme 100 | # further. For a list of options available for each theme, see the 101 | # documentation. 102 | #html_theme_options = {} 103 | 104 | # Add any paths that contain custom themes here, relative to this directory. 105 | #html_theme_path = [] 106 | 107 | # The name for this set of Sphinx documents. If None, it defaults to 108 | # " v documentation". 109 | #html_title = None 110 | 111 | # A shorter title for the navigation bar. Default is the same as html_title. 112 | #html_short_title = None 113 | 114 | # The name of an image file (relative to this directory) to place at the top 115 | # of the sidebar. 116 | #html_logo = None 117 | 118 | # The name of an image file (within the static path) to use as favicon of the 119 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 120 | # pixels large. 121 | #html_favicon = None 122 | 123 | # Add any paths that contain custom static files (such as style sheets) here, 124 | # relative to this directory. They are copied after the builtin static files, 125 | # so a file named "default.css" will overwrite the builtin "default.css". 126 | html_static_path = ['_static'] 127 | 128 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 129 | # using the given strftime format. 130 | #html_last_updated_fmt = '%b %d, %Y' 131 | 132 | # If true, SmartyPants will be used to convert quotes and dashes to 133 | # typographically correct entities. 134 | #html_use_smartypants = True 135 | 136 | # Custom sidebar templates, maps document names to template names. 137 | #html_sidebars = {} 138 | 139 | # Additional templates that should be rendered to pages, maps page names to 140 | # template names. 141 | #html_additional_pages = {} 142 | 143 | # If false, no module index is generated. 144 | #html_domain_indices = True 145 | 146 | # If false, no index is generated. 147 | #html_use_index = True 148 | 149 | # If true, the index is split into individual pages for each letter. 150 | #html_split_index = False 151 | 152 | # If true, links to the reST sources are added to the pages. 153 | #html_show_sourcelink = True 154 | 155 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 156 | #html_show_sphinx = True 157 | 158 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 159 | #html_show_copyright = True 160 | 161 | # If true, an OpenSearch description file will be output, and all pages will 162 | # contain a tag referring to it. The value of this option must be the 163 | # base URL from which the finished HTML is served. 164 | #html_use_opensearch = '' 165 | 166 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 167 | #html_file_suffix = None 168 | 169 | # Output file base name for HTML help builder. 170 | htmlhelp_basename = 'Bitbucket-APIdoc' 171 | 172 | 173 | # -- Options for LaTeX output -------------------------------------------------- 174 | 175 | latex_elements = { 176 | # The paper size ('letterpaper' or 'a4paper'). 177 | #'papersize': 'letterpaper', 178 | 179 | # The font size ('10pt', '11pt' or '12pt'). 180 | #'pointsize': '10pt', 181 | 182 | # Additional stuff for the LaTeX preamble. 183 | #'preamble': '', 184 | } 185 | 186 | # Grouping the document tree into LaTeX files. List of tuples 187 | # (source start file, target name, title, author, documentclass [howto/manual]). 188 | latex_documents = [ 189 | ('index', 'Bitbucket-API.tex', u'Bitbucket-API Documentation', 190 | u'Baptiste Millou', 'manual'), 191 | ] 192 | 193 | # The name of an image file (relative to this directory) to place at the top of 194 | # the title page. 195 | #latex_logo = None 196 | 197 | # For "manual" documents, if this is true, then toplevel headings are parts, 198 | # not chapters. 199 | #latex_use_parts = False 200 | 201 | # If true, show page references after internal links. 202 | #latex_show_pagerefs = False 203 | 204 | # If true, show URL addresses after external links. 205 | #latex_show_urls = False 206 | 207 | # Documents to append as an appendix to all manuals. 208 | #latex_appendices = [] 209 | 210 | # If false, no module index is generated. 211 | #latex_domain_indices = True 212 | 213 | 214 | # -- Options for manual page output -------------------------------------------- 215 | 216 | # One entry per manual page. List of tuples 217 | # (source start file, name, description, authors, manual section). 218 | man_pages = [ 219 | ('index', 'bitbucket-api', u'Bitbucket-API Documentation', 220 | [u'Baptiste Millou'], 1) 221 | ] 222 | 223 | # If true, show URL addresses after external links. 224 | #man_show_urls = False 225 | 226 | 227 | # -- Options for Texinfo output ------------------------------------------------ 228 | 229 | # Grouping the document tree into Texinfo files. List of tuples 230 | # (source start file, target name, title, author, 231 | # dir menu entry, description, category) 232 | texinfo_documents = [ 233 | ('index', 'Bitbucket-API', u'Bitbucket-API Documentation', 234 | u'Baptiste Millou', 'Bitbucket-API', 'One line description of project.', 235 | 'Miscellaneous'), 236 | ] 237 | 238 | # Documents to append as an appendix to all manuals. 239 | #texinfo_appendices = [] 240 | 241 | # If false, no module index is generated. 242 | #texinfo_domain_indices = True 243 | 244 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 245 | #texinfo_show_urls = 'footnote' 246 | 247 | # If true, do not generate a @detailmenu in the "Top" node's menu. 248 | #texinfo_no_detailmenu = False 249 | -------------------------------------------------------------------------------- /bitbucket/bitbucket.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # git+git://github.com/Sheeprider/BitBucket-api.git 3 | 4 | __all__ = ['Bitbucket', ] 5 | 6 | try: 7 | from urlparse import parse_qs 8 | except ImportError: 9 | from urllib.parse import parse_qs 10 | 11 | import json 12 | import re 13 | 14 | from requests import Request, Session 15 | from requests_oauthlib import OAuth1 16 | import requests 17 | 18 | from .issue import Issue 19 | from .repository import Repository 20 | from .service import Service 21 | from .ssh import SSH 22 | from .deploy_key import DeployKey 23 | 24 | 25 | # ======== 26 | # = URLs = 27 | # ======== 28 | URLS = { 29 | 'BASE': 'https://bitbucket.org/!api/1.0/%s', 30 | # Get user profile and repos 31 | 'GET_USER': 'users/%(username)s/', 32 | 'GET_USER_PRIVILEGES': 'user/privileges', 33 | # Search repo 34 | # 'SEARCH_REPO': 'repositories/?name=%(search)s', 35 | # Get tags & branches 36 | 'GET_TAGS': 'repositories/%(username)s/%(repo_slug)s/tags/', 37 | 'GET_BRANCHES': 'repositories/%(username)s/%(repo_slug)s/branches/', 38 | 39 | 'REQUEST_TOKEN': 'oauth/request_token/', 40 | 'AUTHENTICATE': 'oauth/authenticate?oauth_token=%(token)s', 41 | 'ACCESS_TOKEN': 'oauth/access_token/' 42 | } 43 | 44 | 45 | class Bitbucket(object): 46 | """ This class lets you interact with the bitbucket public API. """ 47 | def __init__(self, username='', password='', repo_name_or_slug=''): 48 | self.username = username 49 | self.password = password 50 | self.repo_slug = repo_name_or_slug 51 | self.repo_tree = {} 52 | self.URLS = URLS 53 | 54 | self.repository = Repository(self) 55 | self.service = Service(self) 56 | self.ssh = SSH(self) 57 | self.issue = Issue(self) 58 | self.deploy_key = DeployKey(self) 59 | 60 | self.access_token = None 61 | self.access_token_secret = None 62 | self.consumer_key = None 63 | self.consumer_secret = None 64 | self.oauth = None 65 | 66 | # =================== 67 | # = Getters/Setters = 68 | # =================== 69 | 70 | @property 71 | def auth(self): 72 | """ Return credentials for current Bitbucket user. """ 73 | if self.oauth: 74 | return self.oauth 75 | return (self.username, self.password) 76 | 77 | @property 78 | def username(self): 79 | """Return your repository's username.""" 80 | return self._username 81 | 82 | @username.setter 83 | def username(self, value): 84 | try: 85 | if isinstance(value, basestring): 86 | self._username = unicode(value) 87 | except NameError: 88 | self._username = value 89 | 90 | if value is None: 91 | self._username = None 92 | 93 | @username.deleter 94 | def username(self): 95 | del self._username 96 | 97 | @property 98 | def password(self): 99 | """Return your repository's password.""" 100 | return self._password 101 | 102 | @password.setter 103 | def password(self, value): 104 | try: 105 | if isinstance(value, basestring): 106 | self._password = unicode(value) 107 | except NameError: 108 | self._password = value 109 | 110 | if value is None: 111 | self._password = None 112 | 113 | @password.deleter 114 | def password(self): 115 | del self._password 116 | 117 | @property 118 | def repo_slug(self): 119 | """Return your repository's slug name.""" 120 | return self._repo_slug 121 | 122 | @repo_slug.setter 123 | def repo_slug(self, value): 124 | if value is None: 125 | self._repo_slug = None 126 | else: 127 | try: 128 | if isinstance(value, basestring): 129 | value = unicode(value) 130 | except NameError: 131 | pass 132 | value = value.lower() 133 | self._repo_slug = re.sub(r'[^a-z0-9_-]+', '-', value) 134 | 135 | @repo_slug.deleter 136 | def repo_slug(self): 137 | del self._repo_slug 138 | 139 | # ======================== 140 | # = Oauth authentication = 141 | # ======================== 142 | 143 | def authorize(self, consumer_key, consumer_secret, callback_url=None, 144 | access_token=None, access_token_secret=None): 145 | """ 146 | Call this with your consumer key, secret and callback URL, to 147 | generate a token for verification. 148 | """ 149 | self.consumer_key = consumer_key 150 | self.consumer_secret = consumer_secret 151 | 152 | if not access_token and not access_token_secret: 153 | if not callback_url: 154 | return (False, "Callback URL required") 155 | oauth = OAuth1( 156 | consumer_key, 157 | client_secret=consumer_secret, 158 | callback_uri=callback_url) 159 | r = requests.post(self.url('REQUEST_TOKEN'), auth=oauth) 160 | if r.status_code == 200: 161 | creds = parse_qs(r.content) 162 | 163 | self.access_token = creds.get('oauth_token')[0] 164 | self.access_token_secret = creds.get('oauth_token_secret')[0] 165 | else: 166 | return (False, r.content) 167 | else: 168 | self.finalize_oauth(access_token, access_token_secret) 169 | 170 | return (True, None) 171 | 172 | def verify(self, verifier, consumer_key=None, consumer_secret=None, 173 | access_token=None, access_token_secret=None): 174 | """ 175 | After converting the token into verifier, call this to finalize the 176 | authorization. 177 | """ 178 | # Stored values can be supplied to verify 179 | self.consumer_key = consumer_key or self.consumer_key 180 | self.consumer_secret = consumer_secret or self.consumer_secret 181 | self.access_token = access_token or self.access_token 182 | self.access_token_secret = access_token_secret or self.access_token_secret 183 | 184 | oauth = OAuth1( 185 | self.consumer_key, 186 | client_secret=self.consumer_secret, 187 | resource_owner_key=self.access_token, 188 | resource_owner_secret=self.access_token_secret, 189 | verifier=verifier) 190 | r = requests.post(self.url('ACCESS_TOKEN'), auth=oauth) 191 | if r.status_code == 200: 192 | creds = parse_qs(r.content) 193 | else: 194 | return (False, r.content) 195 | 196 | self.finalize_oauth(creds.get('oauth_token')[0], 197 | creds.get('oauth_token_secret')[0]) 198 | return (True, None) 199 | 200 | def finalize_oauth(self, access_token, access_token_secret): 201 | """ Called internally once auth process is complete. """ 202 | self.access_token = access_token 203 | self.access_token_secret = access_token_secret 204 | 205 | # Final OAuth object 206 | self.oauth = OAuth1( 207 | self.consumer_key, 208 | client_secret=self.consumer_secret, 209 | resource_owner_key=self.access_token, 210 | resource_owner_secret=self.access_token_secret) 211 | 212 | # ====================== 213 | # = High lvl functions = 214 | # ====================== 215 | 216 | def dispatch(self, method, url, auth=None, params=None, **kwargs): 217 | """ Send HTTP request, with given method, 218 | credentials and data to the given URL, 219 | and return the success and the result on success. 220 | """ 221 | r = Request( 222 | method=method, 223 | url=url, 224 | auth=auth, 225 | params=params, 226 | data=kwargs) 227 | s = Session() 228 | resp = s.send(r.prepare()) 229 | status = resp.status_code 230 | text = resp.text 231 | error = resp.reason 232 | if status >= 200 and status < 300: 233 | if text: 234 | try: 235 | return (True, json.loads(text)) 236 | except TypeError: 237 | pass 238 | except ValueError: 239 | pass 240 | return (True, text) 241 | elif status >= 300 and status < 400: 242 | return ( 243 | False, 244 | 'Unauthorized access, ' 245 | 'please check your credentials.') 246 | elif status >= 400 and status < 500: 247 | return (False, 'Service not found.') 248 | elif status >= 500 and status < 600: 249 | return (False, 'Server error.') 250 | else: 251 | return (False, error) 252 | 253 | def url(self, action, **kwargs): 254 | """ Construct and return the URL for a specific API service. """ 255 | # TODO : should be static method ? 256 | return self.URLS['BASE'] % self.URLS[action] % kwargs 257 | 258 | # ===================== 259 | # = General functions = 260 | # ===================== 261 | 262 | def get_user(self, username=None): 263 | """ Returns user informations. 264 | If username is not defined, tries to return own informations. 265 | """ 266 | username = username or self.username or '' 267 | url = self.url('GET_USER', username=username) 268 | response = self.dispatch('GET', url) 269 | try: 270 | return (response[0], response[1]['user']) 271 | except TypeError: 272 | pass 273 | return response 274 | 275 | def get_tags(self, repo_slug=None): 276 | """ Get a single repository on Bitbucket and return its tags.""" 277 | repo_slug = repo_slug or self.repo_slug or '' 278 | url = self.url('GET_TAGS', username=self.username, repo_slug=repo_slug) 279 | return self.dispatch('GET', url, auth=self.auth) 280 | 281 | def get_branches(self, repo_slug=None): 282 | """ Get a single repository on Bitbucket and return its branches.""" 283 | repo_slug = repo_slug or self.repo_slug or '' 284 | url = self.url('GET_BRANCHES', 285 | username=self.username, 286 | repo_slug=repo_slug) 287 | return self.dispatch('GET', url, auth=self.auth) 288 | 289 | def get_privileges(self): 290 | """ Get privledges for this user. """ 291 | url = self.url('GET_USER_PRIVILEGES') 292 | return self.dispatch('GET', url, auth=self.auth) 293 | --------------------------------------------------------------------------------