forked from siriusdevs/homeworks_23
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8690ef5
commit 0d5390d
Showing
16 changed files
with
448 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}.') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
|
||
} |
Oops, something went wrong.