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

Useful wrappers and helpers #67

Merged
merged 12 commits into from
Sep 18, 2023
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[run]
relative_files = True
omit =
sekoia_automation/scripts/new_module/template/*
sekoia_automation/scripts/new_module/template/*
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,12 @@ jobs:

- name: Install dependencies
id: install-dependencies
run: poetry install
run: poetry install --extras=all

- name: Execute Python tests
id: execute-tests
run: |
poetry run python -m pytest --junit-xml=junit.xml --cov-report term --cov-report xml:coverage.xml --cov=sekoia_automation tests
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you remove the directory tests in the command line?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed it because of incorrect configuration between local execution and gitlab ci.
For some reason it does not resolve paths with tests on local env

poetry run python -m pytest --junit-xml=junit.xml --cov-report term --cov-report xml:coverage.xml --cov=sekoia_automation

- name: Upload Test Results
if: always()
Expand Down
33 changes: 18 additions & 15 deletions .pre-commit-config.yaml
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Darkheir Do you agree to this change?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine by me if it works. It will allow to run checks with the same version we will have in the CI pipeline.

Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version.
rev: 'v0.0.285'
- repo: local
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
- id: black
name: Format with Black
entry: poetry run black
language: system
types: [python]

- repo: https://github.com/ambv/black
rev: 23.3.0
hooks:
- id: black
language_version: python3
- id: ruff
name: Format with Ruff
entry: poetry run ruff
language: system
types: [ python ]
args: [ --fix, --exit-non-zero-on-fix, . ]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: 'v1.3.0' # Use the sha / tag you want to point at
hooks:
- id: mypy
args: [--install-types, --non-interactive]
Darkheir marked this conversation as resolved.
Show resolved Hide resolved
exclude: sekoia_automation/scripts/new_module/template/
name: Validate types with MyPy
entry: poetry run mypy
language: system
types: [ python ]
pass_filenames: false
args: [ . ]
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add new wrappers to work with aio libraries such as aiohttp, aiobotocore, etc.
- New AsyncConnector that contains async implementation of push events

## [1.4.1] - 2023-09-13

### Added
Expand Down
958 changes: 923 additions & 35 deletions poetry.lock

Large diffs are not rendered by default.

58 changes: 56 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,55 @@ PyYAML = "^6.0"
Jinja2 = "^3.0.3"
black = "*" # To format files in cli tools
prometheus-client = "^0.16.0"
aiohttp = { version = "^3.8.4", optional = true }
aiolimiter = { version = "^1.1.0", optional = true }
aiobotocore = { version = "^2.5.2", optional = true }
aiofiles = { version = "^23.1.0", optional = true }
aiocsv = { version = "^1.2.4", optional = true }
loguru = { version = "^0.7.0", optional = true }

[tool.poetry.group.dev.dependencies]
unittest-xml-reporting = "^3"
pylint = "*"
pytest = "*"
pytest-cov = "*"
pytest-asyncio = "*"
pytest-xdist = "*"
pytest-env = "*"
requests-mock = "^1.9"
faker = "^19.0.0"
aioresponses = "^0.7.4"
types-aiofiles = "^23.1.0.4"
types-requests = "^2.31.0.1"
types-pyyaml = "^6.0.12.10"
types-python-slugify = "^8.0.0.2"
pre-commit = "^3.3.3"

[tool.poetry.extras]
all = [
"aiohttp",
"aiolimiter",
"aiobotocore",
"aiofiles",
"aiocsv",
"loguru",
]
async-aws = [
"aiobotocore",
]
async-http = [
"aiohttp",
"aiolimiter",
"aiofiles",
]
async-files = [
"aiofiles",
"aiocsv"
]
logging = [
"loguru"
]


[tool.poetry.group.lint.dependencies]
ruff = "*"
Expand All @@ -63,7 +105,15 @@ force-exclude = "tests/expectations/sample_module/main.py|sekoia_automation/scri

[tool.pytest.ini_options]
minversion = "6.0"
addopts = "-ra"
addopts = '''
--asyncio-mode=auto
--cache-clear
--cov=sekoia_automation
--cov-report=html
--cov-report=term-missing:skip-covered
--cov-fail-under=90
-ra
'''
testpaths = [
"tests",
]
Expand All @@ -72,6 +122,7 @@ testpaths = [
select = ["A", "ARG", "E", "F", "I", "N", "RUF", "UP", "W"]
exclude = [
"tests/expectations/sample_module/main.py",
"tests/aio/",
"sekoia_automation/scripts/new_module/template/"
]

Expand All @@ -82,5 +133,8 @@ exclude = [
python_version = "3.11"
ignore_missing_imports = true
show_column_numbers = true
exclude = "sekoia_automation/scripts/new_module/template/"
exclude = [
"sekoia_automation/scripts/new_module/template/",
"tests/"
]
disable_error_code = "annotation-unchecked"
1 change: 1 addition & 0 deletions sekoia_automation/aio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Package contains all utilities and wrappers for asynchronous mode."""
146 changes: 146 additions & 0 deletions sekoia_automation/aio/connector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Contains connector with async version."""
from abc import ABC
from asyncio import AbstractEventLoop, get_event_loop
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from datetime import datetime
from urllib.parse import urljoin

from aiohttp import ClientSession
from aiolimiter import AsyncLimiter

from sekoia_automation.connector import Connector, DefaultConnectorConfiguration


class AsyncConnector(Connector, ABC):
"""Async version of Connector."""

configuration: DefaultConnectorConfiguration

_event_loop: AbstractEventLoop

_session: ClientSession | None = None
_rate_limiter: AsyncLimiter | None = None

def __init__(self, event_loop: AbstractEventLoop | None = None, *args, **kwargs):
"""
Initialize AsyncConnector.

Optionally accepts event_loop to use, otherwise will use default event loop.

Args:
event_loop: AbstractEventLoop | None
"""
super().__init__(*args, **kwargs)

self._event_loop = event_loop or get_event_loop()

@classmethod
def set_client_session(cls, session: ClientSession) -> None:
"""
Set client session.

Args:
session: ClientSession
"""
cls._session = session

@classmethod
def set_rate_limiter(cls, rate_limiter: AsyncLimiter) -> None:
"""
Set rate limiter.

Args:
rate_limiter:
"""
cls._rate_limiter = rate_limiter

@classmethod
def get_rate_limiter(cls) -> AsyncLimiter:
"""
Get or initialize rate limiter.

Returns:
AsyncLimiter:
"""
if cls._rate_limiter is None:
cls._rate_limiter = AsyncLimiter(1, 1)

return cls._rate_limiter

@classmethod
@asynccontextmanager
async def session(cls) -> AsyncGenerator[ClientSession, None]: # pragma: no cover
"""
Get or initialize client session if it is not initialized yet.

Returns:
ClientSession:
"""
if cls._session is None:
cls._session = ClientSession()

async with cls.get_rate_limiter():
yield cls._session

async def push_data_to_intakes(
self, events: list[str]
) -> list[str]: # pragma: no cover
"""
Custom method to push events to intakes.

Args:
events: list[str]

Returns:
list[str]:
"""
self._last_events_time = datetime.utcnow()
batch_api = urljoin(self.configuration.intake_server, "/batch")

self.log(f"Push {len(events)} events to intakes")

result_ids = []

chunks = self._chunk_events(events, self.configuration.chunk_size)

async with self.session() as session:
for chunk_index, chunk in enumerate(chunks):
self.log(
"Start to push chunk {} with data count {} to intakes".format(
chunk_index,
len(chunk),
)
)

request_body = {
"intake_key": self.configuration.intake_key,
"jsons": chunk,
}

for attempt in self._retry():
with attempt:
async with session.post(
batch_api,
headers={"User-Agent": self._connector_user_agent},
json=request_body,
) as response:
if response.status >= 300:
error = await response.text()
error_message = f"Chunk {chunk_index} error: {error}"
exception = RuntimeError(error_message)

self.log(message=error_message, level="error")
self.log_exception(exception)

raise exception

result = await response.json()

self.log(
f"Successfully pushed chunk {chunk_index} to intakes"
)

result_ids.extend(result.get("event_ids", []))

return result_ids
5 changes: 5 additions & 0 deletions sekoia_automation/aio/helpers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
Package contains all utilities and useful helpers.

NOTE!!!: each package inside requires additional libraries to be installed.
"""
7 changes: 7 additions & 0 deletions sekoia_automation/aio/helpers/aws/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""
Utilities and wrappers to work with aws.

To use this package you need to install additional libraries:

* aiobotocore (https://github.com/aio-libs/aiobotocore)
"""
Loading