Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: base project structure #2

Merged
merged 6 commits into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading