Skip to content

Commit

Permalink
add hw2 and tests for hw2
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimmension committed Dec 16, 2023
1 parent 8690ef5 commit 0d5390d
Show file tree
Hide file tree
Showing 16 changed files with 448 additions and 15 deletions.
49 changes: 36 additions & 13 deletions .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
@@ -1,35 +1,58 @@
name: Проверка
name: Main test
on: [push]
jobs:
linter:
name: Линтер
linter_hw1:
name: Linter for HW1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Установка Python
python -m pip install --upgrade pip
pip install flake8==3.9.0 wemake-python-styleguide==0.15.3 bandit==1.7.2
- name: Flake8
run: |
cd hw1
flake8
tests_for_hw1:
name: Tests for hw1
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
pip install pytest==6.2.5
- name: Pytest
run: |
cd hw1
pytest
linter_hw2:
name: Linter for HW2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Python installation
uses: actions/setup-python@v2
with:
python-version: 3.10.6
- name: Установка зависимостей
- name: Dependencies installtion
run: |
python -m pip install --upgrade pip
pip install flake8==3.9.0 wemake-python-styleguide==0.15.3 bandit==1.7.2
- name: Flake8
run: flake8 .
tests:
name: Тесты
run: |
cd hw2
flake8
tests_for_hw2:
name: Tests for hw2
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Установка Python
- name: Python installation
uses: actions/setup-python@v2
with:
python-version: 3.10.6
- name: Установка зависимостей
- name: Dependencies installation
run: |
python -m pip install --upgrade pip
pip install pytest==6.2.5
pip install numpy
- name: Pytest
run: pytest

run: |
cd hw2
pytest
File renamed without changes.
File renamed without changes.
File renamed without changes.
14 changes: 14 additions & 0 deletions hw2/consts_hw2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Provides constants for hw2."""

AGE = 'age'
AGE_MAX = 'age_max'
AGE_MIN = 'age_min'
AGE_AVERAGE = 'age_average'
AGE_MEDIAN = 'age_median'

LAST_LOGIN = 'last_login'
LESS_TWO_DAYS = 'percent_active_users_last_two_days'
LESS_WEEK = 'percent_active_users_last_week'
LESS_MONTH = 'percent_active_users_last_month'
LESS_HALFYEAR = 'percent_active_users_last_halfyear'
MORE_HALFYEAR = 'percent_active_not_users_last_halfyear'
61 changes: 61 additions & 0 deletions hw2/exceptions_hw2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Provides exceptions for hw2."""


class InputFilepathError(Exception):
"""Error that is thrown when the file could not be read."""

def __init__(self, filepath: str) -> None:
"""Initialize the exception.
Args:
filepath (str): path to file.
"""
super().__init__(f'Path {filepath} does not exist.')


class MissingFieldError(Exception):
"""Error that is thrown when one of the users missing a field."""

def __init__(self, fld: str) -> None:
"""Initialize the exception.
Args:
fld (str): field name.
"""
super().__init__(f'"{fld}" field does not exist. Every user should have "{fld}" field.')


class InvalidDateFormatError(Exception):
"""Error that is thrown when date format in field is not correct."""

def __init__(self, got_format: str) -> None:
"""Initialize the exception.
Args:
got_format (str): date format.
"""
super().__init__(f'Wrong date format, expected format: 2020-12-30, but got {got_format}.')


class InvalidJSONFormatError(Exception):
"""Error that is thrown when JSON structure is not correct."""

def __init__(self, filepath: str) -> None:
"""Initialize the exception.
Args:
filepath (str): path to file.
"""
super().__init__(f'An incorrect json file structure was found at path {filepath}.')


class EmptyJSONError(Exception):
"""Error that is thrown when was given empty json file."""

