├── doc ├── source │ ├── _static │ │ └── tmp.txt │ ├── _templates │ │ └── tmp.txt │ ├── query.rst │ ├── client.rst │ ├── template.rst │ ├── workitem.rst │ ├── projectarea.rst │ ├── installation.rst │ ├── models.rst │ ├── index.rst │ ├── advanced_usage.rst │ ├── introduction.rst │ ├── workitem_attr.rst │ ├── conf.py │ └── quickstart.rst ├── requirements.txt ├── Makefile └── make.bat ├── MANIFEST.in ├── setup.cfg ├── rtcclient ├── exception.py ├── __init__.py ├── templates │ └── issue_example.template ├── utils.py ├── models.py ├── project_area.py └── query.py ├── .pre-commit-config.yaml ├── .readthedocs.yaml ├── tests ├── conftest.py ├── fixtures │ ├── administrators.xml │ ├── changesets.xml │ ├── comments.xml │ ├── projectareas.xml │ ├── issue_example.template │ ├── roles.xml │ ├── priorities.xml │ ├── severities.xml │ ├── states.xml │ ├── filedagainsts.xml │ ├── teamareas.xml │ ├── itemtypes.xml │ ├── foundins.xml │ ├── members.xml │ ├── actions.xml │ ├── plannedfors.xml │ ├── attachment.xml │ ├── includedinbuilds.xml │ ├── savedqueries.xml │ └── parent.xml ├── test_template.py ├── test_base.py ├── utils_test.py └── test_projectarea.py ├── examples └── how_to │ ├── workitem │ ├── add_comment.py │ └── get_workitem.py │ └── query │ └── query_workitems.py ├── .gitignore ├── .github └── workflows │ ├── release.yml │ └── ci.yml ├── tox.ini ├── pyproject.toml ├── README.rst └── LICENSE /doc/source/_static/tmp.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/source/_templates/tmp.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | xmltodict 2 | six 3 | lxml 4 | -------------------------------------------------------------------------------- /doc/source/query.rst: -------------------------------------------------------------------------------- 1 | .. _query_api: 2 | 3 | Query 4 | ===== 5 | 6 | .. autoclass:: rtcclient.query.Query 7 | :members: 8 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include ChangeLog 3 | 4 | exclude .gitignore 5 | exclude .gitreview 6 | 7 | global-exclude *.pyc 8 | -------------------------------------------------------------------------------- /doc/source/client.rst: -------------------------------------------------------------------------------- 1 | .. _client_api: 2 | 3 | Client 4 | ====== 5 | 6 | .. autoclass:: rtcclient.client.RTCClient 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/source/template.rst: -------------------------------------------------------------------------------- 1 | .. _template_api: 2 | 3 | Template 4 | ======== 5 | 6 | .. autoclass:: rtcclient.template.Templater 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/source/workitem.rst: -------------------------------------------------------------------------------- 1 | .. _workitem_api: 2 | 3 | Workitem 4 | ======== 5 | 6 | .. autoclass:: rtcclient.workitem.Workitem 7 | :members: 8 | -------------------------------------------------------------------------------- /doc/source/projectarea.rst: -------------------------------------------------------------------------------- 1 | .. _projectarea_api: 2 | 3 | ProjectArea 4 | =========== 5 | 6 | .. autoclass:: rtcclient.project_area.ProjectArea 7 | :members: 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [build_sphinx] 2 | all_files = 1 3 | build-dir = doc/build 4 | source-dir = doc/source 5 | builders = html,man,linkcheck 6 | 7 | [bdist_wheel] 8 | universal=0 9 | -------------------------------------------------------------------------------- /rtcclient/exception.py: -------------------------------------------------------------------------------- 1 | class RTCException(Exception): 2 | """Base exception class for all errors 3 | """ 4 | 5 | pass 6 | 7 | 8 | class BadValue(RTCException): 9 | pass 10 | 11 | 12 | class NotFound(RTCException): 13 | pass 14 | 15 | 16 | class EmptyAttrib(RTCException): 17 | pass 18 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git://github.com/pre-commit/mirrors-autopep8 3 | rev: v1.3 4 | hooks: 5 | - id: autopep8 6 | exclude: doc/*/* 7 | 8 | - repo: git://github.com/pre-commit/pre-commit-hooks 9 | rev: v0.7.0 10 | hooks: 11 | - id: check-json 12 | - id: check-xml 13 | - id: check-yaml 14 | - id: debug-statements 15 | - id: flake8 16 | - id: requirements-txt-fixer 17 | - id: trailing-whitespace 18 | exclude: doc/*/* 19 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: doc/source/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: doc/requirements.txt 23 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from rtcclient.client import RTCClient 3 | import requests 4 | from utils_test import _search_path 5 | 6 | 7 | @pytest.fixture(scope="function") 8 | def rtcclient(mocker): 9 | mock_resp = mocker.MagicMock(spec=requests.Response) 10 | mock_resp.status_code = 200 11 | mock_resp.headers = {"set-cookie": "cookie-id"} 12 | 13 | mocked_headers = mocker.patch("rtcclient.client.RTCClient._get_headers") 14 | mocked_headers.return_value = mock_resp 15 | 16 | return RTCClient(url="http://test.url:9443/jazz", 17 | username="tester1@email.com", 18 | password="password", 19 | searchpath=_search_path) 20 | -------------------------------------------------------------------------------- /tests/fixtures/administrators.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | tester1@email.com 4 | tester1 5 | mailto:tester1%40email.com 6 | 7 | 2009-08-17T10:08:03.721Z 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/how_to/workitem/add_comment.py: -------------------------------------------------------------------------------- 1 | from rtcclient.client import RTCClient 2 | from rtcclient.utils import setup_basic_logging 3 | 4 | if __name__ == "__main__": 5 | # you can remove this if you don't need logging 6 | # default logging for console output 7 | setup_basic_logging() 8 | 9 | url = "https://your_domain:9443/jazz" 10 | username = "your_username" 11 | password = "your_password" 12 | # If your rtc server is too old (such as Rational Team Concert 5.0.1, 5.0.2), 13 | # please set old_rtc_authentication to True. 14 | # Other kwargs, such as ends_with_jazz, old_rtc_authentication 15 | myclient = RTCClient(url, username, password, old_rtc_authentication=False) 16 | 17 | # change workitem id here 18 | workitem_id = 123456 19 | wk = myclient.getWorkitem(workitem_id) 20 | wk.addComment("changeme: add comments here") 21 | -------------------------------------------------------------------------------- /tests/fixtures/changesets.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/comments.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2015-07-27T02:35:47.391Z 4 | 5 | comment test 6 | 7 | 8 | 2015-07-27T10:48:55.197Z 9 | 10 | add comment test2 11 | 12 | -------------------------------------------------------------------------------- /tests/fixtures/projectareas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProjectArea1 4 | Demo for test: Project Area One 5 | 6 | true 7 | true 8 | 9 | 10 | ProjectArea2 11 | Demo for test: Project Area Two 12 | 13 | true 14 | false 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | 59 | # Intellij 60 | .idea 61 | 62 | # virtual env 63 | venv/ 64 | .venv/ 65 | 66 | -------------------------------------------------------------------------------- /tests/fixtures/issue_example.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | 7 | 8 | {{ description }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ title }} 17 | 18 | 19 | -------------------------------------------------------------------------------- /examples/how_to/workitem/get_workitem.py: -------------------------------------------------------------------------------- 1 | from rtcclient.client import RTCClient 2 | from rtcclient.utils import setup_basic_logging 3 | 4 | if __name__ == "__main__": 5 | # you can remove this if you don't need logging 6 | # default logging for console output 7 | setup_basic_logging() 8 | 9 | url = "https://your_domain:9443/jazz" 10 | username = "your_username" 11 | password = "your_password" 12 | # If your rtc server is too old (such as Rational Team Concert 5.0.1, 5.0.2), 13 | # please set old_rtc_authentication to True. 14 | # Other kwargs, such as ends_with_jazz, old_rtc_authentication 15 | myclient = RTCClient(url, username, password, old_rtc_authentication=False) 16 | 17 | # get all workitems 18 | # If both projectarea_id and projectarea_name are None, all the workitems 19 | # in all ProjectAreas will be returned 20 | workitems_list = myclient.getWorkitems(projectarea_id=None, 21 | projectarea_name=None) 22 | 23 | # get a workitem with its id 24 | workitem_id = 123456 25 | wk = myclient.getWorkitem(workitem_id) 26 | -------------------------------------------------------------------------------- /tests/fixtures/roles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | http://test.url:9443/jazz/process/project-areas/_vsoCMN-TEd6VxeRBsGx82Q/roles/Product%20Owner 5 | 6 | Product Owner 7 | Product Owner 8 | 9 | The person responsible for managing the Product Backlog. 10 | 11 | 12 | 13 | 14 | http://test.url:9443/jazz/process/project-areas/_vsoCMN-TEd6VxeRBsGx82Q/roles/Test%20Team 15 | 16 | Test Team 17 | Test Team 18 | A member of the cross-functional team. 19 | 20 | 21 | 22 | http://test.url:9443/jazz/process/project-areas/_vsoCMN-TEd6VxeRBsGx82Q/roles/default 23 | 24 | default 25 | default 26 | 27 | The default role, implicitly assigned to each user. This role cannot be assigned, removed, or reordered. 28 | 29 | 30 | -------------------------------------------------------------------------------- /doc/source/installation.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installation 4 | ============ 5 | 6 | This part of the documentation covers the installation of rtcclient. 7 | The first step to using any software package is getting it properly installed. 8 | 9 | 10 | Distribute & Pip 11 | ---------------- 12 | 13 | Installing rtcclient is simple with `pip `_, just run 14 | this in your terminal:: 15 | 16 | $ pip install rtcclient 17 | 18 | or, with `easy_install `_:: 19 | 20 | $ easy_install rtcclient 21 | 22 | 23 | Get from the Source Code 24 | ------------------------ 25 | 26 | RTCClient is actively developed on GitHub, where the code is 27 | `always available `_. 28 | 29 | You can either clone the public repository and checkout released tags 30 | (e.g. tag 0.1.dev95):: 31 | 32 | $ git clone git://github.com/dixudx/rtcclient.git 33 | $ cd rtcclient 34 | $ git checkout tags/0.1.dev95 35 | 36 | Once you have a copy of the source, you can embed it in your Python package, 37 | or install it into your site-packages easily:: 38 | 39 | $ python setup.py install 40 | -------------------------------------------------------------------------------- /rtcclient/__init__.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | try: 4 | requests.packages.urllib3.disable_warnings() 5 | except ImportError: 6 | pass 7 | 8 | try: 9 | import urlparse 10 | from urllib import quote as urlquote # noqa: F401 11 | from urllib import urlencode 12 | from urllib import unquote as urlunquote 13 | except ImportError: 14 | # Python3 15 | import urllib.parse as urlparse # noqa: F401 16 | from urllib.parse import quote as urlquote # noqa: F401 17 | from urllib.parse import urlencode # noqa: F401 18 | from urllib.parse import unquote as urlunquote # noqa: F401 19 | 20 | try: # pragma no cover 21 | import xmltodict 22 | _xml_to_dict_version = [int(x) for x in xmltodict.__version__.split('.')] 23 | # on xmltodict >= 0.13 it returns a dict 24 | if _xml_to_dict_version[0] == 0 and _xml_to_dict_version[1] >= 13: 25 | OrderedDict = dict 26 | else: 27 | from collections import OrderedDict 28 | except ImportError: # pragma no cover 29 | try: 30 | from ordereddict import OrderedDict 31 | except ImportError: 32 | OrderedDict = dict 33 | 34 | from rtcclient.client import RTCClient # noqa: F401 35 | -------------------------------------------------------------------------------- /tests/fixtures/priorities.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | priority.literal.l01 4 | Unassigned 5 | 6 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/enumeration/unassigned.gif 7 | 8 | 9 | 10 | priority.literal.l11 11 | High 12 | 13 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/enumeration/high.gif 14 | 15 | 16 | -------------------------------------------------------------------------------- /tests/fixtures/severities.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | severity.literal.l1 4 | Unclassified 5 | 6 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/enumeration/unassigned2.gif 7 | 8 | 9 | 10 | severity.literal.l2 11 | Normal 12 | 13 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/enumeration/normal.gif 14 | 15 | 16 | -------------------------------------------------------------------------------- /rtcclient/templates/issue_example.template: -------------------------------------------------------------------------------- 1 | 2 | 3 | false 4 | 5 | 6 | {{ description }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | {{ title }} 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*.*.*' 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Python 17 | uses: actions/setup-python@v4 18 | with: 19 | python-version: "3.10" 20 | - name: Display Python version 21 | run: python --version 22 | 23 | - name: Setup Poetry 24 | uses: abatilo/actions-poetry@v2 25 | 26 | - name: Build project for distribution 27 | run: poetry build 28 | 29 | - name: Check Version 30 | id: check-version 31 | run: | 32 | [[ "$(poetry version --short)" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || echo prerelease=true >> $GITHUB_OUTPUT 33 | 34 | - name: Create Release 35 | uses: ncipollo/release-action@v1 36 | with: 37 | artifacts: "dist/*" 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | draft: false 40 | prerelease: steps.check-version.outputs.prerelease == 'true' 41 | 42 | - name: Publish to PyPI 43 | env: 44 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} 45 | run: poetry publish 46 | -------------------------------------------------------------------------------- /tests/fixtures/states.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | default_workflow.state.s1 4 | Closed 5 | 6 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/workflow/close.gif 7 | 8 | inprogress 9 | 10 | 11 | default_workflow.state.s2 12 | In Progress 13 | 14 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/workflow/inprogress.gif 15 | 16 | closed 17 | 18 | -------------------------------------------------------------------------------- /doc/source/models.rst: -------------------------------------------------------------------------------- 1 | .. _models_api: 2 | 3 | Models 4 | ====== 5 | 6 | 7 | .. autoclass:: rtcclient.models.Role 8 | :members: 9 | 10 | 11 | .. autoclass:: rtcclient.models.Member 12 | :members: 13 | 14 | 15 | .. autoclass:: rtcclient.models.Administrator 16 | :members: 17 | 18 | 19 | .. autoclass:: rtcclient.models.ItemType 20 | :members: 21 | 22 | 23 | .. autoclass:: rtcclient.models.TeamArea 24 | :members: 25 | 26 | 27 | .. autoclass:: rtcclient.models.PlannedFor 28 | :members: 29 | 30 | 31 | .. autoclass:: rtcclient.models.FiledAgainst 32 | :members: 33 | 34 | 35 | .. autoclass:: rtcclient.models.FoundIn 36 | :members: 37 | 38 | 39 | .. autoclass:: rtcclient.models.Severity 40 | :members: 41 | 42 | 43 | .. autoclass:: rtcclient.models.Priority 44 | :members: 45 | 46 | .. autoclass:: rtcclient.models.Action 47 | :members: 48 | 49 | 50 | .. autoclass:: rtcclient.models.State 51 | :members: 52 | 53 | 54 | .. autoclass:: rtcclient.models.Comment 55 | :members: 56 | 57 | 58 | .. autoclass:: rtcclient.models.SavedQuery 59 | :members: 60 | 61 | 62 | .. autoclass:: rtcclient.models.IncludedInBuild 63 | :members: 64 | 65 | 66 | .. autoclass:: rtcclient.models.ChangeSet 67 | :members: 68 | 69 | .. autoclass:: rtcclient.models.Change 70 | :members: 71 | -------------------------------------------------------------------------------- /tests/fixtures/filedagainsts.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Unassigned 5 | Unassigned 6 | 7 | 8 | 0 9 | true 10 | 11 | 12 | 13 | Category 1 14 | Category 1 15 | Category to organize your work items. 16 | 17 | 0 18 | false 19 | 20 | -------------------------------------------------------------------------------- /tests/fixtures/teamareas.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Team1 4 | 5 | false 6 | 7 | 8 | 9 | 10 | 11 | Team2 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/fixtures/itemtypes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | defect 4 | Defect 5 | 6 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/workitemtype/bug.gif 7 | 8 | 9 | defect_task 10 | 11 | 12 | 13 | task 14 | Task 15 | 16 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/workitemtype/task.gif 17 | 18 | 19 | task 20 | 21 | 22 | -------------------------------------------------------------------------------- /tests/fixtures/foundins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sprint1 4 | 5 | 6 | true 7 | true 8 | 9 | 10 | 2009-11-05T11:36:00.596Z 11 | 12 | 13 | 14 | Sprint2 15 | 16 | 17 | false 18 | false 19 | 20 | 21 | 2015-07-21T01:46:12.096Z 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/fixtures/members.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | tester1@email.com 4 | tester1 5 | mailto:tester1%40email.com 6 | 7 | 2009-11-24T19:14:14.595Z 8 | 9 | 10 | 11 | tester2@email.com 12 | tester2 13 | mailto:tester2%40email.com 14 | 15 | 2013-04-22T06:24:34.661Z 16 | 17 | 18 | 19 | tester3@email.com 20 | tester3 21 | mailto:tester3%40email.com 22 | 23 | 2010-05-13T20:34:05.138Z 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/fixtures/actions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | default_workflow.action.a1 4 | 5 | Close 6 | 7 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/workflow/close.gif 8 | 9 | 10 | 11 | default_workflow.action.a2 12 | 13 | Start Working 14 | 15 | http://test.url:9443/jazz/service/com.ibm.team.workitem.common.internal.model.IImageContentService/processattachment/_CuZu0HUwEeKicpXBddtqNA/workflow/inprogress.gif 16 | 17 | 18 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 3.18 3 | envlist = test,pycodestyle,flake 4 | skipsdist = True 5 | isolated_build = True 6 | 7 | [testenv:test] 8 | description = Invoke pytest to run automated tests 9 | sitepackages = True 10 | isolated_build = True 11 | allowlist_externals = 12 | poetry 13 | setenv = 14 | TOXINIDIR = {toxinidir} 15 | VIRTUAL_ENV = {envdir} 16 | commands = 17 | poetry run pytest -v {posargs} 18 | 19 | [testenv:pycodestyle] 20 | description = Check for PEP8 compliance of code. 21 | skip_install = true 22 | isolated_build = True 23 | allowlist_externals = 24 | poetry 25 | commands = 26 | poetry run pycodestyle rtcclient tests --show-source --show-pep8 -v {posargs} 27 | 28 | [testenv:flake] 29 | description = Check for PEP8 compliance of code with flake. 30 | skip_install = true 31 | isolated_build = True 32 | allowlist_externals = 33 | poetry 34 | commands = 35 | poetry run flake8 rtcclient tests/ --count --select=E9,F63,F7,F82 --show-source --statistics 36 | poetry run flake8 rtcclient tests/ --count --max-complexity=20 --max-line-length=127 --statistics 37 | 38 | [testenv:format] 39 | description = Autoformat code. 40 | skip_install = true 41 | isolated_build = True 42 | allowlist_externals = 43 | poetry 44 | commands = 45 | poetry run yapf --style google --recursive --in-place rtcclient tests 46 | 47 | [pycodestyle] 48 | count = False 49 | ignore = E226,E302,E41,W504,E722 50 | max-line-length = 160 51 | statistics = True 52 | exclude = .venv,.tox,dist,doc,build,*.egg,.git,.eggs,__init__.py,__pycache__,.pytest_cache 53 | -------------------------------------------------------------------------------- /examples/how_to/query/query_workitems.py: -------------------------------------------------------------------------------- 1 | from rtcclient.client import RTCClient 2 | from rtcclient.utils import setup_basic_logging 3 | 4 | if __name__ == "__main__": 5 | # you can remove this if you don't need logging 6 | # default logging for console output 7 | setup_basic_logging() 8 | 9 | url = "https://your_domain:9443/jazz" 10 | username = "your_username" 11 | password = "your_password" 12 | projectarea_name = "your_projectarea_name" 13 | # If your rtc server is too old (such as Rational Team Concert 5.0.1, 5.0.2), 14 | # please set old_rtc_authentication to True. 15 | # Other kwargs, such as ends_with_jazz, old_rtc_authentication 16 | myclient = RTCClient(url, username, password, old_rtc_authentication=False) 17 | 18 | # query starts here 19 | myquery = myclient.query 20 | 21 | # customize your query string 22 | # below query string means: query all the workitems whose title 23 | # is "use case 1" 24 | myquerystr = 'dc:title="use case 1"' 25 | 26 | # to create complex query string, fields are appended with " and ", an example 27 | # myquerystr = 'dc:title="use case 1" and dc:type="workitem_type"' 28 | 29 | # specify the returned properties: title, id, state, owner 30 | # This is optional. All properties will be returned if not specified 31 | returned_prop = "dc:title,dc:identifier,rtc_cm:state,rtc_cm:ownedBy" 32 | 33 | queried_wis = myquery.queryWorkitems(query_str=myquerystr, 34 | projectarea_name=projectarea_name, 35 | returned_properties=returned_prop) 36 | -------------------------------------------------------------------------------- /tests/fixtures/plannedfors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1.0 4 | Release 1.0 5 | 6 | 2009-11-02T06:00:00.000Z 7 | 2009-12-12T06:00:00.000Z 8 | 9 | true 10 | true 11 | 12 | 13 | 14 | 15 | 1.0 S1 16 | Sprint 1 (1.0) 17 | 18 | 2013-02-12T06:00:00.000Z 19 | 2013-03-04T06:00:00.000Z 20 | 21 | true 22 | false 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | # Run this workflow every time a new commit pushed to upstream/fork repository. 4 | # Run workflow on fork repository will help contributors find and resolve issues before sending a PR. 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | jobs: 10 | codeformat: 11 | name: check-code-format 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: autoyapf 16 | id: autoyapf 17 | uses: mritunjaysharma394/autoyapf@v2 18 | with: 19 | args: --style google --recursive --in-place . 20 | - name: Check for modified files 21 | run: | 22 | if git diff-index --quiet HEAD --; then 23 | exit 0 24 | else 25 | git diff; 26 | exit 1 27 | fi 28 | 29 | ci: 30 | name: run-unit-tests 31 | runs-on: ${{ matrix.os }} 32 | strategy: 33 | matrix: 34 | os: [ubuntu-latest] 35 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] 36 | steps: 37 | - uses: actions/checkout@v4 38 | - name: Set up Python 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: ${{ matrix.python-version }} 42 | - name: Setup Poetry 43 | uses: abatilo/actions-poetry@v2 44 | - name: Display Python version 45 | run: python --version 46 | - name: Install dependencies 47 | run: | 48 | poetry install --with devel 49 | - name: Check lock file 50 | run: poetry check --lock 51 | - name: Lint with flake8 52 | run: | 53 | poetry run tox -e flake 54 | - name: Test with pytest 55 | run: | 56 | poetry run tox -e test 57 | -------------------------------------------------------------------------------- /tests/fixtures/attachment.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 4 | cgobench1.go 5 | cgobench1.go 6 | application/octet-stream 7 | 351 8 | 2017-05-15T06:12:11.264Z 9 | 10 | 2017-05-15T06:12:11.440Z 11 | 12 | 13 | 14 | 15 | 21 16 | cgobench2.go 17 | cgobench2.go 18 | application/octet-stream 19 | 351 20 | 2017-05-15T03:30:04.686Z 21 | 22 | 2017-05-15T03:30:04.690Z 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "rtcclient" 3 | version = "0.10.0" 4 | homepage = "https://github.com/dixudx/rtcclient" 5 | description = "RTCClient for Rational Team Concert" 6 | authors = ["Di Xu "] 7 | readme = "README.rst" 8 | license = "Apache License Version 2.0" 9 | keywords = ["rtcclient", "Rational Team Concert", "RTC"] 10 | classifiers=[ 11 | "Development Status :: 5 - Production/Stable", 12 | "Topic :: Utilities", 13 | "Environment :: Console", 14 | "Intended Audience :: Developers", 15 | "Intended Audience :: System Administrators", 16 | "Intended Audience :: Information Technology", 17 | "License :: OSI Approved :: Apache Software License", 18 | "Operating System :: OS Independent", 19 | "Programming Language :: Python", 20 | "Programming Language :: Python :: 3", 21 | "Programming Language :: Python :: 3.6", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | "Programming Language :: Python :: 3.10", 26 | ] 27 | packages = [ 28 | { include = "rtcclient" }, 29 | { include = "tests", format = "sdist" }, 30 | ] 31 | 32 | [tool.poetry.dependencies] 33 | python = ">=3.7.0,<4.0" 34 | PySocks = ">=1.5.6" 35 | jinja2 = ">=2.2" 36 | requests = ">=2.10.0" 37 | six = "*" 38 | xmltodict = "^0.13.0" 39 | lxml = "^5.0.0" 40 | 41 | 42 | [tool.poetry.group.devel] 43 | optional = true 44 | [tool.poetry.group.devel.dependencies] 45 | pip = "^23.0.1" 46 | pycodestyle = "*" 47 | pytest = "^7.2.0" 48 | pytest-env = "*" 49 | pytest-mock = ">=0.6.0" 50 | flake8 = [ 51 | { version = "^6.0.0", python = ">=3.8.1" }, 52 | { version = "^5.0.4", python = "<3.8.1" } 53 | ] 54 | yapf = "*" 55 | tox = "*" 56 | toml = "*" 57 | 58 | 59 | [build-system] 60 | requires = ["poetry-core>=1.0.0"] 61 | build-backend = "poetry.core.masonry.api" 62 | -------------------------------------------------------------------------------- /tests/fixtures/includedinbuilds.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | _2NXr8Fx3EeWfxsy-c6nRWw 4 | 20150916-0836 5 | 6 | 7 | 2015-09-16T13:35:51.342Z 8 | 2015-09-16T13:36:01.122Z 9 | 2015-09-16T13:43:20.183Z 10 | MANUAL 11 | COMPLETED 12 | OK 13 | 14 | 15 | 16 | 17 | _b0KuAFuPEeWfxsy-c6nRWw 18 | 20150915-0452 19 | 20 | 21 | 2015-09-15T09:52:10.975Z 22 | 2015-09-15T09:52:19.544Z 23 | 2015-09-15T10:03:05.743Z 24 | MANUAL 25 | COMPLETED 26 | OK 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/fixtures/savedqueries.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Closed created by me 5 | 6 | Work items I have created which have been resolved 7 | 8 | 9 | 2014-02-22T01:23:47.490Z 10 | 11 | 12 | 13 | 14 | Open Track Build Items 15 | 16 | All unresolved Track Build Items 17 | 18 | 19 | 2014-02-22T01:23:47.495Z 20 | 21 | 22 | 23 | 24 | Open Adoptions 25 | 26 | 27 | 28 | 29 | 2014-02-22T01:23:47.499Z 30 | 31 | 32 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. rtcclient documentation master file, created by 2 | sphinx-quickstart on Mon Aug 17 17:21:28 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to rtcclient's documentation! 7 | ===================================== 8 | 9 | IBM® Rational Team Concert™, is built on the Jazz platform, allowing 10 | application development teams to use one tool to plan across teams, code, 11 | run standups, plan sprints, and track work. For more info, please refer 12 | to here_. 13 | 14 | .. _here: http://www.ibm.com/developerworks/downloads/r/rtc/ 15 | 16 | **IMPORTANT NOTE: This is NOT an official-released Python-based RTC Client.** 17 | 18 | This library can help you: 19 | 20 | * Interacts with an RTC server to retrieve objects which contain the detailed information/configuration, including **Project Areas**, **Team Areas**, **Workitems** and etc; 21 | * Creates all kinds of **Workitems** through self-customized templates or copies from some existing **Workitems**; 22 | * Performs some actions on the retrieved **Workitems**, including get/add **Comments**, get/add/remove **Subscribers**/**Children**/**Parent** and etc; 23 | * Query **Workitems** using specified filtered rules or directly from your saved queries; 24 | * Logs all the activities and messages during your operation; 25 | 26 | 27 | Python & Rational Team Concert Versions 28 | --------------------------------------- 29 | 30 | This project has been tested against multiple Python versions, such as "3.7", "3.8", "3.9", "3.10" and "3.11". 31 | 32 | Please install **rtcclient** with version >= 0.9.0, which works well with ``Rational Team Concert`` 6.0.6.1, **5.0.1**, **5.0.2** and ``ELM`` 7.0. 33 | 34 | 35 | Important Links 36 | --------------- 37 | 38 | * Support and bug-reports: https://github.com/dixudx/rtcclient/issues?q=is%3Aopen+sort%3Acomments-desc 39 | 40 | * Project source code: https://github.com/dixudx/rtcclient 41 | 42 | * Project documentation: https://readthedocs.org/projects/rtcclient/ 43 | 44 | 45 | User Guide 46 | ========== 47 | 48 | .. toctree:: 49 | :maxdepth: 2 50 | 51 | authors 52 | introduction 53 | workitem_attr 54 | installation 55 | quickstart 56 | advanced_usage 57 | 58 | 59 | API Documentation 60 | ================= 61 | 62 | .. toctree:: 63 | :maxdepth: 2 64 | 65 | client 66 | projectarea 67 | workitem 68 | query 69 | template 70 | models 71 | 72 | 73 | Indices and tables 74 | ================== 75 | 76 | * :ref:`genindex` 77 | * :ref:`modindex` 78 | * :ref:`search` 79 | -------------------------------------------------------------------------------- /rtcclient/utils.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | from xml.parsers.expat import ExpatError 4 | 5 | import six 6 | import xmltodict 7 | from lxml import etree 8 | 9 | from rtcclient.exception import RTCException, BadValue 10 | 11 | 12 | def setup_basic_logging(): 13 | logging.basicConfig(level=logging.DEBUG, 14 | format="%(asctime)s %(levelname)s %(name)s: " 15 | "%(message)s") 16 | 17 | 18 | def token_expire_handler(func): 19 | 20 | @functools.wraps(func) 21 | def wrapper(*args, **kwargs): 22 | rtc_obj = args[0].get_rtc_obj() 23 | 24 | if not hasattr(rtc_obj, "headers") or rtc_obj.headers is None: 25 | # still in the initialization or relogin 26 | # directly call the method 27 | return func(*args, **kwargs) 28 | else: 29 | # check whether token expires 30 | try: 31 | resp = func(*args, **kwargs) 32 | xmltodict.parse(resp.content) 33 | return resp 34 | except ExpatError as excp: 35 | if "invalid token" in str(excp): 36 | # expires 37 | try: 38 | rtc_obj.relogin() 39 | except RTCException: 40 | raise RTCException("Relogin Failed: " 41 | "Invalid username or password") 42 | kwargs["headers"]["Cookie"] = rtc_obj.headers["Cookie"] 43 | return func(*args, **kwargs) 44 | else: 45 | # not expires 46 | # raise the actual exception 47 | raise ExpatError(excp) 48 | 49 | return wrapper 50 | 51 | 52 | def capitalize(keyword): 53 | """Only capitalize the first character and make the left unchanged 54 | 55 | :param keyword: the input string 56 | :return: the capitalized string 57 | """ 58 | 59 | if keyword is None: 60 | raise BadValue("Invalid value. None is not supported") 61 | 62 | if isinstance(keyword, six.string_types): 63 | if len(keyword) > 1: 64 | return keyword[0].upper() + keyword[1:] 65 | else: 66 | return keyword.capitalize() 67 | else: 68 | raise BadValue("Input value %s is not string type" % keyword) 69 | 70 | 71 | def remove_empty_elements(docs): 72 | root = etree.fromstring(bytes(docs, 'utf-8')) 73 | for element in root.xpath("//*[not(node())]"): 74 | if "rdf:resource" not in str(etree.tostring(element)): 75 | element.getparent().remove(element) 76 | 77 | return etree.tostring(root) 78 | -------------------------------------------------------------------------------- /doc/source/advanced_usage.rst: -------------------------------------------------------------------------------- 1 | .. _advanced_usage: 2 | 3 | 4 | Advanced Usage 5 | ============== 6 | 7 | This document covers some of rtcclient more advanced features. 8 | 9 | 10 | .. _query_syntax: 11 | 12 | Query Syntax [2]_ 13 | ----------------- 14 | 15 | The following section describes the basic query syntax. 16 | 17 | 18 | **Comparison Operators** 19 | 20 | * ``=`` : test for equality of a term, 21 | * ``!=`` : test for inequality of a term, 22 | * ``<`` : test less-than, 23 | * ``>`` : test greater-than, 24 | * ``<=`` : test less-than or equal, 25 | * ``>=`` : test greater-than or equal, 26 | * ``in`` : test for equality of any of the terms. 27 | 28 | 29 | **Boolean Operators** 30 | 31 | * ``and`` : conjunction 32 | 33 | 34 | **Query Modifiers** 35 | 36 | * ``/sort`` : set the sort order for returned items 37 | 38 | 39 | **BNF** 40 | 41 | :: 42 | 43 | query ::= (term (boolean_op term)*)+ modifiers 44 | term ::= (identifier operator)? value+ | (identifier "in")? in_val 45 | operator ::= "=" | "!=" | "<" | ">" | "<=" | ">=" 46 | boolean_op ::= "and" 47 | modifiers ::= sort? 48 | sort ::= "/sort" "=" identifier 49 | identifier ::= word (":" word)? 50 | in_val ::= "[" value ("," value)* "]" 51 | value ::= (integer | string) 52 | word ::= /any sequence of letters and numbers, starting with a letter/ 53 | string ::= '"' + /any sequence of characters/ + '"' 54 | integer ::= /any sequence of integers/ 55 | 56 | 57 | **Notes** 58 | 59 | 1. a word consists of any character with the Unicode class Alpha (alpha-numeric) as well as the characters ".", "-" and "_". 60 | 2. a string may include the quote character if preceded by the escape character "\", as in "my \"quoted\" example". 61 | 62 | 63 | .. _query_compose: 64 | 65 | Compose your Query String 66 | ------------------------- 67 | 68 | Based on the above :ref:`query syntax `, it is easy to compose 69 | your own query string. 70 | 71 | **Important Note**: For the `identifier` in :ref:`query syntax `, please refer 72 | to :ref:`field alias ` and 73 | :ref:`Built-in Attributes `. 74 | 75 | Here are several examples. 76 | 77 | **Example 1**: Query all the defects with tags "bvt" whose state is not "Closed" 78 | 79 | Note: here defects' state "default_workflow.state.s1" means "Closed". This 80 | may vary in your customized workitem type. 81 | 82 | >>> query_str = ('dc:type="defect" and ' 83 | 'rtc_cm:state!="default_workflow.state.s1" and ' 84 | 'dc:subject="bvt"') 85 | 86 | **Example 2**: Query all the defects which are modified after 18:42:30 on Dec. 02, 2008 87 | 88 | Note: here defects' state "default_workflow.state.s1" means "Closed". 89 | 90 | >>> query_str = 'dc:type="defect" and dc:modified>="12-02-2008T18:42:30"' 91 | 92 | **Example 3**: Query all the defects with tags "bvt" or "testautomation" 93 | 94 | >>> query_str = 'dc:type="defect" and dc:subject in ["bvt", "testautomation"]' 95 | 96 | **Example 4**: Query all the defects owned/created/modified by "tester@email.com" 97 | 98 | >>> user_url = "https://your_domain:9443/jts/users/tester@email.com" 99 | >>> query_str = 'dc:type="defect" and rtc_cm:ownedBy="%s"' % user_url 100 | >>> query_str = 'dc:type="defect" and dc:creator="%s"' % user_url 101 | >>> query_str = 'dc:type="defect" and rtc_cm:modifiedBy="%s"' % user_url 102 | 103 | Note: please replace `your_domain` with your actual RTC server domain. 104 | 105 | **Example 5**: Query all the defects whose severity are "Critical" 106 | 107 | >>> projectarea_name="My ProjectArea" 108 | >>> severity = myclient.getSeverity("Critical", 109 | projectarea_name=projectarea_name) 110 | >>> query_str = 'dc:type="defect" and oslc_cm:severity="%s"' % severity.url 111 | 112 | **Example 6**: Query all the defects whose priority are "High" 113 | 114 | >>> projectarea_name="My ProjectArea" 115 | >>> priority = myclient.getPriority("High", 116 | projectarea_name=projectarea_name) 117 | >>> query_str = 'dc:type="defect" and oslc_cm:priority="%s"' % priority.url 118 | 119 | **Example 7**: Query all the defects whose FiledAgainst are "FiledAgainstDemo" 120 | 121 | >>> projectarea_name="My ProjectArea" 122 | >>> filedagainst = myclient.getFiledAgainst("FiledAgainstDemo", 123 | projectarea_name=projectarea_name) 124 | >>> query_str = 'dc:type="defect" and rtc_cm:filedAgainst="%s"' % filedagainst.url 125 | 126 | .. [2] `Change Management Query Syntax `_ 127 | -------------------------------------------------------------------------------- /tests/test_template.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pytest 3 | import utils_test 4 | from rtcclient.exception import BadValue 5 | from jinja2 import exceptions as jinja2_excp 6 | import xmltodict 7 | 8 | 9 | class TestTemplater: 10 | 11 | @pytest.fixture(autouse=True) 12 | def myrtcclient(self, rtcclient): 13 | myclient = rtcclient 14 | return myclient 15 | 16 | @pytest.fixture(autouse=True) 17 | def mytemplater(self, myrtcclient): 18 | return myrtcclient.templater 19 | 20 | def test_init_templater(self, mytemplater, myrtcclient): 21 | assert mytemplater.url == "http://test.url:9443/jazz" 22 | assert mytemplater.searchpath == utils_test._search_path 23 | assert mytemplater.rtc_obj == myrtcclient 24 | assert str(mytemplater) == "".join( 25 | ["Templater for RTC Server ", "at http://test.url:9443/jazz"]) 26 | 27 | def test_list_fields(self, mytemplater): 28 | # invalid template names 29 | invalid_names = [None, True, False] 30 | for invalid_name in invalid_names: 31 | with pytest.raises(BadValue): 32 | mytemplater.listFields(invalid_name) 33 | 34 | # nonexistent template names 35 | fake_names = ["fake_name1", "fake_name2"] 36 | for fake_name in fake_names: 37 | with pytest.raises(jinja2_excp.TemplateNotFound): 38 | mytemplater.listFields(fake_name) 39 | 40 | # existent template 41 | fields = mytemplater.listFields(utils_test.template_name) 42 | fields_set = set([ 43 | "severity", "title", "teamArea", "description", "filedAgainst", 44 | "priority", "ownedBy", "plannedFor" 45 | ]) 46 | assert fields == fields_set 47 | 48 | def test_render(self, mytemplater): 49 | # invalid template names 50 | invalid_names = [None, True, False] 51 | for invalid_name in invalid_names: 52 | with pytest.raises(BadValue): 53 | mytemplater.render(invalid_name) 54 | 55 | def test_get_template(self, mytemplater, mocker): 56 | # invalid template names 57 | invalid_names = [None, True, False, "", u"", 123.4] 58 | for invalid_name in invalid_names: 59 | with pytest.raises(BadValue): 60 | mytemplater.getTemplate(invalid_name, 61 | template_name=None, 62 | template_folder=None, 63 | keep=False, 64 | encoding="UTF-8") 65 | 66 | # valid template name 67 | mocked_get = mocker.patch("requests.get") 68 | mock_resp = mocker.MagicMock(spec=requests.Response) 69 | mock_resp.status_code = 200 70 | mock_resp.content = utils_test.workitem1_raw 71 | mocked_get.return_value = mock_resp 72 | 73 | copied_from_valid_names = [161, "161", u"161"] 74 | for copied_from in copied_from_valid_names: 75 | template_161 = mytemplater.getTemplate(copied_from, 76 | template_name=None, 77 | template_folder=None, 78 | keep=False, 79 | encoding="UTF-8") 80 | assert (list(xmltodict.parse(template_161).items()).sort() == list( 81 | utils_test.template_ordereddict.items()).sort()) 82 | 83 | def test_get_templates_exception(self, mytemplater): 84 | # invalid workitem ids 85 | invalid_names = [ 86 | None, True, False, "", u"", "test", u"test", 123, 123.4 87 | ] 88 | for invalid_name in invalid_names: 89 | with pytest.raises(BadValue): 90 | mytemplater.getTemplates(invalid_name, 91 | template_folder=None, 92 | template_names=None, 93 | keep=False, 94 | encoding="UTF-8") 95 | 96 | # invalid template names 97 | invalid_names = [True, False, "", u"", "test", u"test"] 98 | for invalid_name in invalid_names: 99 | with pytest.raises(BadValue): 100 | mytemplater.getTemplates(["valid_id_1", "valid_id_2"], 101 | template_folder=None, 102 | template_names=invalid_name, 103 | keep=False, 104 | encoding="UTF-8") 105 | 106 | # unequal length 107 | with pytest.raises(BadValue): 108 | mytemplater.getTemplates(["valid_id_1", "valid_id_2"], 109 | template_folder=None, 110 | template_names=["valid_name"], 111 | keep=False, 112 | encoding="UTF-8") 113 | -------------------------------------------------------------------------------- /tests/test_base.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | from rtcclient.base import RTCBase 4 | 5 | 6 | class BaseTestRTC(RTCBase): 7 | 8 | def __str__(self): 9 | return "Test Base" 10 | 11 | def get_rtc_obj(self): 12 | pass 13 | 14 | 15 | class TestRTCBase: 16 | 17 | test_urls = [ 18 | "http://test.url:9443/////", "http://test.url:9443/jazz////", 19 | "http://test.url:9443/jazz" 20 | ] 21 | 22 | valid_test_urls = [ 23 | "http://test.url:9443", "http://test.url:9443/jazz", 24 | "http://test.url:9443/jazz" 25 | ] 26 | 27 | def test_validate_url(self): 28 | for idx, test_url in enumerate(self.test_urls): 29 | test_rtc = BaseTestRTC(test_url) 30 | assert ( 31 | test_rtc.validate_url(test_url) == self.valid_test_urls[idx]) 32 | 33 | def test_validate_url_cls(self): 34 | for idx, test_url in enumerate(self.test_urls): 35 | assert ( 36 | BaseTestRTC.validate_url(test_url) == self.valid_test_urls[idx]) 37 | 38 | def test_str(self): 39 | for test_url in self.test_urls: 40 | test_rtc = BaseTestRTC(test_url) 41 | assert str(test_rtc) == "Test Base" 42 | 43 | def test_repr(self): 44 | for test_url in self.test_urls: 45 | test_rtc = BaseTestRTC(test_url) 46 | assert repr(test_rtc) == "" 47 | 48 | def test_getattr(self): 49 | for idx, test_url in enumerate(self.test_urls): 50 | test_rtc = BaseTestRTC(test_url) 51 | assert test_rtc.url == self.valid_test_urls[idx] 52 | assert test_rtc["url"] == self.valid_test_urls[idx] 53 | 54 | def test_get_resp(self, mocker): 55 | # actually the GET method is inherited from requests.get 56 | # no need for more tests 57 | mocked_get = mocker.patch("requests.get") 58 | test_url = "http://test.url:9443/jazz" 59 | test_rtc = BaseTestRTC(test_url) 60 | 61 | mock_resp = mocker.MagicMock(spec=requests.Response) 62 | mock_resp.status_code = 200 63 | mock_resp.json.return_value = {'get-test': "test"} 64 | mocked_get.return_value = mock_resp 65 | 66 | resp = test_rtc.get(test_rtc.url, 67 | verify=False, 68 | headers=test_rtc.CONTENT_XML, 69 | proxies=None, 70 | timeout=30) 71 | mocked_get.assert_called_once_with(test_rtc.url, 72 | verify=False, 73 | headers=test_rtc.CONTENT_XML, 74 | proxies=None, 75 | timeout=30) 76 | assert resp == mock_resp 77 | 78 | def test_post_resp(self, mocker): 79 | # actually the POST method is inherited from requests.post 80 | # no need for more tests 81 | mocked_post = mocker.patch("requests.post") 82 | test_url = "http://test.url:9443/jazz" 83 | test_rtc = BaseTestRTC(test_url) 84 | 85 | mock_resp = mocker.MagicMock(spec=requests.Response) 86 | mock_resp.status_code = 200 87 | mock_resp.json.return_value = {'post-test': "post"} 88 | mocked_post.return_value = mock_resp 89 | 90 | post_data = {"data": "test"} 91 | resp = test_rtc.post(test_rtc.url, 92 | data=post_data, 93 | json=None, 94 | verify=False, 95 | proxies=None, 96 | headers=test_rtc.CONTENT_XML, 97 | timeout=30) 98 | mocked_post.assert_called_once_with(test_rtc.url, 99 | data=post_data, 100 | json=None, 101 | verify=False, 102 | proxies=None, 103 | headers=test_rtc.CONTENT_XML, 104 | timeout=30) 105 | assert resp == mock_resp 106 | 107 | def test_put_resp(self, mocker): 108 | # actually the PUT method is inherited from requests.put 109 | # no need for more tests 110 | mocked_put = mocker.patch("requests.put") 111 | test_url = "http://test.url:9443/jazz" 112 | test_rtc = BaseTestRTC(test_url) 113 | 114 | mock_resp = mocker.MagicMock(spec=requests.Response) 115 | mock_resp.status_code = 200 116 | mock_resp.json.return_value = {'post-test': "post"} 117 | mocked_put.return_value = mock_resp 118 | 119 | post_data = {"data": "test"} 120 | resp = test_rtc.put(test_rtc.url, 121 | data=post_data, 122 | json=None, 123 | verify=False, 124 | proxies=None, 125 | headers=test_rtc.CONTENT_XML, 126 | timeout=30) 127 | mocked_put.assert_called_once_with(test_rtc.url, 128 | data=post_data, 129 | json=None, 130 | verify=False, 131 | proxies=None, 132 | headers=test_rtc.CONTENT_XML, 133 | timeout=30) 134 | assert resp == mock_resp 135 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | rtcclient 2 | ========= 3 | 4 | .. image:: https://readthedocs.org/projects/rtcclient/badge/?version=latest 5 | :target: https://readthedocs.org/projects/rtcclient 6 | 7 | .. image:: https://img.shields.io/pypi/v/rtcclient.svg 8 | :target: https://pypi.python.org/pypi/rtcclient 9 | 10 | .. image:: https://github.com/dixudx/rtcclient/actions/workflows/ci.yml/badge.svg 11 | :target: https://github.com/dixudx/rtcclient 12 | 13 | .. image:: https://img.shields.io/badge/slack-rtcclient-blue.svg 14 | :target: https://rtcclient.slack.com 15 | 16 | .. image:: https://img.shields.io/badge/Say%20Thanks-!-1EAEDB.svg 17 | :target: https://saythanks.io/to/dixudx 18 | 19 | A Python-based Client/API for Rational Team Concert (RTC) 20 | 21 | About this library 22 | ------------------ 23 | 24 | IBM® Rational Team Concert™, is built on the Jazz platform, allowing 25 | application development teams to use one tool to plan across teams, code, 26 | run standups, plan sprints, and track work. For more info, please refer 27 | to here_. 28 | 29 | .. _here: http://www.ibm.com/developerworks/downloads/r/rtc/ 30 | 31 | **IMPORTANT NOTE: This is NOT an official-released Python-based RTC Client.** 32 | 33 | This library can help you: 34 | 35 | * Interacts with an RTC server to retrieve objects which contain the detailed information/configuration, including **Project Areas**, **Team Areas**, **Workitems** and etc; 36 | * Creates all kinds of **Workitems** through self-customized templates or copies from some existing **Workitems**; 37 | * Performs some actions on the retrieved **Workitems**, including get/add **Comments**, get/add/remove **Subscribers**/**Children**/**Parent**, get/upload **Attachments** and etc; 38 | * Query **Workitems** using specified filtered rules or directly from your saved queries; 39 | * Logs all the activities and messages during your operation; 40 | 41 | 42 | Python & Rational Team Concert Versions 43 | --------------------------------------- 44 | 45 | This project has been tested against multiple Python versions, such as "3.7", "3.8", "3.9", "3.10" and "3.11". 46 | 47 | Please install **rtcclient** with version **>= 0.9.0**, which works well with ``Rational Team Concert`` **6.0.6.1**, **5.0.1**, **5.0.2** and ``ELM`` **7.0**. 48 | 49 | Important Links 50 | --------------- 51 | 52 | Support and bug-reports: 53 | https://github.com/dixudx/rtcclient/issues?q=is%3Aopen+sort%3Acomments-desc 54 | 55 | Project source code: https://github.com/dixudx/rtcclient 56 | 57 | Project documentation: https://readthedocs.org/projects/rtcclient/ 58 | 59 | 60 | Installation 61 | ------------ 62 | 63 | To install rtcclient, simply: 64 | 65 | .. code-block:: bash 66 | 67 | $ pip install rtcclient 68 | 69 | 70 | Example 71 | ------- 72 | 73 | RTCClient is intended to map the objects in RTC (e.g. Project Areas, 74 | Team Areas, Workitems) into easily managed Python objects: 75 | 76 | .. code-block:: python 77 | 78 | >>> from rtcclient.utils import setup_basic_logging 79 | >>> from rtcclient import RTCClient 80 | # you can remove this if you don't need logging 81 | # default debug logging for console output 82 | >>> setup_basic_logging() 83 | # url ends with jazz 84 | >>> url = "https://your_domain:9443/jazz" 85 | >>> username = "your_username" 86 | >>> password = "your_password" 87 | # If your rtc server is behind a proxy, remember to set "proxies" explicitly. 88 | # If your url ends with ccm, set ends_with_jazz to False. 89 | # Please refer to issue #68 for detailed explanation 90 | # If your rtc server is too old (such as Rational Team Concert 5.0.1, 5.0.2), please set old_rtc_authentication to True 91 | >>> myclient = RTCClient(url, username, password, ends_with_jazz=True, old_rtc_authentication=False) 92 | # it will be faster if returned properties is specified 93 | # see in below query example 94 | >>> wk = myclient.getWorkitem(123456) # get a workitem whose id is 123456 95 | # get all workitems 96 | # If both projectarea_id and projectarea_name are None, all the workitems 97 | # in all ProjectAreas will be returned 98 | >>> workitems_list = myclient.getWorkitems(projectarea_id=None, 99 | projectarea_name=None) 100 | >>> myquery = myclient.query # query class 101 | >>> projectarea_name = "your_projectarea_name" 102 | # customize your query string 103 | # below query string means: query all the workitems with title "use case 1" 104 | >>> myquerystr = 'dc:title="use case 1"' 105 | # specify the returned properties: title, id, state, owner 106 | # This is optional. All properties will be returned if not specified 107 | >>> returned_prop = "dc:title,dc:identifier,rtc_cm:state,rtc_cm:ownedBy" 108 | >>> queried_wis = myquery.queryWorkitems(query_str=myquerystr, 109 | projectarea_name=projectarea_name, 110 | returned_properties=returned_prop) 111 | 112 | 113 | Testing 114 | ------- 115 | 116 | Using a virtualenv is recommended. Setuptools will automatically fetch 117 | missing test dependencies. 118 | 119 | If you have installed the tox_ on your system already, you can run 120 | the tests using pytest_ with the following command: 121 | 122 | .. _tox: https://pypi.python.org/pypi/tox 123 | .. _pytest: http://pytest.org/latest/ 124 | 125 | .. code-block:: bash 126 | 127 | virtualenv 128 | source .venv/bin/active 129 | (venv) tox -e test 130 | (venv) tox -e flake 131 | (venv) tox -e pycodestyle 132 | 133 | 134 | Testing with Poetry 135 | ------------------- 136 | 137 | When using poetry_ , all dependencies and test environment are managed by this tool even when using tox_. 138 | 139 | If you have already globally installed poetry_ on your system, you can run 140 | the tests using pytest_ with the following command: 141 | 142 | .. _poetry: https://python-poetry.org/ 143 | 144 | .. code-block:: bash 145 | 146 | poetry install --with devel 147 | poetry run tox -e test 148 | poetry run tox -e flake 149 | poetry run tox -e pycodestyle 150 | -------------------------------------------------------------------------------- /doc/source/introduction.rst: -------------------------------------------------------------------------------- 1 | .. _introduction: 2 | 3 | Introduction 4 | ============ 5 | 6 | In this section, some common terminologies are introduced. For more information, 7 | please visit `Rational Collaborative Lifecycle Management Solution `_ 8 | 9 | 10 | Project Area 11 | ------------ 12 | 13 | Project Area is, quite simply, an area in the repository where information 14 | about the project is stored. 15 | 16 | In each of the Collaborative Lifecycle Management (CLM) applications, 17 | teams perform their work within the context of a project area. 18 | A project area is an area in the repository where information about one 19 | or more software projects is stored. **A project area defines the project 20 | deliverables, team structure, process, and schedule**. You access all project 21 | artifacts, such as iteration plans, work items, requirements, test cases, 22 | and files under source control within the context of a project area. 23 | Each project area has a process, which governs how members work. 24 | 25 | For example, the project area process defines: 26 | 27 | * User roles 28 | * Permissions assigned to roles 29 | * Timelines and iterations 30 | * Operation behavior (preconditions and follow-up actions) for Change and Configuration Management and Quality Management 31 | * Work item types and their state transition models (for Change and Configuration Management and Quality Management) 32 | 33 | A project area is stored as a top-level or root item in a repository. 34 | A project area references project artifacts and stores the relationships 35 | between these artifacts. Access to a project area and its artifacts is 36 | controlled by access control settings and permissions. A project area 37 | cannot be deleted from the repository; however, it can be archived, 38 | which places it in an inactive state. 39 | 40 | 41 | Team Area 42 | --------- 43 | 44 | You can create a team area to assign users in particular roles for work on a 45 | timeline or a particular set of deliverables. You can create a team area 46 | within an existing project area or another team area to establish a team 47 | hierarchy. 48 | 49 | 50 | Component [3]_ 51 | -------------- 52 | 53 | A configuration or set of configurations may be divided into components 54 | representing some user-defined set of object versions and/or 55 | sub-configurations; for example, components might be used to represent 56 | physical components or software modules. A provider is not required to 57 | implement components; they are used only as a way of limiting the scope of 58 | the closure over links. Components might or might not be resources; they 59 | might be dynamic sets of object versions chosen by other criteria such as 60 | property values. A provider can also treat each configuration and 61 | sub-configuration in a hierarchy as being separate components. 62 | 63 | 64 | Change set [3]_ 65 | --------------- 66 | 67 | A set of changes to be made to one or more configurations, where each 68 | change is described in terms of members (direct or indirect) that should 69 | be added to, replaced in, or removed from some configurations. 70 | 71 | 72 | Role 73 | ---- 74 | 75 | Each project area and each team area can define a set of roles. 76 | The defined roles are visible in the area where they're declared and in all 77 | child areas. Roles defined in the project area can be assigned to users for the 78 | whole project area or they can be assigned in any team area. Roles defined in 79 | a team area can similarly be assigned in that team or in any child team. 80 | The ordering of roles in this section determines how they will be ordered in 81 | other sections of the editor, but it does not affect the process runtime. 82 | 83 | 84 | Administrator 85 | ------------- 86 | 87 | If you require permissions, contact an administrator. 88 | Project administrators can modify and save this project area and its team areas. 89 | 90 | 91 | PlannedFor 92 | ---------- 93 | 94 | In modern software development, a release is divided into a series of 95 | fixed-length development periods, typically ranging from two to six weeks, 96 | called iterations. Planning an iteration involves scheduling the work to be 97 | done during an iteration and assigning individual work items to members of the 98 | team. 99 | 100 | Iteration planning takes place in the context of a project area. 101 | Each project area has a development line that is divided into development 102 | phases or iterations. For each iteration, you can create an iteration plan. 103 | 104 | The project `plannedfor` defines a start and end date along with an iteration 105 | breakdown. 106 | 107 | 108 | Workitem 109 | -------- 110 | 111 | You can use work items to manage and track not only your work, but also 112 | the work of your team. 113 | 114 | 115 | Workitem Type 116 | ------------- 117 | 118 | 119 | A workitem type is a classification of work items that has a specific set of 120 | attributes. Each predefined process template includes the work item types that 121 | allow users to work in that process. For example, the Scrum process includes 122 | work item types such as `Epic`, `Story`, `Adoption Item`, `Task`, and 123 | `Retrospective`, which support an agile development model. The Formal Project 124 | Management process, which supports a more traditional development model, 125 | includes workitem types such as `Project Change Request`, `Business Need`, and 126 | `Risk`. Some work item types, such as `Defect` and `Task`, are used by 127 | multiple processes. 128 | 129 | 130 | Workitem Type Category 131 | ---------------------- 132 | 133 | Each work item type belongs to a work item category. Multiple work item types 134 | can belong to the same work item category. The work item types of a work item 135 | type category share workflow and custom attributes. 136 | When you create a work item type, you must associate it with a category. 137 | If you intend to define a unique workflow for the new work item type, 138 | create a new category and associate it with the work item type. 139 | Otherwise, you can associate the work item type with an existing category. 140 | 141 | .. [3] `SCM Data Model `_ 142 | -------------------------------------------------------------------------------- /tests/utils_test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import xmltodict 3 | from rtcclient import OrderedDict 4 | 5 | _path = os.path.realpath(os.path.dirname(__file__)) 6 | _search_path = os.path.join(_path, "fixtures") 7 | 8 | 9 | def read_fixture(file_name): 10 | file_path = os.path.join(_search_path, file_name) 11 | with open(file_path, mode="r") as fh: 12 | return fh.read() 13 | 14 | 15 | pa1 = (xmltodict.parse(read_fixture("projectareas.xml")).get( 16 | "oslc_cm:Collection").get("rtc_cm:Project")[0]) 17 | 18 | pa2 = (xmltodict.parse(read_fixture("projectareas.xml")).get( 19 | "oslc_cm:Collection").get("rtc_cm:Project")[1]) 20 | 21 | ta1 = (xmltodict.parse(read_fixture("teamareas.xml")).get( 22 | "oslc_cm:Collection").get("rtc_cm:Team")[0]) 23 | 24 | ta2 = (xmltodict.parse(read_fixture("teamareas.xml")).get( 25 | "oslc_cm:Collection").get("rtc_cm:Team")[1]) 26 | 27 | plannedfor1 = (xmltodict.parse(read_fixture("plannedfors.xml")).get( 28 | "oslc_cm:Collection").get("rtc_cm:Iteration")[0]) 29 | 30 | plannedfor2 = (xmltodict.parse(read_fixture("plannedfors.xml")).get( 31 | "oslc_cm:Collection").get("rtc_cm:Iteration")[1]) 32 | 33 | severity1 = (xmltodict.parse(read_fixture("severities.xml")).get( 34 | "oslc_cm:Collection").get("rtc_cm:Literal")[0]) 35 | 36 | severity2 = (xmltodict.parse(read_fixture("severities.xml")).get( 37 | "oslc_cm:Collection").get("rtc_cm:Literal")[1]) 38 | 39 | priority1 = (xmltodict.parse(read_fixture("priorities.xml")).get( 40 | "oslc_cm:Collection").get("rtc_cm:Literal")[0]) 41 | 42 | priority2 = (xmltodict.parse(read_fixture("priorities.xml")).get( 43 | "oslc_cm:Collection").get("rtc_cm:Literal")[1]) 44 | 45 | foundin1 = (xmltodict.parse(read_fixture("foundins.xml")).get( 46 | "oslc_cm:Collection").get("rtc_cm:Deliverable")[0]) 47 | 48 | foundin2 = (xmltodict.parse(read_fixture("foundins.xml")).get( 49 | "oslc_cm:Collection").get("rtc_cm:Deliverable")[1]) 50 | 51 | filedagainst1 = (xmltodict.parse(read_fixture("filedagainsts.xml")).get( 52 | "oslc_cm:Collection").get("rtc_cm:Category")[0]) 53 | 54 | filedagainst2 = (xmltodict.parse(read_fixture("filedagainsts.xml")).get( 55 | "oslc_cm:Collection").get("rtc_cm:Category")[1]) 56 | 57 | workitem1 = (xmltodict.parse(read_fixture("workitems.xml")).get( 58 | "oslc_cm:Collection").get("oslc_cm:ChangeRequest")[0]) 59 | 60 | workitem1_origin = OrderedDict() 61 | workitem1_origin["oslc_cm:ChangeRequest"] = workitem1 62 | workitem1_raw = xmltodict.unparse(workitem1_origin) 63 | 64 | workitem2 = (xmltodict.parse(read_fixture("workitems.xml")).get( 65 | "oslc_cm:Collection").get("oslc_cm:ChangeRequest")[1]) 66 | workitem2_origin = OrderedDict() 67 | workitem2_origin["oslc_cm:ChangeRequest"] = workitem2 68 | workitem2_raw = xmltodict.unparse(workitem2_origin) 69 | 70 | template_name = "issue_example.template" 71 | template_raw = read_fixture(template_name) 72 | template_ordereddict = xmltodict.parse(template_raw) 73 | 74 | member1 = (xmltodict.parse( 75 | read_fixture("members.xml")).get("oslc_cm:Collection").get("rtc_cm:User")[0] 76 | ) 77 | 78 | member2 = (xmltodict.parse( 79 | read_fixture("members.xml")).get("oslc_cm:Collection").get("rtc_cm:User")[1] 80 | ) 81 | 82 | member3 = (xmltodict.parse( 83 | read_fixture("members.xml")).get("oslc_cm:Collection").get("rtc_cm:User")[2] 84 | ) 85 | 86 | itemtype1 = (xmltodict.parse(read_fixture("itemtypes.xml")).get( 87 | "oslc_cm:Collection").get("rtc_cm:Type")[0]) 88 | 89 | itemtype2 = (xmltodict.parse(read_fixture("itemtypes.xml")).get( 90 | "oslc_cm:Collection").get("rtc_cm:Type")[1]) 91 | 92 | admin = (xmltodict.parse(read_fixture("administrators.xml")).get( 93 | "oslc_cm:Collection").get("rtc_cm:User")) 94 | 95 | comment1 = (xmltodict.parse(read_fixture("comments.xml")).get( 96 | "oslc_cm:Collection").get("rtc_cm:Comment")[0]) 97 | 98 | comment2 = (xmltodict.parse(read_fixture("comments.xml")).get( 99 | "oslc_cm:Collection").get("rtc_cm:Comment")[1]) 100 | 101 | action1 = (xmltodict.parse(read_fixture("actions.xml")).get( 102 | "oslc_cm:Collection").get("rtc_cm:Action")[0]) 103 | 104 | action2 = (xmltodict.parse(read_fixture("actions.xml")).get( 105 | "oslc_cm:Collection").get("rtc_cm:Action")[1]) 106 | 107 | role1 = xmltodict.parse( 108 | read_fixture("roles.xml")).get("jp06:roles").get("jp06:role")[0] 109 | 110 | role2 = xmltodict.parse( 111 | read_fixture("roles.xml")).get("jp06:roles").get("jp06:role")[1] 112 | 113 | role3 = xmltodict.parse( 114 | read_fixture("roles.xml")).get("jp06:roles").get("jp06:role")[2] 115 | 116 | state1 = (xmltodict.parse(read_fixture("states.xml")).get( 117 | "oslc_cm:Collection").get("rtc_cm:Status")[0]) 118 | 119 | state2 = (xmltodict.parse(read_fixture("states.xml")).get( 120 | "oslc_cm:Collection").get("rtc_cm:Status")[1]) 121 | 122 | savedquery1 = (xmltodict.parse(read_fixture("savedqueries.xml")).get( 123 | "oslc_cm:Collection").get("rtc_cm:Query")[0]) 124 | 125 | savedquery2 = (xmltodict.parse(read_fixture("savedqueries.xml")).get( 126 | "oslc_cm:Collection").get("rtc_cm:Query")[1]) 127 | 128 | savedquery3 = (xmltodict.parse(read_fixture("savedqueries.xml")).get( 129 | "oslc_cm:Collection").get("rtc_cm:Query")[2]) 130 | 131 | includedinbuild1 = (xmltodict.parse(read_fixture("includedinbuilds.xml")).get( 132 | "oslc_cm:Collection").get("oslc_auto:AutomationResult")[0]) 133 | 134 | includedinbuild2 = (xmltodict.parse(read_fixture("includedinbuilds.xml")).get( 135 | "oslc_cm:Collection").get("oslc_auto:AutomationResult")[1]) 136 | 137 | children1 = (xmltodict.parse(read_fixture("children.xml")).get( 138 | "oslc_cm:Collection").get("oslc_cm:ChangeRequest")[0]) 139 | 140 | children2 = (xmltodict.parse(read_fixture("children.xml")).get( 141 | "oslc_cm:Collection").get("oslc_cm:ChangeRequest")[1]) 142 | 143 | parent = (xmltodict.parse(read_fixture("parent.xml")).get( 144 | "oslc_cm:Collection").get("oslc_cm:ChangeRequest")) 145 | 146 | changeset1 = (xmltodict.parse(read_fixture("changesets.xml")).get( 147 | "oslc_cm:Collection").get("rtc_cm:Reference")[0]) 148 | 149 | changeset2 = (xmltodict.parse(read_fixture("changesets.xml")).get( 150 | "oslc_cm:Collection").get("rtc_cm:Reference")[1]) 151 | 152 | changeset3 = (xmltodict.parse(read_fixture("changesets.xml")).get( 153 | "oslc_cm:Collection").get("rtc_cm:Reference")[2]) 154 | 155 | attachment1 = (xmltodict.parse(read_fixture("attachment.xml")).get( 156 | "oslc_cm:Collection").get("rtc_cm:Attachment")[0]) 157 | attachment2 = (xmltodict.parse(read_fixture("attachment.xml")).get( 158 | "oslc_cm:Collection").get("rtc_cm:Attachment")[1]) 159 | -------------------------------------------------------------------------------- /doc/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) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 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/rtcclient.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/rtcclient.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/rtcclient" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/rtcclient" 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 | -------------------------------------------------------------------------------- /doc/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% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 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\rtcclient.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\rtcclient.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 | -------------------------------------------------------------------------------- /doc/source/workitem_attr.rst: -------------------------------------------------------------------------------- 1 | .. _workitemattrs: 2 | 3 | Workitem Attributes [1]_ 4 | ======================== 5 | 6 | Attributes identify the information that you want to capture when users create 7 | and modify work items. Attributes are similar to fields in records. Work item 8 | types include all the built-in attributes that are listed in below Table. 9 | Note, however, that not every ready-to-use work item presentation is configured 10 | to display all of the built-in attributes in the Rational Team Concert™ Eclipse 11 | and web clients. You can customize the attributes that a work item type 12 | contains and the presentations that are used to display these attributes. 13 | For example, you can customize attributes to add behavior. Such behaviors can 14 | include validating an attribute value, or setting an attribute value that is 15 | based on other attribute values. 16 | 17 | All the attributes of the :class:`rtcclient.workitem.Workitem` can be accessed 18 | through **dot notation** and **dictionary**. 19 | 20 | 21 | .. _workitemattrs_table: 22 | 23 | Built-in Attributes 24 | ------------------- 25 | 26 | Table1. Built-in Attributes 27 | 28 | +--------------------+-------------+-------------------+----------------------------------------------+ 29 | | Name | Type | ID | Description | 30 | +====================+=============+===================+==============================================+ 31 | | Archived | Boolean | archived | Specifies whether the work item is archived. | 32 | +--------------------+-------------+-------------------+----------------------------------------------+ 33 | | Comments | Comments | comments | Comments about the work item. | 34 | +--------------------+-------------+-------------------+----------------------------------------------+ 35 | | Corrected Estimate | Duration | correctedEstimate | Correction to the original time estimate | 36 | | | | | (as specified by the Estimate attribute) to | 37 | | | | | resolve the work item. | 38 | +--------------------+-------------+-------------------+----------------------------------------------+ 39 | | Created By | Contributor | creator | User who created the work item. | 40 | +--------------------+-------------+-------------------+----------------------------------------------+ 41 | | Creation Date | Timestamp | created | Date when the work item was created. | 42 | +--------------------+-------------+-------------------+----------------------------------------------+ 43 | | Description | Large HTML | description | Detailed description of the work item. | 44 | | | | | For example, the description for a defect | 45 | | | | | might include a list of steps to follow to | 46 | | | | | reproduce the defect. Any descriptions that | 47 | | | | | are longer than 32 KB are truncated, and the | 48 | | | | | entire description is added as an attachment.| 49 | +--------------------+-------------+-------------------+----------------------------------------------+ 50 | | Due Date | Timestamp | due | Date by which the resolution of the work | 51 | | | | | item is due. | 52 | +--------------------+-------------+-------------------+----------------------------------------------+ 53 | | Estimate | Duration | estimate | Estimated amount of time that it takes to | 54 | | | | | resolve the work item. | 55 | +--------------------+-------------+-------------------+----------------------------------------------+ 56 | | Filed Against | Category | filedAgainst | Category that identifies the component or | 57 | | | | | functional area that the work item belongs | 58 | | | | | to. For example, your project might have GUI,| 59 | | | | | Build, and Documentation categories. | 60 | | | | | Each category is associated with a team area;| 61 | | | | | that team is responsible for responding to | 62 | | | | | the work item. | 63 | +--------------------+-------------+-------------------+----------------------------------------------+ 64 | | Found In | Deliverable | foundIn | Release in which the issue described in the | 65 | | | | | work item was identified. | 66 | +--------------------+-------------+-------------------+----------------------------------------------+ 67 | | Id | Integer | identifier | Identification number that is associated | 68 | | | | | with the work item. | 69 | +--------------------+-------------+-------------------+----------------------------------------------+ 70 | | Modified By | Contributor | modifiedBy | User who last modified the work item. | 71 | +--------------------+-------------+-------------------+----------------------------------------------+ 72 | | Modified Date | Timestamp | modified | Date when the work item was last modified. | 73 | +--------------------+-------------+-------------------+----------------------------------------------+ 74 | | Owned By | Contributor | ownedBy | Owner of the work item. | 75 | +--------------------+-------------+-------------------+----------------------------------------------+ 76 | | Planned For | Iteration | plannedFor | Iteration for which the work item is planned.| 77 | +--------------------+-------------+-------------------+----------------------------------------------+ 78 | | Priority | Priority | priority | Ranked importance of a work item. For | 79 | | | | | example, `Low`, `Medium`, or `High`. | 80 | +--------------------+-------------+-------------------+----------------------------------------------+ 81 | | Project Area | ProjectArea | projectArea | Area in the repository where information | 82 | | | | | about the project is stored. | 83 | +--------------------+-------------+-------------------+----------------------------------------------+ 84 | | Resolution | Small String| resolution | How the work item was resolved. | 85 | +--------------------+-------------+-------------------+----------------------------------------------+ 86 | | Resolution Date | Timestamp | resolved | Date when the work item was resolved. | 87 | +--------------------+-------------+-------------------+----------------------------------------------+ 88 | | Resolved By | Contributor | resolvedBy | User who resolved the work item. | 89 | +--------------------+-------------+-------------------+----------------------------------------------+ 90 | 91 | 92 | Table2. Built-in Attributes (cont'd) 93 | 94 | +--------------------+-------------+-------------------+----------------------------------------------+ 95 | | Name | Type | ID | Description | 96 | +====================+=============+===================+==============================================+ 97 | | Restricted Access | UUID | contextId | Scope of access to the work item. | 98 | +--------------------+-------------+-------------------+----------------------------------------------+ 99 | | Severity | Severity | severity | Indication of the impact of the work item. | 100 | | | | | For example, `Minor`, `Normal`, `Major`, or | 101 | | | | | `Critical`. | 102 | +--------------------+-------------+-------------------+----------------------------------------------+ 103 | | Start Date | Timestamp | startDate | Date when work began on the work item. | 104 | +--------------------+-------------+-------------------+----------------------------------------------+ 105 | | Status | Small String| state | Status of the work item. For example, `New`, | 106 | | | | | `In Progress`, or `Resolved`. | 107 | +--------------------+-------------+-------------------+----------------------------------------------+ 108 | | Subscribed By |Subscriptions| subscribers | Users who are subscribed to the work item. | 109 | +--------------------+-------------+-------------------+----------------------------------------------+ 110 | | Summary | Medium HTML | title | Brief headline that identifies the work item.| 111 | +--------------------+-------------+-------------------+----------------------------------------------+ 112 | | Tags | Tag | subject | Tags that are used for organizing and | 113 | | | | | querying on work items. | 114 | +--------------------+-------------+-------------------+----------------------------------------------+ 115 | | Time Spent | Duration | timeSpent | Length of time that was spent to resolve the | 116 | | | | | work item. | 117 | +--------------------+-------------+-------------------+----------------------------------------------+ 118 | | Type | Type | type | Type of work item. Commonly available types | 119 | | | | | are `Defect`, `Task`, and `Story`. | 120 | +--------------------+-------------+-------------------+----------------------------------------------+ 121 | 122 | .. [1] `Workitem Customization Overview `_ 123 | -------------------------------------------------------------------------------- /doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # rtcclient documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Aug 17 17:21:28 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 os 16 | import sys 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | # sys.path.insert(0, os.path.abspath('.')) 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.ifconfig', 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'rtcclient' 58 | copyright = u'2023, Di Xu' 59 | author = u'Di Xu' 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.9' 67 | # The full version, including alpha/beta/rc tags. 68 | release = '0.9.0' 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 = 'en' 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 = [] 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 | # -- Options for HTML output ---------------------------------------------- 115 | 116 | # The theme to use for HTML and HTML Help pages. See the documentation for 117 | # a list of builtin themes. 118 | html_theme = 'alabaster' 119 | 120 | # Theme options are theme-specific and customize the look and feel of a theme 121 | # further. For a list of options available for each theme, see the 122 | # documentation. 123 | # html_theme_options = {} 124 | 125 | # Add any paths that contain custom themes here, relative to this directory. 126 | # html_theme_path = [] 127 | 128 | # The name for this set of Sphinx documents. If None, it defaults to 129 | # " v documentation". 130 | # html_title = None 131 | 132 | # A shorter title for the navigation bar. Default is the same as html_title. 133 | # html_short_title = None 134 | 135 | # The name of an image file (relative to this directory) to place at the top 136 | # of the sidebar. 137 | # html_logo = None 138 | 139 | # The name of an image file (within the static path) to use as favicon of the 140 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 141 | # pixels large. 142 | # html_favicon = None 143 | 144 | # Add any paths that contain custom static files (such as style sheets) here, 145 | # relative to this directory. They are copied after the builtin static files, 146 | # so a file named "default.css" will overwrite the builtin "default.css". 147 | html_static_path = ['_static'] 148 | 149 | # Add any extra paths that contain custom files (such as robots.txt or 150 | # .htaccess) here, relative to this directory. These files are copied 151 | # directly to the root of the documentation. 152 | # html_extra_path = [] 153 | 154 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 155 | # using the given strftime format. 156 | # html_last_updated_fmt = '%b %d, %Y' 157 | 158 | # If true, SmartyPants will be used to convert quotes and dashes to 159 | # typographically correct entities. 160 | # html_use_smartypants = True 161 | 162 | # Custom sidebar templates, maps document names to template names. 163 | # html_sidebars = {} 164 | 165 | # Additional templates that should be rendered to pages, maps page names to 166 | # template names. 167 | # html_additional_pages = {} 168 | 169 | # If false, no module index is generated. 170 | # html_domain_indices = True 171 | 172 | # If false, no index is generated. 173 | # html_use_index = True 174 | 175 | # If true, the index is split into individual pages for each letter. 176 | # html_split_index = False 177 | 178 | # If true, links to the reST sources are added to the pages. 179 | # html_show_sourcelink = True 180 | 181 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 182 | # html_show_sphinx = True 183 | 184 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 185 | # html_show_copyright = True 186 | 187 | # If true, an OpenSearch description file will be output, and all pages will 188 | # contain a tag referring to it. The value of this option must be the 189 | # base URL from which the finished HTML is served. 190 | # html_use_opensearch = '' 191 | 192 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 193 | # html_file_suffix = None 194 | 195 | # Language to be used for generating the HTML full-text search index. 196 | # Sphinx supports the following languages: 197 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 198 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 199 | # html_search_language = 'en' 200 | 201 | # A dictionary with options for the search language support, empty by default. 202 | # Now only 'ja' uses this config value 203 | # html_search_options = {'type': 'default'} 204 | 205 | # The name of a javascript file (relative to the configuration directory) that 206 | # implements a search results scorer. If empty, the default will be used. 207 | # html_search_scorer = 'scorer.js' 208 | 209 | # Output file base name for HTML help builder. 210 | htmlhelp_basename = 'rtcclientdoc' 211 | 212 | # -- Options for LaTeX output --------------------------------------------- 213 | 214 | latex_elements = { 215 | # The paper size ('letterpaper' or 'a4paper'). 216 | # 'papersize': 'letterpaper', 217 | 218 | # The font size ('10pt', '11pt' or '12pt'). 219 | # 'pointsize': '10pt', 220 | 221 | # Additional stuff for the LaTeX preamble. 222 | # 'preamble': '', 223 | 224 | # Latex figure (float) alignment 225 | # 'figure_align': 'htbp', 226 | } 227 | 228 | # Grouping the document tree into LaTeX files. List of tuples 229 | # (source start file, target name, title, 230 | # author, documentclass [howto, manual, or own class]). 231 | latex_documents = [ 232 | (master_doc, 'rtcclient.tex', u'rtcclient Documentation', u'Di Xu', 233 | 'manual'), 234 | ] 235 | 236 | # The name of an image file (relative to this directory) to place at the top of 237 | # the title page. 238 | # latex_logo = None 239 | 240 | # For "manual" documents, if this is true, then toplevel headings are parts, 241 | # not chapters. 242 | # latex_use_parts = False 243 | 244 | # If true, show page references after internal links. 245 | # latex_show_pagerefs = False 246 | 247 | # If true, show URL addresses after external links. 248 | # latex_show_urls = False 249 | 250 | # Documents to append as an appendix to all manuals. 251 | # latex_appendices = [] 252 | 253 | # If false, no module index is generated. 254 | # latex_domain_indices = True 255 | 256 | # -- Options for manual page output --------------------------------------- 257 | 258 | # One entry per manual page. List of tuples 259 | # (source start file, name, description, authors, manual section). 260 | man_pages = [(master_doc, 'rtcclient', u'rtcclient Documentation', [author], 1)] 261 | 262 | # If true, show URL addresses after external links. 263 | # man_show_urls = False 264 | 265 | # -- Options for Texinfo output ------------------------------------------- 266 | 267 | # Grouping the document tree into Texinfo files. List of tuples 268 | # (source start file, target name, title, author, 269 | # dir menu entry, description, category) 270 | texinfo_documents = [ 271 | (master_doc, 'rtcclient', u'rtcclient Documentation', author, 'rtcclient', 272 | 'One line description of project.', 'Miscellaneous'), 273 | ] 274 | 275 | # Documents to append as an appendix to all manuals. 276 | # texinfo_appendices = [] 277 | 278 | # If false, no module index is generated. 279 | # texinfo_domain_indices = True 280 | 281 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 282 | # texinfo_show_urls = 'footnote' 283 | 284 | # If true, do not generate a @detailmenu in the "Top" node's menu. 285 | # texinfo_no_detailmenu = False 286 | 287 | # Example configuration for intersphinx: refer to the Python standard library. 288 | intersphinx_mapping = {'https://docs.python.org/': None} 289 | -------------------------------------------------------------------------------- /rtcclient/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import re 4 | 5 | import xmltodict 6 | 7 | from rtcclient import urlunquote, OrderedDict 8 | from rtcclient.base import FieldBase 9 | 10 | 11 | class Role(FieldBase): 12 | """The role in the project area or team area""" 13 | 14 | log = logging.getLogger("models.Role") 15 | 16 | def __str__(self): 17 | return self.label 18 | 19 | 20 | class Member(FieldBase): 21 | """The member in the project area""" 22 | 23 | log = logging.getLogger("models.Member") 24 | 25 | def __init__(self, url, rtc_obj, raw_data=None, skip_full_attributes=True): 26 | FieldBase.__init__(self, 27 | url, 28 | rtc_obj, 29 | raw_data=raw_data, 30 | skip_full_attributes=skip_full_attributes) 31 | # add a new attribute mainly for the un-recorded member use 32 | self.email = urlunquote(self.url.split("/")[-1]) 33 | 34 | def __str__(self): 35 | if hasattr(self, "title"): 36 | return self.title 37 | return self.email 38 | 39 | def _initialize(self): 40 | pass 41 | 42 | def __initialize(self): 43 | pass 44 | 45 | 46 | class Administrator(Member): 47 | """The administrator of the project area""" 48 | 49 | log = logging.getLogger("models.Administrator") 50 | 51 | 52 | class ItemType(FieldBase): 53 | """The workitem type""" 54 | 55 | log = logging.getLogger("models.ItemType") 56 | 57 | def __str__(self): 58 | return self.title 59 | 60 | 61 | class TeamArea(FieldBase): 62 | """The team area""" 63 | 64 | log = logging.getLogger("models.TeamArea") 65 | 66 | def __str__(self): 67 | return self.title 68 | 69 | 70 | class PlannedFor(FieldBase): 71 | """The project plannedfor defines a start and end date along with an 72 | iteration breakdown 73 | """ 74 | 75 | log = logging.getLogger("models.PlannedFor") 76 | 77 | def __str__(self): 78 | return self.title 79 | 80 | 81 | class FiledAgainst(FieldBase): 82 | """Category that identifies the component or functional area that the 83 | work item belongs to. 84 | """ 85 | 86 | log = logging.getLogger("models.FiledAgainst") 87 | 88 | def __str__(self): 89 | return self.title 90 | 91 | 92 | class FoundIn(FieldBase): 93 | """Release in which the issue described in the work item was identified. 94 | """ 95 | 96 | log = logging.getLogger("models.FoundIn") 97 | 98 | def __str__(self): 99 | return self.title 100 | 101 | 102 | class Severity(FieldBase): 103 | """Indication of the impact of the work item""" 104 | 105 | log = logging.getLogger("models.Severity") 106 | 107 | def __str__(self): 108 | return self.title 109 | 110 | 111 | class Priority(FieldBase): 112 | """Ranked importance of a work item""" 113 | 114 | log = logging.getLogger("models.Priority") 115 | 116 | def __str__(self): 117 | return self.title 118 | 119 | 120 | class Action(FieldBase): 121 | """The action to change the state of the workitem""" 122 | 123 | log = logging.getLogger("models.Action") 124 | 125 | def __str__(self): 126 | return self.title 127 | 128 | 129 | class State(FieldBase): 130 | """Status of the work item. For example, New, In Progress, or Resolved.""" 131 | 132 | log = logging.getLogger("models.State") 133 | 134 | def __str__(self): 135 | return self.title 136 | 137 | 138 | class Comment(FieldBase): 139 | """Comment about the work item""" 140 | 141 | log = logging.getLogger("models.Comment") 142 | 143 | def __init__(self, url, rtc_obj, raw_data=None, skip_full_attributes=True): 144 | self.id = url.split("/")[-1] 145 | FieldBase.__init__(self, 146 | url, 147 | rtc_obj, 148 | raw_data, 149 | skip_full_attributes=skip_full_attributes) 150 | 151 | def __str__(self): 152 | return self.id 153 | 154 | 155 | class SavedQuery(FieldBase): 156 | """User saved query""" 157 | 158 | log = logging.getLogger("models.SavedQuery") 159 | 160 | def __init__(self, url, rtc_obj, raw_data=None, skip_full_attributes=True): 161 | self.id = url.split("/")[-1] 162 | FieldBase.__init__(self, 163 | url, 164 | rtc_obj, 165 | raw_data, 166 | skip_full_attributes=skip_full_attributes) 167 | 168 | def __str__(self): 169 | return self.title 170 | 171 | 172 | class IncludedInBuild(FieldBase): 173 | """Which build includes the certain workitem""" 174 | 175 | log = logging.getLogger("models.IncludedInBuild") 176 | 177 | def __str__(self): 178 | return self.label 179 | 180 | 181 | class ChangeSet(FieldBase): 182 | """ChangeSet""" 183 | 184 | log = logging.getLogger("models.ChangeSet") 185 | 186 | def __str__(self): 187 | return self.label 188 | 189 | def getChanges(self): 190 | """Get all :class:`rtcclient.models.Change` objects in this changeset 191 | 192 | :return: a :class:`list` contains all the 193 | :class:`rtcclient.models.Change` objects 194 | :rtype: list 195 | """ 196 | 197 | identifier = self.url.split("/")[-1] 198 | resource_url = "/".join([ 199 | "%s" % self.rtc_obj.url, "resource/itemOid", 200 | "com.ibm.team.scm.ChangeSet", 201 | "%s?_mediaType=text/xml" % identifier 202 | ]) 203 | resp = self.get(resource_url, 204 | verify=self.rtc_obj.verify, 205 | proxies=self.rtc_obj.proxies, 206 | headers=self.rtc_obj.headers) 207 | raw_data = xmltodict.parse(resp.content).get("scm:ChangeSet") 208 | common_changes = dict() 209 | changes = raw_data.get("changes") 210 | for (key, value) in raw_data.items(): 211 | if key.startswith("@"): 212 | continue 213 | if "changes" != key: 214 | common_changes[key] = value 215 | return self._handle_changes(changes, common_changes) 216 | 217 | def _handle_changes(self, changes, common_changes): 218 | change_objs = list() 219 | 220 | if isinstance(changes, OrderedDict): 221 | # only one single change 222 | changes.update(common_changes) 223 | change_objs.append(Change(None, self.rtc_obj, raw_data=changes)) 224 | 225 | elif isinstance(changes, list): 226 | # multiple changes 227 | for change in changes: 228 | change.update(common_changes) 229 | change_objs.append(Change(None, self.rtc_obj, raw_data=change)) 230 | 231 | return change_objs 232 | 233 | 234 | class Change(FieldBase): 235 | """Change""" 236 | 237 | log = logging.getLogger("models.Change") 238 | 239 | def __init__(self, url, rtc_obj, raw_data=None, skip_full_attributes=True): 240 | FieldBase.__init__(self, 241 | url, 242 | rtc_obj, 243 | raw_data, 244 | skip_full_attributes=skip_full_attributes) 245 | 246 | def __str__(self): 247 | return self.internalId 248 | 249 | def fetchBeforeStateFile(self, file_folder): 250 | """Fetch the initial file (before the change) to a folder 251 | 252 | If the file is newly added, then `None` will be returned. 253 | 254 | :param file_folder: the folder to store the file 255 | :return: the :class:`string` object 256 | :rtype: string 257 | """ 258 | 259 | if u"true" == self.before: 260 | self.log.info("This file is newly added. No previous file") 261 | else: 262 | self.log.info("Fetching initial file of this Change<%s>:" % self) 263 | return self._fetchFile(self.before, file_folder, override=False) 264 | 265 | def fetchAfterStateFile(self, file_folder): 266 | """Fetch the final file (after the change) to a folder 267 | 268 | If the file has been deleted, then `None` will be returned. 269 | 270 | :param file_folder: the folder to store the file 271 | :return: the :class:`string` object 272 | :rtype: string 273 | """ 274 | 275 | if u"true" == self.after: 276 | self.log.info("This file has been deleted successfully.") 277 | else: 278 | self.log.info("Fetching final file of this Change<%s>:" % self) 279 | return self._fetchFile(self.after, file_folder) 280 | 281 | def fetchCurrentFile(self, file_folder): 282 | """Fetch the current/final file (after the change) to a folder 283 | 284 | If the file has been deleted, then `None` will be returned. 285 | 286 | :param file_folder: the folder to store the file 287 | :return: the :class:`string` object 288 | :rtype: string 289 | """ 290 | 291 | return self.fetchAfterStateFile(file_folder) 292 | 293 | def _fetchFile(self, state_id, file_folder, override=True): 294 | if self.raw_data['item']['@xsi:type'] == 'scm:FolderHandle': 295 | return 296 | 297 | file_url = "/".join([ 298 | "{0}/service", 299 | ("com.ibm.team.filesystem.service.internal." 300 | "rest.IFilesystemContentService"), "-", 301 | ("{1}?itemId={2}&stateId={3}" 302 | "&platformLineDelimiter=CRLF") 303 | ]) 304 | 305 | file_url = file_url.format(self.rtc_obj.url, self.component, self.item, 306 | state_id) 307 | 308 | self.log.debug("Start fetching file from %s ..." % file_url) 309 | 310 | resp = self.get(file_url, 311 | verify=self.rtc_obj.verify, 312 | headers=self.rtc_obj.headers) 313 | file_name = re.findall(r".+filename\*=UTF-8''(.+)", 314 | resp.headers["content-disposition"])[0] 315 | file_path = os.path.join(file_folder, file_name) 316 | 317 | if not override and os.path.exists(file_path): 318 | return 319 | 320 | with open(file_path, "wb") as file_content: 321 | file_content.write(resp.content) 322 | 323 | self.log.info("Successfully Fetching '%s' to '%s'" % 324 | (file_name, file_path)) 325 | return file_path 326 | 327 | 328 | class Attachment(FieldBase): 329 | """Attachment of the work item""" 330 | 331 | log = logging.getLogger("models.Attachment") 332 | 333 | def __init__(self, url, rtc_obj, raw_data=None, skip_full_attributes=True): 334 | FieldBase.__init__(self, 335 | url, 336 | rtc_obj, 337 | raw_data, 338 | skip_full_attributes=skip_full_attributes) 339 | 340 | def __str__(self): 341 | return self.identifier + ": " + self.title 342 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {2015} {Di Xu} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /rtcclient/project_area.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import six 4 | import xmltodict 5 | 6 | from rtcclient import exception 7 | from rtcclient.base import FieldBase 8 | from rtcclient.models import Role 9 | 10 | 11 | class ProjectArea(FieldBase): 12 | """A wrapped class to perform all the operations in a Project Area 13 | 14 | :param url: the project area url 15 | :param rtc_obj: a reference to the 16 | :class:`rtcclient.client.RTCClient` object 17 | :param raw_data: the raw data ( OrderedDict ) of the request response 18 | 19 | """ 20 | 21 | log = logging.getLogger("project_area.ProjectArea") 22 | 23 | def __init__(self, url, rtc_obj, raw_data, skip_full_attributes=True): 24 | FieldBase.__init__(self, 25 | url, 26 | rtc_obj, 27 | raw_data, 28 | skip_full_attributes=skip_full_attributes) 29 | self.id = self.url.split("/")[-1] 30 | 31 | def __str__(self): 32 | return self.title 33 | 34 | def _initialize(self): 35 | """Request to get response""" 36 | 37 | self.log.error("For ProjectArea, raw_data is mandatory") 38 | raise exception.EmptyAttrib("Please input raw_data") 39 | 40 | def getRoles(self): 41 | """Get all :class:`rtcclient.models.Role` objects in this project 42 | area 43 | 44 | If no :class:`Roles` are retrieved, `None` is returned. 45 | 46 | :return: a :class:`list` that contains all 47 | :class:`rtcclient.models.Role` objects 48 | :rtype: list 49 | """ 50 | 51 | # no need to retrieve all the entries from _get_paged_resources 52 | # role raw data is very simple that contains no other links 53 | 54 | self.log.info("Get all the roles in ", self) 55 | roles_url = "/".join( 56 | [self.rtc_obj.url, 57 | "process/project-areas/%s/roles" % self.id]) 58 | resp = self.get(roles_url, 59 | verify=self.rtc_obj.verify, 60 | proxies=self.rtc_obj.proxies, 61 | headers=self.rtc_obj.headers) 62 | 63 | roles_list = list() 64 | raw_data = xmltodict.parse(resp.content) 65 | roles_raw = raw_data['jp06:roles']['jp06:role'] 66 | if not roles_raw: 67 | self.log.warning("There are no roles in ", self) 68 | return None 69 | 70 | for role_raw in roles_raw: 71 | role = Role(role_raw.get("jp06:url"), 72 | self.rtc_obj, 73 | raw_data=role_raw) 74 | roles_list.append(role) 75 | return roles_list 76 | 77 | def getRole(self, label): 78 | """Get the :class:`rtcclient.models.Role` object by the label name 79 | 80 | :param label: the label name of the role 81 | :return: the :class:`rtcclient.models.Role` object 82 | :rtype: :class:`rtcclient.models.Role` 83 | """ 84 | 85 | if not isinstance(label, six.string_types) or not label: 86 | excp_msg = "Please specify a valid role label" 87 | self.log.error(excp_msg) 88 | raise exception.BadValue(excp_msg) 89 | 90 | roles = self.getRoles() 91 | if roles is not None: 92 | for role in roles: 93 | if role.label == label: 94 | self.log.info("Get in ", role, 95 | self) 96 | return role 97 | 98 | excp_msg = "No role's label is %s in " % (label, self) 99 | self.log.error(excp_msg) 100 | raise exception.NotFound(excp_msg) 101 | 102 | def getMembers(self, returned_properties=None): 103 | """Get all the :class:`rtcclient.models.Member` objects in this 104 | project area 105 | 106 | If no :class:`Members` are retrieved, `None` is returned. 107 | 108 | :param returned_properties: the returned properties that you want. 109 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 110 | :return: a :class:`list` that contains all 111 | :class:`rtcclient.models.Member` objects 112 | :rtype: list 113 | """ 114 | 115 | return self._getMembers(returned_properties=returned_properties) 116 | 117 | def getMember(self, email, returned_properties=None): 118 | """Get the :class:`rtcclient.models.Member` object by the 119 | email address 120 | 121 | :param email: the email address (e.g. somebody@gmail.com) 122 | :param returned_properties: the returned properties that you want. 123 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 124 | :return: the :class:`rtcclient.models.Member` object 125 | :rtype: rtcclient.models.Member 126 | """ 127 | 128 | if not isinstance(email, six.string_types) or "@" not in email: 129 | excp_msg = "Please specify a valid email address name" 130 | self.log.error(excp_msg) 131 | raise exception.BadValue(excp_msg) 132 | 133 | self.log.debug("Try to get Member whose email is %s>", email) 134 | members = self._getMembers(returned_properties=returned_properties, 135 | email=email) 136 | if members is not None: 137 | member = members[0] 138 | self.log.info("Get in ", member, self) 139 | return member 140 | 141 | excp_msg = "No member's email is %s in " % (email, self) 142 | self.log.error(excp_msg) 143 | raise exception.NotFound(excp_msg) 144 | 145 | def _getMembers(self, returned_properties=None, email=None): 146 | self.log.warning("If you are not listed, please contact your RTC " 147 | "administrators to add you as a team member") 148 | rp = returned_properties 149 | filter_rule = None 150 | if email is not None: 151 | fmember_rule = ("rtc_cm:userId", None, email) 152 | filter_rule = self.rtc_obj._add_filter_rule(filter_rule, 153 | fmember_rule) 154 | return self.rtc_obj._get_paged_resources("Member", 155 | projectarea_id=self.id, 156 | page_size='100', 157 | returned_properties=rp, 158 | filter_rule=filter_rule) 159 | 160 | def getItemTypes(self, returned_properties=None): 161 | """Get all the :class:`rtcclient.models.ItemType` objects 162 | in this project area 163 | 164 | If no :class:`ItemTypes` are retrieved, `None` is returned. 165 | 166 | :param returned_properties: the returned properties that you want. 167 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 168 | :return: a :class:`list` that contains all 169 | :class:`rtcclient.models.ItemType` objects 170 | :rtype: list 171 | """ 172 | 173 | return self._getItemTypes(returned_properties=returned_properties) 174 | 175 | def getItemType(self, title, returned_properties=None): 176 | """Get the :class:`rtcclient.models.ItemType` object by the title 177 | 178 | :param title: the title (e.g. Story/Epic/..) 179 | :param returned_properties: the returned properties that you want. 180 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 181 | :return: the :class:`rtcclient.models.ItemType` object 182 | :rtype: rtcclient.models.ItemType 183 | """ 184 | 185 | if not isinstance(title, six.string_types) or not title: 186 | excp_msg = "Please specify a valid email address name" 187 | self.log.error(excp_msg) 188 | raise exception.BadValue(excp_msg) 189 | 190 | self.log.debug("Try to get ", title) 191 | itemtypes = self._getItemTypes(returned_properties=returned_properties, 192 | title=title) 193 | if itemtypes is not None: 194 | itemtype = itemtypes[0] 195 | self.log.info("Get in ", itemtype, 196 | self) 197 | return itemtype 198 | 199 | excp_msg = "No itemtype's name is %s in " % (title, 200 | self) 201 | self.log.error(excp_msg) 202 | raise exception.NotFound(excp_msg) 203 | 204 | def _getItemTypes(self, returned_properties=None, title=None): 205 | rp = returned_properties 206 | filter_rule = None 207 | if title is not None: 208 | fit_rule = ("dc:title", None, title) 209 | filter_rule = self.rtc_obj._add_filter_rule(filter_rule, fit_rule) 210 | return self.rtc_obj._get_paged_resources("ItemType", 211 | projectarea_id=self.id, 212 | page_size='10', 213 | returned_properties=rp, 214 | filter_rule=filter_rule) 215 | 216 | def getAdministrators(self, returned_properties=None): 217 | """Get all the :class:`rtcclient.models.Administrator` objects in this 218 | project area 219 | 220 | If no :class:`Administrators` are retrieved, 221 | `None` is returned. 222 | 223 | :param returned_properties: the returned properties that you want. 224 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 225 | :return: a :class:`list` that contains all 226 | :class:`rtcclient.models.Administrator` objects 227 | :rtype: list 228 | """ 229 | 230 | return self._getAdministrators(returned_properties=returned_properties) 231 | 232 | def getAdministrator(self, email, returned_properties=None): 233 | """Get the :class:`rtcclient.models.Administrator` object 234 | by the email address 235 | 236 | :param email: the email address (e.g. somebody@gmail.com) 237 | :param returned_properties: the returned properties that you want. 238 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 239 | :return: the :class:`rtcclient.models.Administrator` object 240 | :rtype: rtcclient.models.Administrator 241 | """ 242 | 243 | if not isinstance(email, six.string_types) or "@" not in email: 244 | excp_msg = "Please specify a valid email address name" 245 | self.log.error(excp_msg) 246 | raise exception.BadValue(excp_msg) 247 | 248 | self.log.debug("Try to get Administrator whose email is %s", email) 249 | rp = returned_properties 250 | administrators = self._getAdministrators(returned_properties=rp, 251 | email=email) 252 | if administrators is not None: 253 | administrator = administrators[0] 254 | self.log.info("Get in ", 255 | administrator, self) 256 | return administrator 257 | 258 | msg = "No administrator's email is %s in " % (email, 259 | self) 260 | self.log.error(msg) 261 | raise exception.NotFound(msg) 262 | 263 | def _getAdministrators(self, returned_properties=None, email=None): 264 | rp = returned_properties 265 | filter_rule = None 266 | if email is not None: 267 | fadmin_rule = ("rtc_cm:userId", None, email) 268 | filter_rule = self.rtc_obj._add_filter_rule(filter_rule, 269 | fadmin_rule) 270 | return self.rtc_obj._get_paged_resources("Administrator", 271 | projectarea_id=self.id, 272 | page_size='10', 273 | returned_properties=rp, 274 | filter_rule=filter_rule) 275 | -------------------------------------------------------------------------------- /rtcclient/query.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import six 4 | 5 | from rtcclient import exception 6 | from rtcclient import urlquote 7 | from rtcclient.base import RTCBase 8 | 9 | 10 | class Query(RTCBase): 11 | """A wrapped class to perform all query-related actions 12 | 13 | :param rtc_obj: a reference to the 14 | :class:`rtcclient.client.RTCClient` object 15 | """ 16 | 17 | log = logging.getLogger("query:Query") 18 | 19 | def __init__(self, rtc_obj, skip_full_attributes=True): 20 | """Initialize object""" 21 | 22 | self.rtc_obj = rtc_obj 23 | RTCBase.__init__(self, 24 | self.rtc_obj.url, 25 | skip_full_attributes=skip_full_attributes) 26 | 27 | def __str__(self): 28 | return "Query @ %s" % self.rtc_obj 29 | 30 | def get_rtc_obj(self): 31 | return self.rtc_obj 32 | 33 | def queryWorkitems(self, 34 | query_str, 35 | projectarea_id=None, 36 | projectarea_name=None, 37 | returned_properties=None, 38 | archived=False, 39 | skip_full_attributes=True): 40 | """Query workitems with the query string in a certain 41 | :class:`rtcclient.project_area.ProjectArea` 42 | 43 | At least either of `projectarea_id` and `projectarea_name` is given 44 | 45 | :param query_str: a valid query string 46 | :param projectarea_id: the :class:`rtcclient.project_area.ProjectArea` 47 | id 48 | :param projectarea_name: the 49 | :class:`rtcclient.project_area.ProjectArea` name 50 | :param returned_properties: the returned properties that you want. 51 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 52 | :param archived: (default is False) whether the 53 | :class:`rtcclient.workitem.Workitem` is archived 54 | :return: a :class:`list` that contains the queried 55 | :class:`rtcclient.workitem.Workitem` objects 56 | :rtype: list 57 | """ 58 | 59 | pa_id = (self.rtc_obj._pre_get_resource( 60 | projectarea_id=projectarea_id, projectarea_name=projectarea_name)) 61 | 62 | self.log.info("Start to query workitems with query string: %s", 63 | query_str) 64 | query_str = urlquote(query_str) 65 | rp = returned_properties 66 | 67 | return (self.rtc_obj._get_paged_resources( 68 | "Query", 69 | projectarea_id=pa_id, 70 | customized_attr=query_str, 71 | page_size="100", 72 | returned_properties=rp, 73 | archived=archived, 74 | skip_full_attributes=skip_full_attributes)) 75 | 76 | def getAllSavedQueries(self, 77 | projectarea_id=None, 78 | projectarea_name=None, 79 | creator=None, 80 | saved_query_name=None): 81 | """Get all saved queries created by somebody (optional) 82 | in a certain project area (optional, either `projectarea_id` 83 | or `projectarea_name` is needed if specified) 84 | 85 | If `saved_query_name` is specified, only the saved queries match the 86 | name will be fetched. 87 | 88 | Note: only if `creator` is added as a member, the saved queries 89 | can be found. Otherwise None will be returned. 90 | 91 | WARNING: now the RTC server cannot correctly list all the saved queries 92 | It seems to be a bug of RTC. Recommend using `runSavedQueryByUrl` to 93 | query all the workitems if the query is saved. 94 | 95 | Note: It will run faster when more attributes are specified. 96 | 97 | :param projectarea_id: the :class:`rtcclient.project_area.ProjectArea` 98 | id 99 | :param projectarea_name: the 100 | :class:`rtcclient.project_area.ProjectArea` name 101 | :param creator: the creator email address 102 | :param saved_query_name: the saved query name 103 | :return: a :class:`list` that contains the saved queried 104 | :class:`rtcclient.models.SavedQuery` objects 105 | :rtype: list 106 | """ 107 | 108 | pa_id = (self.rtc_obj._pre_get_resource( 109 | projectarea_id=projectarea_id, projectarea_name=projectarea_name)) 110 | 111 | filter_rule = None 112 | if creator is not None: 113 | fcreator = self.rtc_obj.getOwnedBy(creator).url 114 | filter_rule = [("dc:creator", "@rdf:resource", fcreator)] 115 | self.log.debug( 116 | "Add rules for fetching all saved queries: " 117 | "created by %s", creator) 118 | 119 | if saved_query_name is not None: 120 | ftitle_rule = ("dc:title", None, saved_query_name) 121 | if filter_rule is None: 122 | filter_rule = [ftitle_rule] 123 | else: 124 | filter_rule.append(ftitle_rule) 125 | self.log.debug( 126 | "Add rules for fetching all saved queries: " 127 | "saved query title is %s", saved_query_name) 128 | 129 | return (self.rtc_obj._get_paged_resources("SavedQuery", 130 | projectarea_id=pa_id, 131 | page_size="100", 132 | filter_rule=filter_rule)) 133 | 134 | def getSavedQueriesByName(self, 135 | saved_query_name, 136 | projectarea_id=None, 137 | projectarea_name=None, 138 | creator=None): 139 | """Get all saved queries match the name created by somebody (optional) 140 | in a certain project area (optional, either `projectarea_id` 141 | or `projectarea_name` is needed if specified) 142 | 143 | Note: only if `creator` is added as a member, the saved queries 144 | can be found. Otherwise None will be returned. 145 | 146 | WARNING: now the RTC server cannot correctly list all the saved queries 147 | It seems to be a bug of RTC. Recommend using `runSavedQueryByUrl` to 148 | query all the workitems if the query is saved. 149 | 150 | :param saved_query_name: the saved query name 151 | :param projectarea_id: the :class:`rtcclient.project_area.ProjectArea` 152 | id 153 | :param projectarea_name: the 154 | :class:`rtcclient.project_area.ProjectArea` name 155 | :param creator: the creator email address 156 | :return: a :class:`list` that contains the saved queried 157 | :class:`rtcclient.models.SavedQuery` objects 158 | :rtype: list 159 | """ 160 | 161 | self.log.info("Start to fetch all saved queries with the name %s", 162 | saved_query_name) 163 | return self.getAllSavedQueries(projectarea_id=projectarea_id, 164 | projectarea_name=projectarea_name, 165 | creator=creator, 166 | saved_query_name=saved_query_name) 167 | 168 | def getMySavedQueries(self, 169 | projectarea_id=None, 170 | projectarea_name=None, 171 | saved_query_name=None): 172 | """Get all saved queries created by me in a certain project 173 | area (optional, either `projectarea_id` or `projectarea_name` is 174 | needed if specified) 175 | 176 | Note: only if myself is added as a member, the saved queries 177 | can be found. Otherwise None will be returned. 178 | 179 | WARNING: now the RTC server cannot correctly list all the saved queries 180 | It seems to be a bug of RTC. Recommend using `runSavedQueryByUrl` to 181 | query all the workitems if the query is saved. 182 | 183 | :param projectarea_id: the :class:`rtcclient.project_area.ProjectArea` 184 | id 185 | :param projectarea_name: the 186 | :class:`rtcclient.project_area.ProjectArea` name 187 | :param saved_query_name: the saved query name 188 | :return: a :class:`list` that contains the saved queried 189 | :class:`rtcclient.models.SavedQuery` objects 190 | :rtype: list 191 | """ 192 | 193 | self.log.info("Start to fetch my saved queries") 194 | return self.getAllSavedQueries(projectarea_id=projectarea_id, 195 | projectarea_name=projectarea_name, 196 | creator=self.rtc_obj.username, 197 | saved_query_name=saved_query_name) 198 | 199 | def runSavedQueryByUrl(self, saved_query_url, returned_properties=None): 200 | """Query workitems using the saved query url 201 | 202 | :param saved_query_url: the saved query url 203 | :param returned_properties: the returned properties that you want. 204 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 205 | :return: a :class:`list` that contains the queried 206 | :class:`rtcclient.workitem.Workitem` objects 207 | :rtype: list 208 | """ 209 | 210 | try: 211 | if "=" not in saved_query_url: 212 | raise exception.BadValue() 213 | saved_query_id = saved_query_url.split("=")[-1] 214 | if not saved_query_id: 215 | raise exception.BadValue() 216 | except Exception: 217 | error_msg = "No saved query id is found in the url" 218 | self.log.error(error_msg) 219 | raise exception.BadValue(error_msg) 220 | return self._runSavedQuery(saved_query_id, 221 | returned_properties=returned_properties) 222 | 223 | def runSavedQueryByID(self, saved_query_id, returned_properties=None): 224 | """Query workitems using the saved query id 225 | 226 | This saved query id can be obtained by below two methods: 227 | 228 | 1. :class:`rtcclient.models.SavedQuery` object (e.g. 229 | mysavedquery.id) 230 | 231 | 2. your saved query url (e.g. 232 | https://myrtc:9443/jazz/web/xxx#action=xxxx%id=_mGYe0CWgEeGofp83pg), 233 | where the last "_mGYe0CWgEeGofp83pg" is the saved query id. 234 | 235 | :param saved_query_id: the saved query id 236 | :param returned_properties: the returned properties that you want. 237 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 238 | :return: a :class:`list` that contains the queried 239 | :class:`rtcclient.workitem.Workitem` objects 240 | :rtype: list 241 | """ 242 | 243 | if not isinstance(saved_query_id, 244 | six.string_types) or not saved_query_id: 245 | excp_msg = "Please specify a valid saved query id" 246 | self.log.error(excp_msg) 247 | raise exception.BadValue(excp_msg) 248 | return self._runSavedQuery(saved_query_id, 249 | returned_properties=returned_properties) 250 | 251 | def runSavedQuery(self, saved_query_obj, returned_properties=None): 252 | """Query workitems using the :class:`rtcclient.models.SavedQuery` 253 | object 254 | 255 | :param saved_query_obj: the :class:`rtcclient.models.SavedQuery` 256 | object 257 | :param returned_properties: the returned properties that you want. 258 | Refer to :class:`rtcclient.client.RTCClient` for more explanations 259 | :return: a :class:`list` that contains the queried 260 | :class:`rtcclient.workitem.Workitem` objects 261 | :rtype: list 262 | """ 263 | 264 | try: 265 | saved_query_id = saved_query_obj.results.split("/")[-2] 266 | except BaseException: 267 | error_msg = "Cannot get the correct saved query id" 268 | self.log.error(error_msg) 269 | raise exception.RTCException(error_msg) 270 | return self._runSavedQuery(saved_query_id, 271 | returned_properties=returned_properties) 272 | 273 | def _runSavedQuery(self, saved_query_id, returned_properties=None): 274 | rp = returned_properties 275 | return (self.rtc_obj._get_paged_resources( 276 | "RunQuery", 277 | page_size="100", 278 | customized_attr=saved_query_id, 279 | returned_properties=rp)) 280 | -------------------------------------------------------------------------------- /tests/test_projectarea.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pytest 3 | import utils_test 4 | from rtcclient.project_area import ProjectArea 5 | from rtcclient.models import Member, ItemType, Administrator, Role 6 | from rtcclient.exception import BadValue, NotFound, EmptyAttrib 7 | 8 | 9 | class TestProjectArea: 10 | 11 | @pytest.fixture(autouse=True) 12 | def myrtcclient(self, rtcclient): 13 | myclient = rtcclient 14 | return myclient 15 | 16 | def test_init_projectarea_exception(self, myrtcclient): 17 | pa_url = "/".join([ 18 | "http://test.url:9443/jazz/oslc", 19 | "projectareas/_0qMJUMfiEd6yW_0tvNlbrw" 20 | ]) 21 | with pytest.raises(EmptyAttrib): 22 | ProjectArea(pa_url, myrtcclient, raw_data=None) 23 | 24 | @pytest.fixture(autouse=True) 25 | def mypa(self, myrtcclient): 26 | pa_url = "/".join([ 27 | "http://test.url:9443/jazz/oslc", 28 | "projectareas/_CuZu0HUwEeKicpXBddtqNA" 29 | ]) 30 | return ProjectArea(pa_url, myrtcclient, utils_test.pa2) 31 | 32 | @pytest.fixture 33 | def mock_get_roles(self, mocker): 34 | mocked_get = mocker.patch("requests.get") 35 | mock_resp = mocker.MagicMock(spec=requests.Response) 36 | mock_resp.status_code = 200 37 | mock_resp.content = utils_test.read_fixture("roles.xml") 38 | mocked_get.return_value = mock_resp 39 | return mocked_get 40 | 41 | def test_get_roles(self, mypa, mock_get_roles, myrtcclient): 42 | # Role1 43 | role1_url = "/".join([ 44 | "http://test.url:9443/jazz/process", 45 | "project-areas/_vsoCMN-TEd6VxeRBsGx82Q", "roles/Product%20Owner" 46 | ]) 47 | role1 = Role(role1_url, myrtcclient, utils_test.role1) 48 | assert str(role1) == "Product Owner" 49 | assert role1.url == role1_url 50 | assert role1.id == "Product Owner" 51 | assert role1.description == " ".join( 52 | ["The person responsible for", "managing the Product Backlog."]) 53 | 54 | # Role2 55 | role2_url = "/".join([ 56 | "http://test.url:9443/jazz/process", 57 | "project-areas_vsoCMN-TEd6VxeRBsGx82Q" 58 | "roles/Test%20Team" 59 | ]) 60 | role2 = Role(role2_url, myrtcclient, utils_test.role2) 61 | 62 | # Role3 63 | role3_url = "/".join([ 64 | "http://test.url:9443/jazz/process", 65 | "project-areas/_vsoCMN-TEd6VxeRBsGx82Q", "roles/default" 66 | ]) 67 | role3 = Role(role3_url, myrtcclient, utils_test.role3) 68 | 69 | roles = mypa.getRoles() 70 | assert roles == [role1, role2, role3] 71 | 72 | def test_get_role(self, mypa, mock_get_roles, myrtcclient): 73 | # invalid role labels 74 | invalid_labels = ["", u"", None, True, False] 75 | for invalid_label in invalid_labels: 76 | with pytest.raises(BadValue): 77 | mypa.getRole(invalid_label) 78 | 79 | # Role1 80 | role1_url = "/".join([ 81 | "http://test.url:9443/jazz/process" 82 | "project-areas_vsoCMN-TEd6VxeRBsGx82Q", "roles/Product%20Owner" 83 | ]) 84 | role1 = Role(role1_url, myrtcclient, utils_test.role1) 85 | 86 | # valid role label 87 | role_valid_names = ["Product Owner", u"Product Owner"] 88 | for role_name in role_valid_names: 89 | role = mypa.getRole(role_name) 90 | assert role == role1 91 | 92 | # undefined role 93 | role_fake_names = ["fake_role", u"fake_role"] 94 | for role_name in role_fake_names: 95 | with pytest.raises(NotFound): 96 | mypa.getRole(role_name) 97 | 98 | @pytest.fixture 99 | def mock_get_members(self, mocker): 100 | mocked_get = mocker.patch("requests.get") 101 | mock_resp = mocker.MagicMock(spec=requests.Response) 102 | mock_resp.status_code = 200 103 | mock_resp.content = utils_test.read_fixture("members.xml") 104 | mocked_get.return_value = mock_resp 105 | return mocked_get 106 | 107 | def test_get_members(self, mypa, mock_get_members, myrtcclient): 108 | 109 | # Member1 110 | m1 = Member("http://test.url:9443/jts/users/tester1%40email.com", 111 | myrtcclient, utils_test.member1) 112 | assert str(m1) == "tester1" 113 | assert m1.email == "tester1@email.com" 114 | assert m1.userId == "tester1@email.com" 115 | assert m1.title == "tester1" 116 | assert m1.emailAddress == "mailto:tester1%40email.com" 117 | assert m1.photo is None 118 | assert m1.modified == "2009-11-24T19:14:14.595Z" 119 | assert m1.modifiedBy == "ADMIN" 120 | 121 | # Member2 122 | m2 = Member("http://test.url:9443/jts/users/tester2%40email.com", 123 | myrtcclient, utils_test.member2) 124 | assert str(m2) == "tester2" 125 | assert m2.email == "tester2@email.com" 126 | assert m2.userId == "tester2@email.com" 127 | assert m2.title == "tester2" 128 | assert m2.emailAddress == "mailto:tester2%40email.com" 129 | assert m2.photo is None 130 | assert m2.modified == "2013-04-22T06:24:34.661Z" 131 | assert m2.modifiedBy == "ADMIN" 132 | 133 | # Member3 134 | m3 = Member("http://test.url:9443/jts/users/tester3%40email.com", 135 | myrtcclient, utils_test.member3) 136 | assert str(m3) == "tester3" 137 | assert m3.email == "tester3@email.com" 138 | assert m3.userId == "tester3@email.com" 139 | assert m3.title == "tester3" 140 | assert m3.emailAddress == "mailto:tester3%40email.com" 141 | assert m3.photo is None 142 | assert m3.modified == "2010-05-13T20:34:05.138Z" 143 | assert m3.modifiedBy == "ADMIN" 144 | 145 | members = mypa.getMembers() 146 | assert members == [m1, m2, m3] 147 | 148 | def test_get_member(self, mypa, myrtcclient, mock_get_members): 149 | # invalid email address 150 | invalid_emails = [ 151 | "", u"", None, True, False, "test.com", u"test.com" 152 | "test%40email.com", u"test%40email.com" 153 | ] 154 | for invalid_email in invalid_emails: 155 | with pytest.raises(BadValue): 156 | mypa.getMember(invalid_email) 157 | 158 | # Member1 159 | m1 = Member("http://test.url:9443/jts/users/tester1%40email.com", 160 | myrtcclient, utils_test.member1) 161 | 162 | # valid email address 163 | member_valid_names = ["tester1@email.com", u"tester1@email.com"] 164 | for member_name in member_valid_names: 165 | member = mypa.getMember(member_name) 166 | assert member == m1 167 | 168 | # undefined member 169 | member_fake_names = ["fake@email.com", u"fake@email.com"] 170 | for member_name in member_fake_names: 171 | with pytest.raises(NotFound): 172 | mypa.getMember(member_name) 173 | 174 | @pytest.fixture 175 | def mock_get_itemtypes(self, mocker): 176 | mocked_get = mocker.patch("requests.get") 177 | mock_resp = mocker.MagicMock(spec=requests.Response) 178 | mock_resp.status_code = 200 179 | mock_resp.content = utils_test.read_fixture("itemtypes.xml") 180 | mocked_get.return_value = mock_resp 181 | return mocked_get 182 | 183 | def test_get_itemtypes(self, mypa, mock_get_itemtypes, myrtcclient): 184 | # ItemType1 185 | it1_url = "/".join([ 186 | "http://test.url:9443/jazz/oslc", 187 | "types/_CuZu0HUwEeKicpXBddtqNA/defect" 188 | ]) 189 | it1 = ItemType(it1_url, myrtcclient, utils_test.itemtype1) 190 | assert it1.url == it1_url 191 | assert str(it1) == "Defect" 192 | assert it1.identifier == "defect" 193 | assert it1.title == "Defect" 194 | assert it1.iconUrl == "".join([ 195 | "http://test.url:9443/jazz/service/", 196 | "com.ibm.team.workitem.common.", 197 | "internal.model.IImageContentService/", "processattachment/", 198 | "_CuZu0HUwEeKicpXBddtqNA/workitemtype", "/bug.gif" 199 | ]) 200 | assert it1.dimmedIconUrl is None 201 | assert it1.category == "defect_task" 202 | # fake data: pls ignore the value 203 | assert it1.projectArea == ["Defect", "Task"] 204 | 205 | # ItemType2 206 | it2_url = "/".join([ 207 | "http://test.url:9443/jazz/oslc", 208 | "types/_CuZu0HUwEeKicpXBddtqNA/task" 209 | ]) 210 | it2 = ItemType(it2_url, myrtcclient, utils_test.itemtype2) 211 | assert it2.url == it2_url 212 | assert str(it2) == "Task" 213 | assert it2.identifier == "task" 214 | assert it2.title == "Task" 215 | assert it2.iconUrl == "".join([ 216 | "http://test.url:9443/jazz/service/", 217 | "com.ibm.team.workitem.common.", 218 | "internal.model.IImageContentService/", "processattachment/", 219 | "_CuZu0HUwEeKicpXBddtqNA/workitemtype", "/task.gif" 220 | ]) 221 | assert it2.dimmedIconUrl is None 222 | assert it2.category == "task" 223 | # fake data: pls ignore the value 224 | assert it2.projectArea == ["Defect", "Task"] 225 | 226 | its = mypa.getItemTypes() 227 | assert its == [it1, it2] 228 | 229 | def test_get_itemtype(self, mypa, myrtcclient, mock_get_itemtypes): 230 | # invalid email address 231 | invalid_titles = ["", u"", None, True, False] 232 | for invalid_title in invalid_titles: 233 | with pytest.raises(BadValue): 234 | mypa.getItemType(invalid_title) 235 | 236 | # ItemType1 237 | it1_url = "/".join([ 238 | "http://test.url:9443/jazz/oslc", 239 | "types/_CuZu0HUwEeKicpXBddtqNA/defect" 240 | ]) 241 | it1 = ItemType(it1_url, myrtcclient, utils_test.itemtype1) 242 | 243 | itemtype_valid_names = ["Defect", u"Defect"] 244 | for itemtype_name in itemtype_valid_names: 245 | itemtype = mypa.getItemType(itemtype_name) 246 | assert itemtype == it1 247 | 248 | # undefined type 249 | itemtype_fake_names = ["fake_type", u"fake_type"] 250 | for itemtype_name in itemtype_fake_names: 251 | with pytest.raises(NotFound): 252 | mypa.getItemType(itemtype_name) 253 | 254 | @pytest.fixture 255 | def mock_get_admins(self, mocker): 256 | mocked_get = mocker.patch("requests.get") 257 | mock_resp = mocker.MagicMock(spec=requests.Response) 258 | mock_resp.status_code = 200 259 | mock_resp.content = utils_test.read_fixture("administrators.xml") 260 | mocked_get.return_value = mock_resp 261 | return mocked_get 262 | 263 | def test_get_admins(self, mypa, mock_get_admins, myrtcclient): 264 | # Administrator 265 | admin_url = "http://test.url:9443/jts/users/tester1%40email.com" 266 | admin = Administrator(admin_url, myrtcclient, utils_test.admin) 267 | assert str(admin) == "tester1" 268 | assert admin.url == admin_url 269 | assert admin.userId == "tester1@email.com" 270 | assert admin.title == "tester1" 271 | assert admin.emailAddress == "mailto:tester1%40email.com" 272 | assert admin.photo is None 273 | assert admin.modified == "2009-08-17T10:08:03.721Z" 274 | assert admin.modifiedBy == "ADMIN" 275 | 276 | admins = mypa.getAdministrators() 277 | assert admins == [admin] 278 | 279 | def test_get_admin(self, mypa, myrtcclient, mock_get_admins): 280 | # invalid email address 281 | invalid_emails = [ 282 | "", u"", None, True, False, "test.com", u"test.com", 283 | "test%40email.com", u"test%40email.com" 284 | ] 285 | for invalid_email in invalid_emails: 286 | with pytest.raises(BadValue): 287 | mypa.getAdministrator(invalid_email) 288 | 289 | # Administrator 290 | ad_url = "http://test.url:9443/jts/users/tester1%40email.com" 291 | ad = Administrator(ad_url, myrtcclient, utils_test.admin) 292 | 293 | # valid email address 294 | admin_valid_names = ["tester1@email.com", u"tester1@email.com"] 295 | for admin_name in admin_valid_names: 296 | admin = mypa.getAdministrator(admin_name) 297 | assert admin == ad 298 | 299 | # undefined admin 300 | admin_fake_names = ["fake@email.com", u"fake@email.com"] 301 | for admin_name in admin_fake_names: 302 | with pytest.raises(NotFound): 303 | mypa.getAdministrator(admin_name) 304 | -------------------------------------------------------------------------------- /doc/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quick Start 4 | =========== 5 | 6 | Eager to get started? This page gives a good introduction in how to get started 7 | with rtcclient. 8 | 9 | First, make sure that: 10 | 11 | * rtcclient is :ref:`installed ` 12 | * rtcclient is up-to-date 13 | 14 | 15 | RTCClient is intended to map the objects in RTC (e.g. Project Areas, 16 | Team Areas, Workitems) into easily managed Python objects 17 | 18 | Let's get started with some simple examples. 19 | 20 | 21 | Setup Logging 22 | ------------- 23 | 24 | You can choose to enable logging during the using of rtcclient. Default logging 25 | is for console output. You can also add your own `logging.conf` to store all 26 | the logs to your specified files. 27 | 28 | >>> from rtcclient.utils import setup_basic_logging 29 | # you can remove this if you don't need logging 30 | >>> setup_basic_logging() 31 | 32 | 33 | Add a Connection to the RTC Server 34 | ---------------------------------- 35 | 36 | Adding a connection with RTC Server is very simple. 37 | 38 | Begin by importing the `RTCClient` module:: 39 | 40 | >>> from rtcclient import RTCClient 41 | 42 | Now, let's input the url, username and password of this to-be-connected 43 | RTC Server. For this example, 44 | 45 | >>> url = "https://your_domain:9443/jazz" 46 | >>> username = "your_username" 47 | >>> password = "your_password" 48 | >>> myclient = RTCClient(url, username, password, ends_with_jazz=True, old_rtc_authentication=False) 49 | 50 | If your url ends with **ccm**, set `ends_with_jazz` to `False`, 51 | refer to **issue #68** for detailed explanation. 52 | 53 | If your rtc server is behind a proxy, remember to set "proxies" explicitly. 54 | 55 | If your rtc server is too old (such as Rational Team Concert 5.0.1, 5.0.2), please set `old_rtc_authentication` to `True`. 56 | 57 | About Proxies 58 | ------------- 59 | 60 | If your RTC Server is behind a proxy, you need to set `proxies` explicitly. 61 | 62 | HTTP Proxies 63 | ~~~~~~~~~~~~ 64 | 65 | >>> url = "https://your_domain:9443/jazz" 66 | >>> username = "your_username" 67 | >>> password = "your_password" 68 | # example http proxy 69 | >>> proxies = { 70 | 'http': 'http://10.10.1.10:3128', 71 | 'https': 'http://10.10.1.10:1080', 72 | } 73 | >>> myclient = RTCClient(url, username, password, proxies=proxies) 74 | 75 | SOCKS Proxies 76 | ~~~~~~~~~~~~ 77 | In addition to basic HTTP proxies, proxies using the SOCKS protocol are also 78 | supported. 79 | 80 | >>> url = "https://your_domain:9443/jazz" 81 | >>> username = "your_username" 82 | >>> password = "your_password" 83 | # example socks proxy 84 | >>> proxies = { 85 | "http": "socks5://127.0.0.1:1080", 86 | "https": "socks5://user:pass@host:port" 87 | } 88 | >>> myclient = RTCClient(url, username, password, proxies=proxies) 89 | 90 | 91 | Get a Workitem 92 | -------------- 93 | 94 | You can get a workitem by calling 95 | :class:`rtcclient.workitem.Workitem.getWorkitem`. The attributes of a workitem 96 | can be accessed through **dot notation** and **dictionary**. 97 | 98 | Some common attributes are listed in 99 | :ref:`Built-in Attributes `. 100 | 101 | For example, 102 | 103 | >>> wk = myclient.getWorkitem(123456) 104 | # get a workitem whose id is 123456 105 | # this also works: getting the workitem using the equivalent string 106 | >>> wk2 = myclient.getWorkitem("123456") 107 | # wk equals wk2 108 | >>> wk == wk2 109 | True 110 | >>> wk 111 | 112 | >>> str(wk) 113 | '141488' 114 | >>> wk.identifier 115 | u'141488' 116 | # access the attributes through dictionary 117 | >>> wk["title"] 118 | u'title demo' 119 | # access the attributes through dot notation 120 | >>> wk.title 121 | u'title demo' 122 | >>> wk.state 123 | u'Closed' 124 | >>> wk.description 125 | u'demo description' 126 | >>> wk.creator 127 | u'tester1@email.com' 128 | >>> wk.created 129 | u'2015-07-16T08:02:30.658Z' 130 | >>> wk.comments 131 | [u'comment test 0', u'add comment test 1', u'add comment test 2'] 132 | 133 | 134 | About Returned Properties 135 | ------------------------- 136 | 137 | You can also customize your preferred properties to be returned 138 | by specifying **returned_properties** when the called methods have 139 | this optional parameter, which can also **GREATLY IMPROVE** the performance 140 | of this client especially when getting or querying lots of workitems. 141 | 142 | For the meanings of these attributes, please refer to 143 | :ref:`Built-in Attributes `. 144 | 145 | Important Note: **returned_properties** is an advanced parameter, the 146 | returned properties can be found in `instance_obj.field_alias.values()`, 147 | e.g. `myworkitem1.field_alias.values()`. If you don't care the performance, 148 | just leave it alone with `None`. 149 | 150 | .. _field_alias: 151 | 152 | >>> import pprint 153 | # print the field alias 154 | >>> pprint.pprint(wk2.field_alias, width=1) 155 | {u'affectedByDefect': u'calm:affectedByDefect', 156 | u'affectsExecutionResult': u'calm:affectsExecutionResult', 157 | u'affectsPlanItem': u'calm:affectsPlanItem', 158 | u'apply_step': u'rtc_cm:apply_step', 159 | u'archived': u'rtc_cm:archived', 160 | u'blocksTestExecutionRecord': u'calm:blocksTestExecutionRecord', 161 | u'comments': u'rtc_cm:comments', 162 | u'contextId': u'rtc_cm:contextId', 163 | u'correctedEstimate': u'rtc_cm:correctedEstimate', 164 | u'created': u'dc:created', 165 | u'creator': u'dc:creator', 166 | u'description': u'dc:description', 167 | u'due': u'rtc_cm:due', 168 | u'elaboratedByArchitectureElement': u'calm:elaboratedByArchitectureElement', 169 | u'estimate': u'rtc_cm:estimate', 170 | u'filedAgainst': u'rtc_cm:filedAgainst', 171 | u'foundIn': u'rtc_cm:foundIn', 172 | u'identifier': u'dc:identifier', 173 | u'implementsRequirement': u'calm:implementsRequirement', 174 | u'modified': u'dc:modified', 175 | u'modifiedBy': u'rtc_cm:modifiedBy', 176 | u'ownedBy': u'rtc_cm:ownedBy', 177 | u'plannedFor': u'rtc_cm:plannedFor', 178 | u'priority': u'oslc_cm:priority', 179 | u'progressTracking': u'rtc_cm:progressTracking', 180 | u'projectArea': u'rtc_cm:projectArea', 181 | u'relatedChangeManagement': u'oslc_cm:relatedChangeManagement', 182 | u'relatedExecutionRecord': u'calm:relatedExecutionRecord', 183 | u'relatedRequirement': u'calm:relatedRequirement', 184 | u'relatedTestCase': u'calm:relatedTestCase', 185 | u'relatedTestPlan': u'calm:relatedTestPlan', 186 | u'relatedTestScript': u'calm:relatedTestScript', 187 | u'relatedTestSuite': u'calm:relatedTestSuite', 188 | u'resolution': u'rtc_cm:resolution', 189 | u'resolved': u'rtc_cm:resolved', 190 | u'resolvedBy': u'rtc_cm:resolvedBy', 191 | u'schedule': u'oslc_pl:schedule', 192 | u'severity': u'oslc_cm:severity', 193 | u'startDate': u'rtc_cm:startDate', 194 | u'state': u'rtc_cm:state', 195 | u'subject': u'dc:subject', 196 | u'subscribers': u'rtc_cm:subscribers', 197 | u'teamArea': u'rtc_cm:teamArea', 198 | u'testedByTestCase': u'calm:testedByTestCase', 199 | u'timeSheet': u'rtc_cm:timeSheet', 200 | u'timeSpent': u'rtc_cm:timeSpent', 201 | u'title': u'dc:title', 202 | u'trackedWorkItem': u'oslc_cm:trackedWorkItem', 203 | u'tracksChanges': u'calm:tracksChanges', 204 | u'tracksRequirement': u'calm:tracksRequirement', 205 | u'tracksWorkItem': u'oslc_cm:tracksWorkItem', 206 | u'type': u'dc:type'} 207 | 208 | Note: these field aliases may differ due to the type of workitems. But most of 209 | the common-used attributes will stay unchanged. 210 | 211 | The `returned_properties` is a string **composed by the above values with 212 | comma separated**. 213 | 214 | It will run faster if `returned_properties` is specified. Because the client 215 | will only get/request the attributes you specified. 216 | 217 | >>> returned_properties = "dc:title,dc:identifier,rtc_cm:state,rtc_cm:ownedBy" 218 | # specify the returned properties: title, identifier, state, owner 219 | # This is optional. All properties will be returned if not specified 220 | >>> wk_rp = myclient.getWorkitem(123456, 221 | returned_properties=returned_properties) 222 | >>> wk_rp.identifier 223 | u'141488' 224 | # access the attributes through dictionary 225 | >>> wk_rp["title"] 226 | # access the attributes through dot notation 227 | u'title demo' 228 | >>> wk_rp.title 229 | u'title demo' 230 | >>> wk_rp.state 231 | u'Closed' 232 | >>> wk_rp.ownedBy 233 | u'tester1@email.com' 234 | 235 | 236 | Add a Comment to a Workitem 237 | --------------------------- 238 | 239 | After getting the :class:`rtcclient.workitem.Workitem` object, you can add a 240 | comment to this workitem by calling :class:`addComment`. 241 | 242 | >>> mycomment = wk.addComment("add comment test 3") 243 | >>> mycomment 244 | 245 | >>> mycomment.created 246 | u'2015-08-22T03:55:00.839Z' 247 | >>> mycomment.creator 248 | u'tester1@email.com' 249 | >>> mycomment.description 250 | u'add comment test 3' 251 | >>> str(mycomment) 252 | '3' 253 | 254 | 255 | Get all Workitems 256 | ----------------- 257 | 258 | All workitems can be fetched by calling 259 | :class:`rtcclient.client.RTCClient.getWorkitems`. It will take 260 | a long time to fetch all the workitems in some certain project areas if there 261 | are already many existing workitems. 262 | 263 | If both `projectarea_id` and `projectarea_name` are None, all the workitems 264 | in all project areas will be returned. 265 | 266 | >>> workitems_list = myclient.getWorkitems(projectarea_id=None, 267 | projectarea_name=None, 268 | returned_properties=returned_properties) 269 | # get all workitems in a specific project area 270 | >>> projectarea_name = "my_projectarea_name" 271 | >>> workitems_list2 = myclient.getWorkitems(projectarea_name=projectarea_name, 272 | returned_properties=returned_properties) 273 | 274 | 275 | Query Workitems 276 | --------------- 277 | 278 | After customizing your query string, all the workitems meet the conditions 279 | will be fetched. 280 | 281 | >>> myquery = myclient.query # query class 282 | >>> projectarea_name = "my_projectarea_name" 283 | # customize your query string 284 | # below query string means: query all the workitems with title "use case 1" 285 | >>> myquerystr = 'dc:title="use case 1"' 286 | >>> returned_prop = "dc:title,dc:identifier,rtc_cm:state,rtc_cm:ownedBy" 287 | >>> queried_wis = myquery.queryWorkitems(myquerystr, 288 | projectarea_name=projectarea_name, 289 | returned_properties=returned_prop) 290 | 291 | More detailed and advanced syntax on querying, please refer to 292 | :ref:`query syntax `. 293 | 294 | 295 | Query Workitems by Saved Query 296 | ------------------------------ 297 | 298 | You may have created several customized queries through RTC Web GUI or got 299 | some saved queries created by other team members. Using these saved queries 300 | 301 | >>> myquery = myclient.query # query class 302 | >>> saved_query_url = 'http://test.url:9443/jazz/xxxxxxxx&id=xxxxx' 303 | >>> projectarea_name = "my_projectarea_name" 304 | # get all saved queries 305 | # WARNING: now the RTC server cannot correctly list all the saved queries 306 | # It seems to be a bug of RTC. Recommend using `runSavedQueryByUrl` to 307 | # query all the workitems if the query is saved. 308 | >>> allsavedqueries = myquery.getAllSavedQueries(projectarea_name=projectarea_name) 309 | # saved queries created by tester1@email.com 310 | >>> allsavedqueries = myquery.getAllSavedQueries(projectarea_name=projectarea_name, 311 | creator="tester1@email.com") 312 | # my saved queries 313 | >>> mysavedqueries = myquery.getMySavedQueries(projectarea_name=projectarea_name) 314 | >>> mysavedquery = mysavedqueries[0] 315 | >>> returned_prop = "dc:title,dc:identifier,rtc_cm:state,rtc_cm:ownedBy" 316 | >>> queried_wis = myquery.runSavedQuery(mysavedquery, 317 | returned_properties=returned_prop) 318 | 319 | 320 | Query Workitems by Saved Query Url 321 | ---------------------------------- 322 | 323 | You can also query all the workitems directly using your saved query's url. 324 | 325 | >>> myquery = myclient.query # query class 326 | >>> saved_query_url = 'http://test.url:9443/jazz/xxxxxxxx&id=xxxxx' 327 | >>> returned_prop = "dc:title,dc:identifier,rtc_cm:state,rtc_cm:ownedBy" 328 | >>> queried_wis = myquery.runSavedQueryByUrl(saved_query_url, 329 | returned_properties=returned_prop) 330 | -------------------------------------------------------------------------------- /tests/fixtures/parent.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | false 7 | 8 | _vsoCMN-TEd6VxeRBsGx82Q 9 | 10 | 11 | 2015-07-27T06:50:40.477Z 12 | 13 | 14 | 15 | 16 | 17 | 141872 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 2015-09-11T08:36:52.599Z 26 | 27 | 28 | 29 | 30 | 31 | 32 | Operational Excellence 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | --------------------------------------------------------------------------------