Skip to content

Commit

Permalink
feat: base project structure
Browse files Browse the repository at this point in the history
  • Loading branch information
danfimov committed Sep 17, 2023
2 parents e077f34 + 8e4ec41 commit 7bbca90
Show file tree
Hide file tree
Showing 28 changed files with 1,952 additions and 1 deletion.
26 changes: 26 additions & 0 deletions .github/workflows/code-check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: Checks
on: [pull_request]

jobs:
lint:
runs-on: ubuntu-latest
name: Lint code
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install --upgrade pip
- run: make install
- run: make lint
test:
runs-on: ubuntu-latest
name: Run tests
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- run: pip install --upgrade pip
- run: make install
- run: make test-cov
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@ cython_debug/
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
.idea/
42 changes: 42 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
args := $(wordlist 2, 100, $(MAKECMDGOALS))

APPLICATION_NAME = ya_tracker_client

HELP_FUN = \
%help; while(<>){push@{$$help{$$2//'options'}},[$$1,$$3] \
if/^([\w-_]+)\s*:.*\#\#(?:@(\w+))?\s(.*)$$/}; \
print"$$_:\n", map" $$_->[0]".(" "x(20-length($$_->[0])))."$$_->[1]\n",\
@{$$help{$$_}},"\n" for keys %help; \
CODE = ya_tracker_client
TEST = poetry run python3 -m pytest --verbosity=2 --showlocals --log-level=DEBUG

ifndef args
MESSAGE = "No such command (or you pass two or many targets to ). List of possible commands: make help"
else
MESSAGE = "Done"
endif


help: ##@Help Show this help
@echo -e "Usage: make [target] ...\n"
@perl -e '$(HELP_FUN)' $(MAKEFILE_LIST)

install: ##@Setup Install project requirements
python3 -m pip install poetry
poetry install

test: ##@Testing Test application with pytest
$(TEST)

test-cov: ##@Testing Test application with pytest and create coverage report
$(TEST) --cov=$(APPLICATION_NAME) --cov-report html --cov-fail-under=85

lint: ##@Code Check code with pylint
poetry run python3 -m ruff $(CODE) tests

format: ##@Code Reformat code with ruff and black
poetry run python3 -m ruff $(CODE) tests --fix

clean: ##@Code Clean directory from garbage files
rm -fr *.egg-info dist
27 changes: 27 additions & 0 deletions examples/get_issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
from asyncio import run

from dotenv import load_dotenv

from ya_tracker_client import YaTrackerClient


load_dotenv()
# from registered application at Yandex OAuth - https://oauth.yandex.ru/
API_TOKEN = os.getenv('API_TOKEN')
# from admin panel at Yandex Tracker - https://tracker.yandex.ru/admin/orgs
API_ORGANISATION_ID = os.getenv('API_ORGANISATION_ID')


async def main() -> None:
client = YaTrackerClient(
organisation_id=API_ORGANISATION_ID,
oauth_token=API_TOKEN,
)
issue = await client.get_issue('TRACKER-1')
print(issue)
await client.stop()


if __name__ == '__main__':
run(main())
1,154 changes: 1,154 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

86 changes: 86 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[tool.poetry]
name = "ya_tracker_client"
version = "0.1.0"
description = "Async Yandex Tracker Client"
authors = ["Дмитрий Анфимов <[email protected]>"]
license = "MIT"
readme = "README.md"
packages = [{include = "ya_tracker_client"}]

[[tool.poetry.source]]
name = "default-pypi"
url = "https://pypi.org/simple"
priority = "default"

[[tool.poetry.source]]
name = "yandex-pypi"
url = "https://pypi.yandex-team.ru/simple"
priority = "supplemental"

[tool.poetry.dependencies]
python = "^3.11"
aiohttp = "^3.8.5"
pydantic = "^2.3.0"
certifi = "^2023.7.22"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.2"
coverage = "^7.3.1"
pytest-asyncio = "^0.21.1"
pytest-cov = "^4.1.0"
ruff = "^0.0.287"
greenlet = "^2.0.2"
polyfactory = "^2.8.2"

[tool.poetry.group.examples.dependencies]
python-dotenv = "^1.0.0"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.pytest]
python_files = "test_*.py"
python_classes = ["*Test", "Test*"]
python_functions = "test_*"
testpaths = "tests/"

[tool.coverage.report]
exclude_lines = [
"# pragma: no cover",
"def __repr__",
"def __str__",
"def __unicode__",
]
show_missing = true
skip_empty = true
omit = [
"*/__init__.py",
]

[tool.coverage.html]
directory = "pytest-cov-report"

[tool.coverage.run]
branch = true
concurrency = ['thread', 'greenlet']

[tool.pytest.ini_options]
asyncio_mode = "auto"

[tool.ruff]
line-length = 120
select = ["W", "E", "F", "Q", "B", "I", "N", "ASYNC", "G", "RUF", "COM", "C90"]
ignore = []
target-version = "py311"

[tool.ruff.flake8-quotes]
docstring-quotes = "double"

[tool.ruff.mccabe]
# Unlike Flake8, default to a complexity level of 10.
max-complexity = 10

[tool.ruff.isort]
known-local-folder = ["ya_tacker_client"]
lines-after-imports = 2
Empty file added tests/conftest.py
Empty file.
9 changes: 9 additions & 0 deletions tests/fixtures/issue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from polyfactory.factories.pydantic_factory import ModelFactory
from polyfactory.pytest_plugin import register_fixture

from ya_tracker_client.domain.entities.issue import Issue


@register_fixture
class IssueFactory(ModelFactory[Issue]):
__model__ = Issue
91 changes: 91 additions & 0 deletions tests/test_domain/test_client/test_initialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from http import HTTPStatus
from random import randint
from typing import Any

import pytest

from ya_tracker_client.domain.client import BaseClient
from ya_tracker_client.domain.client.errors import ClientInitTokenError


class ClientForTestInitialization(BaseClient):
auth_token_header_value = ""
organisation_token_name = ""

async def _make_request(
self,
method: str,
url: str,
params: dict[str, Any] | None = None,
data: bytes | None = None,
) -> tuple[int, bytes]:
return HTTPStatus.OK, b"Test response body"

async def stop(self) -> None:
pass

def test_authorization_header(self) -> None:
assert self._headers.get("Authorization") == self.auth_token_header_value

def test_organisation_header(self) -> None:
assert self._headers.get(self.organisation_token_name)


def get_client_for_test_initialization(
organisation_id: str | int,
oauth_token: str | None = None,
iam_token: str | None = None,
auth_token_header_value: str = "",
organisation_token_name: str = "",
) -> ClientForTestInitialization:
client = ClientForTestInitialization(
organisation_id=organisation_id,
oauth_token=oauth_token,
iam_token=iam_token,
)
client.auth_token_header_value = auth_token_header_value
client.organisation_token_name = organisation_token_name
return client


@pytest.mark.parametrize(
"organisation_id, header_name",
(
(randint(1, 1000), "X-Org-Id"),
(str(randint(1, 1000)), "X-Org-Id"),
("test_organisation_id", "X-Cloud-Org-Id"),
),
)
def test_init__when_organisation_id_passed__then_use_specific_header_name(organisation_id, header_name) -> None:
client = get_client_for_test_initialization(
organisation_id=organisation_id,
oauth_token="some_token",
auth_token_header_value="OAuth some_token",
organisation_token_name=header_name,
)
client.test_organisation_header()


def test_init__when_auth_token_not_passed__then_raise_error() -> None:
with pytest.raises(ClientInitTokenError):
get_client_for_test_initialization(
organisation_id=randint(1, 1000),
)


def test_init__when_oauth_token_passed__then_construct_specific_header_value() -> None:
client = get_client_for_test_initialization(
organisation_id=randint(1, 1000),
oauth_token="some_test_token",
auth_token_header_value="OAuth some_test_token",
)
client.test_authorization_header()


def test_init__when_iam_token_passed__then_construct_specific_header_value() -> None:
client = get_client_for_test_initialization(
organisation_id=randint(1, 1000),
iam_token="some_test_token",
auth_token_header_value="Bearer some_test_token",
)
client.test_authorization_header()
92 changes: 92 additions & 0 deletions tests/test_domain/test_client/test_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
from http import HTTPStatus
from logging import getLogger
from random import randint
from typing import TYPE_CHECKING, Any, Type

import pytest

from ya_tracker_client.domain.client import BaseClient
from ya_tracker_client.domain.client.errors import (
ClientAuthError,
ClientError,
ClientObjectConflictError,
ClientObjectNotFoundError,
ClientSufficientRightsError,
)


if TYPE_CHECKING:
pass


logger = getLogger(__name__)


class ClientForTestRequestStatus(BaseClient):
make_request_status_code = 200

async def _make_request(
self,
method: str,
url: str,
params: dict[str, Any] | None = None,
data: bytes | None = None,
) -> tuple[int, bytes]:
return self.make_request_status_code, b"Test response body"

async def stop(self) -> None:
pass


def create_client_for_test_request_status(status_code: int) -> ClientForTestRequestStatus:
client = ClientForTestRequestStatus(
organisation_id=randint(1, 1000),
oauth_token="test_token",
)
client.make_request_status_code = status_code
return client


class TestCheckStatus:
"""
Test with all statuses from documentation: https://cloud.yandex.com/en/docs/tracker/error-codes
"""

@pytest.mark.parametrize(
"status_code",
(
HTTPStatus.OK,
HTTPStatus.CREATED,
HTTPStatus.NO_CONTENT,
),
)
async def test_request__when_status_ok__then_not_raise_error(self, status_code: int) -> None:
client = create_client_for_test_request_status(status_code)
response_body = await client.request("GET", "/test_uri")
assert response_body == b"Test response body"

@pytest.mark.parametrize(
"status_code, error_type",
(
(HTTPStatus.BAD_REQUEST, ClientError),
(HTTPStatus.UNAUTHORIZED, ClientAuthError),
(HTTPStatus.FORBIDDEN, ClientSufficientRightsError),
(HTTPStatus.NOT_FOUND, ClientObjectNotFoundError),
(HTTPStatus.CONFLICT, ClientObjectConflictError),
(HTTPStatus.PRECONDITION_FAILED, ClientObjectConflictError),
(HTTPStatus.UNPROCESSABLE_ENTITY, ClientError),
(HTTPStatus.PRECONDITION_REQUIRED, ClientError),
),
)
async def test_request__when_status_not_ok__then_raise_specific_error(
self,
status_code: int,
error_type: Type[ClientError],
) -> None:
client = create_client_for_test_request_status(status_code)
if error_type == ClientError: # always raise ClientException with context
with pytest.raises(error_type, match="Test response body"):
await client.request("GET", "/test_uri")
else:
with pytest.raises(error_type):
await client.request("GET", "/test_uri")
Loading

0 comments on commit 7bbca90

Please sign in to comment.