def __init__(self, filepath: str) -> None:
"""Initialize the exception.
Args:
filepath (str): path to file.
"""
super().__init__(f'Empty json file was found at path {filepath}.')
157 changes: 157 additions & 0 deletions hw2/hw2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
"""
Module for homework_2.
Напишите модуль, в котором функция process_data принимает путь к json-файлу
с данными о клиентах сайта (пример файла в data_hw2.json) и путь к json-файлу вывода.
Функция process_data записывает в этот общий json статистику:
минимальный, максимальный, средний и медианный возраст пользователя,
а также процент клиентов, которые были онлайн:
менее двух дней, недели, месяца, полугода, и более полугода назад.
"""
import json
import os
from datetime import datetime

import consts_hw2 as cnst
import exceptions_hw2

TWO_DAYS = 2
WEEK = 7
MONTH = 30
HALFYEAR = 180

ROUND_UP_TO_TWO = 2


def _median(inp_values: list[int | float]) -> int | float:
inp_values = sorted(inp_values)
length = len(inp_values)
middle = length // 2
if length % 2 == 0:
median = (inp_values[middle - 1] + inp_values[middle]) / 2
else:
median = inp_values[middle]
return round(median, ROUND_UP_TO_TWO)


def _average(inp_values: list[int | float]) -> int | float:
return round(sum(inp_values) / max(len(inp_values), 1), ROUND_UP_TO_TWO)


def _last_login_date(user: dict) -> datetime:
try:
return datetime.strptime(user[cnst.LAST_LOGIN], '%Y-%m-%d')
except KeyError:
raise exceptions_hw2.MissingFieldError(cnst.LAST_LOGIN)
except ValueError:
raise exceptions_hw2.InvalidDateFormatError(user[cnst.LAST_LOGIN])


def calculate_ages_stats(users: dict) -> dict:
"""Calculate statistics of the user ages.
Args:
users (dict): user input from JSON file.
Raises:
MissingFieldError: if field does not exist.
Returns:
dict: statistics of user ages.
"""
try:
ages = [users[user][cnst.AGE] for user in users]
except Exception:
raise exceptions_hw2.MissingFieldError(cnst.AGE)

ages_processed = {}
ages_processed[cnst.AGE_MAX] = max(ages)
ages_processed[cnst.AGE_MIN] = min(ages)
ages_processed[cnst.AGE_AVERAGE] = _average(ages)
ages_processed[cnst.AGE_MEDIAN] = _median(ages)

return ages_processed


def calculate_dates_stats(users: dict) -> dict:
"""Calculate statistics of the dates of last user logins.
Args:
users (dict): user input from JSON file.
Returns:
dict: statistics of last user login, percent of total users.
"""
last_login_dates = [_last_login_date(user) for user in users.values()]

last_login_statistics = {
cnst.LESS_TWO_DAYS: 0,
cnst.LESS_WEEK: 0,
cnst.LESS_MONTH: 0,
cnst.LESS_HALFYEAR: 0,
cnst.MORE_HALFYEAR: 0,
}

current_date = datetime.now()

for last_login_date in last_login_dates:
days_passed = (current_date - last_login_date).days
match days_passed:
case _ if days_passed < TWO_DAYS:
last_login_statistics[cnst.LESS_TWO_DAYS] += 1
case _ if days_passed < WEEK:
last_login_statistics[cnst.LESS_WEEK] += 1
case _ if days_passed < MONTH:
last_login_statistics[cnst.LESS_MONTH] += 1
case _ if days_passed < HALFYEAR:
last_login_statistics[cnst.LESS_HALFYEAR] += 1
case _:
last_login_statistics[cnst.MORE_HALFYEAR] += 1

quantity_users = len(last_login_dates)
for period in last_login_statistics.keys():
percent = round(last_login_statistics[period] / quantity_users * 100, ROUND_UP_TO_TWO)
last_login_statistics[period] = f'{percent}%'

return last_login_statistics


def _aggregate_data(users: dict) -> dict:
age_stats = calculate_ages_stats(users)
data_stats = calculate_dates_stats(users)
return {
**age_stats,
**data_stats,
}


