From 0294c3d54079f8c304859e184cfb310b339654d5 Mon Sep 17 00:00:00 2001 From: Eduard Date: Wed, 14 May 2025 18:01:57 +0300 Subject: [PATCH] initial --- .gitignore | 161 +++++++++++++++++++++++++++++++++++++++++++++++ api.py | 49 +++++++++++++++ config.py | 39 ++++++++++++ exceptions.py | 6 ++ main.py | 11 ++++ requirements.txt | 14 +++++ utils.py | 20 ++++++ 7 files changed, 300 insertions(+) create mode 100644 .gitignore create mode 100644 api.py create mode 100644 config.py create mode 100644 exceptions.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e49b42 --- /dev/null +++ b/.gitignore @@ -0,0 +1,161 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# 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/ \ No newline at end of file diff --git a/api.py b/api.py new file mode 100644 index 0000000..e08e9a1 --- /dev/null +++ b/api.py @@ -0,0 +1,49 @@ +from datetime import datetime, timezone + +import requests +from tqdm import tqdm + +from config import ClockifyConfig, config +from exceptions import InvalidToken +from utils import time_to_hour + + +class ClockifyAPI(ClockifyConfig): + def __init__(self, token): + self._check_token(token) + self.token = token + + def get_activities(self, **kwargs): + """ + start: datetime - Начальная дата (обязательно) + end: datetime - Конечная дата (обязательно) + page: int - Номер страницы + page-size: int - Размер страницы (по умолчанию 50) + """ + activities = requests.get(self.base_url + self.time_entries.format(**kwargs), params=kwargs, headers={"X-API-KEY": self.token}).json() + time_worked_out_total = 0 + for activity in tqdm(activities, desc="Парсинг задач"): + for tag in activity["tagIds"]: + start = datetime.strptime(activity["timeInterval"]["start"], config.DATETIME_FORMAT) + end = datetime.strptime(activity["timeInterval"]["end"] or str(datetime.now(timezone.utc).strftime(config.DATETIME_FORMAT)), config.DATETIME_FORMAT) + time_worked_out_total += time_to_hour(end - start) + + print(f"Получено задач: {len(activities)}") + print(f"Отработано часов: {time_worked_out_total}") + return activities + + + def get_tags(self, **kwargs): + """ + workspace_id: str - required + name: str - optional + archived: bool - optional + """ + tags = requests.get(self.base_url + self.tags.format(**kwargs), params=kwargs, headers={"X-API-KEY": self.token}).json() + return {tag["id"]: tag["name"] for tag in tags} + + def _check_token(self, token): + resp = requests.get(self.base_url + self.user_url, headers={"X-API-KEY": token}) + print(resp) + if not resp.ok: + raise InvalidToken \ No newline at end of file diff --git a/config.py b/config.py new file mode 100644 index 0000000..24314d7 --- /dev/null +++ b/config.py @@ -0,0 +1,39 @@ +from pydantic_settings import BaseSettings + + +class Config(BaseSettings): + CLOCKIFY_TOKEN: str + REDMINE_TOKEN: str + CLOCKIFY_USER_ID: str + CLOCKIFY_WORKSPACE_ID: str + + DATETIME_FORMAT: str = "%Y-%m-%dT%H:%M:%SZ" + + + +class RedmineConfig(BaseSettings): + BASE_API_URL: str = "https://redmine.sbps.ru" + + TIME_ENTRIES: str = "/time_entries.json" + + +class ClockifyConfig(BaseSettings): + + @property + def base_url(self): + return "https://api.clockify.me/api/v1" + + @property + def tags_url(self): + return "/workspaces/{workspace_id}/tags" + + @property + def time_entries_url(self): + return "/workspaces/{workspace_id}/user/{user_id}/time-entries" + + @property + def user_url(self): + return "/user" + + +config = Config() diff --git a/exceptions.py b/exceptions.py new file mode 100644 index 0000000..f61a875 --- /dev/null +++ b/exceptions.py @@ -0,0 +1,6 @@ +class InvalidToken(Exception): + def __init__(self, *args): + pass + + def __str__(self): + return "Токен недействителен" \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..fb60ee3 --- /dev/null +++ b/main.py @@ -0,0 +1,11 @@ + +from api import ClockifyAPI +from config import config + + +def main(): + ClockifyAPI(config.CLOCKIFY_TOKEN) + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0b54822 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +annotated-types==0.7.0 +asarPy==1.0.1 +certifi==2025.4.26 +charset-normalizer==3.4.2 +idna==3.10 +pydantic==2.11.4 +pydantic-settings==2.9.1 +pydantic_core==2.33.2 +python-dotenv==1.1.0 +requests==2.32.3 +tqdm==4.67.1 +typing-inspection==0.4.0 +typing_extensions==4.13.2 +urllib3==2.4.0 diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..eeb6549 --- /dev/null +++ b/utils.py @@ -0,0 +1,20 @@ +from datetime import datetime, timedelta + +import config + +def time_to_hour(time: timedelta) -> float: + return time.total_seconds() / 3600 + +def today_start(): + td = datetime.today() + year = td.year + month = td.month + day = td.day + return datetime(year=year, month=month, day=day, hour=0).strftime(config.DATETIME_FORMAT) + +def today_end(): + td = datetime.today() + year = td.year + month = td.month + day = td.day + return datetime(year=year, month=month, day=day, hour=23).strftime(config.DATETIME_FORMAT) \ No newline at end of file