Skip to content

Commit

Permalink
Add pytest testrail API wrapper
Browse files Browse the repository at this point in the history
  • Loading branch information
popescunsergiu committed Nov 28, 2019
1 parent 2c4c50a commit 2d70b6c
Show file tree
Hide file tree
Showing 20 changed files with 2,444 additions and 0 deletions.
Empty file added pytest_testrail/__init__.py
Empty file.
974 changes: 974 additions & 0 deletions pytest_testrail/_category.py

Large diffs are not rendered by default.

67 changes: 67 additions & 0 deletions pytest_testrail/_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os
import sys

import requests

from pytest_testrail.exceptions import MissingCloudCredentialError

# pylint: disable=import-error
if sys.version_info[0] == 2:
import ConfigParser as configparser
else:
import configparser


class Session:
__default_headers = {
'Content-Type': 'application/json',
'User-Agent': 'TestRail API v: 2'
}

def __init__(self, base_url: str = None, email: str = None, key: str = None, **kwargs):
email = email or self.__get_credential('email', ['TESTRAIL_EMAIL'])
key = key or self.__get_credential('key', ['TESTRAIL_KEY'])
base_url = base_url or self.__get_credential('url', ['TESTRAIL_URL'])
verify_ssl = bool(self.__get_credential('verify_ssl', ['TESTRAIL_VERIFY_URL']))

self.__base_url = f'{base_url}/index.php?/api/v2/'
self.__user = email
self.__password = key
self.__headers = kwargs.get('headers', self.__default_headers)
self.__verify_ssl = kwargs.get('verify_ssl', verify_ssl)
self.__timeout = kwargs.get('timeout', 5)
self.__session = requests.Session()

@property
def __name(self):
return type(self).__name__

@property
def __config(self):
name = '.{0}'.format(self.__name.lower())
config = configparser.ConfigParser()
config.read([name, os.path.join(os.path.expanduser('~'), name)])
return config

def __get_credential(self, key, envs):
try:
return self.__config.get('credentials', key)
except (configparser.NoSectionError, configparser.NoOptionError, KeyError):
for env in envs:
value = os.getenv(env)
if value:
return value
raise MissingCloudCredentialError(self.__name, key, envs)

def request(self, method: str, src: str, **kwargs):
"""
Base request method
:param method:
:param src:
:param kwargs:
:return: response
"""
url = f'{self.__base_url}{src}'
response = self.__session.request(method, url, auth=(self.__user, self.__password), headers=self.__headers,
**kwargs)
return response.json()
15 changes: 15 additions & 0 deletions pytest_testrail/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import pytest


class MissingCloudCredentialError(pytest.UsageError):
def __init__(self, driver, key, envs):
super(MissingCloudCredentialError, self).__init__(
"{0} {1} must be set. Try setting one of the following "
"environment variables {2}, or see the documentation for "
"how to use a configuration file.".format(driver, key, envs)
)


class InvalidOptionError(Exception):
def __init__(self, option):
Exception.__init__(self, 'Option %s is not a valid choice' % option)
41 changes: 41 additions & 0 deletions pytest_testrail/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import re
from datetime import timedelta


class TestRailError(Exception):
pass


class ContainerIter:
def __init__(self, objs):
self._objs = list(objs)

def __len__(self):
return len(self._objs)

def __getitem__(self, index):
return self._objs[index]


CUSTOM_METHODS_RE = re.compile(r'^custom_(\w+)')


def custom_methods(content):
matches = [CUSTOM_METHODS_RE.match(method) for method in content]
return dict({match.string: content[match.string] for match in matches if match})


def testrail_duration_to_timedelta(duration):
span = __span()
timedelta_map = {
'weeks': span(re.search(r'\d+w', duration)),
'days': span(re.search(r'\d+d', duration)),
'hours': span(re.search(r'\d+h', duration)),
'minutes': span(re.search(r'\d+m', duration)),
'seconds': span(re.search(r'\d+s', duration))
}
return timedelta(**timedelta_map)


def __span():
return lambda x: int(x.group(0)[:-1]) if x else 0
Empty file.
137 changes: 137 additions & 0 deletions pytest_testrail/model/case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from datetime import datetime

from pytest_testrail.helper import TestRailError, custom_methods


class Case(object):
def __init__(self, content=None):
self._content = content or dict()
self._custom_methods = custom_methods(self._content)

def __getattr__(self, attr):
if attr in self._custom_methods:
return self._content.get(self._custom_methods[attr])
raise AttributeError('"{}" object has no attribute "{}"'.format(
self.__class__.__name__, attr))

def __str__(self):
return self.title

@property
def created_by(self):
return self._content.get('created_by')

@property
def created_on(self):
return datetime.fromtimestamp(int(self._content.get('created_on')))

@property
def estimate(self):
return self._content.get('estimate')

@estimate.setter
def estimate(self, value):
# TODO should have some logic to validate format of timespan
if not isinstance(value, str):
raise TestRailError('input must be a string')
self._content['estimate'] = value

@property
def estimated_forecast(self):
return self._content.get('estimated_forecast')

@property
def id(self):
return self._content.get('id')

@property
def milestone_id(self):
return self._content.get('milestone_id')

@milestone_id.setter
def milestone_id(self, value):
if not isinstance(value, int):
raise TestRailError('input must be an int')
self._content['milestone_id'] = value

@property
def priority_id(self):
return self._content.get('priority_id')

@priority_id.setter
def priority_id(self, value):
if not isinstance(value, int):
raise TestRailError('input must be an int')
self._content['priority_id'] = value

@property
def refs(self):
refs = self._content.get('refs')
return refs.split(',') if refs else list()

@refs.setter
def refs(self, value):
if not isinstance(value, list):
raise TestRailError('input must be a list')
self._content['refs'] = ','.join(value)

@property
def section_id(self):
return self._content.get('section_id')

@section_id.setter
def section_id(self, value):
if not isinstance(value, int):
raise TestRailError('input must be an int')
self._content['section_id'] = value

@property
def suite_id(self):
return self._content.get('suite_id')

@suite_id.setter
def suite_id(self, value):
if not isinstance(value, int):
raise TestRailError('input must be an int')
self._content['suite_id'] = value

@property
def template_id(self):
return self._content.get('template_id')

@template_id.setter
def template_id(self, value):
if not isinstance(value, int):
raise TestRailError('input must be an id')
self._content['template_id'] = value

@property
def title(self):
return self._content.get('title')

@title.setter
def title(self, value):
if not isinstance(value, (str, str)):
raise TestRailError('input must be a string')
self._content['title'] = value

@property
def type_id(self):
return self._content.get('type_id')

@type_id.setter
def type_id(self, value):
if not isinstance(value, int):
raise TestRailError('input must be an id')
self._content['type_id'] = value

@property
def updated_by(self):
return self._content.get('updated_by')

@property
def updated_on(self):
return datetime.fromtimestamp(int(self._content.get('updated_on')))

def raw_data(self):
return self._content
18 changes: 18 additions & 0 deletions pytest_testrail/model/case_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class CaseType(object):
def __init__(self, content):
self._content = content

def __str__(self):
return self.name

@property
def id(self):
return self._content.get('id')

@property
def is_default(self):
return self._content.get('is_default')

@property
def name(self):
return self._content.get('name')
Loading

0 comments on commit 2d70b6c

Please sign in to comment.