def _save_data(input_path: str, output_path: str) -> None:
try:
with open(input_path, 'r') as input_file:
users = json.load(input_file)
except ValueError:
raise exceptions_hw2.InvalidJSONFormatError(input_path)
except FileNotFoundError:
raise exceptions_hw2.InputFilepathError(input_path)
if not users:
raise exceptions_hw2.EmptyJSONError(input_path)

statistics = _aggregate_data(users)
os.makedirs(os.path.abspath(os.path.dirname(output_path)), exist_ok=True)
with open(output_path, 'w') as out:
json.dump(statistics, out, indent=4)


def process_data(input_path: str = 'input.json', output_path: str = 'output.json'):
"""Process data from input json and write statistics to output json file.
Args:
input_path (str): path to source json file. Defaults to 'input.json'.
output_path (str): path to destionation json file. Defaults to 'output.json'.
"""
try:
_save_data(input_path, output_path)
except Exception as error:
with open(output_path, 'w') as out:
json.dump({'ERROR': str(error)}, out, indent=4)
File renamed without changes.
82 changes: 82 additions & 0 deletions hw2/test_hw2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Tests for hw2."""
import json
import tempfile
from typing import Any

import pytest

import consts_hw2 as cnst
import hw2

ZEROS = '0.0%'
ERROR = 'ERROR'
CORRECT_DATA = (
('tests/normal.json', {
cnst.AGE_MAX: 143,
cnst.AGE_MIN: 18,
cnst.AGE_AVERAGE: 80.5,
cnst.AGE_MEDIAN: 80.5,
cnst.LESS_TWO_DAYS: ZEROS,
cnst.LESS_WEEK: ZEROS,
cnst.LESS_MONTH: '50.0%',
cnst.LESS_HALFYEAR: ZEROS,
cnst.MORE_HALFYEAR: '50.0%',
}),
('tests/normal2.json', {
cnst.AGE_MAX: 90,
cnst.AGE_MIN: 4,
cnst.AGE_AVERAGE: 45.0,
cnst.AGE_MEDIAN: 38.5,
cnst.LESS_TWO_DAYS: ZEROS,
cnst.LESS_WEEK: '16.67%',
cnst.LESS_MONTH: '16.67%',
cnst.LESS_HALFYEAR: '33.33%',
cnst.MORE_HALFYEAR: '33.33%',
}),
)

INVALID_DATA = (
('tests/empty.json', {
ERROR: 'Empty json file was found at path tests/empty.json.',
}),
('tests/incorrect_data.json', {
ERROR: 'Wrong date format, expected format: 2020-12-30, but got 2012-1001.',
}),
('tests/invalid_format.json', {
'ERROR': 'An incorrect json file structure was found at path tests/invalid_format.json.',
}),
('tests/no_age.json', {
ERROR: '\"age\" field does not exist. Every user should have \"age\" field.',
}),
('tests/no_last_login.json', {
ERROR: '\"last_login\" field does not exist. Every user should have \"last_login\" field.',
}),
)


@pytest.mark.parametrize('input_path, expected', CORRECT_DATA)
def test_correct_data(input_path: str, expected: dict[str, Any]):
"""Checks that the process data function correctly calculates and saves data.
Args:
input_path (str): path to source json file.
expected (dict[str, Any]): expected processed stats.
"""
with tempfile.NamedTemporaryFile() as output:
hw2.process_data(input_path, output.name)
got = json.load(output)
assert got == expected


@pytest.mark.parametrize('input_path, expected', INVALID_DATA)
def test_exceptions(input_path: str, expected: type):
"""Checks that the process data function correctly calls exceptions.
Args:
input_path (str): path to source json file.
expected (type): exception that should be called.
"""
with tempfile.NamedTemporaryFile() as output:
hw2.process_data(input_path, output.name)
got = json.load(output)
assert got.items() == expected.items()
3 changes: 3 additions & 0 deletions hw2/tests/empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{

}
Loading

0 comments on commit 0d5390d

Please sign in to comment.