Skip to content

Commit 18c3d51

Browse files
committed
Added Zephyr Squad Server support
This commit introduces initial support for the Zephyr Squad (server) variant. Only a number of the available Zephyr API calls are introduced by this commit. All testing for this commit was done on a self-hosted Zephyr Squad instance.
1 parent 4125a73 commit 18c3d51

37 files changed

+1543
-78
lines changed

.pylintrc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,8 +390,8 @@ preferred-modules=
390390
[EXCEPTIONS]
391391

392392
# Exceptions that will emit a warning when caught.
393-
overgeneral-exceptions=BaseException,
394-
Exception
393+
overgeneral-exceptions=builtins.BaseException,
394+
builtins.Exception
395395

396396

397397
[REFACTORING]
@@ -453,7 +453,7 @@ max-locals=15
453453
max-parents=7
454454

455455
# Maximum number of public methods for a class (see R0904).
456-
max-public-methods=20
456+
max-public-methods=30
457457

458458
# Maximum number of return / yield for function / method body.
459459
max-returns=6

README.md

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,33 @@
44
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/zephyr-python-api)
55
![PyPI](https://img.shields.io/pypi/v/zephyr-python-api)
66
![PyPI - License](https://img.shields.io/pypi/l/zephyr-python-api)
7-
### Project description
8-
This is a set of wrappers for Zephyr Scale (TM4J) REST API. This means you can interact with Zephyr Scale without GUI, access it with python code and create automation scripts for your every day interactions.
7+
## Project description
8+
This is a set of wrappers for both Zephyr Scale and Zephyr Squad (TM4J) REST APIs. This means you can interact with Zephyr without GUI, access it with python code and create automation scripts for your every day interactions.
99

1010
To be done:
1111
* More usage examples
1212
* Tests, tests and tests for gods of testing
1313
* Convenient docs
1414
* Implementing higher level wrappers representing Test Case, Test Cycle, etc.
1515

16-
### Installation
16+
## Installation
1717

1818
```
1919
pip install zephyr-python-api
2020
```
2121

22-
### Example usage
22+
## Example usage
2323

24-
Zephyr Cloud auth:
24+
### Zephyr Scale
25+
26+
Zephyr Scale Cloud auth:
2527
```python
2628
from zephyr import ZephyrScale
2729

2830
zscale = ZephyrScale(token=<your_token>)
2931
```
3032

31-
Zephyr Server (TM4J) auth:
33+
Zephyr Scale Server (TM4J) auth:
3234
```python
3335
from zephyr import ZephyrScale
3436

@@ -58,17 +60,62 @@ test_case = zapi.test_cases.get_test_case("<test_case_id>")
5860
creation_result = zapi.test_cases.create_test_case("<project_key>", "test_case_name")
5961
```
6062

61-
### Troubleshooting
63+
### Zephyr Squad
64+
65+
Zephyr Squad Server (TM4J) auth:
66+
```python
67+
from zephyr import ZephyrSquad
68+
69+
# Auth can be made with Jira token
70+
auth = {"token": "<your_jira_token>"}
71+
72+
# or with login and password (suggest using get_pass)
73+
auth = {"username": "<your_login>", "password": "<your_password>"}
74+
75+
# or even session cookie dict
76+
auth = {"cookies": "<session_cookie_dict>"}
77+
78+
zsquad = ZephyrSquad(base_url=base_url, **auth)
79+
```
80+
81+
Then it is possible to interact with api wrappers:
82+
```python
83+
# Obtain a project's information
84+
project_info = zsquad.actions.project.get_project_info("<project_key>")
85+
86+
# Obtain a project's versions/releases
87+
project_versions = zsquad.api.util_resource.get_all_versions("<project_id>")
88+
89+
# Get a single test case by its id
90+
test_case = zsquad.actions.test_cases.get_test_case("<case_key>", fields="id")
91+
92+
# Create a new test case for a project
93+
data = {
94+
"fields": {
95+
"assignee": {
96+
"name": "<jira_username>"
97+
},
98+
"description": "<case_description>"
99+
}
100+
}
101+
creation_result = zsquad.actions.test_cases.create_test_case(projectId="<project_id>", summary="<case_summary>", data=data)
102+
```
103+
104+
## Troubleshooting
62105

63106
For troubleshooting see [TROUBLESHOOTING.md](TROUBLESHOOTING.md)
64107

65108

66-
### License
109+
## License
67110

68111
This library is licensed under the Apache 2.0 License.
69112

70-
### Links
113+
## Links
71114

72115
[Zephyr Scale Cloud API docs](https://support.smartbear.com/zephyr-scale-cloud/api-docs/)
73116

74-
[Zephyr Scale Server API docs](https://support.smartbear.com/zephyr-scale-server/api-docs/v1/)
117+
[Zephyr Scale Server API docs](https://support.smartbear.com/zephyr-scale-server/api-docs/v1/)
118+
119+
[Zephyr Squad Server API docs](https://zephyrsquadserver.docs.apiary.io/)
120+
121+
[Zephyr Squad Server How to API docs](https://support.smartbear.com/zephyr-squad-server/docs/api/index.html)
File renamed without changes.

examples/squad-server.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
Usage examples of Zephyr Squad Server API wrappers.
3+
"""
4+
import logging
5+
6+
from zephyr import ZephyrSquad
7+
8+
# Enable logging with level Debug for more verbosity
9+
logging.basicConfig(level=logging.DEBUG)
10+
11+
12+
# Specify your Jira context to operate with:
13+
base_url = "https://jira.hosted.com/"
14+
15+
# Use the Jira certificate for TLS connections
16+
session_params = {
17+
"verify": "<path-to-certificate>"
18+
}
19+
20+
# Create an instance of Zephyr Squad
21+
zsquad = ZephyrSquad(
22+
base_url=base_url,
23+
token="<token>",
24+
session_attrs=session_params
25+
)
26+
27+
# Now we can start playing with the Zephyr API!
28+
29+
# Obtain a project's information
30+
project_info = zsquad.actions.project.get_project_info("<project_key>")
31+
32+
# Obtain a project's versions/releases
33+
project_versions = zsquad.api.util_resource.get_all_versions("<project_id>")
34+
35+
# Get the data of a testcase
36+
test_case = zsquad.actions.test_cases.get_test_case("<case_key>", fields="id")
37+
38+
# Get the test steps from a testcase
39+
test_steps = zsquad.api.teststep_resource.get_list_of_teststeps("<issue_id>")
40+
41+
# Get the information about a test cycle
42+
test_cycle = zsquad.api.cycle_resource.get_cycle_information(cycle_id="<cycle_id>")
43+
44+
# Get the list of all test cycles for a specific release
45+
test_cycles = zsquad.api.cycle_resource.get_list_of_cycle(project_id="<project_id>", versionId="<version_id>")
46+
47+
# Get all folders from a test cycle
48+
test_cycle_folders = zsquad.api.cycle_resource.get_the_list_of_folder_for_a_cycle(cycle_id="<cycle_id>", project_id="<project_id>", version_id="<version_id>")
49+
50+
# Get all test executions from a test case
51+
test_executions = zsquad.api.traceability_resource.get_list_of_search_execution_by_test(test_id_or_key="<test_id_or_key>")
52+
53+
# Create a new test case for a project
54+
data = {
55+
"fields": {
56+
"assignee": {
57+
"name": "<jira_username>"
58+
},
59+
"description": "<case_description>"
60+
}
61+
}
62+
ret_data = zsquad.actions.test_cases.create_test_case(project_id="<project_id>", summary="<case_summary>", data=data)
63+
64+
# Execute ZQL search query
65+
demo_query = "project = '<project_id>' AND cycleName = '<cycle_name>'"
66+
zql_search_res = zsquad.api.execution_search_resource.execute_search_to_get_search_result(query=demo_query, maxRecords=200)
67+
68+
# Create a new test cycle for a project based on an existing test case
69+
data = {
70+
"clonedCycleId": "<cycle_id>",
71+
"description": "<cycle_description>",
72+
"build": "",
73+
"startDate": "29/Nov/22",
74+
"endDate": "4/Dec/22",
75+
"environment": ""
76+
}
77+
ret_data = zsquad.api.cycle_resource.create_new_cycle(project_id="<project_id>", version_id="<version_id>", name="<cycle_name>", data=data)
78+
79+
# Create a new test folder for a test cycle
80+
data = {
81+
"cycleId": 1508, # it will be rewritten by the function
82+
"name": "<folder_name>",
83+
"description": "<folder_description>",
84+
"projectId": 10600, # it will be rewritten by the function
85+
"versionId": -1, # it will be rewritten by the function
86+
"clonedFolderId": -1
87+
}
88+
ret_data = zsquad.api.folder_resource.create_folder_under_cycle(project_id="<project_id>", version_id="<version_id>", cycle_id="<cycle_id>", data=data)
89+
90+
# Add a new test case for a test cycle
91+
data = {
92+
"issues":["<case_key>"],
93+
}
94+
ret_data = zsquad.api.execution_resource.add_test_to_cycle(project_id="<project_id>", cycle_id="<cycle_id>", method="1", data=data)
95+
96+
# Obtain the execution details
97+
exec_details = zsquad.api.execution_resource.get_execution_information(execution_id="<execution_id>")
98+
99+
# Obtain the execution steps from an execution
100+
exec_steps = zsquad.api.step_result_resource.get_list_of_step_result(execution_id="<execution_id>")
101+
102+
# Update the status of an execution step
103+
step_status = zsquad.api.step_result_resource.update_step_result_information(step_result_id="<execution_step_id>", status=2)
104+
105+
# Update the execution status
106+
exec_status = zsquad.api.execution_resource.update_execution_details(execution_id="<execution_id>", status=2)
107+
108+
# Update a folder name and description
109+
data = {
110+
"description": "<new_folder_decription>"
111+
}
112+
ret_data = zsquad.api.folder_resource.update_folder_information(project_id="<project_id>", version_id="<version_id>", cycle_id="<cycle_id>", folder_id="<folder_id>", name="<new_folder_name>")
113+
114+
# Delete 3 test executions
115+
delete_status = zsquad.api.execution_resource.delete_bulk_execution(execution_id=["<exec_id_1>", "<exec_id_2>", "<exec_id_3>"])
116+
117+
# Show the progress of a job
118+
job_status = zsquad.api.execution_resource.get_job_progress_status(job_progress_token="<job_progress_token>")
119+
120+
# Get a test step's detailed information
121+
test_step = zsquad.api.teststep_resource.get_teststep_information(test_step_id="<test_step_id>", issue_id="<issue_id>")
122+
123+
# Add a attachment (for a execution result: entityId=executionId and entityType='Execution')
124+
attach = zsquad.api.attachment_resource.add_attachment_into_entity(file_path="<file_path>", entity_id="<entity_id>", entity_type='<entity_type>')
125+
126+
# Add a assignee to a execution result
127+
add_assignee = zsquad.api.execution_resource.add_assignee_to_execution(execution_id="<exec_id>", assignee="<username>")
128+
129+
# Create a test execution result in a test cycle
130+
data = {
131+
"folderId": "<folder_id>"
132+
}
133+
ret_data = zsquad.api.execution_resource.create_new_execution(project_id="<project_id>", cycle_id="<cycle_id>", version_id="<version_id>", issue_id="<issue_id>", data=data)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ install_requires =
2727

2828
[options.packages.find]
2929
exclude =
30-
tests*
30+
tests*

tests/unit/test_scale.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from zephyr.scale.scale import DEFAULT_BASE_URL, ZephyrScale
66

77

8-
ZSESSION_PATH = "zephyr.scale.scale.ZephyrSession"
8+
ZSESSION_PATH = "zephyr.scale.scale.ZephyrScaleSession"
99
CLOUD_API_WRAP_PATH = "zephyr.scale.scale.CloudApiWrapper"
1010
SERVER_API_WRAP_PATH = "zephyr.scale.scale.ServerApiWrapper"
1111

tests/unit/test_zephyr_session.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,33 @@
11
import pytest
22
from requests import Session
33

4-
from zephyr.scale.scale import DEFAULT_BASE_URL, ZephyrSession
5-
from zephyr.scale.zephyr_session import INIT_SESSION_MSG, InvalidAuthData
4+
from zephyr.scale.scale import DEFAULT_BASE_URL, ZephyrScaleSession
5+
from zephyr.common.zephyr_session import INIT_SESSION_MSG, InvalidAuthData
66

77
REQUESTS_SESSION_PATH = "requests.sessions.Session"
88
GETLOGGER_PATH = "logging.getLogger"
99
LOGGER_DEBUG_PATH = "logging.Logger.debug"
1010

1111

1212
@pytest.mark.unit
13-
class TestZephyrSession:
13+
class TestZephyrScaleSession:
1414
def test_creation(self, mocker):
1515
"""Tests basic creation logic"""
1616
logger_mock = mocker.patch(GETLOGGER_PATH)
1717

18-
zsession = ZephyrSession(DEFAULT_BASE_URL, token="token_test")
18+
zsession = ZephyrScaleSession(DEFAULT_BASE_URL, token="token_test")
1919

2020
assert zsession.base_url == DEFAULT_BASE_URL, (f"Attribute base_url expected to be {DEFAULT_BASE_URL}, "
2121
f"not {zsession.base_url}")
2222
assert isinstance(zsession._session, Session)
23-
logger_mock.assert_called_with("zephyr.scale.zephyr_session")
23+
logger_mock.assert_called_with("zephyr.common.zephyr_session")
2424

2525
def test_token_auth(self, mocker):
2626
"""Test token auth"""
2727
token = "test_token"
2828
logger_mock = mocker.patch(LOGGER_DEBUG_PATH)
2929

30-
zsession = ZephyrSession(DEFAULT_BASE_URL, token=token)
30+
zsession = ZephyrScaleSession(DEFAULT_BASE_URL, token=token)
3131

3232
logger_mock.assert_called_with(INIT_SESSION_MSG.format("token"))
3333
assert f"Bearer {token}" == zsession._session.headers.get("Authorization")
@@ -38,7 +38,7 @@ def test_credentials_auth(self, mocker):
3838
password = "pwdtest"
3939
logger_mock = mocker.patch(LOGGER_DEBUG_PATH)
4040

41-
zsession = ZephyrSession(DEFAULT_BASE_URL, username=username, password=password)
41+
zsession = ZephyrScaleSession(DEFAULT_BASE_URL, username=username, password=password)
4242

4343
logger_mock.assert_called_with(INIT_SESSION_MSG.format("username and password"))
4444
assert (username, password) == zsession._session.auth
@@ -48,7 +48,7 @@ def test_cookie_auth(self, mocker):
4848
test_cookie = {"cookies": {"cookie.token": "cookie_test"}}
4949
logger_mock = mocker.patch(LOGGER_DEBUG_PATH)
5050

51-
zsession = ZephyrSession(DEFAULT_BASE_URL, cookies=test_cookie)
51+
zsession = ZephyrScaleSession(DEFAULT_BASE_URL, cookies=test_cookie)
5252

5353
logger_mock.assert_called_with(INIT_SESSION_MSG.format("cookies"))
5454
assert test_cookie['cookies'] in zsession._session.cookies.values()
@@ -59,7 +59,7 @@ def test_cookie_auth(self, mocker):
5959
def test_auth_exception(self, auth_data, exception):
6060
"""Test exceptions on auth"""
6161
with pytest.raises(exception):
62-
ZephyrSession(DEFAULT_BASE_URL, **auth_data)
62+
ZephyrScaleSession(DEFAULT_BASE_URL, **auth_data)
6363

6464
@pytest.mark.parametrize("creation_kwargs",
6565
[{"token": "token_test",
@@ -69,7 +69,7 @@ def test_requests_session_attrs(self, creation_kwargs, mocker):
6969
logger_mock = mocker.patch(LOGGER_DEBUG_PATH)
7070
session_attrs = creation_kwargs.get('session_attrs')
7171

72-
zsession = ZephyrSession(DEFAULT_BASE_URL, **creation_kwargs)
72+
zsession = ZephyrScaleSession(DEFAULT_BASE_URL, **creation_kwargs)
7373

7474
logger_mock.assert_called_with(
7575
f"Modify requests session object with {session_attrs}")

zephyr/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
from zephyr.scale import API_V1, API_V2, ZephyrScale
2+
from zephyr.squad import ZephyrSquad
23
from zephyr.utils.common import cookie_str_to_dict

zephyr/scale/zephyr_session.py renamed to zephyr/common/zephyr_session.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import logging
2-
from urllib.parse import urlparse, parse_qs
32

43
from requests import HTTPError, Session
54

@@ -13,7 +12,7 @@ class InvalidAuthData(Exception):
1312

1413
class ZephyrSession:
1514
"""
16-
Zephyr Scale basic session object.
15+
Zephyr basic session object.
1716
1817
:param base_url: url to make requests to
1918
:param token: auth token
@@ -83,24 +82,6 @@ def delete(self, endpoint: str, **kwargs):
8382
"""Delete request wrapper"""
8483
return self._request("delete", endpoint, **kwargs)
8584

86-
def get_paginated(self, endpoint, params=None):
87-
"""Get paginated data"""
88-
self.logger.debug(f"Get paginated data from endpoint={endpoint} and params={params}")
89-
if params is None:
90-
params = {}
91-
92-
while True:
93-
response = self.get(endpoint, params=params)
94-
if "values" not in response:
95-
return
96-
for value in response.get("values", []):
97-
yield value
98-
if response.get("isLast") is True:
99-
break
100-
params_str = urlparse(response.get("next")).query
101-
params.update(parse_qs(params_str))
102-
return
103-
10485
def post_file(self, endpoint: str, file_path: str, to_files=None, **kwargs):
10586
"""
10687
Post wrapper to send a file. Handles single file opening,

0 commit comments

Comments
 (0)