├── .gitignore ├── AUTHORS.md ├── CHANGELOG ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat ├── modules.rst └── trello.rst ├── requirements.txt ├── setup.py ├── test ├── __init__.py ├── test_board.py ├── test_card.py ├── test_checklist.py └── test_trello_client.py ├── tox.ini └── trello ├── __init__.py ├── __main__.py ├── attachments.py ├── base.py ├── board.py ├── card.py ├── checklist.py ├── compat.py ├── customfield.py ├── exceptions.py ├── label.py ├── member.py ├── organization.py ├── powerup.py ├── star.py ├── trelloclient.py ├── trellolist.py ├── util.py └── webhook.py /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | test_env 3 | deploy.sh 4 | *egg-info 5 | dist 6 | *pyc 7 | *swp 8 | .project 9 | .pydevproject 10 | .idea/ 11 | .tox/ 12 | .DS_Store 13 | example 14 | venv 15 | .env* 16 | .tags* 17 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | py-trello was created by [Richard Kolkovich](http://www.sigil.org) 2 | 3 | A comprehensive list of contributors can be found [at Github](https://github.com/sarumont/py-trello/graphs/contributors) 4 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | v0.1.0 2 | Initial release 3 | 4 | v0.1.1 5 | Tweak distribution crap to be more Pythonic 6 | 7 | v0.1.2 8 | More distribution tweaks 9 | 10 | v0.1.5 11 | Require a version of httplib2 that includes the DigiCert root certs. 12 | Add TRELLO_SCOPE to env. variable to util.py 13 | add vim modelines to confirm with the broken PEP-8 14 | Update README 15 | Update README 16 | Update readme 17 | Updated the readme; updated TrelloClient argument changes in the test file 18 | Changed name of util.test_oauth to create_oauth_token; can run the script from command line 19 | Changed order of arguments for TrelloClient constructor; changed arguments for Board.get_cards; open_cards, closed_cards, etc return all card fields by default (to reduce api calls); update comments 20 | Added requirements.txt 21 | oauth script works; add option for expiration 22 | Code reformatting - changed tabs to whitespaces. 23 | 24 | v0.1.6 25 | 26 | Fixed setting attributes on the 5 readonly @property 27 | Refactored JSON deserialization for every class 28 | Changed the due_date format in Card 29 | Rename an item for a Checklist. 30 | Rename header Content-type to Content-Type. 31 | Add setter to Card.description 32 | Add members methods to Board class 33 | rename to changelog 34 | 35 | v0.3.0 36 | 37 | use requests and requests-oauthlib to simplify code and make it Python 3 compatible 38 | bumped version to 0.2.0, updated setup.py and requirements.txt 39 | added test for Board.get_cards and Card.delete -> 0.2.1 40 | Extended test for Board.get_cards 41 | Remove `get_list` from TrelloClient (closes #51) 42 | Fix util.py for Python2 43 | added property Card.date_last_activity : datetime, and test for fetching card attributes (including the new datetime property) 44 | Unified initialization of cards from JSON data. Added optionally lazy properties for comments and checklists. 45 | Python 2 fix (@henriquegemignani ) 46 | version bumped and dependency "dateutil" added 47 | Allow empty due dates 48 | Change requirement dateutil to python-dateutil. 49 | Fix python2 support (no annotations) 50 | Add method get_card, fixes #62 51 | FIX: Inverted arguments 52 | make util script executable 53 | Add shebang to util.py 54 | [NEW]: support attachments from file or url 55 | Added method to Card object to update card name 56 | fetch_checklists: allow the list to be empty 57 | added the rename method on Checklist 58 | added the delete method on Checklist 59 | refactor tests to avoid code duplication and flake8 reports 60 | raised coverage to 80% 61 | Added support for Organizations 62 | Fix Board.from_json call parameterization 63 | Add a default app name 64 | Updated the create_oauth_token() function to include the ability to suppress printing the secrets and tokens, as well as returning the access tokens after the OAuth request. 65 | Added open() to Boards and Lists 66 | added due_date property 67 | handling pos attribute 68 | Added the Label class and fixed the labels property to cards 69 | Added functionality to add a Label to a board 70 | Updated the from_json documentation to reflect the parent board as being on the arguments, as opposed to the Trello Client I had originally 71 | Removed Board.open() and List.open() for the pull request 72 | Added the ability to add a label on card creation. Added a call to add a label to a card directly. Added a call to get all labels for a board 73 | Added the ability to archive all cards, and add a due date on a card. Additionally, I updated creating a new card to use the Card path instead of the List path, since it allows for adding Labels by ID instead of by color 74 | added sphinx docs 75 | improved docs slightly 76 | improved docs slightly 77 | improved docs slightly 78 | card might be initialized with either board or list 79 | card labels are returned as Label class instead of dicts 80 | setUp -> setUpClass 81 | added list to card 82 | Remove unused Member.commentCard attribute 83 | Fix docstring typo in Member.fetch() 84 | 85 | v0.3.1 86 | 87 | bump version since I forgot to sign 88 | 89 | v0.4.0 90 | 91 | Revert "Removed Board.open() and List.open() for the pull request" 92 | datetime.strptime -> dateparser.parse 93 | The fields 'status' and 'initials' appear optional 94 | Use hour, min, sec to help deal with TZ issues 95 | Don't error out if no due date 96 | Add ability to subscribe to a card 97 | broken code into multiple files 98 | broken tests into multiple files + change board test list behavior (each run create a new list) 99 | FIX saner way to deal with circular import between list/card 100 | Add myself to contributors list and cleanup setup.py 101 | 102 | v0.4.1 103 | 104 | FIX bugs from breaking up code 105 | FIX bug when testing card due datetime 106 | 107 | v0.4.0 108 | 109 | Fix import bugs + bug in tests for card due date 110 | bump to 0.4.1 111 | fix unicode bug (tests for python3 are broken) 112 | 113 | v0.4.2 114 | cards : checklists and comments are sorted 115 | cards : checklists and comments no longer raise AttributeError if not already fetched 116 | style nitpicking 117 | members : comments are sorted 118 | trelloclient : add optional import of PyOpenSSL useful for python < 2.7.9 and 3.2 to prevent security issues with openssl (More info : https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning) 119 | more tests 120 | 121 | v0.4.3 122 | 123 | Allow cards to unassign members 124 | Use the proper ref in the lambda (fixes #108) 125 | 126 | v0.5.0 127 | 128 | Add remove_label function to card 129 | Members: Add "fetch_cards" and "fetch_notifications" methods. 130 | A few things I added for a project of mine: 1. all_boards can now filter, for example open (https://developers.trello.com/advanced-reference/member#get-1-members-idmember-or-username-boards) 2. Cards and boards parse dateLastActivity without needing another fetch (as it is returned in the original json). 131 | None out the date_last_activity as I "promoted" it to the json parsing, so it'll be weird if it just didn't exist if parse failed. 132 | How the hell did I manage to add tabs? :) 133 | fix #66 - strings are encoded with UTF-8 to be compatible with python 2 and decoded with utf-8 to be compatible with python 3 134 | Revert "fix #66 - strings are encoded with UTF-8 to be compatible with python 2 and decoded with utf-8 to be compatible with python 3" 135 | Add card filter parameter to List.list_cards() 136 | Add cloning from a source card to add_card() 137 | Add functionality to retrieve a Label object 138 | Add URL shorturl property 139 | Added card_filter argument to Board.get_cards() 140 | Fixed gh-124. -Added support for a source_board parameter to add_board - Added tests for add_board, copy_board, close_board - Modified list boards to only look at open boards - This commit now causes the test account to accumulate closed boards 141 | gh-124 Fix for original PR, unit tests had flaw that hid non-working code for copying board. 142 | added test for closed cards, fixed tests for python3(except test_delete_cards) 143 | added action_limit for board. plus the unittests 144 | action_limit is an optional parameter 145 | add due date 146 | Add ability to specify which organization to create a board on 147 | implement deletion of single checklist items 148 | refactoring by removing some duplication 149 | implement clearing of checklists 150 | Updates to support attachments as per Walter's comments https://trello.com/c/5L574YgX/4-add-attachments-to-card 151 | Added obtaining all checklists directly from the board and the respective test 152 | 153 | v0.5.1 154 | 155 | Once a card has reached done list stop time measurement 156 | Refactoring to avoid repeated code between Card.listCardMove_date and Card.list_movements 157 | Complete statistics about Trello cards 158 | List position should not be computed based on list.pos, it should be computed by an external function passed as a parameter 159 | Improve the comments in Card.get_stats_by_list 160 | Adds date filter when extracting card stats. This way we can have a workaround to the limitation of Trello API to get only last 1000 board movements 161 | added card_created_date() function 162 | changed oauth section run command into block quote to match the rest of the file 163 | try this version 164 | RST hates me 165 | RST hates me 166 | it would help if I wasn't previewing on the master branch 167 | Added remove_attachment shortcut 168 | Implement delete comments method on card 169 | 170 | v0.6.0 171 | 172 | Encoding: use unicode everywhere 173 | tests: add tox support 174 | tests: replace use of deprecated assertEquals with assertEqual 175 | README: formatting fixes 176 | 177 | v0.6.1 178 | 179 | README: fix hybrid markdow/rst syntax 180 | add gitignore .tox/\nupdate all of comment about :rtype: to remove warning 181 | fix inverted logic in python2 check 182 | Python 2 is less than Python 3 183 | Don't call it a checklist item id if it's actually an index 184 | Fix the docstring to match the call signature 185 | Note that 'eager' fetches attachments also 186 | Don't be coy about returning an empty list if there are no attachments 187 | Some whitespace tweaks and a comment fix 188 | 'create_date' is evidently a typo 189 | 190 | v0.7.0 191 | 192 | Don't use actions to compute creation_date, use id of the card that contains the creation_date 193 | Don't use actions to compute creation_date, use id of the card that contains the creation_date 194 | Card and timezone configuration 195 | Don't pass tz to Card.get_stats_by_list because it is already defined in Configuration 196 | dateLastActivity was not initialized in Board when fetching data 197 | Check if actions are already fetched in _list_movements 198 | Ignoring MacOS .DS_Store files 199 | Deleting Configuration class that has been replaced by Organization. Organization was created by upstream 200 | Fix bad import of Organization class. It is trello.organization not trello 201 | Fix circular import in organization.Organization 202 | Board.fetch_actions allows pagination with since parameter 203 | Card needs more attributes to be created from Board.all_cards 204 | Initialize on fetch_checklists() (Closes #145) 205 | Fix "datetime.fromtimestamp(unix_time)" => "datetime.datetime.fromtimestamp(unix_time)" 206 | Fix "AttributeError: 'Card' object has no attribute 'idBoard'" 207 | Fix "AttributeError: 'Card' object has no attribute 'idShort'" 208 | Add list position attribute (pos) to list. This attribute is fetched by default when calling Board.get_lists 209 | Add support for creating public boards 210 | Fix ":token_key:" => ":token:" 211 | Add Board.list_lists 212 | Saves a new Trello board 213 | Saves a new Trello board. Avoid creating default lists. 214 | Add 'pos' parameter to Board.add_list to allow creation of lists with an initial position 215 | Return the new comment data when creating a comment 216 | Adding before parameter to Board.fetch_actions to enable pagination 217 | Adding before parameter to Board.fetch_actions to enable pagination 218 | add change_pos functionality 219 | Add limit param to card comments (now is limited to 50 by default) 220 | Add `Attachments` 221 | Add `Attachments Preview` 222 | Refactoring params 223 | Return python-object "Attachments" 224 | Add new method `Board.set_name` 225 | Add new method `Board.set_description()` 226 | Edit `Attachments.date` return python datetime object 227 | 228 | v0.8.0 229 | 230 | Add pytz in install_requires 231 | Add update_comment for card. 232 | Card.get_stats_by_list: sort card movements to get the time the card spends in each list 233 | 234 | v0.9.0 235 | 236 | Fix #172 237 | Add missing initials for members of an organization (fixes #176) 238 | Organizations do not have a 'closed' attribute 239 | Implement partial search API 240 | Improve documenation of TrelloClient.search return values 241 | Eliminate unnecessary fetch calls for search results 242 | Add position attribute to TrelloList's add_card method to allow creating cards in the top, bottom or an specific position of the list 243 | Position must be optional in List.add_card. Adding new method move(position) that allows moving a List in a board 244 | New operations: add and remove member of a board 245 | New feature: move all list from a card to another list 246 | Add remove due datetime from a card 247 | Add the member type when calling board's get_members 248 | 249 | v0.10.0 250 | 251 | fix checklist item not sorted 252 | Third authentication: initialize TrelloClient correctly 253 | Add the option to choose the third authentication 254 | add function create_label in card.py, which could create new label of the card by given name and color 255 | Fixed fetch_actions for individual cards. 256 | Use UTC as default 257 | Fix typo 258 | Added little documentation for new users of py-trello 259 | Regenerate the docs 260 | Add get_list() to TrelloClient 261 | Handle dateLastActivity being empty 262 | Make card.fetch_actions() and list.fetch_actions() return the result 263 | Add/Remove member to card functions 264 | introduce Board.get_last_activity() 265 | Remove out-of-date contributors list 266 | add AUTHORS file (closes #206) 267 | Add assignee to list.add_card 268 | Python 2 constructor style 269 | Fix TrelloBase import path in attachments.py 270 | Fix Card.get_stats_by_list. In case a movement is from/to a list in other board, it is ignored 271 | Concentrated hash and eq inside TrelloBase to avoid repetition 272 | - Added __hash__ and __eq__ for some more modules 273 | Add util to __init__.py 274 | Refactoring import 275 | Add `Card.set_due_complete` and `Card.remove_due_complete` 276 | Add `Card.is_due_complete` 277 | Added a link to the documentation 278 | Delete `Card.create_date` is deprecated 279 | close #187 280 | add new properrty - plugin_data to cover https://developers.trello.com/advanced-reference/card#get-1-cards-card-id-or-shortlink-pluginData 281 | enhance trellolist list_cards 282 | Add attachements to the card if present in the response JSON 283 | Add a way to know what id assigns Trello to a new attachment 284 | Set name of the list 285 | card comment docstring param 286 | card set_name docstring 287 | card a bit of pep8 fixes 288 | card from_json docstring return type 289 | card from_json method - parameter is named `parent`, but in docstring it was `trello_list` 290 | card init - parameter is named `parent`, but in docstring it was `trello_list` 291 | board get_members docstring 292 | boards fetch_actions pep8 fix 293 | add board fetch_actions docstring 294 | prep for 0.9.0 (fixes #184) 295 | prep for 0.9.0 296 | 297 | v0.11.0 298 | 299 | Add basic support for custom fields 300 | Add set_organization method to Board object to work with organizations 301 | commit check 302 | Fix lazy loading in Card 303 | Inject request 304 | Add get_boards method to Member object with organization support 305 | Update trellolist.py 306 | Added support to get subscription status for lists as well as subscribe/unsubscribe from lists 307 | List, add, and delete stars. 308 | add remove_member and add_member methods to Organization object 309 | untested star implementation 310 | Adds method to delete label from board 311 | 312 | v0.11.1 313 | 314 | Delete star string format hotfix for python 2.7 compatability 315 | 316 | v0.11.2 317 | 318 | Fixes Issue #247 KeyError: 'customFieldItems' 319 | 320 | v0.11.3 321 | 322 | Modifies Cards.from_json to not require customFieldItems 323 | 324 | v0.12.0 325 | 326 | Add check and fix for python3 unicode assertion 327 | Add warning that running tests will delete cards 328 | trelloclient: allow to customize cards_limit 329 | more card API consistency 330 | fixed customFields on cards 331 | added default_lists as parameter to add_board 332 | Add lazy loading for card.customFields 333 | Add function for accessing new/existing custom fields of a card by name 334 | 335 | v0.13.0 336 | 337 | Add ability to pass custom query parameters to get_cards 338 | fix #256 can't set attribute errror 339 | 340 | v0.14.0 341 | 342 | Fix timezone problem with tz aware datetime 343 | null/empty check for actions (closes #266) 344 | provide access to the raw card json 345 | 346 | v0.15.0 347 | 348 | Add set_custom_field to card.py 349 | Allow proxy usage 350 | Fix card.badges unset for cards created by from_json() 351 | fixed KeyError / missing subscribed key in Trello lists 352 | Add documentation blurb about TRELLO_TEST_STAR_COUNT to README.rst 353 | Lazy load property trello.Board.date_last_activity, using property _date_last_activity 354 | fix typo 355 | add keep_from_source to add_card in trelloList 356 | add list_cards_iter 357 | Clean list_movement docstrings 358 | 359 | v0.16.0 360 | 361 | Add missing header and footer 362 | Add __main__.py for creating OAuth token 363 | Fetch comments from copied cards 364 | Adding custom field items when getting single card 365 | add method for get all visible cards 366 | 367 | v0.17.0 368 | 369 | Allow set_custom_field to accept an empty value 370 | add functions to add and delete custom field definitions from board 371 | setup.py: Add GitHub URL 372 | Update card.py 373 | Update card.py 374 | Update card.py 375 | Make consecutive calls work with default query arg 376 | 377 | v0.17.1 378 | 379 | Removes the unused try statement that broke the last version 380 | 381 | v0.18.0 382 | 383 | fix last_activity typo 384 | Add add_organization() 385 | Custom field list updates 386 | Fixing TabError 387 | Added powerups 388 | Add files via upload 389 | Update custom field definition 390 | Updating set_reminder 391 | Two new methods 392 | Added two methods for checkitems 393 | Add OAuth2 support to webhook list 394 | Add OAuth2 support to webhook creation 395 | Return empty cards 396 | fixed a problem with fetching checklists 397 | Update card.py 398 | Added setCover to the attach function 399 | Update trelloclient.py 400 | Update trelloclient.py 401 | Implement the Move List to Board API 402 | update2 __repr__ to display all Board class attributes 403 | update __repr__ to display all Board class attributes 404 | fetch Member email 405 | 406 | v0.19.0 407 | 408 | Not all action has attributes related to movements (listBefore, listAfter), in that way it need to be checked. 409 | Fixing typo reported in #315. Fixing identation when checking last_list (comment was idented correctly, but code not). Fixing the calculation of the time that card has been on a list, correcting last_action_datetime update value by putting it before if/continue statement. 410 | fix #325: allow working with duplicate checklist item names 411 | Minor improvement: add Board.get_card(card_id) 412 | Minor code improvement, instead of creating a member and updating field on Board class, using static factory Member.from_json. Adding Member.member_type; Adding Member.avatar_url; 413 | add 'displayName' property to 'organization' class 414 | Adding get_label() into Board class 415 | deprecate enable/disable power up 416 | fix: incorrect string format index 417 | adapt card checklists handling to new Trello payload 418 | Don't check completed checklist items if None 419 | adding urlSource field for adding cards, which works brilliantly 420 | Added Option To Delete Board 421 | chore: cleanup 422 | 423 | v0.20.0 424 | 425 | Add HTTP User-Agent to fetchJSON (#375) 426 | Fix regression with trello api get requests. (#374) 427 | 428 | v0.20.1 429 | 430 | Update trelloclient.py (#379) 431 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Richard Kolkovich 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | Neither the name of the Sigil.org nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | 11 | 12 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A wrapper around the Trello API written in Python. Each Trello object is 2 | represented by a corresponding Python object. The attributes of these objects 3 | are cached, but the child objects are not. This can possibly be improved when 4 | the API allows for notification subscriptions; this would allow caching 5 | (assuming a connection was available to invalidate the cache as appropriate). 6 | 7 | I've created a `Trello Board `_ 8 | for feature requests, discussion and some development tracking. 9 | 10 | Install 11 | ======= 12 | 13 | :: 14 | 15 | pip install py-trello 16 | 17 | Usage 18 | ===== 19 | 20 | .. code-block:: python 21 | 22 | from trello import TrelloClient 23 | 24 | client = TrelloClient( 25 | api_key='your-key', 26 | api_secret='your-secret', 27 | token='your-oauth-token-key', 28 | token_secret='your-oauth-token-secret' 29 | ) 30 | 31 | Where ``token`` and ``token_secret`` come from the 3-legged OAuth process and 32 | ``api_key`` and ``api_secret`` are your Trello API credentials that are 33 | (`generated here `_). 34 | 35 | To use without 3-legged OAuth, use only ``api_key`` and ``api_secret`` on client. 36 | 37 | Working with boards 38 | -------------------- 39 | 40 | .. code-block:: python 41 | 42 | all_boards = client.list_boards() 43 | last_board = all_boards[-1] 44 | print(last_board.name) 45 | 46 | working with board lists and cards 47 | ---------------------------------- 48 | 49 | .. code-block:: python 50 | 51 | all_boards = client.list_boards() 52 | last_board = all_boards[-1] 53 | last_board.list_lists() 54 | my_list = last_board.get_list(list_id) 55 | 56 | for card in my_list.list_cards(): 57 | print(card.name) 58 | 59 | 60 | Getting your Trello OAuth Token 61 | =============================== 62 | Make sure the following environment variables are set: 63 | 64 | * ``TRELLO_API_KEY`` 65 | * ``TRELLO_API_SECRET`` 66 | 67 | These are obtained from the link mentioned above. 68 | 69 | ``TRELLO_EXPIRATION`` is optional. Set it to a string such as 'never' or '1day'. 70 | Trello's default OAuth Token expiration is 30 days. 71 | 72 | Default permissions are read/write. 73 | 74 | More info on setting the expiration here: 75 | https://trello.com/docs/gettingstarted/#getting-a-token-from-a-user 76 | 77 | Run 78 | 79 | :: 80 | 81 | python -m trello oauth 82 | 83 | Required Python modules 84 | ======================= 85 | 86 | Found in ``requirements.txt`` 87 | 88 | Tests 89 | ===== 90 | 91 | To run the tests, run ``python -m unittest discover``. Four environment variables must be set: 92 | 93 | * ``TRELLO_API_KEY``: your Trello API key 94 | * ``TRELLO_TOKEN``: your Trello OAuth token 95 | * ``TRELLO_TEST_BOARD_COUNT``: the number of boards in your Trello account 96 | * ``TRELLO_TEST_BOARD_NAME``: name of the board to test card manipulation on. Must be unique, or the first match will be used 97 | * ``TRELLO_TEST_STAR_COUNT``: the number of stars on your test Trello board 98 | 99 | *WARNING*: The tests will delete all cards on the board called `TRELLO_TEST_BOARD_NAME`! 100 | 101 | To run tests across various Python versions, 102 | `tox `_ is supported. Install it 103 | and simply run ``tox`` from the ``py-trello`` directory. 104 | -------------------------------------------------------------------------------- /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 coverage 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 " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/py-trello.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/py-trello.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/py-trello" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/py-trello" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # py-trello documentation build configuration file, created by 4 | # sphinx-quickstart on Thu May 21 12:17:47 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | sys.path.insert(0, os.path.abspath('../')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.doctest', 35 | 'sphinx.ext.intersphinx', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.mathjax', 39 | 'sphinx.ext.viewcode', 40 | ] 41 | 42 | # Add any paths that contain templates here, relative to this directory. 43 | templates_path = ['_templates'] 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The encoding of source files. 51 | #source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = 'index' 55 | 56 | # General information about the project. 57 | project = u'py-trello' 58 | copyright = u'2015, Adrien Lemaire, Kyle Valade, Rick van Hattem' 59 | author = u'Adrien Lemaire, Kyle Valade, Rick van Hattem' 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = '0.2.3' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '0.2.3' 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | #today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | #today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ['_build'] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | #default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | #add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | #add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | #show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = 'sphinx' 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | #modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | #keep_warnings = False 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = True 113 | 114 | 115 | # -- Options for HTML output ---------------------------------------------- 116 | 117 | # The theme to use for HTML and HTML Help pages. See the documentation for 118 | # a list of builtin themes. 119 | # html_theme = 'alabaster' 120 | html_theme = 'default' 121 | 122 | # Theme options are theme-specific and customize the look and feel of a theme 123 | # further. For a list of options available for each theme, see the 124 | # documentation. 125 | #html_theme_options = {} 126 | 127 | # Add any paths that contain custom themes here, relative to this directory. 128 | #html_theme_path = [] 129 | 130 | # The name for this set of Sphinx documents. If None, it defaults to 131 | # " v documentation". 132 | #html_title = None 133 | 134 | # A shorter title for the navigation bar. Default is the same as html_title. 135 | #html_short_title = None 136 | 137 | # The name of an image file (relative to this directory) to place at the top 138 | # of the sidebar. 139 | #html_logo = None 140 | 141 | # The name of an image file (within the static path) to use as favicon of the 142 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 143 | # pixels large. 144 | #html_favicon = None 145 | 146 | # Add any paths that contain custom static files (such as style sheets) here, 147 | # relative to this directory. They are copied after the builtin static files, 148 | # so a file named "default.css" will overwrite the builtin "default.css". 149 | html_static_path = ['_static'] 150 | 151 | # Add any extra paths that contain custom files (such as robots.txt or 152 | # .htaccess) here, relative to this directory. These files are copied 153 | # directly to the root of the documentation. 154 | #html_extra_path = [] 155 | 156 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 157 | # using the given strftime format. 158 | #html_last_updated_fmt = '%b %d, %Y' 159 | 160 | # If true, SmartyPants will be used to convert quotes and dashes to 161 | # typographically correct entities. 162 | #html_use_smartypants = True 163 | 164 | # Custom sidebar templates, maps document names to template names. 165 | #html_sidebars = {} 166 | 167 | # Additional templates that should be rendered to pages, maps page names to 168 | # template names. 169 | #html_additional_pages = {} 170 | 171 | # If false, no module index is generated. 172 | #html_domain_indices = True 173 | 174 | # If false, no index is generated. 175 | #html_use_index = True 176 | 177 | # If true, the index is split into individual pages for each letter. 178 | #html_split_index = False 179 | 180 | # If true, links to the reST sources are added to the pages. 181 | #html_show_sourcelink = True 182 | 183 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 184 | #html_show_sphinx = True 185 | 186 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 187 | #html_show_copyright = True 188 | 189 | # If true, an OpenSearch description file will be output, and all pages will 190 | # contain a tag referring to it. The value of this option must be the 191 | # base URL from which the finished HTML is served. 192 | #html_use_opensearch = '' 193 | 194 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 195 | #html_file_suffix = None 196 | 197 | # Language to be used for generating the HTML full-text search index. 198 | # Sphinx supports the following languages: 199 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 200 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 201 | #html_search_language = 'en' 202 | 203 | # A dictionary with options for the search language support, empty by default. 204 | # Now only 'ja' uses this config value 205 | #html_search_options = {'type': 'default'} 206 | 207 | # The name of a javascript file (relative to the configuration directory) that 208 | # implements a search results scorer. If empty, the default will be used. 209 | #html_search_scorer = 'scorer.js' 210 | 211 | # Output file base name for HTML help builder. 212 | htmlhelp_basename = 'py-trellodoc' 213 | 214 | # -- Options for LaTeX output --------------------------------------------- 215 | 216 | latex_elements = { 217 | # The paper size ('letterpaper' or 'a4paper'). 218 | #'papersize': 'letterpaper', 219 | 220 | # The font size ('10pt', '11pt' or '12pt'). 221 | #'pointsize': '10pt', 222 | 223 | # Additional stuff for the LaTeX preamble. 224 | #'preamble': '', 225 | 226 | # Latex figure (float) alignment 227 | #'figure_align': 'htbp', 228 | } 229 | 230 | # Grouping the document tree into LaTeX files. List of tuples 231 | # (source start file, target name, title, 232 | # author, documentclass [howto, manual, or own class]). 233 | latex_documents = [ 234 | (master_doc, 'py-trello.tex', u'py-trello Documentation', 235 | u'Adrien Lemaire, Kyle Valade, Rick van Hattem', 'manual'), 236 | ] 237 | 238 | # The name of an image file (relative to this directory) to place at the top of 239 | # the title page. 240 | #latex_logo = None 241 | 242 | # For "manual" documents, if this is true, then toplevel headings are parts, 243 | # not chapters. 244 | #latex_use_parts = False 245 | 246 | # If true, show page references after internal links. 247 | #latex_show_pagerefs = False 248 | 249 | # If true, show URL addresses after external links. 250 | #latex_show_urls = False 251 | 252 | # Documents to append as an appendix to all manuals. 253 | #latex_appendices = [] 254 | 255 | # If false, no module index is generated. 256 | #latex_domain_indices = True 257 | 258 | 259 | # -- Options for manual page output --------------------------------------- 260 | 261 | # One entry per manual page. List of tuples 262 | # (source start file, name, description, authors, manual section). 263 | man_pages = [ 264 | (master_doc, 'py-trello', u'py-trello Documentation', 265 | [author], 1) 266 | ] 267 | 268 | # If true, show URL addresses after external links. 269 | #man_show_urls = False 270 | 271 | 272 | # -- Options for Texinfo output ------------------------------------------- 273 | 274 | # Grouping the document tree into Texinfo files. List of tuples 275 | # (source start file, target name, title, author, 276 | # dir menu entry, description, category) 277 | texinfo_documents = [ 278 | (master_doc, 'py-trello', u'py-trello Documentation', 279 | author, 'py-trello', 'One line description of project.', 280 | 'Miscellaneous'), 281 | ] 282 | 283 | # Documents to append as an appendix to all manuals. 284 | #texinfo_appendices = [] 285 | 286 | # If false, no module index is generated. 287 | #texinfo_domain_indices = True 288 | 289 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 290 | #texinfo_show_urls = 'footnote' 291 | 292 | # If true, do not generate a @detailmenu in the "Top" node's menu. 293 | #texinfo_no_detailmenu = False 294 | 295 | 296 | # Example configuration for intersphinx: refer to the Python standard library. 297 | intersphinx_mapping = {'https://docs.python.org/': None} 298 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to py-trello's documentation! 2 | ===================================== 3 | 4 | Modules 5 | ======= 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | trello 11 | 12 | Readme 13 | ====== 14 | 15 | .. include:: ../README.rst 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /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 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\py-trello.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\py-trello.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /docs/modules.rst: -------------------------------------------------------------------------------- 1 | trello 2 | ====== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | trello 8 | -------------------------------------------------------------------------------- /docs/trello.rst: -------------------------------------------------------------------------------- 1 | trello package 2 | ============== 3 | 4 | Submodules 5 | ---------- 6 | 7 | trello\.attachments module 8 | -------------------------- 9 | 10 | .. automodule:: trello.attachments 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | trello\.base module 16 | ------------------- 17 | 18 | .. automodule:: trello.base 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | trello\.board module 24 | -------------------- 25 | 26 | .. automodule:: trello.board 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | trello\.card module 32 | ------------------- 33 | 34 | .. automodule:: trello.card 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | trello\.checklist module 40 | ------------------------ 41 | 42 | .. automodule:: trello.checklist 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | trello\.compat module 48 | --------------------- 49 | 50 | .. automodule:: trello.compat 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | trello\.exceptions module 56 | ------------------------- 57 | 58 | .. automodule:: trello.exceptions 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | trello\.label module 64 | -------------------- 65 | 66 | .. automodule:: trello.label 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | trello\.member module 72 | --------------------- 73 | 74 | .. automodule:: trello.member 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | trello\.organization module 80 | --------------------------- 81 | 82 | .. automodule:: trello.organization 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | trello\.trelloclient module 88 | --------------------------- 89 | 90 | .. automodule:: trello.trelloclient 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | trello\.trellolist module 96 | ------------------------- 97 | 98 | .. automodule:: trello.trellolist 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | trello\.util module 104 | ------------------- 105 | 106 | .. automodule:: trello.util 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | 111 | trello\.webhook module 112 | ---------------------- 113 | 114 | .. automodule:: trello.webhook 115 | :members: 116 | :undoc-members: 117 | :show-inheritance: 118 | 119 | 120 | Module contents 121 | --------------- 122 | 123 | .. automodule:: trello 124 | :members: 125 | :undoc-members: 126 | :show-inheritance: 127 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | requests-oauthlib 3 | python-dateutil 4 | pytz 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name="py-trello", 7 | version="0.20.1", 8 | 9 | description='Python wrapper around the Trello API', 10 | long_description=open('README.rst').read(), 11 | author='Richard Kolkovich', 12 | author_email='richard@sigil.org', 13 | url='https://trello.com/board/py-trello/4f145d87b2f9f15d6d027b53', 14 | download_url='https://github.com/sarumont/py-trello', 15 | keywords='python', 16 | license='BSD License', 17 | classifiers=[ 18 | "Development Status :: 4 - Beta", 19 | 'Intended Audience :: Developers', 20 | 'License :: OSI Approved :: BSD License', 21 | 'Operating System :: OS Independent', 22 | 'Programming Language :: Python', 23 | 'Programming Language :: Python :: 2', 24 | 'Programming Language :: Python :: 2.7', 25 | 'Programming Language :: Python :: 3', 26 | 'Programming Language :: Python :: 3.3', 27 | ], 28 | install_requires=["requests", "requests-oauthlib >= 0.4.1", "python-dateutil", "pytz"], 29 | packages=find_packages(), 30 | include_package_data=True, 31 | ) 32 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 33 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sarumont/py-trello/f89a72a218295c572921103a08d4ce3ec225c353/test/__init__.py -------------------------------------------------------------------------------- /test/test_board.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import with_statement, print_function 3 | from datetime import datetime 4 | import unittest 5 | import os 6 | from trello import TrelloClient, Board 7 | 8 | 9 | class TrelloBoardTestCase(unittest.TestCase): 10 | """ 11 | Tests for TrelloClient API. Note these test are in order to 12 | preserve dependencies, as an API integration cannot be tested 13 | independently. 14 | """ 15 | 16 | @classmethod 17 | def setUpClass(cls): 18 | cls._trello = TrelloClient(os.environ['TRELLO_API_KEY'], 19 | token=os.environ['TRELLO_TOKEN']) 20 | for b in cls._trello.list_boards(): 21 | if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: 22 | cls._board = b 23 | break 24 | if not cls._board: 25 | cls.fail("Couldn't find test board") 26 | cls._list = cls._board.add_list(str(datetime.now())) 27 | 28 | def _add_card(self, name, description=None): 29 | try: 30 | card = self._list.add_card(name, description) 31 | self.assertIsNotNone(card, msg="card is None") 32 | self.assertIsNotNone(card.id, msg="id not provided") 33 | self.assertEqual(card.name, name) 34 | return card 35 | except Exception as e: 36 | print(str(e)) 37 | self.fail("Caught Exception adding card") 38 | 39 | def _add_checklist(self, card, name, items=[], itemstates=None): 40 | checklist = card.add_checklist(name, items, itemstates) 41 | self.assertIsNotNone(checklist, msg="checklist is None") 42 | self.assertIsNotNone(checklist.id, msg="id not provided") 43 | self.assertEqual(checklist.name, name) 44 | return checklist 45 | 46 | def test_get_cards(self): 47 | # Let's ensure we have no cards in board 48 | for card in self._board.get_cards(): 49 | card.delete() 50 | 51 | nb_cards = 3 52 | names = ["Card #" + str(i) for i in range(nb_cards)] 53 | for i in range(nb_cards): 54 | self._add_card(names[i]) 55 | cards = self._board.get_cards() 56 | self.assertEqual(len(cards), nb_cards) 57 | self.assertEqual(len(cards), len(self._board.open_cards())) 58 | 59 | for card in cards: 60 | self.assertTrue(card.name in names, 'Unexpected card found') 61 | 62 | self.assertIsInstance(self._board.all_cards(), list) 63 | self.assertIsInstance(self._board.open_cards(), list) 64 | self.assertIsInstance(self._board.closed_cards(), list) 65 | self.assertIsInstance(self._board.visible_cards(), list) 66 | 67 | def test_fetch_action_limit(self): 68 | card = self._add_card('For action limit testing') 69 | card.set_closed(True) 70 | self._board.fetch_actions(action_filter='all', action_limit=2) 71 | actions = sorted(self._board.actions,key=lambda act: act['date'], reverse=True) 72 | self.assertEqual(len(actions), 2) 73 | self.assertEqual(actions[0]['type'], 'updateCard') 74 | self.assertFalse(actions[0]['data']['old']['closed']) 75 | self.assertEqual(actions[1]['type'], 'createCard') 76 | 77 | def test_fetch_action_filter(self): 78 | card = self._add_card('For action filter testing') 79 | card.set_closed(True) # This action will be skipped by filter 80 | self._board.fetch_actions(action_filter='createCard', action_limit=1) 81 | actions = self._board.actions 82 | self.assertEqual(len(actions), 1) 83 | self.assertEqual(actions[0]['type'], 'createCard') 84 | 85 | def test_delete_cards(self): 86 | self._add_card("card to be deleted") 87 | cards = self._board.open_cards() 88 | nb_open_cards = len(cards) 89 | for card in cards: 90 | card.delete() 91 | self._board.fetch_actions(action_filter='all', action_limit=nb_open_cards) 92 | self.assertEqual(len(self._board.actions), nb_open_cards) 93 | for action in self._board.actions: 94 | self.assertEqual(action['type'], 'deleteCard') 95 | 96 | def test_close_cards(self): 97 | nb_closed_cards = len(self._board.closed_cards()) 98 | self._add_card("card to be closed") 99 | cards = self._board.open_cards() 100 | nb_open_cards = len(cards) 101 | for card in cards: 102 | card.set_closed(True) 103 | cards_after = self._board.closed_cards() 104 | nb_cards_after = len(cards_after) 105 | self.assertEqual(nb_cards_after, nb_closed_cards + nb_open_cards) 106 | 107 | 108 | def test_all_cards_reachable(self): 109 | if not len(self._board.open_cards()): 110 | self._add_card("an open card") 111 | if not len(self._board.closed_cards()): 112 | card = self._add_card("card to be closed") 113 | card.set_closed(True) 114 | self.assertEqual(len(self._board.all_cards()), 115 | len(self._board.open_cards()) + len(self._board.closed_cards())) 116 | 117 | def test70_all_members(self): 118 | self.assertTrue(len(self._board.all_members()) > 0) 119 | 120 | def test71_normal_members(self): 121 | self.assertTrue(len(self._board.normal_members()) >= 0) 122 | 123 | def test72_admin_members(self): 124 | self.assertTrue(len(self._board.admin_members()) > 0) 125 | 126 | def test73_owner_members(self): 127 | members = self._board.owner_members() 128 | self.assertTrue(len(members) > 0) 129 | member = members[0].fetch() 130 | self.assertNotEqual(member.status, None) 131 | self.assertNotEqual(member.id, None) 132 | self.assertNotEqual(member.bio, None) 133 | self.assertNotEqual(member.url, None) 134 | self.assertNotEqual(member.username, None) 135 | self.assertNotEqual(member.full_name, None) 136 | self.assertNotEqual(member.initials, None) 137 | member2 = self._trello.get_member(member.id) 138 | self.assertEqual(member.username, member2.username) 139 | 140 | def test90_get_board(self): 141 | board = self._trello.get_board(self._board.id) 142 | self.assertEqual(self._board.name, board.name) 143 | 144 | def test100_add_board(self): 145 | test_board = self._trello.add_board("test_create_board") 146 | test_list = test_board.add_list("test_list") 147 | test_list.add_card("test_card") 148 | open_boards = self._trello.list_boards(board_filter="open") 149 | self.assertEqual(len([x for x in open_boards if x.name == "test_create_board"]), 1) 150 | 151 | def test110_copy_board(self): 152 | boards = self._trello.list_boards(board_filter="open") 153 | source_board = next( x for x in boards if x.name == "test_create_board") 154 | self._trello.add_board("copied_board", source_board=source_board) 155 | listed_boards = self._trello.list_boards(board_filter="open") 156 | copied_board = next(iter([x for x in listed_boards if x.name == "copied_board"]), None) 157 | self.assertIsNotNone(copied_board) 158 | open_lists = copied_board.open_lists() 159 | self.assertEqual(len(open_lists), 4) # default lists plus mine 160 | test_list = open_lists[0] 161 | self.assertEqual(len(test_list.list_cards()), 1) 162 | test_card = next ( iter([ x for x in test_list.list_cards() if x.name == "test_card"]), None ) 163 | self.assertIsNotNone(test_card) 164 | 165 | def test120_close_board(self): 166 | boards = self._trello.list_boards(board_filter="open") 167 | open_count = len(boards) 168 | test_create_board = next( x for x in boards if x.name == "test_create_board") # type: Board 169 | copied_board = next( x for x in boards if x.name == "copied_board") # type: Board 170 | test_create_board.close() 171 | copied_board.close() 172 | still_open_boards = self._trello.list_boards(board_filter="open") 173 | still_open_count = len(still_open_boards) 174 | self.assertEqual(still_open_count, open_count - 2) 175 | 176 | def test130_get_checklists_board(self): 177 | chklists = self._board.get_checklists(cards = 'open') 178 | for chklst in chklists: 179 | chklst.delete() 180 | card = self._add_card('For checklist testing') 181 | chklist = self._add_checklist(card, "Test Checklist", items=["item1","item2"], itemstates = [True, False]) 182 | new_chklists = self._board.get_checklists() 183 | test_chk = new_chklists[0] 184 | self.assertEqual(test_chk.name, "Test Checklist") 185 | self.assertEqual(test_chk.trello_card, card.id) 186 | self.assertEqual(len(new_chklists), 1) 187 | i1 = test_chk.items[0] 188 | i2 = test_chk.items[1] 189 | self.assertEqual(len(test_chk.items), 2) 190 | self.assertEqual(i1['name'], "item1") 191 | self.assertEqual(i1['state'], "complete") 192 | self.assertEqual(i2['name'], "item2") 193 | self.assertEqual(i2['state'], "incomplete") 194 | 195 | def test_last_activity(self): 196 | self.assertIsInstance(self._board.date_last_activity, datetime) 197 | self.assertIsInstance(self._board.get_last_activity(), datetime) 198 | 199 | def suite(): 200 | # tests = ['test01_list_boards', 'test10_board_attrs', 'test20_add_card'] 201 | # return unittest.TestSuite(map(TrelloBoardTestCase, tests)) 202 | return unittest.TestLoader().loadTestsFromTestCase(TrelloBoardTestCase) 203 | 204 | if __name__ == "__main__": 205 | unittest.main() 206 | -------------------------------------------------------------------------------- /test/test_card.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import with_statement, print_function 3 | from datetime import datetime, timedelta 4 | import unittest 5 | import os 6 | from trello import TrelloClient, ResourceUnavailable 7 | 8 | 9 | class TrelloCardTestCase(unittest.TestCase): 10 | """ 11 | Tests for TrelloClient API. Note these test are in order to 12 | preserve dependencies, as an API integration cannot be tested 13 | independently. 14 | """ 15 | 16 | @classmethod 17 | def setUpClass(cls): 18 | cls._trello = TrelloClient(os.environ['TRELLO_API_KEY'], 19 | token=os.environ['TRELLO_TOKEN']) 20 | for b in cls._trello.list_boards(): 21 | if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: 22 | cls._board = b 23 | break 24 | if not cls._board: 25 | cls.fail("Couldn't find test board") 26 | cls._list = cls._board.add_list(str(datetime.now())) 27 | 28 | def _add_card(self, name, description=None): 29 | try: 30 | card = self._list.add_card(name, description) 31 | self.assertIsNotNone(card, msg="card is None") 32 | self.assertIsNotNone(card.id, msg="id not provided") 33 | self.assertEqual(card.name, name) 34 | return card 35 | except Exception as e: 36 | print(str(e)) 37 | self.fail("Caught Exception adding card") 38 | 39 | def test40_add_card(self): 40 | name = "Testing from Python - no desc" 41 | card = self._add_card(name) 42 | 43 | self.assertIsNotNone(card.closed, msg="closed not provided") 44 | self.assertFalse(card.closed, msg="Card should not be closed") 45 | 46 | card2 = self._trello.get_card(card.id) 47 | self.assertEqual(card.name, card2.name) 48 | 49 | def test41_add_card_desc(self): 50 | name = "Testing from Python" 51 | description = "Description goes here" 52 | card = self._add_card(name, description) 53 | 54 | card.fetch() 55 | self.assertEqual(description, card.description) 56 | self.assertIsNotNone(card.url, msg="url not provided") 57 | self.assertIsNotNone(card.member_id) 58 | self.assertIsNotNone(card.short_id) 59 | self.assertIsNotNone(card.list_id) 60 | self.assertIsNotNone(card.comments) 61 | self.assertIsNotNone(card.checklists) 62 | self.assertIsInstance(card.created_date, datetime) 63 | 64 | def test42_add_card_with_comments_fetch(self): 65 | name = "Card with comments" 66 | comment = "Hello World!" 67 | card = self._add_card(name) 68 | card.comment(comment) 69 | card.fetch(True) 70 | 71 | self.assertEqual(len(card.comments), 1) 72 | self.assertEqual(card.comments[0]['data']['text'], comment) 73 | 74 | def test43_get_comments(self): 75 | name = "Card with comments 2" 76 | comment = "Hello World!" 77 | card = self._add_card(name) 78 | card.comment(comment) 79 | card._comments = card.get_comments() 80 | self.assertEqual(len(card.comments), 1) 81 | self.assertEqual(card.comments[0]['data']['text'], comment) 82 | 83 | def test44_attach_url_to_card(self): 84 | name = "Testing from Python - url" 85 | card = self._add_card(name) 86 | 87 | card.attach(name='lwn', url='http://lwn.net/') 88 | card.fetch() 89 | self.assertEqual(card.badges['attachments'], 1) 90 | card.delete() 91 | 92 | def test52_add_card_set_due(self): 93 | name = "Testing set due" 94 | card = self._add_card(name) 95 | 96 | # Set the due date to be 3 days from now 97 | today = datetime.today() 98 | day_detla = timedelta(3) 99 | due_date = today + day_detla 100 | card.set_due(due_date) 101 | expected_due_date = card.due 102 | # Refresh the due date from cloud 103 | card.fetch() 104 | actual_due_date = card.due 105 | self.assertEqual(expected_due_date[:8], actual_due_date[:8]) 106 | 107 | def test_set_name(self): 108 | name = "Testing set card name" 109 | card = self._list.add_card('noname') 110 | card.set_name(name) 111 | self.assertEqual(card.name, name) 112 | 113 | def test_set_desc(self): 114 | card = self._list.add_card("Testing set card desc") 115 | description = "Description goes here" 116 | card.set_description(description) 117 | self.assertEqual(card.description, description) 118 | card.fetch() 119 | self.assertEqual(card.description, description) 120 | 121 | def test_set_closed(self): 122 | name = "Testing set card closed" 123 | card = self._list.add_card(name) 124 | self.assertFalse(card.closed) 125 | card.set_closed(True) 126 | self.assertTrue(card.closed) 127 | card.fetch() 128 | self.assertTrue(card.closed) 129 | 130 | def test81_resource_unavailable(self): 131 | self.assertRaises(ResourceUnavailable, 132 | self._trello.get_card, '0dsfkjhsdf87342ed') 133 | 134 | 135 | def suite(): 136 | # tests = ['test01_list_boards', 'test10_board_attrs', 'test20_add_card'] 137 | # return unittest.TestSuite(map(TrelloBoardTestCase, tests)) 138 | return unittest.TestLoader().loadTestsFromTestCase(TrelloBoardTestCase) 139 | 140 | 141 | if __name__ == "__main__": 142 | unittest.main() 143 | -------------------------------------------------------------------------------- /test/test_checklist.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import with_statement, print_function 3 | from datetime import datetime 4 | import unittest 5 | import os 6 | from trello import TrelloClient 7 | 8 | 9 | class TrelloChecklistTestCase(unittest.TestCase): 10 | """ 11 | Tests for TrelloClient API. Note these test are in order to 12 | preserve dependencies, as an API integration cannot be tested 13 | independently. 14 | """ 15 | 16 | @classmethod 17 | def setUpClass(cls): 18 | cls._trello = TrelloClient(os.environ['TRELLO_API_KEY'], 19 | token=os.environ['TRELLO_TOKEN']) 20 | for b in cls._trello.list_boards(): 21 | if b.name == os.environ['TRELLO_TEST_BOARD_NAME']: 22 | cls._board = b 23 | break 24 | if not cls._board: 25 | cls.fail("Couldn't find test board") 26 | cls._list = cls._board.add_list(str(datetime.now())) 27 | 28 | def _add_card(self, name, description=None): 29 | try: 30 | card = self._list.add_card(name, description) 31 | self.assertIsNotNone(card, msg="card is None") 32 | self.assertIsNotNone(card.id, msg="id not provided") 33 | self.assertEqual(card.name, name) 34 | return card 35 | except Exception as e: 36 | print(str(e)) 37 | self.fail("Caught Exception adding card") 38 | 39 | def _add_checklist(self, card, name, items=[]): 40 | checklist = card.add_checklist(name, items) 41 | self.assertIsNotNone(checklist, msg="checklist is None") 42 | self.assertIsNotNone(checklist.id, msg="id not provided") 43 | self.assertEqual(checklist.name, name) 44 | return checklist 45 | 46 | def test_delete_checklist(self): 47 | name = "Card with checklist, test deletion" 48 | card = self._list.add_card(name) 49 | card.fetch(eager=True) 50 | checklist = self._add_checklist(card, 'ChecklistsToDelete', ['item1', 'item2']) 51 | checklist.delete() 52 | card._checklists = card.fetch_checklists() 53 | self.assertEqual(len(card.checklists), 0) 54 | 55 | def test_delete_checklist_remaining(self): 56 | name = "Card with checklist, test deletion" 57 | card = self._list.add_card(name) 58 | card.fetch(eager=True) 59 | 60 | name = 'ChecklistsToDelete' 61 | checklist_delete = self._add_checklist(card, name, ['item1', 'item2']) 62 | 63 | name_keep = 'ChecklistsToKeep' 64 | self._add_checklist(card, name_keep, []) 65 | checklist_delete.delete() 66 | card._checklists = card.fetch_checklists() 67 | self.assertEqual(len(card.checklists), 1) 68 | self.assertEqual(card.checklists[0].name, name_keep) 69 | 70 | def test_checklist_rename(self): 71 | name = "Testing checklist rename" 72 | description = "Description goes here" 73 | card = self._list.add_card(name, description) 74 | 75 | name = 'ToBeRenamed' 76 | new_name = "Renamed" 77 | checklist = self._add_checklist(card, name, ['item1', 'item2']) 78 | checklist.rename(new_name) 79 | self.assertEqual(checklist.name, new_name) 80 | card._checklists = card.fetch_checklists() 81 | self.assertEqual(len(card.checklists), 1) 82 | self.assertEqual(card.checklists[0].name, new_name) 83 | 84 | def test_delete_checklist_item(self): 85 | name = "Testing checklist item delete" 86 | card = self._list.add_card(name, "Description goes here") 87 | 88 | name = 'Checklist' 89 | checklist = self._add_checklist(card, name, ['item1', 'item2']) 90 | checklist.delete_checklist_item('item2') 91 | 92 | checklists = card.fetch_checklists() 93 | self.assertEqual(len(checklists[0].items), 1) 94 | self.assertEqual(checklists[0].items[0]['name'], 'item1') 95 | 96 | def test_clear_checklist(self): 97 | name = "Testing checklist clear" 98 | card = self._list.add_card(name, "Description goes here") 99 | 100 | name = 'Checklist' 101 | checklist = self._add_checklist(card, name, ['item1', 'item2', 'item3']) 102 | checklist.clear() 103 | 104 | checklists = card.fetch_checklists() 105 | self.assertEqual(len(checklists[0].items), 0) 106 | 107 | 108 | def suite(): 109 | # tests = ['test01_list_boards', 'test10_board_attrs', 'test20_add_card'] 110 | # return unittest.TestSuite(map(TrelloBoardTestCase, tests)) 111 | return unittest.TestLoader().loadTestsFromTestCase(TrelloChecklistTestCase) 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /test/test_trello_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import with_statement, print_function 3 | import os 4 | import unittest 5 | from datetime import datetime 6 | from trello import TrelloClient, Unauthorized, ResourceUnavailable 7 | 8 | 9 | class TrelloClientTestCase(unittest.TestCase): 10 | """ 11 | 12 | Tests for TrelloClient API. Note these test are in order to 13 | preserve dependencies, as an API integration cannot be tested 14 | independently. 15 | 16 | """ 17 | 18 | def setUp(self): 19 | self._trello = TrelloClient(os.environ['TRELLO_API_KEY'], 20 | token=os.environ['TRELLO_TOKEN']) 21 | 22 | def test01_list_boards(self): 23 | self.assertEqual( 24 | len(self._trello.list_boards(board_filter="open")), 25 | int(os.environ['TRELLO_TEST_BOARD_COUNT'])) 26 | 27 | def test10_board_attrs(self): 28 | boards = self._trello.list_boards() 29 | for b in boards: 30 | self.assertIsNotNone(b.id, msg="id not provided") 31 | self.assertIsNotNone(b.name, msg="name not provided") 32 | self.assertIsNotNone(b.description, msg="description not provided") 33 | self.assertIsNotNone(b.closed, msg="closed not provided") 34 | self.assertIsNotNone(b.url, msg="url not provided") 35 | 36 | def test20_board_all_lists(self): 37 | boards = self._trello.list_boards() 38 | for b in boards: 39 | try: 40 | b.all_lists() 41 | except Exception: 42 | self.fail("Caught Exception getting lists") 43 | 44 | def test21_board_open_lists(self): 45 | boards = self._trello.list_boards() 46 | for b in boards: 47 | try: 48 | b.open_lists() 49 | except Exception: 50 | self.fail("Caught Exception getting open lists") 51 | 52 | def test22_board_closed_lists(self): 53 | boards = self._trello.list_boards() 54 | for b in boards: 55 | try: 56 | b.closed_lists() 57 | except Exception: 58 | self.fail("Caught Exception getting closed lists") 59 | 60 | def test30_list_attrs(self): 61 | boards = self._trello.list_boards() 62 | for b in boards: 63 | for l in b.all_lists(): 64 | self.assertIsNotNone(l.id, msg="id not provided") 65 | self.assertIsNotNone(l.name, msg="name not provided") 66 | self.assertIsNotNone(l.closed, msg="closed not provided") 67 | break # only need to test one board's lists 68 | 69 | def test50_list_cards(self): 70 | boards = self._trello.list_boards() 71 | for b in boards: 72 | for l in b.all_lists(): 73 | for c in l.list_cards(): 74 | self.assertIsNotNone(c.id, msg="id not provided") 75 | self.assertIsNotNone(c.name, msg="name not provided") 76 | self.assertIsNotNone(c.description, 77 | msg="description not provided") 78 | self.assertIsNotNone(c.closed, msg="closed not provided") 79 | self.assertIsNotNone(c.url, msg="url not provided") 80 | break 81 | break 82 | pass 83 | 84 | def test51_fetch_cards(self): 85 | """ 86 | Tests fetching all attributes for all cards 87 | """ 88 | boards = self._trello.list_boards() 89 | for b in boards: 90 | for l in b.all_lists(): 91 | for c in l.list_cards(): 92 | c.fetch() 93 | 94 | self.assertIsInstance(c.date_last_activity, datetime, 95 | msg='date not provided') 96 | self.assertTrue(len(c.board_id) > 0, 97 | msg='board id not provided') 98 | break 99 | break 100 | pass 101 | 102 | def test52_list_hooks(self): 103 | self.assertIsInstance(self._trello.list_hooks(), list) 104 | 105 | def test53_unauthorized(self): 106 | client = TrelloClient('a') 107 | self.assertRaises(Unauthorized, 108 | client.list_boards) 109 | 110 | def test54_resource_unavailable(self): 111 | self.assertRaises(ResourceUnavailable, 112 | self._trello.get_card, '0') 113 | 114 | def test_list_stars(self): 115 | """ 116 | Test trello client star list 117 | """ 118 | self.assertEqual(len(self._trello.list_stars()), int(os.environ["TRELLO_TEST_STAR_COUNT"]), "Number of stars does not match TRELLO_TEST_STAR_COUNT") 119 | 120 | def test_add_delete_star(self): 121 | """ 122 | Test add and delete star to/from test board 123 | """ 124 | test_board_id = self._trello.search(os.environ["TRELLO_TEST_BOARD_NAME"])[0].id 125 | new_star = self._trello.add_star(test_board_id) 126 | star_list = self._trello.list_stars() 127 | self.assertTrue(new_star in star_list, "Star id was not added in list of starred boards") 128 | deleted_star = self._trello.delete_star(new_star) 129 | star_list = self._trello.list_stars() 130 | self.assertFalse(deleted_star in star_list, "Star id was not deleted from list of starred boards") 131 | 132 | class TrelloClientTestCaseWithoutOAuth(unittest.TestCase): 133 | """ 134 | 135 | Tests for TrelloClient API when OAuth not activated. 136 | 137 | """ 138 | 139 | def setUp(self): 140 | self._trello = TrelloClient(os.environ['TRELLO_API_KEY'], 141 | api_secret=os.environ['TRELLO_TOKEN']) 142 | 143 | def test01_oauth_not_activated(self): 144 | self.assertIsNone(self._trello.oauth) 145 | 146 | 147 | def suite(): 148 | test_classes_to_run = [TrelloClientTestCase, 149 | TrelloClientTestCaseWithoutOAuth] 150 | 151 | loader = unittest.TestLoader() 152 | 153 | suites_list = [] 154 | for test_class in test_classes_to_run: 155 | suite = loader.loadTestsFromTestCase(test_class) 156 | suites_list.append(suite) 157 | 158 | return unittest.TestSuite(suites_list) 159 | 160 | 161 | if __name__ == "__main__": 162 | unittest.main() 163 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py27, 4 | py34, 5 | py35 6 | 7 | [testenv] 8 | passenv = TRELLO_* 9 | setenv = 10 | PYTHONPATH={toxinidir} 11 | commands = 12 | python -m unittest discover 13 | deps = 14 | requests 15 | requests-oauthlib>=0.4.1 16 | python-dateutil 17 | -------------------------------------------------------------------------------- /trello/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from trello.attachments import * 3 | from trello.board import * 4 | from trello.card import * 5 | from trello.checklist import * 6 | from trello.exceptions import * 7 | from trello.label import * 8 | from trello.member import * 9 | from trello.organization import * 10 | from trello.star import* 11 | from trello.trelloclient import * 12 | from trello.trellolist import * 13 | from trello.webhook import * 14 | from trello.util import * 15 | 16 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 17 | -------------------------------------------------------------------------------- /trello/__main__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys 3 | 4 | from trello.util import create_oauth_token 5 | 6 | 7 | def main(): 8 | try: 9 | command = sys.argv[1] 10 | except IndexError: 11 | return 12 | 13 | if command == "oauth": 14 | create_oauth_token() 15 | 16 | 17 | if __name__ == "__main__": 18 | sys.exit(main()) 19 | 20 | # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 21 | -------------------------------------------------------------------------------- /trello/attachments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from dateutil import parser as dateparser 3 | 4 | from trello.base import TrelloBase 5 | 6 | 7 | class Attachments(TrelloBase): 8 | """ 9 | https://developers.trello.com/advanced-reference/card#get-1-cards-card-id-or-shortlink-attachments 10 | """ 11 | def __init__(self, id, bytes, date, edge_color, idMember, is_upload, mime_type, name, previews, url): 12 | super(Attachments, self).__init__() 13 | self.id = id 14 | self.bytes = bytes 15 | self.date = dateparser.parse(date) 16 | self.edge_color = edge_color 17 | self.idMember = idMember 18 | self.is_upload = is_upload 19 | self.mime_type = mime_type 20 | self.name = name 21 | self.previews = previews 22 | self.url = url 23 | 24 | @staticmethod 25 | def from_json(json_obj): 26 | id = json_obj.get("id") 27 | bytes = json_obj.get("bytes") 28 | date = json_obj.get("date") 29 | edge_color = json_obj.get("edgeColor") 30 | idMember = json_obj.get("idMember") 31 | is_upload = json_obj.get("isUpload") 32 | mime_type = json_obj.get("mimeType") 33 | name = json_obj.get("name") 34 | previews = [AttachmentsPreview.from_json(preview_json) for preview_json in json_obj.get("previews")] 35 | url = json_obj.get("url") 36 | 37 | return Attachments(id, bytes, date, edge_color, idMember, is_upload, mime_type, name, previews, url) 38 | 39 | def __repr__(self): 40 | return u"".format(self.name) 41 | 42 | 43 | class AttachmentsPreview(object): 44 | def __init__(self, bytes, url, width, height, is_scaled): 45 | self.bytes = bytes 46 | self.url = url 47 | self.width = width 48 | self.height = height 49 | self.is_scaled = is_scaled 50 | 51 | @staticmethod 52 | def from_json(json_obj): 53 | bytes = json_obj.get("bytes") 54 | url = json_obj.get("url") 55 | width = json_obj.get("width") 56 | height = json_obj.get("height") 57 | is_scaled = json_obj.get("scaled") 58 | 59 | return AttachmentsPreview(bytes, url, width, height, is_scaled) 60 | 61 | def __repr__(self): 62 | return u"' % self.name) 72 | 73 | def fetch(self): 74 | """Fetch all attributes for this board""" 75 | json_obj = self.client.fetch_json('/boards/' + self.id) 76 | self.name = json_obj['name'] 77 | self.description = json_obj.get('desc', '') 78 | self.closed = json_obj['closed'] 79 | self.url = json_obj['url'] 80 | self.customFieldDefinitions = None 81 | 82 | # Saves a Trello Board 83 | def save(self): 84 | json_obj = self.client.fetch_json( 85 | '/boards/', 86 | http_method='POST', 87 | post_args={'name': self.name, "desc": self.description, "defaultLists": False}, ) 88 | # Set initial data from Trello 89 | self.from_json(json_obj=json_obj) 90 | self.id = json_obj["id"] 91 | 92 | def set_name(self, name): 93 | self.client.fetch_json( 94 | '/boards/{board_id}/name'.format(board_id=self.id), 95 | http_method='PUT', 96 | post_args={'value': name}) 97 | self.name = name 98 | 99 | def set_description(self, desc): 100 | self.client.fetch_json( 101 | '/boards/{board_id}/desc'.format(board_id=self.id), 102 | http_method='PUT', 103 | post_args={'value': desc}) 104 | self.description = desc 105 | 106 | def set_organization(self, desc): 107 | self.client.fetch_json( 108 | '/boards/{board_id}/idOrganization'.format(board_id=self.id), 109 | http_method='PUT', 110 | post_args={'value': desc}) 111 | self.description = desc 112 | 113 | def close(self): 114 | self.client.fetch_json( 115 | '/boards/' + self.id + '/closed', 116 | http_method='PUT', 117 | post_args={'value': 'true', }, ) 118 | self.closed = True 119 | 120 | def open(self): 121 | self.client.fetch_json( 122 | '/boards/' + self.id + '/closed', 123 | http_method='PUT', 124 | post_args={'value': 'false', }, ) 125 | self.closed = False 126 | 127 | def delete(self): 128 | self.client.fetch_json( 129 | '/boards/' + self.id, 130 | http_method='DELETE', ) 131 | 132 | def get_list(self, list_id): 133 | """Get list 134 | 135 | :rtype: List 136 | """ 137 | obj = self.client.fetch_json('/lists/' + list_id) 138 | return List.from_json(board=self, json_obj=obj) 139 | 140 | def all_lists(self): 141 | """Returns all lists on this board 142 | 143 | :rtype: list of List 144 | """ 145 | return self.get_lists('all') 146 | 147 | def open_lists(self): 148 | """Returns all open lists on this board 149 | 150 | :rtype: list of List 151 | """ 152 | return self.get_lists('open') 153 | 154 | def closed_lists(self): 155 | """Returns all closed lists on this board 156 | 157 | :rtype: list of List 158 | """ 159 | return self.get_lists('closed') 160 | 161 | def get_lists(self, list_filter): 162 | """Get lists from filter 163 | 164 | :rtype: list of List 165 | """ 166 | # error checking 167 | json_obj = self.client.fetch_json( 168 | '/boards/' + self.id + '/lists', 169 | query_params={'cards': 'none', 'filter': list_filter}) 170 | return [List.from_json(board=self, json_obj=obj) for obj in json_obj] 171 | 172 | def list_lists(self, list_filter='all'): 173 | """Get lists from filter 174 | 175 | :rtype: list of List 176 | """ 177 | return self.get_lists(list_filter=list_filter) 178 | 179 | def get_custom_field_definitions(self): 180 | """Get all custom field definitions for this board 181 | 182 | :rtype: list of CustomFieldDefinition 183 | """ 184 | if self.customFieldDefinitions is None: 185 | json_obj = self.client.fetch_json('/boards/' + self.id + '/customFields') 186 | self.customFieldDefinitions = CustomFieldDefinition.from_json_list(self, json_obj) 187 | return self.customFieldDefinitions 188 | 189 | def add_custom_field_definition(self, name, type, options=None, display_on_card=None, pos=None): 190 | """Add a custom field definition to this board 191 | 192 | :name: name for the field 193 | :type: type of field: "checkbox", "list", "number", "text", "date" 194 | :options: list of options for field, only valid for "list" type 195 | :display_on_card: boolean whether this field should be shown on the front of cards 196 | :pos: position of the list: "bottom", "top" or a positive number 197 | :return: the custom_field_definition 198 | :rtype: CustomFieldDefinition 199 | """ 200 | arguments = {'idModel': self.id, "modelType": "board", 'name': name, 'type': type} 201 | if options: 202 | arguments["options"] = options 203 | if display_on_card: 204 | arguments["display_cardFront"] = display_on_card 205 | if pos: 206 | arguments["pos"] = pos 207 | obj = self.client.fetch_json( 208 | '/customFields', 209 | http_method='POST', 210 | post_args=arguments, ) 211 | return CustomFieldDefinition.from_json(board=self, json_obj=obj) 212 | 213 | def update_custom_field_definition(self, custom_field_definition_id, name=None, display_on_card=None, pos=None): 214 | """Update a custom field definition on this board 215 | 216 | :custom_field_definition_id: the ID of the CustomFieldDefinition to update. 217 | :name: new name for the field 218 | :display_on_card: boolean whether this field should be shown on the front of cards 219 | :pos: position of the list: "bottom", "top" or a positive number 220 | :return: the custom_field_definition 221 | :rtype: CustomFieldDefinition 222 | """ 223 | arguments = {} 224 | if name: 225 | arguments["name"] = name 226 | if not display_on_card is None: 227 | arguments["display/cardFront"] = u"true" if display_on_card else u"false" 228 | if pos: 229 | arguments["pos"] = pos 230 | 231 | json_obj = self.client.fetch_json( 232 | '/customFields/{0}'.format(custom_field_definition_id), 233 | http_method='PUT', 234 | post_args=arguments, ) 235 | return json_obj 236 | 237 | def delete_custom_field_definition(self, custom_field_definition_id): 238 | """Delete a custom_field_definition from this board 239 | 240 | :custom_field_definition_id: the ID of the CustomFieldDefinition to delete. 241 | :return: the custom field definition 242 | :rtype: json 243 | """ 244 | json_obj = self.client.fetch_json( 245 | '/customFields/{0}'.format(custom_field_definition_id), 246 | http_method='DELETE', ) 247 | return json_obj 248 | 249 | def get_custom_field_list_options(self,custom_field_definition_id,values_only=False): 250 | """Get custom field definition list options on this board 251 | 252 | :custom_field_definition_id: the ID of the CustomFieldDefinition. 253 | :values_only: Boolean to return only the values of the options, excluding color & position 254 | :return: the custom_field_definition 255 | :rtype: CustomFieldDefinition 256 | """ 257 | 258 | json_obj = self.client.fetch_json( 259 | 'customFields/{0}/options'.format(custom_field_definition_id), 260 | http_method='GET', 261 | ) 262 | if values_only: 263 | return [v for jo in json_obj for v in jo['value'].values()] 264 | return json_obj 265 | 266 | def add_custom_field_list_option(self,custom_field_definition_id,new_option): 267 | """Update a custom field definition on this board 268 | 269 | :custom_field_definition_id: the ID of the CustomFieldDefinition to update. 270 | :new_option: The new option to add to the list 271 | :return: the custom_field_definition 272 | :rtype: CustomFieldDefinition 273 | """ 274 | 275 | json_obj = self.client.fetch_json( 276 | 'customFields/{0}/options'.format(custom_field_definition_id), 277 | http_method='POST', 278 | post_args={'value': {'text':new_option}, 279 | }, ) 280 | return json_obj 281 | 282 | def get_custom_field_list_option(self,custom_field_definition_id,option_id): 283 | """Get a specific custom field definition list option on this board 284 | 285 | :custom_field_definition_id: the ID of the CustomFieldDefinition. 286 | :option_id: the ID of the option 287 | :return: the custom_field_definition 288 | :rtype: CustomFieldDefinition 289 | """ 290 | 291 | json_obj = self.client.fetch_json( 292 | 'customFields/{0}/options/{1}'.format(custom_field_definition_id,option_id), 293 | http_method='GET', 294 | ) 295 | # if values_only: 296 | # return [v for jo in json_obj for v in jo['value'].values()] 297 | return json_obj 298 | 299 | def delete_custom_field_list_option(self,custom_field_definition_id,option_id): 300 | """DELETE a specific custom field definition list option on this board 301 | 302 | :custom_field_definition_id: the ID of the CustomFieldDefinition. 303 | :option_id: the ID of the option 304 | :return: the custom_field_definition 305 | :rtype: CustomFieldDefinition 306 | """ 307 | 308 | json_obj = self.client.fetch_json( 309 | 'customFields/{0}/options/{1}'.format(custom_field_definition_id,option_id), 310 | http_method='DELETE', 311 | ) 312 | # if values_only: 313 | # return [v for jo in json_obj for v in jo['value'].values()] 314 | return json_obj 315 | 316 | def get_labels(self, fields='all', limit=50): 317 | """Get label 318 | 319 | :rtype: list of Label 320 | """ 321 | json_obj = self.client.fetch_json( 322 | '/boards/' + self.id + '/labels', 323 | query_params={'fields': fields, 'limit': limit}) 324 | return Label.from_json_list(self, json_obj) 325 | 326 | def get_label(self, label_id): 327 | """ 328 | :label_id: str label id 329 | 330 | :rtype: instance of Label 331 | """ 332 | json_obj = self.client.fetch_json( 333 | '/boards/' + self.id + '/labels/' + label_id 334 | ) 335 | 336 | return Label.from_json(self, json_obj) 337 | 338 | def get_checklists(self, cards='all'): 339 | """Get checklists 340 | 341 | :rtype: list of Checklist 342 | """ 343 | checklists = [] 344 | json_obj = self.client.fetch_json( 345 | '/boards/' + self.id + '/checklists', 346 | query_params={'cards': cards}) 347 | json_obj = sorted(json_obj, key=lambda checklist: checklist['pos']) 348 | for cl in json_obj: 349 | checklists.append(Checklist(self.client, cl, trello_card=cl.get('idCard'))) 350 | return checklists 351 | 352 | def add_list(self, name, pos=None): 353 | """Add a list to this board 354 | 355 | :name: name for the list 356 | :pos: position of the list: "bottom", "top" or a positive number 357 | :return: the list 358 | :rtype: List 359 | """ 360 | arguments = {'name': name, 'idBoard': self.id} 361 | if pos: 362 | arguments["pos"] = pos 363 | obj = self.client.fetch_json( 364 | '/lists', 365 | http_method='POST', 366 | post_args=arguments, ) 367 | return List.from_json(board=self, json_obj=obj) 368 | 369 | def add_label(self, name, color): 370 | """Add a label to this board 371 | 372 | :name: name of the label 373 | :color: the color, either green, yellow, orange 374 | red, purple, blue, sky, lime, pink, or black 375 | :return: the label 376 | :rtype: Label 377 | """ 378 | obj = self.client.fetch_json( 379 | '/labels', 380 | http_method='POST', 381 | post_args={'name': name, 'idBoard': self.id, 'color': color}, ) 382 | return Label.from_json(board=self, json_obj=obj) 383 | 384 | def delete_label(self, label_id): 385 | """Delete a label from this board 386 | 387 | :label_id: the ID of the label to delete. 388 | :return: the label 389 | :rtype: json 390 | """ 391 | json_obj = self.client.fetch_json( 392 | '/labels/{0}'.format(label_id), 393 | http_method='DELETE', 394 | post_args={'id': label_id}, ) 395 | return json_obj 396 | 397 | def all_cards(self, custom_field_items='true'): 398 | """Returns all cards on this board 399 | 400 | :rtype: list of Card 401 | """ 402 | filters = { 403 | 'filter': 'all', 404 | 'fields': 'all', 405 | 'customFieldItems': custom_field_items 406 | } 407 | return self.get_cards(filters) 408 | 409 | def open_cards(self, custom_field_items='true'): 410 | """Returns all open cards on this board 411 | 412 | :rtype: list of Card 413 | """ 414 | filters = { 415 | 'filter': 'open', 416 | 'fields': 'all', 417 | 'customFieldItems': custom_field_items 418 | } 419 | return self.get_cards(filters) 420 | 421 | def closed_cards(self, custom_field_items='true'): 422 | """Returns all closed cards on this board 423 | 424 | :rtype: list of Card 425 | """ 426 | filters = { 427 | 'filter': 'closed', 428 | 'fields': 'all', 429 | 'customFieldItems': custom_field_items 430 | } 431 | return self.get_cards(filters) 432 | 433 | def visible_cards(self, custom_field_items='true'): 434 | """Returns all visible cards on this board 435 | 436 | :rtype: list of Card 437 | """ 438 | filters = { 439 | 'filter': 'visible', 440 | 'fields': 'all', 441 | 'customFieldItems': custom_field_items 442 | } 443 | return self.get_cards(filters) 444 | 445 | def get_cards(self, filters=None, card_filter=""): 446 | """ 447 | :filters: dict containing query parameters. Eg. {'fields': 'all'} 448 | :card_filter: filters on card status ('open', 'closed', 'all') 449 | 450 | More info on card queries: 451 | https://trello.com/docs/api/board/index.html#get-1-boards-board-id-cards 452 | 453 | :rtype: list of Card 454 | """ 455 | json_obj = self.client.fetch_json( 456 | '/boards/' + self.id + '/cards/' + card_filter, 457 | query_params=filters 458 | ) 459 | 460 | return list([Card.from_json(self, json) for json in json_obj]) 461 | 462 | def get_card(self, card_id): 463 | """ 464 | :card_id: str card id. 465 | 466 | More info on card queries: 467 | https://developer.atlassian.com/cloud/trello/rest/api-group-boards/#api-boards-id-cards-idcard-get 468 | 469 | :rtype: instance of Card 470 | """ 471 | json_obj = self.client.fetch_json( 472 | '/boards/' + self.id + '/cards/' + card_id 473 | ) 474 | 475 | return Card.from_json(self, json_obj) 476 | 477 | def all_members(self): 478 | """Returns all members on this board 479 | 480 | :rtype: list of Member 481 | """ 482 | filters = { 483 | 'filter': 'all', 484 | 'fields': 'all' 485 | } 486 | return self.get_members(filters) 487 | 488 | def normal_members(self): 489 | """Returns all normal members on this board 490 | 491 | :rtype: list of Member 492 | """ 493 | filters = { 494 | 'filter': 'normal', 495 | 'fields': 'all' 496 | } 497 | return self.get_members(filters) 498 | 499 | def admin_members(self): 500 | """Returns all admin members on this board 501 | 502 | :rtype: list of Member 503 | """ 504 | filters = { 505 | 'filter': 'admins', 506 | 'fields': 'all' 507 | } 508 | return self.get_members(filters) 509 | 510 | def owner_members(self): 511 | """Returns all owner members on this board 512 | 513 | :rtype: list of Member 514 | """ 515 | filters = { 516 | 'filter': 'owners', 517 | 'fields': 'all' 518 | } 519 | return self.get_members(filters) 520 | 521 | def get_members(self, filters=None): 522 | """Get members with filter 523 | 524 | :filters: dict containing query parameters. 525 | Eg. {'fields': 'all', 'filter': 'admins'} 526 | 527 | More info on possible filters: 528 | https://developers.trello.com/advanced-reference/board#get-1-boards-board-id-members 529 | 530 | :rtype: list of Member 531 | """ 532 | json_obj = self.client.fetch_json( 533 | '/boards/' + self.id + '/members', 534 | query_params=filters) 535 | members = list() 536 | for obj in json_obj: 537 | m = Member.from_json(self.client, obj) 538 | members.append(m) 539 | 540 | return members 541 | 542 | # Add a member to a board 543 | def add_member(self, member, member_type="normal"): 544 | json_obj = self.client.fetch_json( 545 | '/boards/{0}/members/{1}'.format(self.id, member.id), 546 | http_method='PUT', 547 | post_args={'idMember': member.id, "type": member_type}, 548 | ) 549 | return json_obj 550 | 551 | # Removes an existing member of a board 552 | def remove_member(self, member): 553 | json_obj = self.client.fetch_json( 554 | '/boards/{0}/members/{1}'.format(self.id, member.id), 555 | http_method='DELETE', 556 | post_args={'idMember': member.id}, 557 | ) 558 | return json_obj 559 | 560 | def fetch_actions(self, action_filter, action_limit=50, before=None, since=None): 561 | """Returns all actions that conform to the given filters. 562 | 563 | :action_filter: str of possible actions separated by comma 564 | ie. 'createCard,updateCard' 565 | :action_limit: int of max items returned 566 | :before: datetime obj 567 | :since: datetime obj 568 | 569 | More info on action filter values: 570 | https://developers.trello.com/advanced-reference/board#get-1-boards-board-id-actions 571 | 572 | :rtype: json list of past actions 573 | """ 574 | query_params = {'filter': action_filter, 'limit': action_limit} 575 | 576 | if since: 577 | query_params["since"] = since 578 | 579 | if before: 580 | query_params["before"] = before 581 | 582 | json_obj = self.client.fetch_json('/boards/' + self.id + '/actions', 583 | query_params=query_params) 584 | 585 | self.actions = json_obj 586 | return self.actions 587 | 588 | @property 589 | def date_last_activity(self): 590 | if self._date_last_activity is None: 591 | self._date_last_activity = self.get_last_activity() 592 | return self._date_last_activity 593 | 594 | def get_last_activity(self): 595 | """Return the date of the last action done on the board. 596 | 597 | :rtype: datetime.datetime 598 | """ 599 | json_obj = self.client.fetch_json( 600 | '/boards/{0}/dateLastActivity'.format(self.id)) 601 | if json_obj['_value']: 602 | return dateparser.parse(json_obj['_value']) 603 | def get_power_ups(self, board_id=None, name='', filters=None): 604 | ''' 605 | List the Power-Ups on a board 606 | :filters: defaults to enabled 607 | valid values: enabled, available 608 | ''' 609 | arguments = {} 610 | if board_id is None: 611 | board_id = self.id 612 | if filters: 613 | if not filters in ("enabled", "available"): 614 | filers = "enabled" 615 | arguments['filter'] = filters 616 | json_obj = self.client.fetch_json( 617 | '/boards/' + board_id +'/plugins', 618 | http_method='GET',post_args=arguments, ) 619 | return list([PowerUp.from_json(self, json_obj=json) for json in json_obj]) 620 | def enable_power_up(self, powerup_id,board_id=None): 621 | from warnings import warn 622 | warn("Atlassian has DEPRECATED enabling power ups! enable_power_up will be updated once an alternative becomes available",DeprecationWarning,stacklevel=2) 623 | if board_id is None: 624 | board_id = self.id 625 | json_obj = self.client.fetch_json( 626 | 'boards/' + board_id + '/boardPlugins', 627 | http_method='POST', 628 | post_args={'idPlugin':powerup_id},) 629 | return json_obj 630 | def disable_power_up(self, powerup_id, board_id=None): 631 | from warnings import warn 632 | warn("Atlassian has DEPRECATED disabling power ups! disable_power_up will be updated once an alternative becomes available",DeprecationWarning,stacklevel=2) 633 | if board_id is None: 634 | board_id = self.id 635 | json_obj = self.client.fetch_json( 636 | 'boards/' + board_id + '/boardPlugins' + powerup_id, 637 | http_method='DELETE') 638 | return json_obj 639 | def get_enabled_power_ups(self, board_id=None, name='',): 640 | ''' 641 | List the enabled Power-Ups on a board 642 | ''' 643 | if board_id is None: 644 | board_id = self.id 645 | json_obj = self.client.fetch_json( 646 | 'boards/' + board_id + '/boardPlugins', 647 | http_method='GET') 648 | print(json_obj) 649 | return list([PowerUp.from_json(self, json_obj=json) for json in json_obj]) 650 | 651 | -------------------------------------------------------------------------------- /trello/card.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import with_statement, print_function, absolute_import 3 | 4 | import datetime 5 | from operator import itemgetter 6 | 7 | import pytz 8 | from dateutil import parser as dateparser 9 | 10 | from trello import TrelloBase 11 | from trello.attachments import Attachments 12 | from trello.checklist import Checklist 13 | from trello.compat import force_str 14 | from trello.label import Label 15 | from trello.organization import Organization 16 | from trello.customfield import CustomField, CustomFieldText, CustomFieldCheckbox, CustomFieldNumber, CustomFieldDate, CustomFieldList 17 | 18 | 19 | class Card(TrelloBase): 20 | """ 21 | Class representing a Trello card. Card attributes are stored on 22 | the object 23 | 24 | https://developers.trello.com/advanced-reference/card 25 | """ 26 | 27 | @property 28 | def short_url(self): 29 | return self.shortUrl 30 | 31 | @property 32 | def member_id(self): 33 | return self.idMembers 34 | 35 | @property 36 | def short_id(self): 37 | return self.idShort 38 | 39 | @property 40 | def list_id(self): 41 | return self.idList 42 | 43 | @property 44 | def board_id(self): 45 | return self.idBoard 46 | 47 | @property 48 | def description(self): 49 | return self.desc 50 | 51 | @property 52 | def date_last_activity(self): 53 | return self.dateLastActivity 54 | 55 | @property 56 | def labels(self): 57 | return self._labels if self._labels else [] 58 | 59 | @property 60 | def custom_fields(self): 61 | """ 62 | Lazily loads and returns the custom fields 63 | """ 64 | if self.customFields is None: 65 | self.customFields = self.fetch_custom_fields() 66 | return self.customFields 67 | 68 | @property 69 | def comments(self): 70 | """ 71 | Lazily loads and returns the comments 72 | """ 73 | if self._comments is None: 74 | self._comments = self.fetch_comments() 75 | return self._comments 76 | 77 | @property 78 | def checklists(self): 79 | """ 80 | Lazily loads and returns the checklists 81 | """ 82 | if self._checklists is None: 83 | self._checklists = self.fetch_checklists() 84 | return self._checklists 85 | 86 | @property 87 | def plugin_data(self): 88 | """ 89 | Lazily loads and returns the plugin data 90 | """ 91 | if self._plugin_data is None: 92 | self._plugin_data = self.fetch_plugin_data() 93 | return self._plugin_data 94 | 95 | @property 96 | def attachments(self): 97 | """ 98 | Lazily loads and returns the attachments 99 | """ 100 | if self._attachments is None: 101 | self._attachments = self.fetch_attachments() 102 | return self._attachments 103 | 104 | def __init__(self, parent, card_id, name=''): 105 | """ 106 | :parent: reference to the parent trello list 107 | :card_id: ID for this card 108 | """ 109 | super(Card, self).__init__() 110 | if isinstance(parent, List): 111 | self.trello_list = parent 112 | self.board = parent.board 113 | else: 114 | self.board = parent 115 | 116 | self.client = parent.client 117 | self.id = card_id 118 | self.name = name 119 | 120 | self.customFields = None 121 | self._checklists = None 122 | self._comments = None 123 | self._plugin_data = None 124 | self._attachments = None 125 | self._labels = None 126 | self._json_obj = None 127 | 128 | @classmethod 129 | def from_json(cls, parent, json_obj): 130 | """ 131 | Deserialize the card json object to a Card object 132 | 133 | :parent: the list object that the card belongs to 134 | :json_obj: json object 135 | 136 | :rtype: Card 137 | """ 138 | if 'id' not in json_obj: 139 | raise Exception("key 'id' is not in json_obj") 140 | card = cls(parent, 141 | json_obj['id'], 142 | name=json_obj['name']) 143 | card._json_obj = json_obj 144 | card.desc = json_obj.get('desc', '') 145 | card.due = json_obj.get('due', '') 146 | card.is_due_complete = json_obj['dueComplete'] 147 | card.closed = json_obj['closed'] 148 | card.url = json_obj['url'] 149 | card.pos = json_obj['pos'] 150 | card.shortUrl = json_obj['shortUrl'] 151 | card.idMembers = json_obj['idMembers'] 152 | card.member_ids = json_obj['idMembers'] 153 | card.idLabels = json_obj['idLabels'] 154 | card.idBoard = json_obj['idBoard'] 155 | card.idList = json_obj['idList'] 156 | card.idShort = json_obj['idShort'] 157 | card.badges = json_obj['badges'] 158 | card.customFields = card.fetch_custom_fields(json_obj=json_obj) 159 | card.countCheckItems = json_obj['badges']['checkItems'] 160 | card.countCheckLists = len(json_obj['idChecklists']) 161 | card._labels = Label.from_json_list(card.board, json_obj['labels']) 162 | card.dateLastActivity = dateparser.parse(json_obj['dateLastActivity']) 163 | if "attachments" in json_obj: 164 | card._attachments = [] 165 | for attachment_json in json_obj["attachments"]: 166 | card._attachments.append(attachment_json) 167 | if 'actions' in json_obj: 168 | card.actions = json_obj['actions'] 169 | return card 170 | 171 | def __repr__(self): 172 | return force_str(u'' % self.name) 173 | 174 | def fetch(self, eager=True): 175 | """ 176 | Fetch all attributes for this card 177 | 178 | :param eager: If eager, comments, checklists and attachments will be fetched immediately, otherwise on demand 179 | """ 180 | json_obj = self.client.fetch_json( 181 | '/cards/' + self.id, 182 | query_params={'badges': False, 'customFieldItems': 'true'}) 183 | self.id = json_obj['id'] 184 | self.name = json_obj['name'] 185 | self.desc = json_obj.get('desc', '') 186 | self.closed = json_obj['closed'] 187 | self.url = json_obj['url'] 188 | self.shortUrl = json_obj['shortUrl'] 189 | self.idMembers = json_obj['idMembers'] 190 | self.idShort = json_obj['idShort'] 191 | self.idList = json_obj['idList'] 192 | self.idBoard = json_obj['idBoard'] 193 | self.idLabels = json_obj['idLabels'] 194 | self._labels = Label.from_json_list(self.board, json_obj['labels']) 195 | self.badges = json_obj['badges'] 196 | self.pos = json_obj['pos'] 197 | if json_obj.get('due', ''): 198 | self.due = json_obj.get('due', '') 199 | else: 200 | self.due = '' 201 | self.dateLastActivity = dateparser.parse(json_obj['dateLastActivity']) 202 | 203 | self._customFields = self.fetch_custom_fields(json_obj=json_obj) 204 | self._plugin_data = self.fetch_plugin_data() if eager else None 205 | self._checklists = self.fetch_checklists() if eager else None 206 | self._comments = self.fetch_comments() if eager else None 207 | self._attachments = self.fetch_attachments() if eager else None 208 | 209 | def fetch_custom_fields(self, json_obj=None): 210 | """ 211 | Fetch current set of custom fields from card or json_obj. 212 | """ 213 | if json_obj is None: 214 | json_obj = self.client.fetch_json( 215 | '/cards/' + self.id, 216 | query_params={'badges': False, 'customFieldItems': 'true'}) 217 | return CustomField.from_json_list( 218 | self, json_obj.get('customFieldItems', {})) 219 | 220 | def fetch_comments(self, force=False, limit=None): 221 | comments = [] 222 | 223 | if (force is True) or (self.badges['comments'] > 0): 224 | query_params = {'filter': 'commentCard,copyCommentCard'} 225 | if limit is not None: 226 | query_params['limit'] = limit 227 | comments = self.client.fetch_json( 228 | '/cards/' + self.id + '/actions', 229 | query_params=query_params) 230 | return sorted(comments, key=lambda comment: comment['date']) 231 | return comments 232 | 233 | def get_list(self): 234 | obj = self.client.fetch_json('/lists/' + self.idList) 235 | return List.from_json(board=self, json_obj=obj) 236 | 237 | def get_comments(self): 238 | """Alias for fetch_comments for backward compatibility. 239 | Always contact server 240 | """ 241 | return self.fetch_comments(force=True) 242 | 243 | def fetch_checklists(self): 244 | 245 | if self.countCheckLists == 0: 246 | return [] 247 | 248 | checklists = [] 249 | json_obj = self.client.fetch_json( 250 | '/cards/' + self.id + '/checklists', ) 251 | # Thanks https://github.com/HuffAndPuff for noticing checklist 252 | # were not sorted 253 | json_obj = sorted(json_obj, key=lambda checklist: checklist['pos']) 254 | for cl in json_obj: 255 | checklists.append(Checklist(self.client, cl, 256 | trello_card=self.id)) 257 | return checklists 258 | 259 | def fetch_plugin_data(self): 260 | items = self.client.fetch_json( 261 | '/cards/' + self.id + '/pluginData') 262 | return items 263 | 264 | def fetch_attachments(self, force=False): 265 | if (force is True) or (self.badges['attachments'] > 0): 266 | items = self.client.fetch_json( 267 | '/cards/' + self.id + '/attachments', 268 | query_params={'filter':'false'}) 269 | return items 270 | return [] 271 | 272 | def get_attachments(self): 273 | return [Attachments.from_json(attachments_json) for attachments_json in self.fetch_attachments(force=True)] 274 | 275 | def fetch_actions(self, action_filter='createCard', since=None, before=None, action_limit=50): 276 | """ 277 | Fetch actions for this card can give more argv to action_filter, 278 | split for ',' json_obj is list 279 | """ 280 | query_params={'filter': action_filter, 'limit': action_limit} 281 | if since: 282 | query_params["since"] = since 283 | 284 | if before: 285 | query_params["before"] = before 286 | 287 | json_obj = self.client.fetch_json( 288 | '/cards/' + self.id + '/actions', 289 | query_params=query_params) 290 | 291 | self.actions = json_obj 292 | return self.actions 293 | 294 | def attriExp(self, multiple): 295 | """ 296 | Provides the option to explore what comes from trello 297 | :multiple is one of the attributes of GET /1/cards/[card id or shortlink]/actions 298 | """ 299 | self.fetch_actions(multiple) 300 | return self.actions 301 | 302 | @staticmethod 303 | def _movement_as_triplet(source_list, destination_list, movement_datetime): 304 | return [source_list["name"], destination_list["name"], movement_datetime] 305 | 306 | @staticmethod 307 | def _movement_as_dict(source_list, destination_list, movement_datetime): 308 | _movement = { 309 | "source": source_list, 310 | "destination": destination_list, 311 | "datetime": movement_datetime, 312 | } 313 | return _movement 314 | 315 | def _list_movements(self, movement_function, filter_by_date_interval=None): 316 | """ 317 | Returns the list of movements of this card. 318 | The list of movements is in descending date and time order. First movement is the closest one to now. 319 | Its structure is a list of dicts where the lists are "source" and "destination" and both are also dicts. 320 | Date and time of the movement is in key "datetime" as a datetime object. 321 | :param movement_function: function that returns a representation of the movement. 322 | :param filter_by_date_interval: Date interval used to filter card movements to return. Optional 323 | :return: list with the movements. 324 | """ 325 | 326 | action_since = None if not filter_by_date_interval else filter_by_date_interval[0] 327 | action_before = None if not filter_by_date_interval else filter_by_date_interval[1] 328 | if not hasattr(self, "actions") or self.actions is None: 329 | self.fetch_actions('updateCard:idList,', action_since, action_before) 330 | 331 | movements = [] 332 | 333 | for idx in self.actions: 334 | date_str = idx['date'] 335 | movement_datetime = dateparser.parse(date_str) 336 | try: 337 | source_list = idx['data']['listBefore'] 338 | destination_list = idx['data']['listAfter'] 339 | except KeyError: 340 | continue 341 | movement = movement_function(source_list, destination_list, movement_datetime) 342 | movements.append(movement) 343 | 344 | return movements 345 | 346 | def listCardMove_date(self): 347 | """Will return the history of transitions of a card from one list to 348 | another. The lower the index the more recent the historical item. 349 | 350 | It returns a list of lists. The sublists are triplets of 351 | starting list, ending list and when the transition occurred. 352 | """ 353 | return self._list_movements(movement_function=Card._movement_as_triplet) 354 | 355 | def list_movements(self, list_cmp=None, filter_by_date_interval=None): 356 | """Will return the history of transitions of a card from one list to 357 | another. The lower the index the more recent the historical item. 358 | 359 | It returns a list of dicts in date and time descending order (the 360 | first movement is the most recent). 361 | Dicts are of the form source: 362 | destination: datetime: 363 | 364 | :param: list_cmp Comparison function between lists. For list_cmp(a, b) returns -1 if list a is greater that list b. Returns 1 otherwise. 365 | :param: filter_by_date_interval: pair of two dates (two strings in YYYY-MM-DD format) to filter card movements by date. 366 | """ 367 | movement_as_dict_function = Card._movement_as_dict 368 | if list_cmp: 369 | def movement_as_dict_function(_source_list, _destination_list, _movement_datetime): 370 | _movement = Card._movement_as_dict(_source_list, _destination_list, _movement_datetime) 371 | _source_list_id = _source_list["id"] 372 | _destination_list_id = _destination_list["id"] 373 | _movement["moving_forward"] = list_cmp(_source_list_id, _destination_list_id) > 0 374 | return _movement 375 | 376 | return self._list_movements(movement_function=movement_as_dict_function, filter_by_date_interval=filter_by_date_interval) 377 | 378 | def get_stats_by_list(self, lists, list_cmp=None, done_list=None, time_unit="seconds", card_movements_filter=None): 379 | """Gets several stats about the card by each list of the board: 380 | - time: The time that the card has been in each column in seconds (minutes or hours). 381 | - forward_moves: How many times this card has been the source of a forward movement. 382 | - backward_moves: How many times this card has been the source of a backward movement. 383 | 384 | Returns a dict where the key is list id and value is a dict with keys 385 | time, forward_moves and backward_moves. 386 | 387 | :param lists: list of board lists. 388 | :param list_cmp: function that compares two lists a,b given id_a, id_b. If b is in a forward position returns 1 else -1. 389 | :param time_unit: default to seconds. Allow specifying time in "minutes" or "hours". 390 | :param done_list: Column that implies that the task is done. If present, time measurement will be stopped if is current task list. 391 | :param card_movements_filter: Pair of two dates (two strings in YYYY-MM-DD format) that will filter the movements of the card. Optional. 392 | :return: dict of the form {list_id: {time: