├── src ├── __init__.py ├── enums │ ├── __init__.py │ ├── global_enums.py │ └── user_enums.py ├── schemas │ ├── __init__.py │ ├── physical.py │ ├── user.py │ └── computer.py ├── baseclasses │ ├── __init__.py │ ├── pyenum.py │ ├── builder.py │ └── response.py └── generators │ ├── __init__.py │ ├── item_type_generator.py │ ├── player.py │ └── player_localization.py ├── tests ├── __init__.py ├── users │ ├── __init__.py │ ├── conftest.py │ └── test_users.py ├── somethig │ ├── __init__.py │ ├── conftest.py │ ├── experiments.py │ └── test_something.py └── conftest.py ├── .dockerignore ├── configuration.py ├── requirements.txt ├── pytest.ini ├── examples.py ├── db.py ├── Dockerfile ├── tables.py ├── .gitignore ├── README.md └── LICENSE /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/enums/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/schemas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/users/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/baseclasses/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/generators/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/somethig/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | venv 2 | *.md 3 | __pycache__ 4 | env -------------------------------------------------------------------------------- /configuration.py: -------------------------------------------------------------------------------- 1 | """ 2 | Обычный конфигурационный файл, который должен быть в любом проекте :) 3 | 4 | Default configuration file, that should be in each project :) 5 | """ 6 | 7 | SERVICE_URL = 'https://gorest.co.in/public/v1/users' 8 | 9 | CONNECTION_ROW = \ 10 | "postgresql://twoe_hd_kino:qwertyQWERTY@localhost:5432/twoe_hd_kino_db" 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/schemas/physical.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, HttpUrl, UUID4 2 | 3 | from pydantic.color import Color 4 | 5 | """ 6 | Пример описания pydantic model с нестандартными типами полей. 7 | Позже эта модель будет использована как часть другой модели. 8 | 9 | Example of describing pydantic mode with rare types. 10 | Later that model we will use as a part of another model. 11 | """ 12 | 13 | 14 | class Physical(BaseModel): 15 | color: Color 16 | photo: HttpUrl 17 | uuid: UUID4 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | allure-pytest==2.9.45 2 | allure-python-commons==2.9.45 3 | attrs==21.4.0 4 | certifi==2021.10.8 5 | charset-normalizer==2.0.12 6 | dnspython==2.2.0 7 | email-validator==1.1.3 8 | Faker==12.3.3 9 | greenlet==1.1.2 10 | idna==3.3 11 | iniconfig==1.1.1 12 | packaging==21.3 13 | pluggy==1.0.0 14 | psycopg2-binary==2.9.3 15 | py==1.11.0 16 | pydantic==1.9.0 17 | pyparsing==3.0.7 18 | pytest==7.0.1 19 | python-dateutil==2.8.2 20 | requests==2.27.1 21 | six==1.16.0 22 | SQLAlchemy==1.4.36 23 | tomli==2.0.1 24 | typing-extensions==4.1.1 25 | urllib3==1.26.8 26 | -------------------------------------------------------------------------------- /src/enums/global_enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class GlobalErrors(Enum): 5 | """ 6 | Обычный класс с ошибками которые стоит использовать в ваших Assert и в 7 | Response класс. Нужно это для того, чтобы: 8 | 1. Унифицировать всё это дело 9 | 2. Облегчить использование и обновление ошибок 10 | 11 | Default ENUM class with error that should be used in your asserts and in 12 | Response class. It needs for: 13 | 1. Unification 14 | 2. Helps in code support 15 | """ 16 | WRONG_STATUS_CODE = 'Status code is different than expected' 17 | -------------------------------------------------------------------------------- /src/generators/item_type_generator.py: -------------------------------------------------------------------------------- 1 | from src.baseclasses.builder import BuilderBaseClass 2 | 3 | from faker import Faker 4 | 5 | fake = Faker() 6 | 7 | """ 8 | Простенький генератор ItemType. 9 | 10 | Common generator of ItemType. 11 | """ 12 | 13 | 14 | class ItemsTypeBuilder(BuilderBaseClass): 15 | 16 | def __init__(self): 17 | super().__init__() 18 | self.result = {} 19 | self.reset() 20 | 21 | def set_item_type(self, item_type=fake.word()): 22 | self.result['item_type'] = item_type 23 | return self 24 | 25 | def reset(self): 26 | self.set_item_type() 27 | 28 | def build(self): 29 | return self.result 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /tests/users/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | 4 | from configuration import SERVICE_URL 5 | 6 | 7 | @pytest.fixture 8 | def get_users(): 9 | """ 10 | Пример фикстуры которая получает часто запрашиваемые данные с сервера. 11 | К примеру, вам нужно получать каких-то юзеров постоянно и брать одного 12 | рандомного, в таком случае этот вариант может стать отличным решением. 13 | 14 | Example of fixture that accept useful data from a server. 15 | For example, you need to get some users from the server regular and pick 16 | one random from them. So for that case fetching data using fixtures is a 17 | best solution. 18 | """ 19 | response = requests.get(SERVICE_URL) 20 | return response 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/baseclasses/pyenum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class PyEnum(Enum): 5 | """ 6 | Обычный ENUM класс с одним лишь отличием, мы расширили его одним методом 7 | который будет очень полезным для нас. 8 | 9 | Так как вы имеете список всех статусов и других параметров в виде ENUM, 10 | то почему бы не использовать это? Метод list будет возвращать все наши 11 | значения в виде списка, которые мы сможем использовать в нашем parametrize. 12 | 13 | Default ENUM class but with one difference. We have been added one 14 | additional method for us that returns all enum values as list, so as 15 | a result we can use them in parametrize construction in our autotests. 16 | """ 17 | @classmethod 18 | def list(cls): 19 | return list(map(lambda c: c.value, cls)) 20 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | development: marker for running test only on dev env. 4 | production: marker for run test on prod. 5 | 6 | # Используя конструкцию выше, вы можете создавать свои маркеры для тестирования. После этого достаточно лишь 7 | # пометить им ваш автотест и запустить. Очень удобно для маркировки тестового скоупа для разных окружениях. 8 | # 9 | # Using construction above we can create personal markers for testing. After it you need to mark some tests 10 | # using @pytest.mark.development and run it for development scope. 11 | 12 | addopts= -s -v --env=prod 13 | # Этот параметр помогает автоматически добавлять какие-то ключи при запуске тестов, что-то каждый раз их не вводить. 14 | # 15 | # The parameter automatically adds keys into pytest run, it gives to you possibility to forget about typing them each 16 | # time when you would like to run your tests. 17 | 18 | -------------------------------------------------------------------------------- /examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | Пример объекта на котором вы можете потренироваться, используя pydantic схемы. 3 | 4 | Example of object for training with pydantic schemas. 5 | """ 6 | 7 | computer = { 8 | "id": 21, 9 | "status": "ACTIVE", 10 | "activated_at": "2013-06-01", 11 | "expiration_at": "2040-06-01", 12 | "host_v4": "91.192.222.17", 13 | "host_v6": "2001:0db8:85a3:0000:0000:8a2e:0370:7334", 14 | "detailed_info": { 15 | "physical": { 16 | "color": 'green', 17 | "photo": 'https://images.unsplash.com/photo-1587831990711-23ca6441447b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MXx8ZGVza3RvcCUyMGNvbXB1dGVyfGVufDB8fDB8fA%3D%3D&w=1000&q=80', 18 | "uuid": "73860f46-5606-4912-95d3-4abaa6e1fd2c" 19 | }, 20 | "owners": [{ 21 | "name": "Stephan Nollan", 22 | "card_number": "4000000000000002", 23 | "email": "shtephan.nollan@gmail.com", 24 | }] 25 | } 26 | } -------------------------------------------------------------------------------- /src/schemas/user.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, validator 2 | 3 | from src.enums.user_enums import Genders, Statuses, UserErrors 4 | 5 | """ 6 | Пример описания pydantic model с использованием Enum и validator. 7 | 8 | Example of describing pydantic model with using ENUM and validator features. 9 | """ 10 | 11 | 12 | class User(BaseModel): 13 | id: int 14 | name: int 15 | email: str 16 | password: str 17 | gender: Genders 18 | status: Statuses 19 | 20 | @validator('email') 21 | def check_that_dog_presented_in_email_address(cls, email): 22 | """ 23 | Проверяем наше поле email, что в нём присутствует @ и в случае 24 | если она отсутствует, возвращаем ошибку. 25 | 26 | Checking fild email that in the filed contain @ and if it absent returns 27 | error, if not pass. 28 | """ 29 | if '@' in email: 30 | return email 31 | else: 32 | raise ValueError(UserErrors.WRONG_EMAIL.value) 33 | -------------------------------------------------------------------------------- /src/baseclasses/builder.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class BuilderBaseClass: 4 | """ 5 | Базовый класс для билдера. Вы его можете дополнить ещё другими полезными 6 | методами, сейчас представлен только один. 7 | 8 | Base class for builder. You can add additional useful methods, but for now 9 | it has only one. 10 | """ 11 | def __init__(self): 12 | self.result = {} 13 | 14 | def update_inner_value(self, keys, value): 15 | """ 16 | Этот метод помогает обновить/добавить новое значение в объекте на 17 | указанном вами уровне. 18 | 19 | The method helps us update and add new values into object on specified 20 | level. 21 | """ 22 | if not isinstance(keys, list): 23 | self.result[keys] = value 24 | else: 25 | temp = self.result 26 | for item in keys[:-1]: 27 | if item not in temp.keys(): 28 | temp[item] = {} 29 | temp = temp[item] 30 | temp[keys[-1]] = value 31 | return self 32 | 33 | def build(self): 34 | return self.result 35 | -------------------------------------------------------------------------------- /src/generators/player.py: -------------------------------------------------------------------------------- 1 | from src.enums.user_enums import Statuses 2 | 3 | from src.baseclasses.builder import BuilderBaseClass 4 | from src.generators.player_localization import PlayerLocalization 5 | 6 | """ 7 | Не совсем простенький генератор Player. 8 | 9 | Uncommon generator for Player ^_^. 10 | """ 11 | 12 | 13 | class Player(BuilderBaseClass): 14 | 15 | def __init__(self): 16 | super().__init__() 17 | self.reset() 18 | 19 | def set_status(self, status=Statuses.ACTIVE.value): 20 | self.result['account_status'] = status 21 | return self 22 | 23 | def set_balance(self, balance=0): 24 | self.result['balance'] = balance 25 | return self 26 | 27 | def set_avatar(self, avatar="https://google.com/"): 28 | self.result['avatar'] = avatar 29 | 30 | def reset(self): 31 | self.set_status() 32 | self.set_avatar() 33 | self.set_balance() 34 | self.result["localize"] = { 35 | "en": PlayerLocalization('en_US').build(), 36 | "ru": PlayerLocalization('ru_RU').build() 37 | } 38 | return self 39 | 40 | 41 | -------------------------------------------------------------------------------- /db.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import sessionmaker 2 | 3 | from sqlalchemy import create_engine 4 | 5 | from sqlalchemy.ext.declarative import declarative_base 6 | 7 | from configuration import CONNECTION_ROW 8 | 9 | """ 10 | Создаём модель для того, чтобы потом использовать её для описания наших таблиц. 11 | 12 | Here we creates model. It needs for future using and table describing. 13 | """ 14 | Model = declarative_base(name='Model') 15 | 16 | 17 | """ 18 | Создаём коннекшен к нашей базе данных, передавая ему креденшеналы и другую 19 | информацию для этого. Пример коннекшен строки вы можете найти в файле 20 | configuration.py. 21 | 22 | In next row we creates connection to our database. It receives credential and 23 | another info about connection. More info and full connection sting example 24 | you can find in configuration.py. 25 | """ 26 | engine = create_engine( 27 | CONNECTION_ROW 28 | ) 29 | 30 | """ 31 | Создаём экземпляр сессии, которая даст нам возможность каждый раз генерировать 32 | новую сессию. 33 | 34 | We create instanse of session maker, that gives possibility to create fresh 35 | database session. 36 | """ 37 | Session = sessionmaker( 38 | engine, 39 | autoflush=False, 40 | autocommit=False 41 | ) 42 | 43 | -------------------------------------------------------------------------------- /tests/somethig/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture 5 | def get_testing_scenarios(request): 6 | """ 7 | Пример фикстуры, которая на основании полученных данных, возвращает 8 | разные значения. 9 | 10 | Example of fixture returns different data according to received params. 11 | """ 12 | if request.param == 'scenario_1': 13 | return {"name": "John"} 14 | elif request.param == 'scenario_2': 15 | return {"name": "Ann"} 16 | else: 17 | return {"name": "Anton"} 18 | 19 | 20 | @pytest.fixture 21 | def get_magic_method(get_number): 22 | """ 23 | Пример фикстуры которая возвращает метод как объект в автотест и при этом 24 | сохраняет контекст. Обратите внимание на параметры которые принимает 25 | фикстура, это совсем другая фикстура, результат который мы используем в 26 | методе который возвращаем. 27 | 28 | Example of fixture returns method as object in autotest and saves fixture 29 | context. Attention please for fixture params, it calls another one and uses 30 | results of it in returned method. 31 | """ 32 | print(f"Polychili chislo bratik {get_number}") 33 | def _wrapped(additional_number): 34 | return additional_number + get_number 35 | return _wrapped 36 | -------------------------------------------------------------------------------- /src/enums/user_enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from src.baseclasses.pyenum import PyEnum 4 | 5 | """ 6 | Один из тысяч файлов ENUM которые будут у вас в проекте. 7 | Нужны они для того, чтобы упростить поддержку вашего проекта и упростить 8 | фикс самых простых изменений, которые могли бы занять у вас много времени. 9 | К примеру, если изменится значение какого-то параметра, то вам не нужно будет 10 | бегать по всему проекту, достаточно будет изменить только здесь и всё. 11 | 12 | It is one of billion ENUM files in your project. 13 | They will help you in your project support and error fixing. 14 | """ 15 | 16 | 17 | class Genders(Enum): 18 | """ 19 | Класс для хранения пола пользователя. 20 | 21 | Example of gender enum. 22 | """ 23 | female = "female" 24 | male = "male" 25 | 26 | 27 | class Statuses(PyEnum): 28 | """ 29 | Вариант хранения всех возможных статусов пользователя. 30 | 31 | Example of status enums. 32 | """ 33 | ACTIVE = "ACTIVE" 34 | BANNED = "BANNED" 35 | DELETED = "DELETED" 36 | INACTIVE = "INACTIVE" 37 | MERGED = "MERGED" 38 | 39 | 40 | class UserErrors(Enum): 41 | """ 42 | Enum с кастомными ошибками для какого-то конкретной сущности или 43 | тестируемого ендпоинта. 44 | 45 | Enum with custom errors, for some difficult cases or testing endpoint. 46 | """ 47 | WRONG_EMAIL = "Email doesn't contain @" 48 | -------------------------------------------------------------------------------- /src/generators/player_localization.py: -------------------------------------------------------------------------------- 1 | from faker import Faker 2 | 3 | """ 4 | Пример описания билдера для локализации. 5 | 6 | Example of declaration for localization builder. 7 | """ 8 | 9 | 10 | class PlayerLocalization: 11 | 12 | def __init__(self, lang): 13 | """ 14 | В зависимости от того, какой язык будет передан в этот билдер, на таком 15 | языке и будет работать наш фейкер. Дальше, дело за малым, объект будет 16 | наполнен точно так же, как и другие подобные объекты, только каждый на 17 | своём языке. 18 | 19 | Example of customizing low level builder. According to received lang, 20 | values in builder will be populated on the set language without any 21 | changes in logic of populating. 22 | """ 23 | self.fake = Faker(lang) 24 | self.result = { 25 | "nickname": self.fake.first_name() 26 | } 27 | 28 | def set_number(self, number=11): 29 | """ 30 | Добавляет в результат ключ number, для которого будет использовано 31 | переданное значение, если же такое отсутствует, то используем значение 32 | 11 по-умолчанию. 33 | 34 | Method adds into result key "number" with value that has been received 35 | from user, if not, we will set default that equal to 11. 36 | """ 37 | self.result['number'] = number 38 | return self 39 | 40 | def build(self): 41 | """ 42 | Возвращает наш обьект в виде JSON. 43 | 44 | Returns object as JSON. 45 | """ 46 | return self.result 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Указываем с какого имеджа нужно собрать контейнер, наш гарнир :) 2 | FROM python:3.8-alpine 3 | 4 | # Переменные окружения. Помните, что ARG доступен только когда мы собираем наш контейнер 5 | # когда мы будем его запускать, то доступ к этой переменной получить уже не сможем. 6 | ARG run_env=development 7 | ENV env $run_env 8 | 9 | # С помощью этих штук, вы можете оставить какую-то информацию о себе 10 | LABEL "channel"="SolveMe" 11 | LABEL "creator"="SolveMe community" 12 | 13 | # Указываем директорию в которой мы будем работать внутри докера 14 | WORKDIR ./usr/lessons 15 | 16 | # Создаём вольюм, для того чтобы иметь возможность получить данные после того, как контейнер закончит свою работу 17 | VOLUME /allureResults 18 | 19 | # Этой командой обновляем наш базовый образ 20 | RUN apk update && apk upgrade && apk add bash 21 | 22 | # Копируем отдельно наш файл с зависимостями 23 | COPY requirements.txt . 24 | 25 | # Инстайлим наши зависимости внутри контейнера 26 | RUN pip3 install -r requirements.txt 27 | 28 | # Копируем наши файлики внутрь контейнера 29 | COPY . . 30 | 31 | # Ну и наконец-то запускаем наши тесты 32 | CMD pytest -m "$env" -s -v tests/* --alluredir=allureResults 33 | 34 | 35 | #Эту команду мы запускаем чтобы собрать наш контейнер 36 | #docker build --build-arg env=development -t automation-tests . 37 | 38 | #Эта команда нужна чтобы запустить наш созданый контейнер 39 | #docker run automation-tests 40 | 41 | #Эти 2 команды нам нужны чтобы скопировать данные из контейнера и чтобы сгенерировать из результата репорт 42 | #docker cp $(docker ps -a -q | head -1):/usr/lessons/allureResults . 43 | #allure serve allureResults/ 44 | #Две команды ниже, помогут вам в эксперементах, чтобы после них почистить свой компьютер 45 | #docker rm $(docker ps -a -q) 46 | #docker kill $(docker ps -q) -------------------------------------------------------------------------------- /src/schemas/computer.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, EmailStr, validator 2 | from pydantic.types import PastDate, FutureDate, List, PaymentCardNumber 3 | from pydantic.networks import IPv4Address, IPv6Address 4 | 5 | from src.schemas.physical import Physical 6 | 7 | """ 8 | Именно в этом файле можно поиграться с уже готовой моделью и примером 9 | тестового объекта для неё (Human). 10 | 11 | That file gives to you possibility to play with ready to use model (Human). 12 | """ 13 | 14 | from src.enums.user_enums import Statuses 15 | 16 | 17 | class Owners(BaseModel): 18 | name: str 19 | card_number: PaymentCardNumber 20 | email: EmailStr 21 | 22 | 23 | class DetailedInfo(BaseModel): 24 | physical: Physical 25 | owners: List[Owners] 26 | 27 | 28 | class Computer(BaseModel): 29 | id: int 30 | status: Statuses 31 | activated_at: PastDate 32 | expiration_at: FutureDate 33 | host_v4: IPv4Address 34 | host_v6: IPv6Address 35 | detailed_info: DetailedInfo 36 | 37 | 38 | class Human(BaseModel): 39 | name: str 40 | last_name: str 41 | surname: str = None 42 | is_hide: bool 43 | 44 | @validator('is_hide') 45 | def validate_surname_showing(cls, hide_value, values): 46 | """ 47 | Пример валидатора, который используется для проверки значения в поле 48 | is_hide. 49 | 50 | Example of validator that we use for checking is_hide field. 51 | """ 52 | if hide_value is False and values.get('surname') is None: 53 | raise ValueError('Surname should be presented') 54 | return hide_value 55 | 56 | 57 | human = Human.parse_obj({ 58 | "name": "Andrii", 59 | "last_name": "Shevchenko", 60 | "is_hide": True 61 | }) 62 | 63 | 64 | class Inventory(BaseModel): 65 | sold: int 66 | string: int 67 | unavailable: int 68 | pending: int 69 | available: int 70 | not_available: int 71 | status01: int 72 | status: int 73 | -------------------------------------------------------------------------------- /tests/somethig/experiments.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | class Letter: 5 | 6 | def __init__(self, letter, position): 7 | self.letter = letter 8 | self.position = position 9 | 10 | def __str__(self): 11 | """ 12 | Магический метод который отвечает за строчное отображение инстансов 13 | нашего класса. 14 | 15 | Magic method that we described for params displaying in test name. 16 | """ 17 | return f"Letter {self.letter}, Position {self.position}" 18 | 19 | 20 | def get_cases(): 21 | return [ 22 | Letter('a', 1), 23 | Letter('b', 2) 24 | ] 25 | 26 | 27 | @pytest.mark.parametrize("my_value", get_cases(), ids=str) 28 | def test_my_magic_method(my_value): 29 | """ 30 | Пример использования ids=str, для того, чтобы отображать конкретные данные, 31 | которые мы передали нашему тесту. 32 | 33 | Example of using ids=str, that needs to concreate displaying of data that we 34 | passed into autotest. 35 | """ 36 | print(my_value) 37 | 38 | 39 | @pytest.mark.parametrize("get_testing_scenarios", ['scenario_1'], indirect=True) 40 | def test_data_indirect(get_testing_scenarios): 41 | """ 42 | Пример использования фикстуры с indirect параметром. В этом кейсе мы 43 | вызываем фикстуру напрямую и передаём ей параметры, а она же, возвращает 44 | нам какие-то данные основываясь на них. 45 | 46 | Example of using fixture with indirect parameter. In the case we call 47 | fixture with params, so it returns some data according to the params 48 | """ 49 | print(get_testing_scenarios) 50 | 51 | 52 | def test_magic_method(get_magic_method): 53 | """ 54 | Пример использования метода, который мы получили из фикстуры как объект. 55 | 56 | Example of using method that we received from fixture as object. 57 | """ 58 | print(get_magic_method(1)) 59 | 60 | 61 | def test_option(getting_env): 62 | """ 63 | В этом примере мы получаем значение кастомного параметра. 64 | 65 | In the test we get custom params from console during pytest run. 66 | """ 67 | print(getting_env) 68 | -------------------------------------------------------------------------------- /tests/users/test_users.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.baseclasses.response import Response 4 | from src.schemas.user import User 5 | from src.schemas.computer import Computer 6 | 7 | from examples import computer 8 | 9 | 10 | def test_getting_users_list(get_users, make_number): 11 | """ 12 | Пример использования фикстуры которая отправляет запрос и возвращает 13 | респонс. Далее мы просто обрабатываем его с помощью нашего Response class 14 | применяя все доступные валидации. 15 | 16 | Example of using fixture that requesting server and returns raw response 17 | object. After it we put that data into our response class and accept all 18 | possible validation methods. 19 | """ 20 | Response(get_users).assert_status_code(200).validate(User) 21 | 22 | 23 | @pytest.mark.development 24 | @pytest.mark.production 25 | @pytest.mark.skip('[ISSUE-23414] Issue with network connection') 26 | def test_another(): 27 | """ 28 | Обычный тест, но не совсем. Обратите внимание на декораторы к нему. 29 | Мы скипаем его с определённым сообщением, а так же помечаем с каким скоупом 30 | его выполнять. 31 | 32 | It is just common test. Please check decorators of the test. Here is you 33 | can find decorator for skip test with some message and useful tags for 34 | case when you need to run some scope of tests. 35 | """ 36 | assert 1 == 2 37 | 38 | 39 | @pytest.mark.development 40 | @pytest.mark.parametrize('first_value, second_value, result', [ 41 | (1, 2, 3), 42 | (-1, -2, -3), 43 | (-1, 2, 1), 44 | ('b', -2, None), 45 | ('b', 'b', None) 46 | ]) 47 | def test_calculator(first_value, second_value, result, calculate): 48 | """ 49 | Вариант параметризации нашего теста, с несколькими параметрами за один 50 | раз. 51 | 52 | Example of parametrization, when during one iteration should be passed 53 | more than one value. 54 | """ 55 | assert calculate(first_value, second_value) == result 56 | 57 | 58 | def test_pydantic_object(): 59 | """ 60 | Пример того, как после инициализации pydantic объекта, можно получить 61 | доступ к любому из его параметров. 62 | 63 | Example for case, when after initialization your JSON as a pydantic object 64 | you can get access to all parameters. 65 | """ 66 | comp = Computer.parse_obj(computer) 67 | print(comp.detailed_info.physical.color) 68 | -------------------------------------------------------------------------------- /tables.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Boolean, Column, Integer, String 2 | 3 | from db import Model 4 | 5 | 6 | class Films(Model): 7 | """ 8 | Класс, который описывает таблицу в базе данных, наследуемся от класса Model. 9 | 10 | The class that describes our table in database. 11 | """ 12 | 13 | """ 14 | __tablename__ 15 | Обязательное к заполнению поле. Значение должно точно соответствовать 16 | названию таблички в базе данных, чтобы sqlalchemy понимала, с какой именно 17 | табличкой она должна взаимодействовать. 18 | 19 | __tablename__ 20 | Required parameter that should be filled. The value should be equal to 21 | table name in database. It needs for sqlalchemy, because sqlalchemy doesn't 22 | have any another ways to detect and match needed table. 23 | """ 24 | __tablename__ = 'films' 25 | 26 | """ 27 | Описание полей в табличке. Вам не обязательно указывать все поля которые 28 | там присутствуют, достаточно только тех, которые вы планируете использовать. 29 | 30 | !!! Все поля нужно указать только в том случае, если вы планируете создавать 31 | новые записи в базе данных, тогда нужно указать либо все, либо только 32 | обязательные к заполнению. 33 | 34 | ::primary_key -> Хотя бы одно поле в табличке которую вы описали, 35 | обязательно должно иметь такой параметр. 36 | ::index -> Не обязательное к заполнению значение, но ходят слухи, 37 | что ОРМ использует этот параметр в моменте построения запроса, 38 | если это действительно так, то вы можете ускорить свои тесты. 39 | 40 | Field describe. You don't have to describe all existed fields here that 41 | presented in database, just paste only needed for tests. 42 | 43 | !!! You have to describe all required field in case when u going to add 44 | some data into database. 45 | 46 | ::primary_key -> One field should be with true flag. 47 | ::index -> It isn't required field, but information from some sources says, 48 | that sqlalchemy pick fields with this parameter firstly during query 49 | building for better optimization. 50 | """ 51 | film_id = Column(Integer, primary_key=True) 52 | status = Column(String, index=True) 53 | title = Column(String) 54 | is_premiere = Column(Boolean) 55 | 56 | 57 | class ItemType(Model): 58 | 59 | __tablename__ = 'item_type' 60 | 61 | item_id = Column(Integer, primary_key=True) 62 | item_type = Column(String) 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | allure-results 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 98 | __pypackages__/ 99 | 100 | # Celery stuff 101 | celerybeat-schedule 102 | celerybeat.pid 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | .dmypy.json 129 | dmypy.json 130 | 131 | # Pyre type checker 132 | .pyre/ 133 | 134 | # pytype static type analyzer 135 | .pytype/ 136 | 137 | # Cython debug symbols 138 | cython_debug/ 139 | .idea 140 | 141 | 142 | # My files 143 | allureress/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pytest lessons 2 | 3 | ## EN Version 4 | 5 | ### Short preview 6 | 7 | Hello my dear friend. The project has been created based on Youtube lessons. 8 | Each symbol of code has been written during video recording and my monolog ^_^ 9 | 10 | ## Youtube 11 | https://www.youtube.com/c/SolveMeChannel 12 | 13 | ## A little about of the project idea: 14 | I hope, all things that you can find in the project will help you on your way to python automation 15 | and testing with pytest. I tried to write comments for each method and test for you, it will fix time to 16 | understand some cases without additional investigation. 17 | 18 | ## Write api automation tests eazy 19 | Check the automation framework PyCamel based on pytest and pydantic that will save a lot of your code and nerves 20 | 21 | https://github.com/canyoupleasecreateanaccount/pycamel 22 | 23 | 24 | ## Practice 25 | It is ready to use backend API where you can practice with automation case writing. 26 | 27 | ```https://send-request.me/``` 28 | 29 | ## Contact us 30 | Email: ``` solveme.solutions@gmail.com ``` 31 | 32 | Telegram: ```https://t.me/automation_testing_with_solveme``` 33 | 34 | ### Donation 35 | For people, who would like to support us. God bless U ^_^ 36 | 37 | #### BSC20 38 | ``0x3EC81929e06950322d5125d8e6CA834F3d9B21f8`` 39 | 40 | DOGE | BNB | CAKE | ADA | BUSD | TRX | MATIC | AVAX | ATOM | DIA | DOT 41 | 42 | ## RU Version 43 | 44 | ### Вступление 45 | Привет, дорогой друг, этот проект был создан на основе курсов по Pytest. 46 | Каждый кусочек кода написан и оговорен в видео на ютуб канале. 47 | 48 | ### Ссылка на Youtube канал: 49 | https://www.youtube.com/c/SolveMeChannel 50 | 51 | ### Немного об идее: 52 | Надеюсь, всё добро, которое вы сможете найти в этом проекте, станет для вас 53 | небольшой подсказкой и первой ступенью на пути к освоению автоматизации на Python + Pytest. 54 | Я постарался написать небольшие комментарии к каждому кусочку кода, чтобы вам было проще понять, 55 | не особо вдаваясь в подробности и не тратя много времени на разбор. 56 | 57 | ## Пишем интеграционные тесты легко и просто 58 | Взгляните на PyCamel, который поможет сохранить ваши нервы и тонны кода 59 | 60 | https://github.com/canyoupleasecreateanaccount/pycamel 61 | 62 | ## Практика 63 | Если хотите получить практические знания и проверить, насколько вы готовы к реальной автоматизации, 64 | то этот сайт поможет вам. Это уже готовое Backend API с багами и самыми популярными случаями. 65 | 66 | ```https://send-request.me/``` 67 | 68 | ## Связь с нами 69 | Email: ``` solveme.solutions@gmail.com ``` 70 | 71 | Telegram: ```https://t.me/automation_testing_with_solveme``` 72 | 73 | ### Donation 74 | Для тех, кто хочет нас поддержать :) 75 | 76 | #### BSC20 77 | ``0x3EC81929e06950322d5125d8e6CA834F3d9B21f8`` 78 | 79 | DOGE | BNB | CAKE | ADA | BUSD | TRX | MATIC | AVAX | ATOM | DIA | DOT 80 | 81 | -------------------------------------------------------------------------------- /src/baseclasses/response.py: -------------------------------------------------------------------------------- 1 | from pydantic.error_wrappers import ValidationError 2 | 3 | 4 | class Response: 5 | 6 | """ 7 | Полезный класс, который помогает нам экономить тонны кода в процессе 8 | валидации данных. На вход он принимает объект респонса и разбирает его. 9 | Вы можете добавить кучу различных методов в этом классе, которые нужны 10 | вам в работе с данными после их получения. 11 | 12 | It's useful class that helps to save a lot of code during validatio 13 | process in our tests. It receives response object and gets from it all 14 | values that should be validated. You can add additional methods into the 15 | Class if it needs for your project testing. 16 | """ 17 | 18 | def __init__(self, response): 19 | self.response = response 20 | self.response_json = response.json() 21 | self.response_status = response.status_code 22 | self.parsed_object = None 23 | 24 | def validate(self, schema): 25 | try: 26 | if isinstance(self.response_json, list): 27 | for item in self.response_json: 28 | parsed_object = schema.parse_obj(item) 29 | self.parsed_object = parsed_object 30 | else: 31 | schema.parse_obj(self.response_json) 32 | except ValidationError: 33 | raise AssertionError( 34 | "Could not map received object to pydantic schema" 35 | ) 36 | 37 | def assert_status_code(self, status_code): 38 | """ 39 | Метод для валидации статус кода. Из объекта респонса, 40 | который мы получили, мы берём статус и сравнимаем с тем, который 41 | нам был передан как параметр. 42 | 43 | Method for status code validation. It compares value from response 44 | object and compare it with received value from method params. 45 | """ 46 | if isinstance(status_code, list): 47 | assert self.response_status in status_code, self 48 | else: 49 | assert self.response_status == status_code, self 50 | return self 51 | 52 | def get_parsed_object(self): 53 | return self.parsed_object 54 | 55 | def __str__(self): 56 | """ 57 | Метод отвечает за строковое представление нашего объекта. Что весьма 58 | удобно, ведь в случае срабатывания валидации, мы получаем полную картину 59 | всего происходящего и все параметры которые нам нужны для определения 60 | ошибки. 61 | 62 | Method for string displaying of class instance. In case when our 63 | validation will be failed, we will get full information about our 64 | object and comparation data, that will help us in fail investigation. 65 | """ 66 | return \ 67 | f"\nStatus code: {self.response_status} \n" \ 68 | f"Requested url: {self.response.url} \n" \ 69 | f"Response body: {self.response_json}" 70 | -------------------------------------------------------------------------------- /tests/somethig/test_something.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pytest 3 | 4 | from src.generators.player_localization import PlayerLocalization 5 | from src.enums.user_enums import Statuses 6 | from src.baseclasses.response import Response 7 | from src.schemas.computer import Inventory 8 | 9 | import tables 10 | 11 | """ 12 | В этом файле рассмотрим примеры работы: 13 | 1. С базой данных 14 | 2. Параметризация ключей и поочерёдное их удаление 15 | 3. Использование генератора в тестах 16 | 4. Кастомная генерацию локализаций 17 | 18 | In the file we will learn: 19 | 1. Working with database 20 | 2. Key parametrize and delete it in object one by one 21 | 3. Using generator in autotests 22 | 4. Generator of custom localization 23 | """ 24 | 25 | 26 | @pytest.mark.parametrize("status", Statuses.list()) 27 | def test_generator_changing(status, get_player_generator): 28 | """ 29 | Играемся с генератором, который был получен с помощью фикстуры. 30 | Вы можете попробовать изменить значение, написать новые методы и посмотреть 31 | как он будет реагировать. 32 | 33 | Playing with generator, that we received from fixture. 34 | Here you can change values, write some new useful methods and check how 35 | will it work. 36 | """ 37 | print(get_player_generator.set_status(status).build()) 38 | 39 | 40 | @pytest.mark.parametrize("delete_key", [ 41 | "account_status", 42 | "balance", 43 | "localize", 44 | "avatar" 45 | ]) 46 | def test_deleting_keys_in_object(delete_key, get_player_generator): 47 | """ 48 | Пример того, как мы в определённом порядке удаляем каждое поле в объекте, 49 | который нам вернул генератор. 50 | 51 | Example of case when we delete one by one keys in received object. 52 | """ 53 | object_to_send = get_player_generator.build() 54 | del object_to_send[delete_key] 55 | # print(object_to_send) 56 | 57 | 58 | @pytest.mark.parametrize("localizations, loc", [ 59 | ("fr", "fr_FR") 60 | ]) 61 | def test_updating_localization_in_generator( 62 | get_player_generator, 63 | localizations, 64 | loc 65 | ): 66 | """ 67 | В этом примере мы получаем 2 генератора, один базовый и один, который ниже 68 | уровнем. Когда мы получили их, изменяем в генераторе локализацию, создаём 69 | экземпляр и обновляем им наш главный объект. 70 | 71 | In the test we receive two generators, first is main and second is included 72 | into first. We change localization in generator and update main using 73 | instance of second. 74 | """ 75 | object_to_send = get_player_generator.update_inner_value( 76 | ['localize', localizations], 77 | PlayerLocalization(loc).set_number(15).build() 78 | ).build() 79 | print(object_to_send) 80 | 81 | 82 | def test_human(): 83 | """ 84 | Пример отправки запроса, получения данных и использования Response class 85 | для работы с валидацией данных. 86 | 87 | Example of requesting data and push response into our Response class for 88 | future validation. 89 | """ 90 | r = requests.get('https://petstore.swagger.io/v2/store/inventory') 91 | response = Response(r) 92 | response.validate(Inventory) 93 | 94 | 95 | def test_get_data_films(get_db_session): 96 | """ 97 | Получение сессии базы данных и использование её для того, чтобы достать 98 | нужную информацию. 99 | 100 | Getting database session. In the test, we get info from DB using the 101 | session. 102 | """ 103 | data = get_db_session.query(tables.Films).first() 104 | print(data.film_id) 105 | 106 | 107 | def test_try_to_delete_something(get_delete_method, get_db_session): 108 | """ 109 | Пример того, как использовать удаление в тесте, когда мы не знаем ID. 110 | Просто получаем фикстуру которая умеет это делать и удаляем. 111 | Если тест упадёт раньше, то до этой строки кода мы не дойдём, так как 112 | удалять нет чего, если же мы дошли, то удалять есть что :). 113 | 114 | Example of case, when we know nothing about id that should be deleted from 115 | DB after test execution. So in that case we just paste our method at the end 116 | If our case fails we don't have to delete anything, if not 117 | our code will do what it has to do :) 118 | """ 119 | #some code 120 | get_delete_method(get_db_session, tables.ItemType, ( 121 | tables.ItemType.item_id == 3) 122 | ) 123 | 124 | 125 | def test_try_to_add_testdata( 126 | get_db_session, get_add_method, get_item_type_generator 127 | ): 128 | """ 129 | Добавление в базу тестовых данных в самом тесте, плохой пример но может 130 | пригодится. Лучше используйте фикстуры. 131 | 132 | Adding test data into database in our test. It is very bad example. 133 | Please don't do like this. 134 | """ 135 | item = tables.ItemType(**get_item_type_generator.build()) 136 | get_add_method(get_db_session, item) 137 | print(item.item_id) 138 | 139 | 140 | def test_try_to_add_testdata_and_delete_after_test(generate_item_type): 141 | """ 142 | Пример идеального флоу, когда мы создаём и удаляем после себя данные в базе 143 | тем самым оставляя тест чистым. 144 | PS: Смотрите фикстуру 145 | 146 | Example of case, when we create and delete test data in our fixture. 147 | PS: Check fixtures. 148 | """ 149 | print(generate_item_type.item_id) 150 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from random import randrange 4 | 5 | import tables 6 | 7 | from db import Session 8 | 9 | from src.generators.player import Player 10 | from src.generators.item_type_generator import ItemsTypeBuilder 11 | 12 | 13 | @pytest.fixture 14 | def get_player_generator(): 15 | """ 16 | Пример фикстуры для инициализации объекта генератора и передачу его в 17 | тест. 18 | 19 | Fixture that initialize generator object and returns it into autotest. 20 | """ 21 | return Player() 22 | 23 | 24 | @pytest.fixture 25 | def get_item_type_generator(): 26 | """ 27 | Пример фикстуры для инициализации объекта генератора и передачу его в 28 | тест. 29 | 30 | Fixture that initialize generator object and returns it into autotest. 31 | """ 32 | return ItemsTypeBuilder() 33 | 34 | 35 | @pytest.fixture 36 | def get_number(): 37 | """ 38 | Просто метод для генерации рандомного числа :) 39 | 40 | Method that generates random number :) 41 | """ 42 | return randrange(1, 1000, 5) 43 | 44 | 45 | def _calculate(a, b): 46 | """ 47 | Функция которая выполняет какую-то логику. При этом, ниже фикстура, которая 48 | отдаёт его в тест как объект, чтобы можно было применить там как 49 | именно как функцию. 50 | 51 | It is function that does some logic. Please check fixture below, that 52 | returns that function as object into autotest where you can call it as 53 | a common function and pass some param into it. 54 | """ 55 | if isinstance(a, int) and isinstance(b, int): 56 | return a + b 57 | else: 58 | return None 59 | 60 | 61 | @pytest.fixture 62 | def calculate(): 63 | """ 64 | Передача функции в тест используя фикстуру. 65 | 66 | Return function as an object into autotest. 67 | """ 68 | return _calculate 69 | 70 | 71 | @pytest.fixture 72 | def make_number(): 73 | """ 74 | Делаем какую-то логику, передаём управление тесту используя конструкцию 75 | yield и уже после того, как тест закончился, выполняем вторую часть кода. 76 | 77 | In that case we do some logic, than return number to autotest, wait till 78 | test has been passed and after it again do some logic. 79 | """ 80 | print("I'm getting number") 81 | number = randrange(1, 1000, 5) 82 | yield number 83 | print(f"Number at home {number}") 84 | 85 | 86 | @pytest.fixture 87 | def get_db_session(): 88 | """ 89 | Создание сессии для работы с базой данных. 90 | Пожалуйста, обратите внимание, что мы в любом случае закрываем нашу сессию. 91 | 92 | Creating of database session and return it into our autotest. 93 | Please check, that in any case, we close our db session. 94 | """ 95 | session = Session() 96 | try: 97 | yield session 98 | finally: 99 | session.close() 100 | 101 | 102 | def delete_test_data(session, table, filter_data): 103 | """ 104 | Функция для удаления данных из базы. 105 | 106 | Example of function for delete test data from database. 107 | """ 108 | session.query(table).filter(filter_data).delete() 109 | session.commit() 110 | 111 | 112 | def add_method(session, item): 113 | """ 114 | Функция для добавления данных в базу. 115 | 116 | Example of function for add test data from database. 117 | """ 118 | session.add(item) 119 | session.commit() 120 | 121 | 122 | @pytest.fixture 123 | def generate_item_type( 124 | get_db_session, 125 | get_item_type_generator, 126 | get_add_method, 127 | get_delete_method 128 | ): 129 | """ 130 | Пример фикстуры которая использует другие фикстуры. С помощью этого примера 131 | мы можем подготовить себе тестовые данные в базе, передать их в тест, а 132 | уже после выполнения удалить. 133 | 134 | Example of fixture that uses another fixtures inside itself. 135 | Using that example we can create test data, return it into autotest and 136 | after test execution delete all from database. 137 | """ 138 | item = tables.ItemType(**get_item_type_generator.build()) 139 | get_add_method(get_db_session, item) 140 | yield item 141 | get_delete_method( 142 | get_db_session, 143 | tables.ItemType, 144 | (tables.ItemType.item_id == item.item_id) 145 | ) 146 | 147 | 148 | @pytest.fixture 149 | def get_add_method(): 150 | """ 151 | Пример фикстуры которая передаёт функцию для добавления данных в базу 152 | как объект в тест. 153 | 154 | Example of fixture, that returns add method as object into our tests. 155 | """ 156 | return add_method 157 | 158 | 159 | @pytest.fixture 160 | def get_delete_method(): 161 | """ 162 | Пример фикстуры которая передаёт функцию для удаления данных в базу 163 | как объект в тест. 164 | 165 | Example of fixture, that returns delete method as object into our tests. 166 | """ 167 | return delete_test_data 168 | 169 | 170 | def pytest_addoption(parser): 171 | """ 172 | Этот метод добавляет к стандартным ключам запуска, кастомные ключи, которые 173 | вы можете указать ниже. 174 | 175 | The method gives possibility to add custom console keys to your project. 176 | """ 177 | parser.addoption( 178 | '--env', 179 | default='development', 180 | help='It is env variable where our tests will be run. Possible values:' 181 | 'prod, development(default), qa' 182 | ) 183 | 184 | 185 | @pytest.fixture(autouse=True) 186 | def getting_env(request): 187 | """ 188 | Фикстура парсит значение ключа и возвращает его в тест. 189 | 190 | The fixture parses env variable and return it into test case. 191 | """ 192 | env = request.config.getoption('--env') 193 | yield env 194 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | --------------------------------------------------------------------------------