Compare commits
29 Commits
a3d29cd867
...
v0.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 405d7da754 | |||
| c316104d94 | |||
| eaeecc926a | |||
| 51950cb7d2 | |||
| 08380b2ca3 | |||
| b2bea677ef | |||
| 431541b3cb | |||
| a7a85b1816 | |||
| a1679b69c3 | |||
| 919bf6e69c | |||
| feb579ca31 | |||
| 57a758f93e | |||
| 5893920d69 | |||
| b7aab16a00 | |||
| 9844bccd65 | |||
| 2cba6321c2 | |||
| 305001c9ab | |||
| bfe13039a3 | |||
| f28b44b7fd | |||
| 158c381ac4 | |||
| 045cc3641f | |||
| ff602097be | |||
| 0f9f3afbb4 | |||
| 3672d8b321 | |||
| 016994d594 | |||
| e95de1b553 | |||
| 8971752e5b | |||
| 3aa887c5a4 | |||
| d236f3a14a |
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.0] - 2023-09-16
|
||||||
|
### Added
|
||||||
|
- Web interface
|
||||||
|
- Fully featured RestFullAPI v1;
|
||||||
|
- Monitoring free space in storage;
|
||||||
|
- Deleting an archive or ticket also deletes physical files;
|
||||||
|
- Flexible deployment configuration using environment variables;
|
||||||
|
- Dockerized app, the image size is less than 150mb;
|
||||||
|
- Support sqlite3 and PostgreSQL^15;
|
||||||
|
- Whitenoise Static management;
|
||||||
|
- healthcheck checking application availability;
|
||||||
50
Dockerfile
50
Dockerfile
@@ -8,23 +8,57 @@ ENV PYTHONDONTWRITEBYTECODE 1
|
|||||||
ENV PYTHONUNBUFFERED 1
|
ENV PYTHONUNBUFFERED 1
|
||||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=on
|
ENV PIP_DISABLE_PIP_VERSION_CHECK=on
|
||||||
|
|
||||||
# install dependences
|
# install app dependences
|
||||||
COPY requirements.txt ./
|
COPY requirements.txt ./
|
||||||
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
|
RUN pip install --no-cache-dir --root-user-action=ignore -r requirements.txt
|
||||||
|
|
||||||
# Now multistage builds
|
# now multistage builds
|
||||||
FROM python:3.10-alpine
|
FROM python:3.10-alpine
|
||||||
|
|
||||||
|
# set env variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE 1
|
||||||
|
ENV PYTHONUNBUFFERED 1
|
||||||
|
|
||||||
|
# default build args
|
||||||
|
ARG VERSION=0.1.0 \
|
||||||
|
APP_DIR=/app \
|
||||||
|
DATA_DIR=/app/data \
|
||||||
|
SRC_DIR=./logs_collector \
|
||||||
|
SCRIPTS_DIR=./scripts \
|
||||||
|
WEB_PORT=8000 \
|
||||||
|
USER_NAME=collector \
|
||||||
|
USER_GROUP=collector \
|
||||||
|
APP_UID=1000 \
|
||||||
|
APP_GID=1000
|
||||||
|
|
||||||
|
# copy app dependences
|
||||||
COPY --from=base /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
|
COPY --from=base /usr/local/lib/python3.10/site-packages/ /usr/local/lib/python3.10/site-packages/
|
||||||
COPY --from=base /usr/local/bin/ /usr/local/bin/
|
COPY --from=base /usr/local/bin/ /usr/local/bin/
|
||||||
|
|
||||||
|
# add curl and createa user to avoid running container as root &&
|
||||||
|
# create storage dir
|
||||||
|
RUN apk add --no-cache --upgrade curl && \
|
||||||
|
addgroup --system ${USER_GROUP} --gid ${APP_GID} && \
|
||||||
|
adduser --system --uid ${APP_UID} --ingroup ${USER_GROUP} ${USER_NAME} && \
|
||||||
|
mkdir -p ${APP_DIR}/data && \
|
||||||
|
chown -R ${USER_NAME}:${USER_GROUP} ${DATA_DIR}
|
||||||
|
|
||||||
|
# switch to user
|
||||||
|
USER ${USER_NAME}
|
||||||
|
|
||||||
|
# copy src and entrypoint.sh to app dir
|
||||||
|
COPY --chown=${USER_NAME}:${USER_GROUP} ${SRC_DIR} ${APP_DIR}
|
||||||
|
COPY --chown=${USER_NAME}:${USER_GROUP} ${SCRIPTS_DIR}/entrypoint.sh ${APP_DIR}
|
||||||
|
|
||||||
|
# set workdir
|
||||||
|
WORKDIR ${APP_DIR}
|
||||||
|
|
||||||
|
# app listens on this port by default
|
||||||
|
EXPOSE ${WEB_PORT}
|
||||||
|
|
||||||
# set lables about app
|
# set lables about app
|
||||||
LABEL maintainer="s.zhukovskii@ispsystem.com"
|
LABEL maintainer="s.zhukovskii@ispsystem.com"
|
||||||
LABEL ru.isptech.logs-collector.version=v0.1.0
|
LABEL me.zhukovsky.logs-collector.version=v${VERSION}
|
||||||
|
|
||||||
COPY ./logs_collector /app
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY entrypoint.sh ./
|
|
||||||
|
|
||||||
|
# run app
|
||||||
ENTRYPOINT [ "sh", "entrypoint.sh" ]
|
ENTRYPOINT [ "sh", "entrypoint.sh" ]
|
||||||
|
|||||||
324
README-ru.md
Normal file
324
README-ru.md
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
# LOGS-COLLECTOR
|
||||||
|
|
||||||
|
```sh
|
||||||
|
█░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█
|
||||||
|
█▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄
|
||||||
|
```
|
||||||
|
### [English lang: README.md](README.md)
|
||||||
|
|
||||||
|
### [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
|
||||||
|
|
||||||
|
## Цель
|
||||||
|
|
||||||
|
Если вы являетесь разработчиком ПО которое в дальнейшем клиенты используют в своей инфраструктуре, вы должны понимать, как иногда бывает трудно изучить проблему с ПО не имея доступа к серверу на котором это ПО работает.
|
||||||
|
|
||||||
|
|
||||||
|
Для решения этой задачи вы можете настраивать ПО на автоматическую отправку обезличенных отчетов о сбоях например использовать Sentry. Это не всегда приемлемо для клиента, к тому же информация может быть не полной или клиенту требуется повышенная конфиденциальность.
|
||||||
|
|
||||||
|
|
||||||
|
В таком случае вы можете попросить клиента отправить вам нужные лог файлы и изучить их в последствии. Но тут возникает другая проблема вам нужен безопасный способ передачи этих файлов как для вас так и для клиента.
|
||||||
|
Это мог быть FTP, SFTP, облако etc. Но что если вы не хотите давать клиенту данные для аутентификации и авторизации?
|
||||||
|
|
||||||
|
Возможно у вас есть доступ к серверу клиента и вы можете прочитать лог файлы на месте. И казалось бы проблема решена. Но на сервере клиента могут отсутствовать инструменты для удобного изучения лог файлов.
|
||||||
|
Даже если сотрудник поддержки может забрать себе нужные файлы и изучить их локально, возникает проблема распространения этих файлов между другими сотрудниками.
|
||||||
|
|
||||||
|
Logs-collector позволяет решить эти задачи.
|
||||||
|
|
||||||
|
Logs-collector является удаленным хранилищем и может принимать и отдавать файлы.
|
||||||
|
|
||||||
|
|
||||||
|
## Термины
|
||||||
|
- Платформа: это ПО разработанное вашей компанией
|
||||||
|
- Тикет: это номер связанный с тикетом в вашей help desk системе
|
||||||
|
- Архив: это загруженный лог файл (поддерживается любой формат)
|
||||||
|
|
||||||
|
## Как это работает?
|
||||||
|
|
||||||
|
- Создаете платформы
|
||||||
|
- Создаете тикет связанный с платформой и номером
|
||||||
|
- Передаете клиенту уникальный токен тикета
|
||||||
|
- Клиент загружает архив лог файлов
|
||||||
|
- Скачиваете архив (находите решение проблемы)
|
||||||
|
- Удаляете архив или тикет или отмечаете тикет решенным
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
- Централизованное хранилище
|
||||||
|
- Для загрузки файла не нужно давать auth credentials
|
||||||
|
- Каждый токен на загрузку уникален и связан только с одним тикетом
|
||||||
|
- Токен имеет ограничение на количество попыток и время жизни
|
||||||
|
- Загрузить файл можно из консоли или через веб
|
||||||
|
- Полнофункциональный RestFullAPI v1
|
||||||
|
- Мониторинг свободного пространства в хранилище
|
||||||
|
- Удаление архива или тикета так же удаляет физические файлы
|
||||||
|
- Приложение соответствует архитектуре приложения 12 факторов
|
||||||
|
- Гибкая настройка развертывания переменными окружения
|
||||||
|
- Приложение докеризировано, размер образа меньше 150mb
|
||||||
|
- Может работать как с sqlite3 так и с PostgreSQL^15
|
||||||
|
- Управление статикой без настройки для этого веб сервера
|
||||||
|
- healthcheck проверка доступности приложения
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Токен на загрузку не связан с авторизацией
|
||||||
|
- Токен на загрузку обладает высокой энтропией.
|
||||||
|
- Двухфакторная аутентификация для пользователей
|
||||||
|
- Для скачивания файла - 2FA должна быть принудительно включена
|
||||||
|
- Админ панель пропатчена на принудительное использование 2FA
|
||||||
|
- Пользователь в контейнере является не привилегированным
|
||||||
|
- Стандартные методы защиты Django и DRF
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
### Из docker образа:
|
||||||
|
- Создайте директорию для приложения где вам удобно
|
||||||
|
- Создайте файл docker-compose.yml в директории приложения
|
||||||
|
- Создайте файл .env в директории приложения
|
||||||
|
- Наполните файл .env требуемыми переменными окружения см. ниже
|
||||||
|
|
||||||
|
>Пример файла с использованием хранилища докер и sqlite как база данных по умолчанию:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
# to set environment variables:
|
||||||
|
# create a .env file in the same directory as docker-compose.yaml
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: mois3y/logs_collector:0.1.0
|
||||||
|
container_name: logs-collector
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
ports:
|
||||||
|
- "80:8000"
|
||||||
|
volumes:
|
||||||
|
- /etc/timezone:/etc/timezone:ro # optional
|
||||||
|
- /etc/localtime:/etc/localtime:ro # optional
|
||||||
|
- logs_collector_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs_collector_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
### Из исходников:
|
||||||
|
- Клонируйте репозиторий
|
||||||
|
- docker-compose.yaml уже есть в директории с проектом
|
||||||
|
- создайте в корне проекта файл .env
|
||||||
|
- наполните .env требуемыми переменными окружения см. ниже
|
||||||
|
- соберите образ и запустите контейнер в фоне:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
- Вы можете создать свой файл и внести нужные правки:
|
||||||
|
#### docker-compose-example-psql.yaml c PostgreSQL по умолчанию:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
logs_collector:
|
||||||
|
container_name: logs-collector
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- VERSION=${VERSION}
|
||||||
|
- SRC_DIR=${SRC_DIR}
|
||||||
|
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||||
|
- APP_DIR=${APP_DIR}
|
||||||
|
- DATA_DIR=${DATA_DIR}
|
||||||
|
- WEB_PORT=${WEB_PORT}
|
||||||
|
- USER_NAME=${USER_NAME}
|
||||||
|
- USER_GROUP=${USER_GROUP}
|
||||||
|
- APP_UID=${APP_UID}
|
||||||
|
- APP_GID=${APP_GID}
|
||||||
|
ports:
|
||||||
|
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: logs_collector_data
|
||||||
|
target: ${APP_DIR}/data
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine3.18
|
||||||
|
container_name: psql-collector
|
||||||
|
volumes:
|
||||||
|
- logs_collector_psql_data:/var/lib/postgresql/data/
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs_collector_data:
|
||||||
|
logs_collector_psql_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
#### docker-compose-example-psql.yaml c sqlite и bind-mount:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
# to set environment variables:
|
||||||
|
# create a .env file in the same directory as docker-compose.yaml
|
||||||
|
|
||||||
|
services:
|
||||||
|
logs_collector:
|
||||||
|
container_name: logs-collector
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- VERSION=${VERSION}
|
||||||
|
- SRC_DIR=${SRC_DIR}
|
||||||
|
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||||
|
- APP_DIR=${APP_DIR}
|
||||||
|
- DATA_DIR=${DATA_DIR}
|
||||||
|
- WEB_PORT=${WEB_PORT}
|
||||||
|
- USER_NAME=${USER_NAME}
|
||||||
|
- USER_GROUP=${USER_GROUP}
|
||||||
|
- APP_UID=${APP_UID}
|
||||||
|
- APP_GID=${APP_GID}
|
||||||
|
ports:
|
||||||
|
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||||
|
volumes:
|
||||||
|
- "/opt/collector/data:${DATA_DIR}"
|
||||||
|
- "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3"
|
||||||
|
env_file:
|
||||||
|
- /.env
|
||||||
|
```
|
||||||
|
|
||||||
|
🔴
|
||||||
|
|
||||||
|
❗ВАЖНО❗
|
||||||
|
|
||||||
|
|
||||||
|
Если вы используете bind-mount и монтируете его в хранилище приложения, помните
|
||||||
|
пользователь в контейнере не привилегирован UID 1000 если примонтированный файл
|
||||||
|
или директория будет принадлежать root приложение не сможет его прочитать и
|
||||||
|
следовательно работать.
|
||||||
|
|
||||||
|
В продакшн среде используйте приложение за вашим любимым обратным прокси.
|
||||||
|
|
||||||
|
Просто добавьте его в стек docker-compose.yaml
|
||||||
|
|
||||||
|
>Можно этого не делать, но Gunicorn рекомендуют придерживаться этого правила.
|
||||||
|
>
|
||||||
|
>Я солидарен с ними, так что вас предупредили)
|
||||||
|
|
||||||
|
🔴
|
||||||
|
|
||||||
|
## Переменные окружения:
|
||||||
|
>Приложение можно настроить, для этого передайте следующие возможные переменные
|
||||||
|
>окружения.
|
||||||
|
>Если переменная не передана, будет использоваться переменная окружения по умолчанию
|
||||||
|
|
||||||
|
```
|
||||||
|
█▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀
|
||||||
|
█▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄
|
||||||
|
```
|
||||||
|
|
||||||
|
| ENV | DEFAULT | INFO |
|
||||||
|
| -------------------- | --------------- | ------------------------ |
|
||||||
|
| SECRET_KEY | j9QGbvM9Z4otb47 | ❗change this immediately|
|
||||||
|
| DEBUG | False | use only False in prod |
|
||||||
|
| ALLOWED_HOSTS | '*' | list separated by commas |
|
||||||
|
| CSRF_TRUSTED_ORIGINS | | list separated by commas |
|
||||||
|
| DB_URL | | url for connect db |
|
||||||
|
| TZ | 'UTC' | server timezone |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins)
|
||||||
|
|
||||||
|
Требуется в среде докер в продакшн окружении
|
||||||
|
принимает список url разделенных запятой
|
||||||
|
>http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0
|
||||||
|
|
||||||
|
|
||||||
|
[DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html)
|
||||||
|
|
||||||
|
Нужно указывать если вы хотите использовать PostgreSQL
|
||||||
|
Эти данные должны совпадать с переменными контейнера PostgreSQL
|
||||||
|
|
||||||
|
| ENV | VALUE |
|
||||||
|
| ----------------- | -------------- |
|
||||||
|
| POSTGRES_USER | admin |
|
||||||
|
| POSTGRES_PASSWORD | ddkwndkjdX7RrP |
|
||||||
|
| POSTGRES_DB | collector |
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
#### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector
|
||||||
|
- Протокол: **psql://**
|
||||||
|
- Пользователь: **admin**
|
||||||
|
- Пароль: **ddkwndkjdX7RrP**
|
||||||
|
- IP адрес: **psql-collector**
|
||||||
|
- Порт: **5432**
|
||||||
|
- Имя БД: **collector**
|
||||||
|
|
||||||
|
```
|
||||||
|
█▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀
|
||||||
|
█▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄
|
||||||
|
```
|
||||||
|
|
||||||
|
| ENV | DEFAULT |
|
||||||
|
| --------------------------- | -------------- |
|
||||||
|
| GUNICORN_BIND | '0.0.0.0:8000' |
|
||||||
|
| GUNICORN_BACKLOG | 2048 |
|
||||||
|
| GUNICORN_WORKERS | 2 |
|
||||||
|
| GUNICORN_WORKER_CLASS | 'sync' |
|
||||||
|
| GUNICORN_WORKER_CONNECTIONS | 1000 |
|
||||||
|
| GUNICORN_THREADS | 1 |
|
||||||
|
| GUNICORN_TIMEOUT | 3600 |
|
||||||
|
| GUNICORN_KEEPALIVE | 2 |
|
||||||
|
| GUNICORN_LOGLEVEL | 'info' |
|
||||||
|
|
||||||
|
[GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html)
|
||||||
|
|
||||||
|
Подробная информация о каждой переменной окружения доступна в официальной документации.
|
||||||
|
|
||||||
|
GUNICORN_BIND не изменяйте это так как переменная отвечает за прослушиваемый адрес и порт внутри контейнера.
|
||||||
|
|
||||||
|
GUNICORN_TIMEOUT по умолчанию установлена в 3600. Такой большой таймаут нужен для загрузки больших файлов.
|
||||||
|
Поскольку я старался сделать приложение минималистичным и не использовать менеджер задач загрузка файла идет в один поток.
|
||||||
|
|
||||||
|
Если время загрузки будет больше часа соединение разорвется, это особенность синхронной работы воркеров gunicorn если вам не хватает времени на загрузку вы можете увеличить это значение.
|
||||||
|
|
||||||
|
❗ВАЖНО❗
|
||||||
|
|
||||||
|
Gunicorn настроен писать в лог в следующем формате:
|
||||||
|
```python
|
||||||
|
'%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||||
|
```
|
||||||
|
Это значит что в логе будет видно IP адрес запроса только из заголовка
|
||||||
|
|
||||||
|
**X-Forwarded-For**
|
||||||
|
|
||||||
|
В продакшн среде приложение должно быть за обратным прокси
|
||||||
|
|
||||||
|
|
||||||
|
## Помощники
|
||||||
|
В корне репозитория проекта есть директория scripts в ней лежит скрипт uploader.sh с помощью которого можно отправить файлы из консоли используя curl.
|
||||||
|
|
||||||
|
Синтаксис простой:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
Usage: ./uploader.sh [options [parameters]]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-f | --file full path to upload file required
|
||||||
|
-t | --token access token required
|
||||||
|
-u | --url target url required
|
||||||
|
-v | --version print version
|
||||||
|
-h | --help print help
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
GNU GPL 3.0
|
||||||
330
README.md
330
README.md
@@ -1,3 +1,329 @@
|
|||||||
# logs-collector
|
# LOGS-COLLECTOR
|
||||||
|
|
||||||
Серверная сторона для получения и хранения лог файлов
|
```sh
|
||||||
|
█░░ █▀█ █▀▀ █▀ ▄▄ █▀▀ █▀█ █░░ █░░ █▀▀ █▀▀ ▀█▀ █▀█ █▀█
|
||||||
|
█▄▄ █▄█ █▄█ ▄█ ░░ █▄▄ █▄█ █▄▄ █▄▄ ██▄ █▄▄ ░█░ █▄█ █▀▄
|
||||||
|
```
|
||||||
|
|
||||||
|
### [CHANGELOG.md](CHANGELOG.md)
|
||||||
|
|
||||||
|
### [Russian lang: README.md](README-ru.md)
|
||||||
|
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
If you are a developer of software that clients later use in their infrastructure,
|
||||||
|
you must understand how sometimes it can be difficult to research a problem
|
||||||
|
with software without access to the server on which this software runs.
|
||||||
|
|
||||||
|
To solve this problem, you can configure the software to automatically send
|
||||||
|
anonymized crash reports, for example, use Sentry.
|
||||||
|
This is not always acceptable to the client;
|
||||||
|
Moreover, the information may not be complete or the client
|
||||||
|
requires increased confidentiality.
|
||||||
|
|
||||||
|
|
||||||
|
## Terms
|
||||||
|
- Platform: this is software developed by your company
|
||||||
|
- Ticket: this is the number associated with the ticket in your help desk system
|
||||||
|
- Archive: this is an uploaded log file (any format is supported)
|
||||||
|
|
||||||
|
## How it works?
|
||||||
|
|
||||||
|
- Create platforms
|
||||||
|
- Create a ticket associated with the platform and number
|
||||||
|
- Transfer a unique ticket token to the client
|
||||||
|
- The client downloads an archive of log files
|
||||||
|
- Download the archive (find a solution to the problem)
|
||||||
|
- Delete the archive or ticket or mark the ticket as resolved
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Centralized storage;
|
||||||
|
- To download a file you do not need to provide auth credentials;
|
||||||
|
- Each download token is unique and associated with only one ticket;
|
||||||
|
- The token has a limit on the number of attempts and lifetime;
|
||||||
|
- You can download the file from the console or via the web;
|
||||||
|
- Fully featured RestFullAPI v1;
|
||||||
|
- Monitoring free space in storage;
|
||||||
|
- Deleting an archive or ticket also deletes physical files;
|
||||||
|
- The application follows the 12 factors application architecture;
|
||||||
|
- Flexible deployment configuration using environment variables;
|
||||||
|
- The application is dockerized, the image size is less than 150mb;
|
||||||
|
- Can work with both sqlite3 and PostgreSQL^15;
|
||||||
|
- Static management without configuration for this web server;
|
||||||
|
- healthcheck checking application availability;
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- The download token is not associated with authorization
|
||||||
|
- The download token has high entropy.
|
||||||
|
- Two-factor authentication for users
|
||||||
|
- To download a file - 2FA must be forcibly enabled
|
||||||
|
- The admin panel has been patched to force the use of 2FA
|
||||||
|
- The user in the container is not privileged
|
||||||
|
- Standard Django and DRF protection methods
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
### From the docker image:
|
||||||
|
- Create a directory for the application wherever it is convenient for you
|
||||||
|
- Create a docker-compose.yml file in the application directory
|
||||||
|
- Create a .env file in the application directory
|
||||||
|
- Fill the .env file with the required environment variables, see below
|
||||||
|
|
||||||
|
>Example file using docker store and sqlite as default database:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
# to set environment variables:
|
||||||
|
# create a .env file in the same directory as docker-compose.yaml
|
||||||
|
|
||||||
|
services:
|
||||||
|
server:
|
||||||
|
image: mois3y/logs_collector:0.1.0
|
||||||
|
container_name: logs-collector
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
ports:
|
||||||
|
- "80:8000"
|
||||||
|
volumes:
|
||||||
|
- /etc/timezone:/etc/timezone:ro # optional
|
||||||
|
- /etc/localtime:/etc/localtime:ro # optional
|
||||||
|
- logs_collector_data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs_collector_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
### From the source:
|
||||||
|
- Clone the repository
|
||||||
|
- docker-compose.yaml is already in the project directory
|
||||||
|
- create a .env file in the project root
|
||||||
|
- fill .env with the required environment variables, see below
|
||||||
|
- build the image and run the container in the background:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
- You can create your own file and make the necessary edits:
|
||||||
|
#### docker-compose.yaml PostgreSQL by default:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
logs_collector:
|
||||||
|
container_name: logs-collector
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- VERSION=${VERSION}
|
||||||
|
- SRC_DIR=${SRC_DIR}
|
||||||
|
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||||
|
- APP_DIR=${APP_DIR}
|
||||||
|
- DATA_DIR=${DATA_DIR}
|
||||||
|
- WEB_PORT=${WEB_PORT}
|
||||||
|
- USER_NAME=${USER_NAME}
|
||||||
|
- USER_GROUP=${USER_GROUP}
|
||||||
|
- APP_UID=${APP_UID}
|
||||||
|
- APP_GID=${APP_GID}
|
||||||
|
ports:
|
||||||
|
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||||
|
volumes:
|
||||||
|
- type: volume
|
||||||
|
source: logs_collector_data
|
||||||
|
target: ${APP_DIR}/data
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:15-alpine3.18
|
||||||
|
container_name: psql-collector
|
||||||
|
volumes:
|
||||||
|
- logs_collector_psql_data:/var/lib/postgresql/data/
|
||||||
|
env_file:
|
||||||
|
- ./.env
|
||||||
|
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs_collector_data:
|
||||||
|
logs_collector_psql_data:
|
||||||
|
```
|
||||||
|
|
||||||
|
#### docker-compose-example-psql.yaml c sqlite и bind-mount:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: "3"
|
||||||
|
|
||||||
|
# to set environment variables:
|
||||||
|
# create a .env file in the same directory as docker-compose.yaml
|
||||||
|
|
||||||
|
services:
|
||||||
|
logs_collector:
|
||||||
|
container_name: logs-collector
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
args:
|
||||||
|
- VERSION=${VERSION}
|
||||||
|
- SRC_DIR=${SRC_DIR}
|
||||||
|
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||||
|
- APP_DIR=${APP_DIR}
|
||||||
|
- DATA_DIR=${DATA_DIR}
|
||||||
|
- WEB_PORT=${WEB_PORT}
|
||||||
|
- USER_NAME=${USER_NAME}
|
||||||
|
- USER_GROUP=${USER_GROUP}
|
||||||
|
- APP_UID=${APP_UID}
|
||||||
|
- APP_GID=${APP_GID}
|
||||||
|
ports:
|
||||||
|
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||||
|
volumes:
|
||||||
|
- "/opt/collector/data:${DATA_DIR}"
|
||||||
|
- "/opt/collector/data/db.sqlite3:${DATA_DIR}/db.sqlite3"
|
||||||
|
env_file:
|
||||||
|
- /.env
|
||||||
|
```
|
||||||
|
|
||||||
|
🔴
|
||||||
|
|
||||||
|
❗IMPORTANT❗
|
||||||
|
|
||||||
|
If you are using bind-mount and mounting it to your application's storage,
|
||||||
|
remember user in container is not privileged UID 1000 if mounted file
|
||||||
|
or the directory will belong to the root
|
||||||
|
application will not be able to read it and therefore work.
|
||||||
|
|
||||||
|
In a production environment, use the application behind your favorite reverse proxy.
|
||||||
|
|
||||||
|
Just add it to the docker-compose.yaml stack
|
||||||
|
|
||||||
|
>You don't have to do this, but Gunicorn recommends following this rule.
|
||||||
|
>
|
||||||
|
>I agree with them, so you have been warned)
|
||||||
|
|
||||||
|
🔴
|
||||||
|
|
||||||
|
## Environment:
|
||||||
|
>The application can be configured,
|
||||||
|
>to do this, pass the following possible variables surroundings.
|
||||||
|
>If no variable is passed, the default environment variable will be used
|
||||||
|
|
||||||
|
```
|
||||||
|
█▀▄ ░░█ ▄▀█ █▄░█ █▀▀ █▀█ ▀
|
||||||
|
█▄▀ █▄█ █▀█ █░▀█ █▄█ █▄█ ▄
|
||||||
|
```
|
||||||
|
|
||||||
|
| ENV | DEFAULT | INFO |
|
||||||
|
| -------------------- | --------------- | ------------------------ |
|
||||||
|
| SECRET_KEY | j9QGbvM9Z4otb47 | ❗change this immediately|
|
||||||
|
| DEBUG | False | use only False in prod |
|
||||||
|
| ALLOWED_HOSTS | '*' | list separated by commas |
|
||||||
|
| CSRF_TRUSTED_ORIGINS | | list separated by commas |
|
||||||
|
| DB_URL | | url for connect db |
|
||||||
|
| TZ | 'UTC' | server timezone |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[CSRF_TRUSTED_ORIGINS](https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins)
|
||||||
|
|
||||||
|
Required in a Docker environment in a production environment
|
||||||
|
accepts a list of urls separated by commas
|
||||||
|
>http://localhost,http://*.domain.com,http://127.0.0.1,http://0.0.0.0
|
||||||
|
|
||||||
|
|
||||||
|
[DB_URL](https://django-environ.readthedocs.io/en/latest/quickstart.html)
|
||||||
|
|
||||||
|
Must be specified if you want to use PostgreSQL
|
||||||
|
This data must match the PostgreSQL container variables
|
||||||
|
|
||||||
|
| ENV | VALUE |
|
||||||
|
| ----------------- | -------------- |
|
||||||
|
| POSTGRES_USER | admin |
|
||||||
|
| POSTGRES_PASSWORD | ddkwndkjdX7RrP |
|
||||||
|
| POSTGRES_DB | collector |
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
#### psql://admin:ddkwndkjdX7RrP@psql-collector:5432/collector
|
||||||
|
- Protocol: **psql://**
|
||||||
|
- User: **admin**
|
||||||
|
- Password: **ddkwndkjdX7RrP**
|
||||||
|
- Address: **psql-collector**
|
||||||
|
- Port: **5432**
|
||||||
|
- Database name: **collector**
|
||||||
|
|
||||||
|
```
|
||||||
|
█▀▀ █░█ █▄░█ █ █▀▀ █▀█ █▀█ █▄░█ ▀
|
||||||
|
█▄█ █▄█ █░▀█ █ █▄▄ █▄█ █▀▄ █░▀█ ▄
|
||||||
|
```
|
||||||
|
|
||||||
|
| ENV | DEFAULT |
|
||||||
|
| --------------------------- | -------------- |
|
||||||
|
| GUNICORN_BIND | '0.0.0.0:8000' |
|
||||||
|
| GUNICORN_BACKLOG | 2048 |
|
||||||
|
| GUNICORN_WORKERS | 2 |
|
||||||
|
| GUNICORN_WORKER_CLASS | 'sync' |
|
||||||
|
| GUNICORN_WORKER_CONNECTIONS | 1000 |
|
||||||
|
| GUNICORN_THREADS | 1 |
|
||||||
|
| GUNICORN_TIMEOUT | 3600 |
|
||||||
|
| GUNICORN_KEEPALIVE | 2 |
|
||||||
|
| GUNICORN_LOGLEVEL | 'info' |
|
||||||
|
|
||||||
|
[GUNICORN_*](https://docs.gunicorn.org/en/stable/settings.html)
|
||||||
|
|
||||||
|
Detailed information about each environment variable is available in
|
||||||
|
the official documentation.
|
||||||
|
|
||||||
|
**GUNICORN_BIND** do not change this since the variable
|
||||||
|
is responsible for the listening address and port inside the container.
|
||||||
|
|
||||||
|
**GUNICORN_TIMEOUT** is set to 3600 by default.
|
||||||
|
Such a large timeout is needed to download large files.
|
||||||
|
Since I tried to make the application minimalistic and not use a task manager,
|
||||||
|
the file is downloaded in one thread.
|
||||||
|
|
||||||
|
If the loading time is more than an hour, the connection will be broken,
|
||||||
|
this is a feature of the synchronous operation of gunicorn workers;
|
||||||
|
if you do not have enough time to load, you can increase this value.
|
||||||
|
|
||||||
|
|
||||||
|
❗IMPORTANT❗
|
||||||
|
|
||||||
|
Gunicorn is configured to write to the log in the following format:
|
||||||
|
```python
|
||||||
|
'%({X-Forwarded-For}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||||
|
```
|
||||||
|
This means that the log will show the IP address of the request only from the header
|
||||||
|
|
||||||
|
**X-Forwarded-For**
|
||||||
|
|
||||||
|
In a production environment, the application must be behind a reverse proxy
|
||||||
|
|
||||||
|
|
||||||
|
## Helpers
|
||||||
|
At the root of the project repository there is a scripts directory,
|
||||||
|
it contains the uploader.sh script with which you can send files
|
||||||
|
from the console using **curl**.
|
||||||
|
|
||||||
|
The syntax is simple:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
Usage: ./uploader.sh [options [parameters]]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
-f | --file full path to upload file required
|
||||||
|
-t | --token access token required
|
||||||
|
-u | --url target url required
|
||||||
|
-v | --version print version
|
||||||
|
-h | --help print help
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GNU GPL 3.0
|
||||||
|
|||||||
@@ -1,16 +1,38 @@
|
|||||||
version: "3.7"
|
version: "3"
|
||||||
|
|
||||||
|
# to set environment variables:
|
||||||
|
# create a .env file in the same directory as docker-compose.yaml
|
||||||
|
|
||||||
services:
|
services:
|
||||||
logs_collector:
|
logs_collector:
|
||||||
container_name: logs_collector
|
container_name: logs-collector
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
expose:
|
args:
|
||||||
- 8000
|
- VERSION=${VERSION}
|
||||||
|
- SRC_DIR=${SRC_DIR}
|
||||||
|
- SCRIPTS_DIR=${SCRIPTS_DIR}
|
||||||
|
- APP_DIR=${APP_DIR}
|
||||||
|
- DATA_DIR=${DATA_DIR}
|
||||||
|
- WEB_PORT=${WEB_PORT}
|
||||||
|
- USER_NAME=${USER_NAME}
|
||||||
|
- USER_GROUP=${USER_GROUP}
|
||||||
|
- APP_UID=${APP_UID}
|
||||||
|
- APP_GID=${APP_GID}
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "${WEB_HOST}:${WEB_PORT}:${WEB_PORT}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs_collector/db.sqlite3:/app/db.sqlite3
|
- type: volume
|
||||||
- ./logs_collector/archives:/app/archives
|
source: logs_collector_data
|
||||||
|
target: ${APP_DIR}/data
|
||||||
env_file:
|
env_file:
|
||||||
- ./logs_collector/.env
|
- ./.env
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://${WEB_HOST}:${WEB_PORT}/${HEALTHCHECK_URL}"]
|
||||||
|
interval: 1m30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
logs_collector_data:
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
#!/usr/bin/env sh
|
|
||||||
|
|
||||||
python manage.py migrate --no-input
|
|
||||||
python manage.py collectstatic --no-input
|
|
||||||
|
|
||||||
gunicorn logs_collector.wsgi:application
|
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.contrib.auth.admin import UserAdmin
|
||||||
|
from .models import User
|
||||||
|
|
||||||
# Register your models here.
|
|
||||||
|
admin.site.register(User, UserAdmin)
|
||||||
|
|||||||
36
logs_collector/account/forms.py
Normal file
36
logs_collector/account/forms.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.utils.safestring import mark_safe
|
||||||
|
from crispy_forms.helper import FormHelper
|
||||||
|
from crispy_forms.layout import Layout, Submit, Div
|
||||||
|
from crispy_forms.bootstrap import PrependedText
|
||||||
|
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserProfileForm(forms.ModelForm):
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = User
|
||||||
|
fields = [
|
||||||
|
'email',
|
||||||
|
'first_name',
|
||||||
|
'last_name',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(UserProfileForm, self).__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper(self)
|
||||||
|
self.helper.form_show_labels = False
|
||||||
|
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Div(
|
||||||
|
PrependedText(
|
||||||
|
'email',
|
||||||
|
mark_safe('<i class="bi bi-envelope-at"></i>'),
|
||||||
|
placeholder="email"
|
||||||
|
),
|
||||||
|
PrependedText('first_name', 'First name:'),
|
||||||
|
PrependedText('last_name', 'Last name:'),
|
||||||
|
),
|
||||||
|
Submit('submit', 'Save', css_class='btn btn-primary'),
|
||||||
|
)
|
||||||
44
logs_collector/account/migrations/0001_initial.py
Normal file
44
logs_collector/account/migrations/0001_initial.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
# Generated by Django 4.2 on 2023-09-08 12:27
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.contrib.auth.validators
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('auth', '0012_alter_user_first_name_max_length'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='User',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
|
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||||
|
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||||
|
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||||
|
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||||
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||||
|
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||||
|
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||||
|
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
||||||
|
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'user',
|
||||||
|
'verbose_name_plural': 'users',
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
managers=[
|
||||||
|
('objects', django.contrib.auth.models.UserManager()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,3 +1,10 @@
|
|||||||
from django.db import models
|
from django.urls import reverse
|
||||||
|
from django.contrib.auth.models import AbstractUser
|
||||||
|
|
||||||
# Create your models here.
|
|
||||||
|
# using-a-custom-user-model-when-starting-a-project
|
||||||
|
# https://docs.djangoproject.com/en/4.2/topics/auth/customizing/
|
||||||
|
class User(AbstractUser):
|
||||||
|
|
||||||
|
def get_absolute_url(self):
|
||||||
|
return reverse('account:show_profile')
|
||||||
|
|||||||
30
logs_collector/account/templates/account/base.html
Normal file
30
logs_collector/account/templates/account/base.html
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block account_head %}
|
||||||
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
|
{% endblock account_head %}
|
||||||
|
|
||||||
|
{% block account_content %}
|
||||||
|
<header class="sticky-top">
|
||||||
|
<section>
|
||||||
|
{% include 'includes/navigation.html' %}
|
||||||
|
</section>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
{% block main %}{% endblock main %}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<footer class="footer mt-auto">
|
||||||
|
<section>
|
||||||
|
{% include 'includes/footer.html' %}
|
||||||
|
</section>
|
||||||
|
</footer>
|
||||||
|
{% endblock account_content %}
|
||||||
|
|
||||||
|
{% block account_scripts %}
|
||||||
|
<script src="{% static 'collector/js/jquery-3.7.0.min.js' %}"></script>
|
||||||
|
{% block bs %}{% endblock bs %}
|
||||||
|
{% block jquery %}{% endblock jquery %}
|
||||||
|
{% endblock account_scripts %}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h5 class="card-title">Authentication</h5>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text"><i class="bi bi-person-circle"></i></span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="{{ request.user.username }}"
|
||||||
|
aria-label="Username"
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text"><i class="bi bi-shield-lock"></i></span>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="●●●●●●●●"
|
||||||
|
aria-label="Password"
|
||||||
|
disabled
|
||||||
|
readonly
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
href="{% url 'account:password_change' %}"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h5 class="card-title">Profile</h5>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text"><i class="bi bi-envelope-at"></i></span>
|
||||||
|
<input type="text" class="form-control" placeholder="{{ request.user.email }}" aria-label="Email" disabled readonly>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">First name:</span>
|
||||||
|
<input type="text" class="form-control" placeholder="{{ request.user.first_name }}" aria-label="Username" disabled readonly>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<span class="input-group-text">Last name:</i></span>
|
||||||
|
<input type="text" class="form-control" placeholder="{{ request.user.last_name }}" aria-label="Email" disabled readonly>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="{% url 'account:update_profile' %}"
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil-square"></i> Edit
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{% extends 'account/profile.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% block password_change %}
|
||||||
|
<form method="post">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
<p><input class="btn btn-primary" type="submit" value="Change" /></p>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock password_change %}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{% extends 'account/profile_info.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% block profile_alerts %}
|
||||||
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||||||
|
<div class=" d-flex align-items-center mt-1">
|
||||||
|
<h5><i class="bi bi-check-circle-fill"></i> Password changed</h5>
|
||||||
|
</div>
|
||||||
|
Your password has been successfully changed.
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
{% endblock profile_alerts %}
|
||||||
17
logs_collector/account/templates/account/profile.html
Normal file
17
logs_collector/account/templates/account/profile.html
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{% extends 'account/base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% block title %} {{ title }} {% endblock title %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="container mt-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Account:</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% block profile_info %}{% endblock profile_info %}
|
||||||
|
{% block profile_update %}{% endblock profile_update %}
|
||||||
|
{% block password_change %}{% endblock password_change %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock main %}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{% extends 'account/profile.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% block profile_info %}
|
||||||
|
{% block profile_alerts %}{% endblock profile_alerts %}
|
||||||
|
{% include 'account/includes/auth_credentials.html' %}
|
||||||
|
{% include 'account/includes/profile_credentials.html' %}
|
||||||
|
{% endblock profile_info %}
|
||||||
15
logs_collector/account/templates/account/profile_update.html
Normal file
15
logs_collector/account/templates/account/profile_update.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'account/profile.html' %}
|
||||||
|
{% load static %}
|
||||||
|
{% load crispy_forms_tags %}
|
||||||
|
{% block profile_update %}
|
||||||
|
{% include 'account/includes/auth_credentials.html' %}
|
||||||
|
<div class="container">
|
||||||
|
<h5 class="card-title">Profile</h5>
|
||||||
|
<hr />
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
{% crispy form %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock profile_update %}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
0
logs_collector/account/tests/__init__.py
Normal file
0
logs_collector/account/tests/__init__.py
Normal file
36
logs_collector/account/tests/test_urls.py
Normal file
36
logs_collector/account/tests/test_urls.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import resolve, reverse
|
||||||
|
from django.contrib.auth.views import (
|
||||||
|
LogoutView,
|
||||||
|
PasswordChangeView,
|
||||||
|
PasswordChangeDoneView
|
||||||
|
)
|
||||||
|
|
||||||
|
from account import views
|
||||||
|
|
||||||
|
|
||||||
|
class TestUrls(TestCase):
|
||||||
|
|
||||||
|
# READ:
|
||||||
|
def test_account_logout_url_is_resolved(self):
|
||||||
|
url = reverse('account:logout')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, LogoutView)
|
||||||
|
|
||||||
|
def test_account_show_url_is_resolved(self):
|
||||||
|
url = reverse('account:show_profile')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, views.DetailProfile)
|
||||||
|
|
||||||
|
def test_password_change_done_url_is_resolved(self):
|
||||||
|
url = reverse('account:password_change_done')
|
||||||
|
self.assertEquals(
|
||||||
|
resolve(url).func.view_class, PasswordChangeDoneView
|
||||||
|
)
|
||||||
|
|
||||||
|
# UPDATE:
|
||||||
|
def test_password_change_url_is_resolved(self):
|
||||||
|
url = reverse('account:password_change')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, PasswordChangeView)
|
||||||
|
|
||||||
|
def test_account_update_url_is_resolved(self):
|
||||||
|
url = reverse('account:update_profile')
|
||||||
|
self.assertEquals(resolve(url).func.view_class, views.UpdateProfile)
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.urls import path
|
from django.urls import path, reverse_lazy
|
||||||
from django.contrib.auth.views import LogoutView
|
from django.contrib.auth.views import (
|
||||||
|
LogoutView,
|
||||||
|
PasswordChangeView,
|
||||||
|
PasswordChangeDoneView
|
||||||
|
)
|
||||||
|
|
||||||
from rest_framework_simplejwt.views import (
|
from rest_framework_simplejwt.views import (
|
||||||
TokenObtainPairView,
|
TokenObtainPairView,
|
||||||
@@ -8,6 +12,8 @@ from rest_framework_simplejwt.views import (
|
|||||||
TokenVerifyView
|
TokenVerifyView
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import views
|
||||||
|
|
||||||
|
|
||||||
app_name = 'account'
|
app_name = 'account'
|
||||||
|
|
||||||
@@ -17,7 +23,35 @@ urlpatterns = [
|
|||||||
'account/logout/',
|
'account/logout/',
|
||||||
LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL),
|
LogoutView.as_view(next_page=settings.LOGOUT_REDIRECT_URL),
|
||||||
name='logout'
|
name='logout'
|
||||||
)
|
),
|
||||||
|
# CHANGE PASSWORD:
|
||||||
|
path(
|
||||||
|
'account/password-change/',
|
||||||
|
PasswordChangeView.as_view(
|
||||||
|
template_name='account/password_change.html',
|
||||||
|
success_url=reverse_lazy('account:password_change_done'),
|
||||||
|
),
|
||||||
|
name='password_change'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'account/password-change/done/',
|
||||||
|
PasswordChangeDoneView.as_view(
|
||||||
|
template_name='account/password_change_done.html'
|
||||||
|
),
|
||||||
|
name='password_change_done'
|
||||||
|
),
|
||||||
|
# UPDATE:
|
||||||
|
path(
|
||||||
|
'account/update/',
|
||||||
|
views.UpdateProfile.as_view(),
|
||||||
|
name='update_profile'
|
||||||
|
),
|
||||||
|
# READ:
|
||||||
|
path(
|
||||||
|
'account/show/',
|
||||||
|
views.DetailProfile.as_view(),
|
||||||
|
name='show_profile'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
urlpatterns += [
|
urlpatterns += [
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.views import generic
|
||||||
|
|
||||||
|
from collector.utils.mixins import ExtraContextMixin
|
||||||
|
|
||||||
|
from .forms import UserProfileForm
|
||||||
|
from .models import User
|
||||||
|
|
||||||
|
|
||||||
|
class DetailProfile(LoginRequiredMixin, ExtraContextMixin, generic.DetailView):
|
||||||
|
model = User
|
||||||
|
template_name = 'account/profile_info.html'
|
||||||
|
context_object_name = 'profile'
|
||||||
|
|
||||||
|
def get_title(self, **kwargs):
|
||||||
|
return f'{self.title} - {self.request.user}'
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.model.objects.get(username=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateProfile(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
|
||||||
|
model = User
|
||||||
|
template_name = 'account/profile_update.html'
|
||||||
|
context_object_name = 'profile'
|
||||||
|
form_class = UserProfileForm
|
||||||
|
|
||||||
|
def get_object(self):
|
||||||
|
return self.model.objects.get(username=self.request.user)
|
||||||
|
|
||||||
|
def get_title(self, **kwargs):
|
||||||
|
return f'{self.title} - {self.kwargs.get("username", "account")}'
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# █▀▄▀█ █▀▀ ▀█▀ ▄▀█ ▀
|
||||||
|
# █░▀░█ ██▄ ░█░ █▀█ ▄
|
||||||
|
# -------------------
|
||||||
|
__author__ = "MOIS3Y"
|
||||||
|
__credits__ = ["Stepan Zhukovsky"]
|
||||||
|
__license__ = "GPL v3.0"
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__maintainer__ = "Stepan Zhukovsky"
|
||||||
|
__email__ = "stepan@zhukovsky.me"
|
||||||
|
__status__ = "Production"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from django.utils.html import format_html
|
|||||||
from django.utils.translation import ngettext
|
from django.utils.translation import ngettext
|
||||||
|
|
||||||
from .models import Platform, Archive, Ticket
|
from .models import Platform, Archive, Ticket
|
||||||
from .utils import sizify
|
from .utils.helpers import sizify
|
||||||
|
|
||||||
|
|
||||||
class PlatformAdmin(admin.ModelAdmin):
|
class PlatformAdmin(admin.ModelAdmin):
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from django import forms
|
||||||
from django_filters.rest_framework import (
|
from django_filters.rest_framework import (
|
||||||
CharFilter,
|
CharFilter,
|
||||||
FilterSet,
|
FilterSet,
|
||||||
NumberFilter,
|
NumberFilter,
|
||||||
|
BaseInFilter,
|
||||||
)
|
)
|
||||||
from django_filters import widgets
|
from django_filters import widgets
|
||||||
|
|
||||||
@@ -9,21 +11,46 @@ from collector.models import Archive, Ticket
|
|||||||
from .utils import DateTimeFilterMixin
|
from .utils import DateTimeFilterMixin
|
||||||
|
|
||||||
|
|
||||||
|
class TextareaCSVWidget(widgets.BaseCSVWidget, forms.Textarea):
|
||||||
|
"""
|
||||||
|
The widget should create textarea.
|
||||||
|
"""
|
||||||
|
def render(self, name, value, attrs=None, renderer=None):
|
||||||
|
print("Row value: ", value)
|
||||||
|
if not self._isiterable(value):
|
||||||
|
value = [value]
|
||||||
|
|
||||||
|
if len(value) <= 1:
|
||||||
|
# delegate to main widget (Select, etc...) if not multiple values
|
||||||
|
value = value[0] if value else ''
|
||||||
|
return super(TextareaCSVWidget, self).render(name, value, attrs)
|
||||||
|
|
||||||
|
value = ','.join(value)
|
||||||
|
return super(TextareaCSVWidget, self).render(name, value, attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class NumberInFilter(BaseInFilter, NumberFilter):
|
||||||
|
"""
|
||||||
|
The filter should accept coma separated integers.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ArchiveFilter(DateTimeFilterMixin, FilterSet):
|
class ArchiveFilter(DateTimeFilterMixin, FilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Archive
|
model = Archive
|
||||||
fields = {
|
fields = {
|
||||||
'id': ['exact', 'in', 'lte', 'gte'],
|
'id': ['exact', 'lte', 'gte'],
|
||||||
'ticket': ['exact', 'in', 'lte', 'gte'],
|
'ticket': ['exact', 'lte', 'gte'],
|
||||||
'time_create': ['exact', 'lte', 'gte']
|
'time_create': ['exact', 'lte', 'gte']
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class TicketFilter(DateTimeFilterMixin, FilterSet):
|
class TicketFilter(DateTimeFilterMixin, FilterSet):
|
||||||
number = NumberFilter(
|
number = NumberInFilter(
|
||||||
field_name='number',
|
field_name='number',
|
||||||
widget=widgets.CSVWidget(),
|
widget=TextareaCSVWidget(),
|
||||||
)
|
)
|
||||||
user = CharFilter(
|
user = CharFilter(
|
||||||
field_name='user__username'
|
field_name='user__username'
|
||||||
|
|||||||
@@ -11,3 +11,15 @@ class IsGuestUpload(permissions.BasePermission):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return request.user.is_authenticated
|
return request.user.is_authenticated
|
||||||
|
|
||||||
|
|
||||||
|
class IsGuestCheckUrls(permissions.BasePermission):
|
||||||
|
"""
|
||||||
|
Special permission class for the ability to upload attachments
|
||||||
|
to an unauthorized user using a ticket token
|
||||||
|
"""
|
||||||
|
def has_permission(self, request, view):
|
||||||
|
if request.method in ('HEAD', 'OPTIONS', 'GET',):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return request.user.is_authenticated
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from drf_spectacular.openapi import OpenApiTypes
|
|||||||
from collector.models import Archive, Platform, Ticket
|
from collector.models import Archive, Platform, Ticket
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.NUMBER)
|
@extend_schema_field(OpenApiTypes.INT)
|
||||||
class TimestampField(serializers.Field):
|
class TimestampField(serializers.Field):
|
||||||
def to_representation(self, value) -> int:
|
def to_representation(self, value) -> int:
|
||||||
return value.timestamp()
|
return value.timestamp()
|
||||||
|
|
||||||
|
|
||||||
@extend_schema_field(OpenApiTypes.NUMBER)
|
@extend_schema_field(OpenApiTypes.INT)
|
||||||
class JsTimestampField(serializers.Field):
|
class JsTimestampField(serializers.Field):
|
||||||
def to_representation(self, value) -> int:
|
def to_representation(self, value) -> int:
|
||||||
return round(value.timestamp()*1000)
|
return round(value.timestamp()*1000)
|
||||||
@@ -26,7 +26,7 @@ class PublicArchiveUploadSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class ArchiveSerializer(serializers.ModelSerializer):
|
class ArchiveSerializer(serializers.ModelSerializer):
|
||||||
time_create = JsTimestampField(read_only=True)
|
time_create = serializers.DateTimeField(read_only=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Archive
|
model = Archive
|
||||||
@@ -41,8 +41,8 @@ class PlatformSerializer(serializers.ModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class TicketSerializer(serializers.ModelSerializer):
|
class TicketSerializer(serializers.ModelSerializer):
|
||||||
time_create = JsTimestampField(read_only=True)
|
time_create = serializers.DateTimeField(read_only=True)
|
||||||
time_update = JsTimestampField(read_only=True)
|
time_update = serializers.DateTimeField(read_only=True)
|
||||||
token = serializers.UUIDField(read_only=True)
|
token = serializers.UUIDField(read_only=True)
|
||||||
user = serializers.ReadOnlyField(source='user.username')
|
user = serializers.ReadOnlyField(source='user.username')
|
||||||
|
|
||||||
@@ -59,3 +59,33 @@ class TicketSerializer(serializers.ModelSerializer):
|
|||||||
'time_update',
|
'time_update',
|
||||||
'user'
|
'user'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class StorageInfoSerializer(serializers.Serializer):
|
||||||
|
total = serializers.IntegerField(read_only=True)
|
||||||
|
used = serializers.IntegerField(read_only=True)
|
||||||
|
free = serializers.IntegerField(read_only=True)
|
||||||
|
used_percent = serializers.IntegerField(read_only=True)
|
||||||
|
status = serializers.CharField(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStateRootSerializer(serializers.Serializer):
|
||||||
|
info = serializers.CharField(read_only=True, default="manual message")
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStateSerializer(serializers.ModelSerializer):
|
||||||
|
token = serializers.UUIDField(read_only=True)
|
||||||
|
attempts = serializers.IntegerField(read_only=True)
|
||||||
|
resolved = serializers.BooleanField(read_only=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Ticket
|
||||||
|
fields = [
|
||||||
|
'token',
|
||||||
|
'attempts',
|
||||||
|
'resolved'
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class AppHealthInfoSerializer(serializers.Serializer):
|
||||||
|
status = serializers.CharField(read_only=True, default="ok")
|
||||||
|
|||||||
@@ -29,3 +29,7 @@ class TestUrls(TestCase):
|
|||||||
def test_detail_platform_url_is_resolved(self):
|
def test_detail_platform_url_is_resolved(self):
|
||||||
url = reverse('collector_api:platform-detail', args=['vm6'])
|
url = reverse('collector_api:platform-detail', args=['vm6'])
|
||||||
self.assertEquals(resolve(url).func.cls, views.PlatformViewSet)
|
self.assertEquals(resolve(url).func.cls, views.PlatformViewSet)
|
||||||
|
|
||||||
|
def test_storage_info_url_is_resolved(self):
|
||||||
|
url = reverse('collector_api:storage-info')
|
||||||
|
self.assertEquals(resolve(url).func.cls, views.StorageInfo)
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from rest_framework.test import APITestCase
|
from rest_framework.test import APITestCase
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
from collector.models import Archive, Platform, Ticket
|
from collector.models import Archive, Platform, Ticket
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,31 @@ router.register(r'archives', views.ArchiveViewSet)
|
|||||||
router.register(r'platforms', views.PlatformViewSet)
|
router.register(r'platforms', views.PlatformViewSet)
|
||||||
router.register(r'tickets', views.TicketViewSet)
|
router.register(r'tickets', views.TicketViewSet)
|
||||||
|
|
||||||
|
check_urlpatterns = [
|
||||||
|
path(
|
||||||
|
'health/',
|
||||||
|
views.AppHealthInfo.as_view(),
|
||||||
|
name='app-info'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'storage/',
|
||||||
|
views.StorageInfo.as_view(),
|
||||||
|
name='storage-info'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'token/',
|
||||||
|
views.TokenStateRoot.as_view(),
|
||||||
|
name='token-root'
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
'token/<str:token>',
|
||||||
|
views.TokenStateInfo.as_view(),
|
||||||
|
name='token-info'
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# CRUD:
|
# CRUD:
|
||||||
path('v1/', include(router.urls)),
|
path('v1/', include(router.urls)),
|
||||||
|
path('v1/check/', include(check_urlpatterns)),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
from django.core.exceptions import ValidationError, ObjectDoesNotExist
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
# from rest_framework.decorators import action
|
# from rest_framework.decorators import action
|
||||||
@@ -9,26 +10,41 @@ from rest_framework.parsers import (
|
|||||||
)
|
)
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import viewsets
|
from rest_framework import filters, generics, views, viewsets
|
||||||
from rest_framework import filters
|
|
||||||
|
|
||||||
from django_filters.rest_framework import DjangoFilterBackend
|
from django_filters.rest_framework import DjangoFilterBackend
|
||||||
|
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema, extend_schema_view
|
||||||
from drf_spectacular.openapi import OpenApiParameter
|
from drf_spectacular.openapi import OpenApiParameter
|
||||||
|
|
||||||
from collector.models import Archive, Ticket, Platform
|
from collector.models import Archive, Ticket, Platform
|
||||||
|
from collector.utils.helpers import get_mount_fs_info
|
||||||
|
|
||||||
from .filters import ArchiveFilter, TicketFilter
|
from .filters import ArchiveFilter, TicketFilter
|
||||||
from .permissions import IsGuestUpload
|
from .permissions import IsGuestUpload, IsGuestCheckUrls
|
||||||
from .serializers import (
|
from .serializers import (
|
||||||
PublicArchiveUploadSerializer,
|
PublicArchiveUploadSerializer,
|
||||||
ArchiveSerializer,
|
ArchiveSerializer,
|
||||||
PlatformSerializer,
|
PlatformSerializer,
|
||||||
TicketSerializer
|
TicketSerializer,
|
||||||
|
StorageInfoSerializer,
|
||||||
|
TokenStateSerializer,
|
||||||
|
AppHealthInfoSerializer,
|
||||||
|
TokenStateRootSerializer,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
description='Archives that contains log files for checking',
|
||||||
|
summary='Show all archives'
|
||||||
|
),
|
||||||
|
create=extend_schema(summary='Create (upload) a new archive'),
|
||||||
|
retrieve=extend_schema(summary='Show archive by id'),
|
||||||
|
update=extend_schema(summary='Update archive'),
|
||||||
|
partial_update=extend_schema(summary='Update archive field'),
|
||||||
|
destroy=extend_schema(summary='Delete archive'),
|
||||||
|
)
|
||||||
class ArchiveViewSet(viewsets.ModelViewSet):
|
class ArchiveViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Archive.objects.order_by('-time_create')
|
queryset = Archive.objects.order_by('-time_create')
|
||||||
serializer_class = ArchiveSerializer
|
serializer_class = ArchiveSerializer
|
||||||
@@ -52,10 +68,10 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
|||||||
},
|
},
|
||||||
parameters=[
|
parameters=[
|
||||||
OpenApiParameter(
|
OpenApiParameter(
|
||||||
name='Upload-Token',
|
name='Upload-Token',
|
||||||
type=str,
|
type=str,
|
||||||
location=OpenApiParameter.HEADER,
|
location=OpenApiParameter.HEADER,
|
||||||
description="upload permission token",
|
description="upload permission token",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
@@ -67,29 +83,35 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
|||||||
bound_ticket = Ticket.objects.get(token=upload_token)
|
bound_ticket = Ticket.objects.get(token=upload_token)
|
||||||
if bound_ticket.resolved:
|
if bound_ticket.resolved:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'ticket {bound_ticket} already resolved'},
|
{'detail': f'ticket {bound_ticket} already resolved'},
|
||||||
status=status.HTTP_423_LOCKED
|
status=status.HTTP_423_LOCKED
|
||||||
)
|
)
|
||||||
if bound_ticket.attempts <= 0:
|
if bound_ticket.attempts <= 0:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'token {upload_token} expired'},
|
{'detail': f'token {upload_token} expired'},
|
||||||
status=status.HTTP_423_LOCKED
|
status=status.HTTP_423_LOCKED
|
||||||
)
|
)
|
||||||
bound_ticket.attempts -= 1
|
bound_ticket.attempts -= 1
|
||||||
bound_ticket.save()
|
bound_ticket.save()
|
||||||
# ? mixin bound ticket number to request.data from user
|
# ? mixin bound ticket number to request.data from user
|
||||||
request.data['ticket'] = bound_ticket.number
|
try:
|
||||||
|
request.data['ticket'] = bound_ticket.number
|
||||||
|
except AttributeError:
|
||||||
|
return Response(
|
||||||
|
{'detail': 'Bad Request'},
|
||||||
|
status=status.HTTP_400_BAD_REQUEST
|
||||||
|
)
|
||||||
# ? change serializer for guest user
|
# ? change serializer for guest user
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
self.serializer_class = PublicArchiveUploadSerializer
|
self.serializer_class = PublicArchiveUploadSerializer
|
||||||
except (ValidationError, ObjectDoesNotExist,):
|
except (ValidationError, ObjectDoesNotExist,):
|
||||||
return Response(
|
return Response(
|
||||||
{'error': f'token {upload_token} is not valid'},
|
{'detail': f'token {upload_token} is not valid'},
|
||||||
status=status.HTTP_403_FORBIDDEN
|
status=status.HTTP_403_FORBIDDEN
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
return Response(
|
||||||
{'error': 'Header Upload-Token is required'},
|
{'detail': 'Header Upload-Token is required'},
|
||||||
status=status.HTTP_401_UNAUTHORIZED
|
status=status.HTTP_401_UNAUTHORIZED
|
||||||
)
|
)
|
||||||
# ! default create method:
|
# ! default create method:
|
||||||
@@ -104,6 +126,17 @@ class ArchiveViewSet(viewsets.ModelViewSet):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
description='Platforms are needed to relative the ticket and software',
|
||||||
|
summary='Show all platforms'
|
||||||
|
),
|
||||||
|
create=extend_schema(summary='Create a new platform'),
|
||||||
|
retrieve=extend_schema(summary='Show platform by internal name'),
|
||||||
|
update=extend_schema(summary='Update platform'),
|
||||||
|
partial_update=extend_schema(summary='Update platform field'),
|
||||||
|
destroy=extend_schema(summary='Delete platform'),
|
||||||
|
)
|
||||||
class PlatformViewSet(viewsets.ModelViewSet):
|
class PlatformViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Platform.objects.all()
|
queryset = Platform.objects.all()
|
||||||
lookup_field = 'name'
|
lookup_field = 'name'
|
||||||
@@ -111,6 +144,17 @@ class PlatformViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = (IsAuthenticated, )
|
permission_classes = (IsAuthenticated, )
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
list=extend_schema(
|
||||||
|
description='Tickets that will be related with the uploaded archive',
|
||||||
|
summary='Show all tickets'
|
||||||
|
),
|
||||||
|
create=extend_schema(summary='Create a new ticket'),
|
||||||
|
retrieve=extend_schema(summary='Show ticket by number'),
|
||||||
|
update=extend_schema(summary='Update ticket'),
|
||||||
|
partial_update=extend_schema(summary='Update ticket field'),
|
||||||
|
destroy=extend_schema(summary='Delete ticket'),
|
||||||
|
)
|
||||||
class TicketViewSet(viewsets.ModelViewSet):
|
class TicketViewSet(viewsets.ModelViewSet):
|
||||||
queryset = Ticket.objects.order_by('-time_create')
|
queryset = Ticket.objects.order_by('-time_create')
|
||||||
lookup_field = 'number'
|
lookup_field = 'number'
|
||||||
@@ -122,3 +166,53 @@ class TicketViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
serializer.save(user=self.request.user)
|
serializer.save(user=self.request.user)
|
||||||
|
|
||||||
|
|
||||||
|
class StorageInfo(views.APIView):
|
||||||
|
"""Info about storage total/used/free space"""
|
||||||
|
|
||||||
|
permission_classes = (IsAuthenticated, )
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses=StorageInfoSerializer,
|
||||||
|
summary='Show storage space in bytes'
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
return Response(get_mount_fs_info(settings.DATA_DIR))
|
||||||
|
|
||||||
|
|
||||||
|
class TokenStateRoot(views.APIView):
|
||||||
|
""" Show the message of a specific upload token URL"""
|
||||||
|
permission_classes = (IsGuestCheckUrls,)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses=TokenStateRootSerializer,
|
||||||
|
summary='Show info message how get token status'
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
message = "to find out the status of the token, place it in the URL"
|
||||||
|
return Response({"detail": message}, status=status.HTTP_303_SEE_OTHER)
|
||||||
|
|
||||||
|
|
||||||
|
@extend_schema_view(
|
||||||
|
get=extend_schema(
|
||||||
|
summary='Show the status of a specific upload token'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class TokenStateInfo(generics.RetrieveAPIView):
|
||||||
|
""" Show the status of a specific upload token"""
|
||||||
|
queryset = Ticket.objects.order_by('-time_create')
|
||||||
|
lookup_field = 'token'
|
||||||
|
serializer_class = TokenStateSerializer
|
||||||
|
permission_classes = (IsGuestCheckUrls,)
|
||||||
|
|
||||||
|
|
||||||
|
class AppHealthInfo(views.APIView):
|
||||||
|
permission_classes = (IsGuestCheckUrls,)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
responses=AppHealthInfoSerializer,
|
||||||
|
summary='Show app status'
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
return Response({'status': 'ok'}, status=status.HTTP_200_OK)
|
||||||
|
|||||||
16
logs_collector/collector/context_processors.py
Normal file
16
logs_collector/collector/context_processors.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
from . import __author__
|
||||||
|
from .utils.helpers import get_mount_fs_info
|
||||||
|
|
||||||
|
|
||||||
|
def metadata(request):
|
||||||
|
return {
|
||||||
|
"version": settings.VERSION,
|
||||||
|
"environment": settings.ENVIRONMENT,
|
||||||
|
"author": __author__,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def storage_info(request):
|
||||||
|
return {'storage': get_mount_fs_info(settings.DATA_DIR)}
|
||||||
0
logs_collector/collector/management/__init__.py
Normal file
0
logs_collector/collector/management/__init__.py
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.apps import apps
|
||||||
|
from django.db.models import Q
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db.models import FileField
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
logging.config.dictConfig({
|
||||||
|
'version': 1,
|
||||||
|
'disable_existing_loggers': False,
|
||||||
|
'formatters': {
|
||||||
|
'console': {
|
||||||
|
'format': '%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'handlers': {
|
||||||
|
'console': {
|
||||||
|
'class': 'logging.StreamHandler',
|
||||||
|
'formatter': 'console'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'loggers': {
|
||||||
|
'': {
|
||||||
|
'level': 'INFO',
|
||||||
|
'handlers': ['console']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
# HELP MESSAGE:
|
||||||
|
help_part1 = 'This command deletes all media files from'
|
||||||
|
help_part2 = 'the MEDIA_ROOT directory which are no longer referenced'
|
||||||
|
help_part3 = 'by any of the models from installed_apps'
|
||||||
|
help = f'{help_part1} {help_part2} {help_part3}'
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
logger.info('Start cleanup storage....')
|
||||||
|
all_models = apps.get_models()
|
||||||
|
physical_files = set()
|
||||||
|
db_files = set()
|
||||||
|
# Get all files from the database
|
||||||
|
logger.info('Get all files from the database....')
|
||||||
|
for model in all_models:
|
||||||
|
file_fields = []
|
||||||
|
filters = Q()
|
||||||
|
for f_ in model._meta.fields:
|
||||||
|
if isinstance(f_, FileField):
|
||||||
|
file_fields.append(f_.name)
|
||||||
|
is_null = {'{}__isnull'.format(f_.name): True}
|
||||||
|
is_empty = {'{}__exact'.format(f_.name): ''}
|
||||||
|
filters &= Q(**is_null) | Q(**is_empty)
|
||||||
|
# only retrieve the models which have non-empty,
|
||||||
|
# non-null file fields
|
||||||
|
if file_fields:
|
||||||
|
files = model.objects.exclude(filters).values_list(
|
||||||
|
*file_fields,
|
||||||
|
flat=True
|
||||||
|
).distinct()
|
||||||
|
db_files.update(files)
|
||||||
|
logger.info(f'Find: {len(db_files)} files from the database')
|
||||||
|
# Get all files from the MEDIA_ROOT, recursively
|
||||||
|
logger.info('Get all files from the MEDIA_ROOT, recursively....')
|
||||||
|
media_root = getattr(settings, 'MEDIA_ROOT', None)
|
||||||
|
if media_root is not None:
|
||||||
|
for relative_root, dirs, files in os.walk(media_root):
|
||||||
|
for file_ in files:
|
||||||
|
# Compute the relative file path to the media directory,
|
||||||
|
# so it can be compared to the values from the db
|
||||||
|
relative_file = os.path.join(
|
||||||
|
os.path.relpath(relative_root, media_root), file_
|
||||||
|
)
|
||||||
|
physical_files.add(relative_file)
|
||||||
|
logger.info(f'Find: {len(physical_files)} files from the MEDIA_ROOT')
|
||||||
|
# Compute the difference and delete those files
|
||||||
|
logger.info('Compute the difference and delete those files....')
|
||||||
|
deletables = physical_files - db_files
|
||||||
|
logger.info(f'Find: {len(deletables)} orphan files')
|
||||||
|
if deletables:
|
||||||
|
for file_ in deletables:
|
||||||
|
logger.info(f"Delete orphan file: {file_}")
|
||||||
|
os.remove(os.path.join(media_root, file_))
|
||||||
|
# Bottom-up - delete all empty folders
|
||||||
|
logger.info('Bottom-up - delete all empty folders....')
|
||||||
|
for relative_root, dirs, files in os.walk(
|
||||||
|
media_root, topdown=False):
|
||||||
|
for dir_ in dirs:
|
||||||
|
if not os.listdir(os.path.join(relative_root, dir_)):
|
||||||
|
os.rmdir(os.path.join(relative_root, dir_))
|
||||||
|
logger.info('Done! Storage has been cleaned up')
|
||||||
|
logger.info('Done! Nothing to delete')
|
||||||
24
logs_collector/collector/middleware.py
Normal file
24
logs_collector/collector/middleware.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from django.http import HttpResponse
|
||||||
|
from django.template import loader
|
||||||
|
|
||||||
|
|
||||||
|
class HttpResponseNotAllowedMiddleware:
|
||||||
|
def __init__(self, get_response):
|
||||||
|
self.get_response = get_response
|
||||||
|
# One-time configuration and initialization.
|
||||||
|
|
||||||
|
def __call__(self, request):
|
||||||
|
|
||||||
|
# Code to be executed for each request before
|
||||||
|
# the view (and later middleware) are called.
|
||||||
|
|
||||||
|
response = self.get_response(request)
|
||||||
|
|
||||||
|
# Code to be executed for each request/response after
|
||||||
|
# the view is called.
|
||||||
|
if response.status_code == 405:
|
||||||
|
context = {}
|
||||||
|
template = loader.get_template('405.html')
|
||||||
|
return HttpResponse(template.render(context, request))
|
||||||
|
|
||||||
|
return response
|
||||||
@@ -47,7 +47,7 @@ class Migration(migrations.Migration):
|
|||||||
name='Archive',
|
name='Archive',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('file', models.FileField(upload_to=collector.utils.logs_dir_path)),
|
('file', models.FileField(upload_to=collector.utils.helpers.logs_dir_path)),
|
||||||
('size', models.BigIntegerField(editable=False)),
|
('size', models.BigIntegerField(editable=False)),
|
||||||
('md5', models.CharField(editable=False, max_length=1024)),
|
('md5', models.CharField(editable=False, max_length=1024)),
|
||||||
('time_create', models.DateTimeField(auto_now_add=True)),
|
('time_create', models.DateTimeField(auto_now_add=True)),
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import uuid
|
|||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
from django.core.validators import MaxValueValidator, MinValueValidator
|
from django.core.validators import MaxValueValidator, MinValueValidator
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from .utils import logs_dir_path
|
from account.models import User
|
||||||
|
|
||||||
|
from .utils.helpers import logs_dir_path
|
||||||
|
|
||||||
|
|
||||||
class Archive(models.Model):
|
class Archive(models.Model):
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
const tooltipTriggerList = document.querySelectorAll(
|
|
||||||
'[data-bs-toggle="tooltip"]'
|
|
||||||
)
|
|
||||||
const tooltipList = [...tooltipTriggerList].map(
|
|
||||||
(tooltipTriggerEl) => new bootstrap.Tooltip(tooltipTriggerEl)
|
|
||||||
)
|
|
||||||
83
logs_collector/collector/static/collector/js/helpers.js
Normal file
83
logs_collector/collector/static/collector/js/helpers.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// formatted byte size to human readable:
|
||||||
|
const sizify = (value) => {
|
||||||
|
let ext = ''
|
||||||
|
if (value < 512000) {
|
||||||
|
value = value / 1024.0
|
||||||
|
ext = 'KB'
|
||||||
|
} else if (value < 4194304000) {
|
||||||
|
value = value / 1048576.0
|
||||||
|
ext = 'MB'
|
||||||
|
} else {
|
||||||
|
value = value / 1073741824.0
|
||||||
|
ext = 'GB'
|
||||||
|
};
|
||||||
|
return `${Math.round(value * 10) / 10} ${ext}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// fix update bootstrap tooltip func:
|
||||||
|
const updateBsTooltip = (instance) => {
|
||||||
|
let tt = bootstrap.Tooltip.getInstance(instance);
|
||||||
|
tt.dispose();
|
||||||
|
bootstrap.Tooltip.getOrCreateInstance(instance);
|
||||||
|
};
|
||||||
|
|
||||||
|
// update storage info widget:
|
||||||
|
const updateStorageInfo = () => {
|
||||||
|
// set storage items vars:
|
||||||
|
let storageIcon = $("#storage_icon")
|
||||||
|
let storageProgressContainer = $("#storage_progress_container")
|
||||||
|
let storage_progress = $("#storage_progress")
|
||||||
|
// set API url:
|
||||||
|
const storageUrl = storage_progress.attr("storage-url")
|
||||||
|
$.ajax({
|
||||||
|
type: "GET",
|
||||||
|
url: storageUrl,
|
||||||
|
headers: {
|
||||||
|
"Content-Type":"application/json"
|
||||||
|
},
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data, textStatus, jqXHR) {
|
||||||
|
// JSON answer:
|
||||||
|
let storage = data;
|
||||||
|
// set updated fields:
|
||||||
|
let storageInfoNewFields = [
|
||||||
|
`Total: ${sizify(storage.total)}`,
|
||||||
|
'<br>',
|
||||||
|
`Used: ${sizify(storage.used)}`,
|
||||||
|
'<br>',
|
||||||
|
`Free: ${sizify(storage.free)}`
|
||||||
|
].join('')
|
||||||
|
// progress bar update:
|
||||||
|
storage_progress.attr("style", `width:${storage.used_percent}%`)
|
||||||
|
// progress bar color update:
|
||||||
|
if (storage.used_percent > 90) {
|
||||||
|
storage_progress.attr("class", "progress-bar bg-danger");
|
||||||
|
} else if (storage.used_percent > 80) {
|
||||||
|
storage_progress.attr("class", "progress-bar bg-warning");
|
||||||
|
} else {
|
||||||
|
storage_progress.attr("class", "progress-bar bg-success");
|
||||||
|
};
|
||||||
|
// tooltips update:
|
||||||
|
storageIcon.attr("data-bs-title", `Storage used: ${storage.used_percent}%`)
|
||||||
|
storageProgressContainer.attr("data-bs-title", storageInfoNewFields)
|
||||||
|
updateBsTooltip(storageIcon)
|
||||||
|
updateBsTooltip(storageProgressContainer)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const genAlertMessage = (
|
||||||
|
alertMessage='Success message',
|
||||||
|
alertType='success',
|
||||||
|
extraClass=''
|
||||||
|
) => {
|
||||||
|
let alertMessageHTML = [
|
||||||
|
`<div class="alert alert-${alertType} alert-dismissible ${extraClass}" role="alert">`,
|
||||||
|
` <div>${alertMessage}</div>`,
|
||||||
|
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
||||||
|
'</div>'
|
||||||
|
].join('')
|
||||||
|
return alertMessageHTML
|
||||||
|
}
|
||||||
|
|
||||||
|
export {sizify, updateBsTooltip, updateStorageInfo, genAlertMessage};
|
||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import {updateStorageInfo} from "./helpers.js";
|
||||||
|
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
console.log("JQ is ready to work");
|
console.log("JQ is ready to work");
|
||||||
|
|
||||||
@@ -24,8 +27,11 @@ $(function () {
|
|||||||
success: function (data, textStatus, jqXHR) {
|
success: function (data, textStatus, jqXHR) {
|
||||||
console.log(jqXHR.status);
|
console.log(jqXHR.status);
|
||||||
$(archiveListElement).hide(1500);
|
$(archiveListElement).hide(1500);
|
||||||
|
setTimeout(() => {
|
||||||
|
updateStorageInfo();
|
||||||
|
}, 3000);
|
||||||
},
|
},
|
||||||
error: function (data, textStatus, jqXHR) {
|
error: function (jqXHR, textStatus, errorThrown) {
|
||||||
console.log(jqXHR.status);
|
console.log(jqXHR.status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -57,7 +63,7 @@ $(function () {
|
|||||||
success: function (data, textStatus, jqXHR) {
|
success: function (data, textStatus, jqXHR) {
|
||||||
console.log(jqXHR.status)
|
console.log(jqXHR.status)
|
||||||
},
|
},
|
||||||
error: function (data, textStatus, jqXHR) {
|
error: function (jqXHR, textStatus, errorThrown) {
|
||||||
console.log(data)
|
console.log(data)
|
||||||
console.log(jqXHR.status)
|
console.log(jqXHR.status)
|
||||||
}
|
}
|
||||||
@@ -82,11 +88,14 @@ $(function () {
|
|||||||
console.log(jqXHR.status);
|
console.log(jqXHR.status);
|
||||||
if (delDiv.length) {
|
if (delDiv.length) {
|
||||||
delDiv.hide(1500);
|
delDiv.hide(1500);
|
||||||
|
setTimeout(() => {
|
||||||
|
updateStorageInfo();
|
||||||
|
}, 3000);
|
||||||
} else {
|
} else {
|
||||||
window.location.href = redirectUrl;
|
window.location.href = redirectUrl;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function (data, textStatus, jqXHR) {
|
error: function (jqXHR, textStatus, errorThrown) {
|
||||||
console.log(jqXHR.status);
|
console.log(jqXHR.status);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,81 +1,138 @@
|
|||||||
|
import {updateStorageInfo, genAlertMessage} from "./helpers.js";
|
||||||
|
|
||||||
$(function () {
|
$(function () {
|
||||||
|
// set global variables:
|
||||||
const uploadForm = document.getElementById('upload_form');
|
const uploadForm = document.getElementById('upload_form');
|
||||||
const input_file = document.getElementById('id_file');
|
const inputFile = document.getElementById('id_file');
|
||||||
const progress_bar = document.getElementById('progress');
|
const progressBar = document.getElementById('progress');
|
||||||
const alert_container = document.getElementById('alert');
|
const alertContainer = document.getElementById('alert');
|
||||||
|
// get upload form:
|
||||||
$("#upload_form").submit(function(e){
|
$("#upload_form").submit(function(e){
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
$form = $(this)
|
// collect request data:
|
||||||
let formData = new FormData(this);
|
let formData = new FormData(this);
|
||||||
let upload_token = formData.get("token")
|
let uploadToken = formData.get("token")
|
||||||
const media_data = input_file.files[0];
|
// generate the URL for token validation:
|
||||||
if(media_data != null){
|
let tokenStatusUrl = [
|
||||||
progress_bar.classList.remove("not-visible");
|
progressBar.getAttribute('token-status-url'),
|
||||||
}
|
uploadToken
|
||||||
$.ajax({
|
].join('')
|
||||||
type: 'POST',
|
// init upload file func:
|
||||||
url: progress_bar.getAttribute("upload-url"),
|
const uploadFile = () => {
|
||||||
data: formData,
|
// toggle visible progress bar:
|
||||||
dataType: 'json',
|
const mediaData = inputFile.files[0];
|
||||||
xhr:function(){
|
if(mediaData != null){
|
||||||
const xhr = new window.XMLHttpRequest();
|
progressBar.classList.remove("not-visible");
|
||||||
xhr.timeout = 3600000; // increase request timeout to 1 hour
|
}
|
||||||
xhr.upload.addEventListener('progress', e=>{
|
// upload file (chunk) xrh request:
|
||||||
if(e.lengthComputable){
|
$.ajax({
|
||||||
const percentProgress = (e.loaded/e.total)*100;
|
type: 'POST',
|
||||||
console.log(percentProgress);
|
url: progressBar.getAttribute("upload-url"),
|
||||||
progress_bar.innerHTML = `
|
data: formData,
|
||||||
<div
|
dataType: 'json',
|
||||||
class="progress-bar progress-bar-striped progress-bar-animated"
|
xhr:function(){
|
||||||
style="width: ${percentProgress}%"
|
const xhr = new window.XMLHttpRequest();
|
||||||
>
|
xhr.timeout = 3600000; // increase request timeout to 1 hour
|
||||||
</div>`
|
xhr.upload.addEventListener('progress', e=>{
|
||||||
|
if(e.lengthComputable){
|
||||||
|
const percentProgress = (e.loaded/e.total)*100;
|
||||||
|
console.log(percentProgress);
|
||||||
|
progressBar.innerHTML = `
|
||||||
|
<div
|
||||||
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
style="width: ${percentProgress}%"
|
||||||
|
>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return xhr
|
||||||
|
},
|
||||||
|
// set auth method:
|
||||||
|
beforeSend: function(xhr) {
|
||||||
|
if (uploadToken) {
|
||||||
|
xhr.setRequestHeader("Upload-Token", uploadToken);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
return xhr
|
success: function(data, textStatus, jqXHR){
|
||||||
|
alertContainer.innerHTML = genAlertMessage(
|
||||||
|
'The file has been successfully uploaded to the server. Thank you!',
|
||||||
|
'success',
|
||||||
|
'col-lg-6'
|
||||||
|
)
|
||||||
|
uploadForm.reset()
|
||||||
|
progressBar.classList.add('not-visible')
|
||||||
|
try {
|
||||||
|
updateStorageInfo();
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
error: function(jqXHR, textStatus, errorThrown){
|
||||||
|
let errorMessage = "Unexpected error. Try again please"
|
||||||
|
if (jqXHR.status === 423 || jqXHR.status === 403) {
|
||||||
|
errorMessage = `Error ${jqXHR.status} <br> ${jqXHR.responseJSON.detail}`
|
||||||
|
}
|
||||||
|
if (jqXHR.status === 401) {
|
||||||
|
errorMessage = `Error ${jqXHR.status} <br> The token field cannot be empty`
|
||||||
|
}
|
||||||
|
if (jqXHR.status === 400) {
|
||||||
|
errorMessage = `Error ${jqXHR.status} <br> ${jqXHR.responseJSON.detail}`
|
||||||
|
}
|
||||||
|
alertContainer.innerHTML = genAlertMessage(
|
||||||
|
errorMessage,
|
||||||
|
'danger',
|
||||||
|
'col-lg-6'
|
||||||
|
)
|
||||||
|
progressBar.classList.add('not-visible')
|
||||||
|
},
|
||||||
|
cache: false,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// check token status and upload file if token valid:
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
url: tokenStatusUrl,
|
||||||
|
dataType: "json",
|
||||||
|
success: function (data, textStatus, jqXHR) {
|
||||||
|
if (data.attempts === 0) {
|
||||||
|
alertContainer.innerHTML = genAlertMessage(
|
||||||
|
`Error 423 <br> Token: ${uploadToken} expired`,
|
||||||
|
'danger',
|
||||||
|
'col-lg-6'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (data.resolved === true) {
|
||||||
|
alertContainer.innerHTML = genAlertMessage(
|
||||||
|
`Error 423 <br> Ticket bound with token: ${uploadToken} <br> already resolved`,
|
||||||
|
'danger',
|
||||||
|
'col-lg-6'
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
alertContainer.innerHTML = genAlertMessage(
|
||||||
|
`Token: ${uploadToken} is valid. <br> Starting to upload...`,
|
||||||
|
'success',
|
||||||
|
'col-lg-6'
|
||||||
|
);
|
||||||
|
uploadFile();
|
||||||
|
};
|
||||||
},
|
},
|
||||||
beforeSend: function(xhr) {
|
error: function(jqXHR){
|
||||||
if (upload_token) {
|
if (jqXHR.responseJSON.detail) {
|
||||||
xhr.setRequestHeader("Upload-Token", upload_token);
|
alertContainer.innerHTML = genAlertMessage(
|
||||||
|
`Error 403 <br> Token: ${uploadToken} is not valid`,
|
||||||
|
'danger',
|
||||||
|
'col-lg-6'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
alertContainer.innerHTML = genAlertMessage(
|
||||||
|
`Unexpected error. Try again please`,
|
||||||
|
'danger',
|
||||||
|
'col-lg-6'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
success: function(data, textStatus, jqXHR){
|
|
||||||
console.log(jqXHR.status);
|
|
||||||
let type = "success";
|
|
||||||
alert_container.innerHTML = [
|
|
||||||
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
|
|
||||||
` <div>The file has been successfully uploaded to the server. Thank you!</div>`,
|
|
||||||
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
|
||||||
'</div>'
|
|
||||||
].join('')
|
|
||||||
uploadForm.reset()
|
|
||||||
progress_bar.classList.add('not-visible')
|
|
||||||
},
|
|
||||||
error: function(jqXHR, textStatus, errorThrown){
|
|
||||||
console.log(jqXHR);
|
|
||||||
let type = "danger";
|
|
||||||
let error_message = "Unexpected error. Try again please"
|
|
||||||
if (jqXHR.status === 423) {
|
|
||||||
error_message = `Error ${jqXHR.status}: ${jqXHR.responseJSON.error}`
|
|
||||||
}
|
|
||||||
if (jqXHR.status === 403) {
|
|
||||||
error_message = `Error ${jqXHR.status}: ${jqXHR.responseJSON.error}`
|
|
||||||
}
|
|
||||||
if (jqXHR.status === 401) {
|
|
||||||
error_message = 'The token field cannot be empty'
|
|
||||||
}
|
|
||||||
alert_container.innerHTML = [
|
|
||||||
`<div class="alert alert-${type} alert-dismissible col-lg-6" role="alert">`,
|
|
||||||
` <div>${error_message}</div>`,
|
|
||||||
' <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>',
|
|
||||||
'</div>'
|
|
||||||
].join('')
|
|
||||||
progress_bar.classList.add('not-visible')
|
|
||||||
},
|
|
||||||
cache: false,
|
|
||||||
contentType: false,
|
|
||||||
processData: false,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
<div class="container mt-3">
|
<div class="container mt-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Archive upload:</h3>
|
<h3 class="card-title">Archive upload:</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
<div
|
<div
|
||||||
id="progress"
|
id="progress"
|
||||||
upload-url="{% url 'collector_api:archive-list' %}"
|
upload-url="{% url 'collector_api:archive-list' %}"
|
||||||
class="progress"
|
token-status-url="{% url 'collector_api:token-root' %}"
|
||||||
|
class="progress not-visible"
|
||||||
role="progressbar"
|
role="progressbar"
|
||||||
aria-label="Example 20px high"
|
aria-label="Example 20px high"
|
||||||
aria-valuenow="25"
|
aria-valuenow="25"
|
||||||
@@ -51,5 +52,5 @@
|
|||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
|
|
||||||
{% block jquery %}
|
{% block jquery %}
|
||||||
<script src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
|
<script type="module" src="{% static 'collector/js/jq.upload.progress.js' %}"></script>
|
||||||
{% endblock jquery %}
|
{% endblock jquery %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
{% endblock collector_head %}
|
{% endblock collector_head %}
|
||||||
|
|
||||||
{% block collector_content %}
|
{% block collector_content %}
|
||||||
<header>
|
<header class="sticky-top">
|
||||||
<section>
|
<section>
|
||||||
{% include 'includes/navigation.html' %}
|
{% include 'includes/navigation.html' %}
|
||||||
</section>
|
</section>
|
||||||
@@ -16,9 +16,9 @@
|
|||||||
{% block main %}{% endblock main %}
|
{% block main %}{% endblock main %}
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer class="footer mt-auto">
|
||||||
<section>
|
<section>
|
||||||
{% block footer %}{% endblock footer %}
|
{% include 'includes/footer.html' %}
|
||||||
</section>
|
</section>
|
||||||
</footer>
|
</footer>
|
||||||
{% endblock collector_content %}
|
{% endblock collector_content %}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="LabelTicket">
|
<h5 class="modal-title" id="LabelTicket">
|
||||||
Delete ticket #{{ ticket.number }} ?
|
Delete ticket {{ ticket.number }}?
|
||||||
</h5>
|
</h5>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -2,37 +2,32 @@
|
|||||||
<li
|
<li
|
||||||
id="li-archive-{{ archive.id }}"
|
id="li-archive-{{ archive.id }}"
|
||||||
class="list-group-item list-group-item-action">
|
class="list-group-item list-group-item-action">
|
||||||
<smal>
|
<div class="input-group input-group-sm mb-1">
|
||||||
<b>File:</b>
|
<span class="input-group-text" style="width: 60px">File</span>
|
||||||
<span style="word-wrap: break-word">{{ archive.file.name|clean_filename }}</span>
|
<input type="text" class="form-control" value="{{ archive.file.name|clean_filename }}" disabled readonly>
|
||||||
</small>
|
</div>
|
||||||
<small>
|
<div class="input-group input-group-sm mb-1">
|
||||||
<br>
|
<span class="input-group-text" style="width: 60px">MD5</span>
|
||||||
<b>MD5:</b>
|
<input type="text" class="form-control" value="{{ archive.md5 }}" disabled readonly>
|
||||||
<span style="word-wrap: break-word">{{ archive.md5 }}</span>
|
</div>
|
||||||
</small>
|
<div class="input-group input-group-sm mb-1">
|
||||||
<small>
|
<span class="input-group-text" style="width: 60px">Time</span>
|
||||||
<br>
|
<input type="text" class="form-control" value="{{ archive.time_update|date:"D d.m.y H:i" }}" disabled readonly>
|
||||||
<b>Uploaded:</b>
|
</div>
|
||||||
<span style="word-wrap: break-word">{{ archive.time_update|date:"D d.m.y H:i" }}</span>
|
<div class="input-group input-group-sm mb-1">
|
||||||
</small>
|
<span class="input-group-text" style="width: 60px">Size</span>
|
||||||
<br>
|
<input type="text" class="form-control" value="{{ archive.size|filesizeformat }}" disabled readonly>
|
||||||
<small>
|
</div>
|
||||||
<b>Size:</b>
|
<div class="d-flex justify-content-sm-start justify-content-between" >
|
||||||
<span style="word-wrap: break-word">{{ archive.size|filesizeformat }}</span>
|
<a
|
||||||
</small>
|
class="btn btn-outline-success btn-sm mt-2"
|
||||||
<div class="row">
|
href="{{ archive.get_absolute_url }}"
|
||||||
<div class="d-flex justify-content-sm-start justify-content-between" >
|
><i class="bi bi-download"></i> GET</a>
|
||||||
<a
|
<button
|
||||||
class="btn btn-outline-success btn-sm mt-2"
|
button type="button"
|
||||||
href="{{ archive.get_absolute_url }}"
|
class="btn btn-outline-danger btn-sm ms-2 mt-2"
|
||||||
><i class="bi bi-download"></i> GET</a>
|
data-bs-toggle="modal"
|
||||||
<button
|
data-bs-target="#modal-archive-del-{{ archive.id }}"
|
||||||
button type="button"
|
><i class="bi bi-trash"></i> DEL</button>
|
||||||
class="btn btn-outline-danger btn-sm ms-2 mt-2"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#modal-archive-del-{{ archive.id }}"
|
|
||||||
><i class="bi bi-trash"></i> DEL</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<div class="d-sm-flex w-100 justify-content-between mb-2">
|
<div class="d-sm-flex w-100 justify-content-between mb-2">
|
||||||
<h4 class="card-title mb-1">Ticket: {{ ticket.number }}</h4>
|
<h4 class="card-title mb-1"><i class="bi bi-bookmarks"></i> Ticket: {{ ticket.number }}</h4>
|
||||||
<small><i class="bi bi-clock-history"></i> {{ ticket.time_create|date:"D d.m.y H:i" }}</small>
|
<small><i class="bi bi-clock-history"></i> {{ ticket.time_create|date:"D d.m.y H:i" }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check form-switch form-check-reverse d-flex w-100 justify-content-left">
|
<div class="form-check form-switch form-check-reverse d-flex w-100 justify-content-left mb-2">
|
||||||
<label class="form-check-label" for="ticket-state">Resolved:</label>
|
<label class="form-check-label" for="ticket-state"><i class="bi bi-award"></i> Resolved:</label>
|
||||||
<input
|
<input
|
||||||
class="form-check-input ms-2 mb-2"
|
class="form-check-input ms-2 mb-2"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -14,13 +14,29 @@
|
|||||||
{% if ticket.resolved %} checked {% endif %}>
|
{% if ticket.resolved %} checked {% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-xl-6 mb-2">
|
<div class="col-xl-6 mb-2">
|
||||||
<h6 class="card-title mb-1">Platform: {{ ticket.platform.pretty_name }}</h6>
|
<h6 class="card-title mb-1">
|
||||||
<h6 class="card-title mb-3">Owner: {{ ticket.user.username }}</h6>
|
<small class="me-1"><i class="bi bi-box"></i></small>
|
||||||
|
Platform: {{ ticket.platform.pretty_name }}
|
||||||
|
</h6>
|
||||||
|
<h6 class="card-title mb-1">
|
||||||
|
<small class="me-1"><i class="bi bi-person-circle"></i></small>
|
||||||
|
User: {{ ticket.user.username }}
|
||||||
|
</h6>
|
||||||
|
<h6 class="card-title mb-3">
|
||||||
|
<small class="me-1"><i class="bi bi-clock-history"></i></small>
|
||||||
|
Last modified: {{ ticket.time_update|date:"d.m.y H:i" }}
|
||||||
|
</h6>
|
||||||
<!-- Token -->
|
<!-- Token -->
|
||||||
<div class="input-group input-group mb-3">
|
<div class="input-group input-group mb-3">
|
||||||
<span class="input-group-text" id="inputGroup-sizing-sm"><i class="bi bi-key"></i></span>
|
<span class="input-group-text" id="inputGroup-sizing-sm"><i class="bi bi-key"></i></span>
|
||||||
<!--Token attempts-->
|
<!--Token attempts-->
|
||||||
<span class="input-group-text" id="inputGroup-sizing-sm">
|
<span class="input-group-text" id="inputGroup-sizing-sm">
|
||||||
|
<a
|
||||||
|
href="{% url 'collector:update' ticket.platform.name ticket.number %}"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="top"
|
||||||
|
data-bs-title="Attempts for upload archive"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="badge
|
class="badge
|
||||||
{% if ticket.attempts <= 0 %}
|
{% if ticket.attempts <= 0 %}
|
||||||
@@ -31,6 +47,7 @@
|
|||||||
bg-primary
|
bg-primary
|
||||||
{% endif %} rounded-pill">{{ ticket.attempts }}
|
{% endif %} rounded-pill">{{ ticket.attempts }}
|
||||||
</span>
|
</span>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
name="ticket-token"
|
name="ticket-token"
|
||||||
|
|||||||
39
logs_collector/collector/templates/collector/storage.html
Normal file
39
logs_collector/collector/templates/collector/storage.html
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<li class="nav-item col-lg-auto d-flex align-items-center">
|
||||||
|
<i
|
||||||
|
class="nav-link me-1 bi bi-sd-card"
|
||||||
|
aria-current="page"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
data-bs-title="Storage used: {{ storage.used_percent }}%"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
<div
|
||||||
|
class="progress"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="storage used"
|
||||||
|
aria-valuenow="25"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
style="width: 125px"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-html="true"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
data-bs-title="
|
||||||
|
Total: {{ storage.total|filesizeformat }}
|
||||||
|
<br>
|
||||||
|
Used: {{ storage.used|filesizeformat }}
|
||||||
|
<br>
|
||||||
|
Free: {{ storage.free|filesizeformat }}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="progress-bar
|
||||||
|
{% if storage.used_percent > 90 %} bg-danger
|
||||||
|
{% elif storage.used_percent > 80 %} bg-warning
|
||||||
|
{% else %} bg-success
|
||||||
|
{% endif %}"
|
||||||
|
style="width: {{ storage.used_percent }}%"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
@@ -4,46 +4,44 @@
|
|||||||
{% block title %} {{ title }} {% endblock title %}
|
{% block title %} {{ title }} {% endblock title %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="container mt-3">
|
<div class="container mt-3">
|
||||||
<div class="row">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<div class="card">
|
||||||
<div class="card">
|
<div class="card-body" aria-current="true">
|
||||||
<div class="card-body" aria-current="true">
|
{% include 'collector/includes/ticket_info.html' %}
|
||||||
{% include 'collector/includes/ticket_info.html' %}
|
<div class="col-xl-6 mt-1 mb-2">
|
||||||
<div class="col-xl-6 mt-1 mb-2">
|
{% if ticket.note %}
|
||||||
{% if ticket.note %}
|
<div class="card">
|
||||||
<div class="card">
|
<div class="card-header">
|
||||||
<div class="card-header">
|
Note:
|
||||||
Note:
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-text">
|
||||||
|
{{ ticket.note | markdown | safe }}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<div class="card-text">
|
</div>
|
||||||
{{ ticket.note | markdown | safe }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<!-- Logs -->
|
|
||||||
{% if ticket.archive_set.all %}
|
|
||||||
<ul class="list-group col-xl-6 mb-2 mt-2">
|
|
||||||
{% for archive in ticket.archive_set.all %}
|
|
||||||
{% include 'collector/includes/ticket_archives.html' %}
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Card buttons -->
|
</div>
|
||||||
<div class="d-flex w-100 justify-content-between">
|
<!-- Logs -->
|
||||||
<a
|
{% if ticket.archive_set.all %}
|
||||||
href="{% url 'collector:update' ticket.platform.name ticket.number %}"
|
<ul class="list-group col-xl-6 mb-2 mt-2">
|
||||||
class="btn btn-outline-warning mb-1 mt-1"
|
{% for archive in ticket.archive_set.all %}
|
||||||
><i class="bi bi-pencil-square"></i> Edit</a>
|
{% include 'collector/includes/ticket_archives.html' %}
|
||||||
<button
|
{% endfor %}
|
||||||
class="btn btn-outline-danger mb-1 mt-1"
|
</ul>
|
||||||
data-bs-toggle="modal"
|
{% endif %}
|
||||||
data-bs-target="#modal-ticket-del-{{ ticket.number }}"
|
<!-- Card buttons -->
|
||||||
><i class="bi bi-trash"></i> Delete
|
<div class="d-flex w-100 justify-content-between">
|
||||||
</button>
|
<a
|
||||||
</div>
|
href="{% url 'collector:update' ticket.platform.name ticket.number %}"
|
||||||
|
class="btn btn-outline-warning mb-1 mt-1"
|
||||||
|
><i class="bi bi-pencil-square"></i> Edit</a>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-danger mb-1 mt-1"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#modal-ticket-del-{{ ticket.number }}"
|
||||||
|
><i class="bi bi-trash"></i> Delete
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -56,5 +54,5 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
{% block jquery %}
|
{% block jquery %}
|
||||||
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
<script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||||
{% endblock jquery %}
|
{% endblock jquery %}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<div class="container mt-3">
|
<div class="container mt-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3>Ticket:</h3>
|
<h3 class="card-title">Ticket:</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% crispy form %}
|
{% crispy form %}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
{% extends 'collector/base.html' %}
|
|
||||||
{% load static %}
|
|
||||||
{% block title %} {{ title }} {% endblock title %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="container mt-3">
|
|
||||||
<div class="row">
|
|
||||||
<form method="post" action="{% url 'collector:delete' ticket.number %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
>Cancel</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-danger btn-archive-eraser"
|
|
||||||
data-bs-dismiss="modal"
|
|
||||||
>Delete</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -86,9 +86,6 @@
|
|||||||
{% include 'collector/includes/pagination.html' %}
|
{% include 'collector/includes/pagination.html' %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock main %}
|
{% endblock main %}
|
||||||
{% block bs %}
|
|
||||||
<script src="{% static 'collector/js/bs.tooltip.js' %}"></script>
|
|
||||||
{% endblock bs %}
|
|
||||||
{% block jquery %}
|
{% block jquery %}
|
||||||
<script src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
<script type="module" src="{% static 'collector/js/jq.ticket.detail.js' %}"></script>
|
||||||
{% endblock jquery %}
|
{% endblock jquery %}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
from account.models import User
|
||||||
from collector.models import Platform, Ticket, Archive
|
from collector.models import Platform, Ticket, Archive
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from django.urls import resolve, reverse
|
from django.urls import resolve, reverse
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
from collector import views
|
from collector import views
|
||||||
from collector.models import Ticket, Platform
|
from collector.models import Ticket, Platform
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from django.test import TestCase, Client
|
from django.test import TestCase, Client
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth.models import User
|
|
||||||
|
from account.models import User
|
||||||
|
|
||||||
from collector.models import Ticket, Platform
|
from collector.models import Ticket, Platform
|
||||||
|
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
def logs_dir_path(instance, filename):
|
|
||||||
"""
|
|
||||||
file will be uploaded to
|
|
||||||
MEDIA_ROOT_FOR_SENSITIVE_FILES/<ticket-token>/<filename>
|
|
||||||
"""
|
|
||||||
return f'{instance.ticket.number}/{filename}'
|
|
||||||
|
|
||||||
|
|
||||||
def sizify(value: int) -> str:
|
|
||||||
"""Simple kb/mb/gb size snippet for admin panel custom field:
|
|
||||||
|
|
||||||
Args:
|
|
||||||
value (int): size of file from Filefield
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: format human readable size like 4.2 Gb
|
|
||||||
"""
|
|
||||||
if value < 512000:
|
|
||||||
value = value / 1024.0
|
|
||||||
ext = 'Kb'
|
|
||||||
elif value < 4194304000:
|
|
||||||
value = value / 1048576.0
|
|
||||||
ext = 'Mb'
|
|
||||||
else:
|
|
||||||
value = value / 1073741824.0
|
|
||||||
ext = 'Gb'
|
|
||||||
return f'{round(value, 2)} {ext}'
|
|
||||||
|
|
||||||
|
|
||||||
class PageTitleViewMixin:
|
|
||||||
title = 'Collector'
|
|
||||||
|
|
||||||
def get_title(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Return the class title attr by default,
|
|
||||||
but you can override this method to further customize
|
|
||||||
"""
|
|
||||||
return self.title
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
|
||||||
context = super().get_context_data(**kwargs)
|
|
||||||
context['title'] = self.get_title()
|
|
||||||
return context
|
|
||||||
0
logs_collector/collector/utils/__init__.py
Normal file
0
logs_collector/collector/utils/__init__.py
Normal file
61
logs_collector/collector/utils/helpers.py
Normal file
61
logs_collector/collector/utils/helpers.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import shutil
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
|
||||||
|
def logs_dir_path(instance, filename: str) -> str:
|
||||||
|
"""
|
||||||
|
file will be uploaded to
|
||||||
|
MEDIA_ROOT/view/<filename>
|
||||||
|
"""
|
||||||
|
return f'{instance.ticket.number}/{filename}'
|
||||||
|
|
||||||
|
|
||||||
|
def sizify(value: int) -> str:
|
||||||
|
"""Simple kb/mb/gb size snippet for admin panel custom field:
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (int): size of file from Filefield
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: format human readable size like 4.2 Gb
|
||||||
|
"""
|
||||||
|
if value < 512000:
|
||||||
|
value = value / 1024.0
|
||||||
|
ext = 'KB'
|
||||||
|
elif value < 4194304000:
|
||||||
|
value = value / 1048576.0
|
||||||
|
ext = 'MB'
|
||||||
|
else:
|
||||||
|
value = value / 1073741824.0
|
||||||
|
ext = 'GB'
|
||||||
|
return f'{round(value, 1)} {ext}'
|
||||||
|
|
||||||
|
|
||||||
|
def get_mount_fs_info(path: type[pathlib.PosixPath]) -> dict:
|
||||||
|
"""
|
||||||
|
Get directory information for storing uploaded files.
|
||||||
|
Includes information total/used/free space on mount device
|
||||||
|
|
||||||
|
Args:
|
||||||
|
path (pathlib.PosixPath): path to storage dir
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: storage mount info
|
||||||
|
"""
|
||||||
|
mount_info: dict = {}
|
||||||
|
try:
|
||||||
|
mount_info = shutil.disk_usage(path)._asdict()
|
||||||
|
mount_info['used_percent'] = round(
|
||||||
|
mount_info['used'] / mount_info['total'] * 100,
|
||||||
|
)
|
||||||
|
mount_info['status'] = 'mount'
|
||||||
|
except Exception as error: # expected FileNotFoundError
|
||||||
|
mount_info = {
|
||||||
|
'total': 0,
|
||||||
|
'used': 0,
|
||||||
|
'free': 0,
|
||||||
|
'used_percent': 0,
|
||||||
|
'status': 'error',
|
||||||
|
'traceback': f'{error}'
|
||||||
|
}
|
||||||
|
return mount_info
|
||||||
23
logs_collector/collector/utils/mixins.py
Normal file
23
logs_collector/collector/utils/mixins.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
class ExtraContextMixin:
|
||||||
|
"""The class adds additional context
|
||||||
|
to all child view classes that inherit from it.
|
||||||
|
Overrides the get_context_data method for CBV
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = 'Collector'
|
||||||
|
|
||||||
|
def get_title(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Return the class title attr by default,
|
||||||
|
but you can override this method to further customize
|
||||||
|
"""
|
||||||
|
return self.title
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = {}
|
||||||
|
try:
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
context['title'] = self.get_title()
|
||||||
|
return context
|
||||||
@@ -1,27 +1,24 @@
|
|||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import FileResponse
|
from django.http import FileResponse, Http404
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
from django.views.generic.detail import SingleObjectMixin
|
from django.views.generic.detail import SingleObjectMixin
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.shortcuts import render
|
|
||||||
|
|
||||||
from two_factor.views import OTPRequiredMixin
|
from two_factor.views import OTPRequiredMixin
|
||||||
|
|
||||||
from .forms import TicketForm, ArchiveForm
|
from .forms import TicketForm, ArchiveForm
|
||||||
from .models import Archive, Ticket
|
from .models import Archive, Ticket
|
||||||
from .utils import PageTitleViewMixin
|
from .utils.mixins import ExtraContextMixin
|
||||||
|
|
||||||
|
|
||||||
class ArchiveUploadView(PageTitleViewMixin, generic.View):
|
class ArchiveUploadView(ExtraContextMixin, generic.TemplateView):
|
||||||
form_class = ArchiveForm()
|
form_class = ArchiveForm()
|
||||||
template = 'collector/archive_upload.html',
|
template_name = 'collector/archive_upload.html'
|
||||||
|
|
||||||
def get(self, request):
|
def get_context_data(self, **kwargs):
|
||||||
return render(
|
context = super().get_context_data(**kwargs)
|
||||||
request,
|
context['form'] = self.form_class
|
||||||
self.template,
|
return context
|
||||||
context={'form': self.form_class}
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_title(self):
|
def get_title(self):
|
||||||
return f'{self.title} - upload'
|
return f'{self.title} - upload'
|
||||||
@@ -38,10 +35,14 @@ class ArchiveHandlerView(
|
|||||||
|
|
||||||
def get(self, request, path):
|
def get(self, request, path):
|
||||||
self.object = self.get_object()
|
self.object = self.get_object()
|
||||||
|
try:
|
||||||
|
self.object.file.size
|
||||||
|
except FileNotFoundError:
|
||||||
|
raise Http404(f'File: {self.object.file} not found')
|
||||||
return FileResponse(self.object.file)
|
return FileResponse(self.object.file)
|
||||||
|
|
||||||
|
|
||||||
class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
class CreateTicket(LoginRequiredMixin, ExtraContextMixin, generic.CreateView):
|
||||||
model = Ticket
|
model = Ticket
|
||||||
form_class = TicketForm
|
form_class = TicketForm
|
||||||
template_name = 'collector/ticket_create.html'
|
template_name = 'collector/ticket_create.html'
|
||||||
@@ -54,7 +55,7 @@ class CreateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.CreateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
class UpdateTicket(LoginRequiredMixin, ExtraContextMixin, generic.UpdateView):
|
||||||
model = Ticket
|
model = Ticket
|
||||||
form_class = TicketForm
|
form_class = TicketForm
|
||||||
template_name = 'collector/ticket_create.html'
|
template_name = 'collector/ticket_create.html'
|
||||||
@@ -69,7 +70,7 @@ class UpdateTicket(LoginRequiredMixin, PageTitleViewMixin, generic.UpdateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
class ListAllTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView):
|
||||||
model = Ticket
|
model = Ticket
|
||||||
template_name = 'collector/tickets.html'
|
template_name = 'collector/tickets.html'
|
||||||
context_object_name = 'tickets'
|
context_object_name = 'tickets'
|
||||||
@@ -98,7 +99,7 @@ class ListAllTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView):
|
|||||||
return super().get_queryset()
|
return super().get_queryset()
|
||||||
|
|
||||||
|
|
||||||
class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListView): # noqa:E501
|
class ListPlatformTickets(LoginRequiredMixin, ExtraContextMixin, generic.ListView): # noqa:E501
|
||||||
model = Ticket
|
model = Ticket
|
||||||
template_name = 'collector/tickets.html'
|
template_name = 'collector/tickets.html'
|
||||||
context_object_name = 'tickets'
|
context_object_name = 'tickets'
|
||||||
@@ -114,7 +115,7 @@ class ListPlatformTickets(LoginRequiredMixin, PageTitleViewMixin, generic.ListVi
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DetailTicket(LoginRequiredMixin, PageTitleViewMixin, generic.DetailView):
|
class DetailTicket(LoginRequiredMixin, ExtraContextMixin, generic.DetailView):
|
||||||
model = Ticket
|
model = Ticket
|
||||||
template_name = 'collector/ticket.html'
|
template_name = 'collector/ticket.html'
|
||||||
context_object_name = 'ticket'
|
context_object_name = 'ticket'
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""
|
||||||
|
An application for uploading archives with log files
|
||||||
|
for their subsequent download and check issues
|
||||||
|
that have arisen with software products.
|
||||||
|
The purpose of creating this application is
|
||||||
|
the ability to securely exchange and store log files containing sensitive data.
|
||||||
|
I have not found an application that would allow an unauthorized client
|
||||||
|
to upload data without providing him with authorization credentials.
|
||||||
|
You can use other applications for this,
|
||||||
|
such as Google cloud, Yandex cloud, DropBox etc, but in this case,
|
||||||
|
you do not have a tool that would allow you to automatically restrict uploads
|
||||||
|
later until you explicitly deny access to the shared link.
|
||||||
|
This app allows you to upload files using a unique token
|
||||||
|
associated with a support ticket.
|
||||||
|
This token has a limit on the number of file upload attempts.
|
||||||
|
Also, if the ticket is resolved, then the token is invalid.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# █▀▄▀█ █▀▀ ▀█▀ ▄▀█ ▀
|
||||||
|
# █░▀░█ ██▄ ░█░ █▀█ ▄
|
||||||
|
# -------------------
|
||||||
|
__author__ = "MOIS3Y"
|
||||||
|
__credits__ = ["Stepan Zhukovsky"]
|
||||||
|
__license__ = "GPL v3.0"
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
__maintainer__ = "Stepan Zhukovsky"
|
||||||
|
__email__ = "stepan@zhukovsky.me"
|
||||||
|
__status__ = "Production"
|
||||||
|
|||||||
@@ -2,36 +2,59 @@ import environ
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from . import __version__, __status__
|
||||||
|
|
||||||
|
|
||||||
|
# █▀█ █▀█ █▀█ ▀█▀ ▀
|
||||||
|
# █▀▄ █▄█ █▄█ ░█░ ▄
|
||||||
|
# -- -- -- -- -- --
|
||||||
|
|
||||||
env = environ.Env(
|
|
||||||
# set casting default value
|
|
||||||
DEBUG=(bool, False),
|
|
||||||
SECRET_KEY=(str, 'j9QGbvM9Z4otb47')
|
|
||||||
)
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
# █▀▀ █▄░█ █░█ ▀
|
||||||
|
# ██▄ █░▀█ ▀▄▀ ▄
|
||||||
|
# -- -- -- -- --
|
||||||
|
|
||||||
|
# Set default environ variables:
|
||||||
|
env = environ.Env(
|
||||||
|
# set casting default value
|
||||||
|
VERSION=(str, __version__),
|
||||||
|
ENVIRONMENT=(str, __status__),
|
||||||
|
DEBUG=(bool, False),
|
||||||
|
SECRET_KEY=(str, 'j9QGbvM9Z4otb47'),
|
||||||
|
DATA_DIR=(Path, BASE_DIR / 'data'),
|
||||||
|
CSRF_TRUSTED_ORIGINS=(list, []),
|
||||||
|
ALLOWED_HOSTS=(list, ['*']),
|
||||||
|
TZ=(str, 'UTC'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Read .env file if exist:
|
||||||
environ.Env.read_env(BASE_DIR / '.env')
|
environ.Env.read_env(BASE_DIR / '.env')
|
||||||
|
|
||||||
|
|
||||||
|
# █▀▀ █▀█ █▀█ █▀▀ ▀
|
||||||
|
# █▄▄ █▄█ █▀▄ ██▄ ▄
|
||||||
|
# -- -- -- -- -- -
|
||||||
|
|
||||||
|
VERSION = env('VERSION')
|
||||||
|
|
||||||
|
ENVIRONMENT = env('ENVIRONMENT')
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = env('SECRET_KEY')
|
SECRET_KEY = env('SECRET_KEY')
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = env('DEBUG')
|
DEBUG = env('DEBUG')
|
||||||
|
|
||||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=["*"])
|
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
|
||||||
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#csrf-trusted-origins
|
||||||
if not DEBUG:
|
if not DEBUG:
|
||||||
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
|
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS")
|
||||||
|
|
||||||
# TODO: required for docker image
|
|
||||||
# CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=["*"])
|
|
||||||
|
|
||||||
|
|
||||||
# Application definition
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
'django.contrib.admin',
|
'django.contrib.admin',
|
||||||
'django.contrib.auth',
|
'django.contrib.auth',
|
||||||
@@ -66,6 +89,7 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
'whitenoise.middleware.WhiteNoiseMiddleware',
|
'whitenoise.middleware.WhiteNoiseMiddleware',
|
||||||
|
'collector.middleware.HttpResponseNotAllowedMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'logs_collector.urls'
|
ROOT_URLCONF = 'logs_collector.urls'
|
||||||
@@ -77,10 +101,14 @@ TEMPLATES = [
|
|||||||
'APP_DIRS': True,
|
'APP_DIRS': True,
|
||||||
'OPTIONS': {
|
'OPTIONS': {
|
||||||
'context_processors': [
|
'context_processors': [
|
||||||
|
# default:
|
||||||
'django.template.context_processors.debug',
|
'django.template.context_processors.debug',
|
||||||
'django.template.context_processors.request',
|
'django.template.context_processors.request',
|
||||||
'django.contrib.auth.context_processors.auth',
|
'django.contrib.auth.context_processors.auth',
|
||||||
'django.contrib.messages.context_processors.messages',
|
'django.contrib.messages.context_processors.messages',
|
||||||
|
# collector:
|
||||||
|
'collector.context_processors.metadata',
|
||||||
|
'collector.context_processors.storage_info',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -88,18 +116,6 @@ TEMPLATES = [
|
|||||||
|
|
||||||
WSGI_APPLICATION = 'logs_collector.wsgi.application'
|
WSGI_APPLICATION = 'logs_collector.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
|
||||||
|
|
||||||
DATABASES = {
|
|
||||||
'default': env.db_url(
|
|
||||||
'SQLITE_URL',
|
|
||||||
default=f'sqlite:///{BASE_DIR / "db.sqlite3"}'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
@@ -118,19 +134,22 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
TIME_ZONE = env('TZ', default='UTC')
|
TIME_ZONE = env('TZ')
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
|
|
||||||
|
# █▀ ▀█▀ ▄▀█ ▀█▀ █ █▀▀ ▀
|
||||||
|
# ▄█ ░█░ █▀█ ░█░ █ █▄▄ ▄
|
||||||
|
# -- -- -- -- -- -- -- -
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||||
# Whitenoise:
|
# Whitenoise:
|
||||||
@@ -138,20 +157,26 @@ USE_TZ = True
|
|||||||
STATIC_URL = 'static/'
|
STATIC_URL = 'static/'
|
||||||
STATIC_ROOT = BASE_DIR / 'static'
|
STATIC_ROOT = BASE_DIR / 'static'
|
||||||
|
|
||||||
# Default primary key field type
|
|
||||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
# █▀▄ ▄▀█ ▀█▀ ▄▀█ ▀
|
||||||
|
# █▄▀ █▀█ ░█░ █▀█ ▄
|
||||||
|
# -- -- -- -- -- --
|
||||||
|
|
||||||
MEDIA_ROOT = BASE_DIR / 'archives'
|
# Build paths inside the project for db and storage.
|
||||||
|
DATA_DIR = env('DATA_DIR')
|
||||||
|
|
||||||
|
# Create DATA_DIR ignore if exist:
|
||||||
|
Path(DATA_DIR).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Custom file storage path
|
||||||
|
MEDIA_ROOT = DATA_DIR / 'archives'
|
||||||
|
|
||||||
STORAGES = {
|
STORAGES = {
|
||||||
# ...
|
|
||||||
"default": {
|
"default": {
|
||||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"location": MEDIA_ROOT,
|
"location": MEDIA_ROOT,
|
||||||
"base_url": "/archives/",
|
"base_url": "/download/archives/",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"staticfiles": {
|
"staticfiles": {
|
||||||
@@ -159,48 +184,76 @@ STORAGES = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# █▀▄ ▄▀█ ▀█▀ ▄▀█ █▄▄ ▄▀█ █▀ █▀▀ ▀
|
||||||
|
# █▄▀ █▀█ ░█░ █▀█ █▄█ █▀█ ▄█ ██▄ ▄
|
||||||
|
# -- -- -- -- -- -- -- -- -- -- --
|
||||||
|
|
||||||
|
# Database
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||||
|
DATABASES = {
|
||||||
|
'default': env.db_url(
|
||||||
|
'DB_URL',
|
||||||
|
default=f'sqlite:///{DATA_DIR / "db.sqlite3"}'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default primary key field type
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||||
|
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||||
|
|
||||||
|
|
||||||
|
# █▀▀ ▀▄▀ ▀█▀ █▀▀ █▄░█ ▀█▀ █ █▀█ █▄░█ █▀ ▀
|
||||||
|
# ██▄ █░█ ░█░ ██▄ █░▀█ ░█░ █ █▄█ █░▀█ ▄█ ▄
|
||||||
|
# -- -- -- -- -- -- -- -- -- -- -- -- -- -
|
||||||
|
|
||||||
# django-crispy-forms and crispy-bootstrap5
|
# django-crispy-forms and crispy-bootstrap5
|
||||||
# https://django-crispy-forms.readthedocs.io/en/latest/
|
# https://django-crispy-forms.readthedocs.io/en/latest/
|
||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||||
|
|
||||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||||
|
|
||||||
# https://www.django-rest-framework.org/api-guide/settings/
|
# https://www.django-rest-framework.org/api-guide/settings/
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
'DEFAULT_RENDERER_CLASSES': [
|
'DEFAULT_RENDERER_CLASSES': [
|
||||||
'rest_framework.renderers.JSONRenderer',
|
'rest_framework.renderers.JSONRenderer',
|
||||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
|
||||||
],
|
],
|
||||||
'DEFAULT_PARSER_CLASSES': [
|
'DEFAULT_PARSER_CLASSES': [
|
||||||
'rest_framework.parsers.JSONParser',
|
'rest_framework.parsers.JSONParser',
|
||||||
'rest_framework.renderers.BrowsableAPIRenderer',
|
|
||||||
'rest_framework.parsers.MultiPartParser'
|
'rest_framework.parsers.MultiPartParser'
|
||||||
],
|
],
|
||||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||||
'rest_framework.authentication.SessionAuthentication',
|
'rest_framework.authentication.SessionAuthentication',
|
||||||
'rest_framework.authentication.BasicAuthentication'
|
# 'rest_framework.authentication.BasicAuthentication'
|
||||||
],
|
],
|
||||||
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend', ], # noqa:E501
|
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend', ], # noqa:E501
|
||||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||||
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501
|
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', # noqa:E501
|
||||||
# 'PAGE_SIZE': 3,
|
# 'PAGE_SIZE': 3,
|
||||||
|
'DEFAULT_METADATA_CLASS': 'rest_framework.metadata.SimpleMetadata',
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://drf-spectacular.readthedocs.io/en/latest/readme.html
|
if DEBUG:
|
||||||
# TODO: set environ vars config!
|
REST_FRAMEWORK.get(
|
||||||
|
'DEFAULT_RENDERER_CLASSES', []
|
||||||
|
).append('rest_framework.renderers.BrowsableAPIRenderer')
|
||||||
|
REST_FRAMEWORK.get(
|
||||||
|
'DEFAULT_PARSER_CLASSES', []
|
||||||
|
).append('rest_framework.renderers.BrowsableAPIRenderer')
|
||||||
|
|
||||||
|
# https://drf-spectacular.readthedocs.io/en/latest/readme.html
|
||||||
SPECTACULAR_SETTINGS = {
|
SPECTACULAR_SETTINGS = {
|
||||||
'TITLE': 'Logs collector API',
|
'TITLE': 'Logs collector API',
|
||||||
'DESCRIPTION': 'Collector of archives with log files for further analysis',
|
'DESCRIPTION': 'Collector of archives with log files for further analysis',
|
||||||
'VERSION': '0.1.0',
|
'VERSION': VERSION,
|
||||||
'SERVE_INCLUDE_SCHEMA': True,
|
'SERVE_INCLUDE_SCHEMA': True,
|
||||||
'SERVE_PUBLIC': False,
|
'SERVE_PUBLIC': False,
|
||||||
|
"SWAGGER_UI_SETTINGS": {
|
||||||
|
"filter": True,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
|
# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/
|
||||||
|
|
||||||
SIMPLE_JWT = {
|
SIMPLE_JWT = {
|
||||||
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
"ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
|
||||||
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
"REFRESH_TOKEN_LIFETIME": timedelta(days=1),
|
||||||
@@ -241,6 +294,14 @@ SIMPLE_JWT = {
|
|||||||
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", # noqa:E501
|
"SLIDING_TOKEN_REFRESH_SERIALIZER": "rest_framework_simplejwt.serializers.TokenRefreshSlidingSerializer", # noqa:E501
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ▄▀█ █░█ ▀█▀ █░█ ▀
|
||||||
|
# █▀█ █▄█ ░█░ █▀█ ▄
|
||||||
|
# -- -- -- -- -- --
|
||||||
|
|
||||||
LOGIN_URL = 'two_factor:login'
|
LOGIN_URL = 'two_factor:login'
|
||||||
LOGIN_REDIRECT_URL = 'collector:index'
|
LOGIN_REDIRECT_URL = 'collector:index'
|
||||||
LOGOUT_REDIRECT_URL = 'two_factor:login'
|
LOGOUT_REDIRECT_URL = 'two_factor:login'
|
||||||
|
|
||||||
|
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-user-model
|
||||||
|
AUTH_USER_MODEL = 'account.User'
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path, include
|
from django.urls import path, include
|
||||||
|
|
||||||
from drf_spectacular.views import (
|
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
|
||||||
SpectacularAPIView,
|
|
||||||
SpectacularRedocView,
|
|
||||||
SpectacularSwaggerView
|
|
||||||
)
|
|
||||||
|
|
||||||
from two_factor.urls import urlpatterns as tf_urls
|
from two_factor.urls import urlpatterns as tf_urls
|
||||||
from account.utils import AdminSiteOTPRequiredMixinRedirectSetup
|
from account.utils import AdminSiteOTPRequiredMixinRedirectSetup
|
||||||
|
|
||||||
@@ -48,9 +43,4 @@ urlpatterns += [
|
|||||||
SpectacularSwaggerView.as_view(url_name='schema'),
|
SpectacularSwaggerView.as_view(url_name='schema'),
|
||||||
name='swagger-ui'
|
name='swagger-ui'
|
||||||
),
|
),
|
||||||
path(
|
|
||||||
'api/v1/schema/redoc/',
|
|
||||||
SpectacularRedocView.as_view(url_name='schema'),
|
|
||||||
name='redoc'
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
10
logs_collector/templates/403_csrf.html
Normal file
10
logs_collector/templates/403_csrf.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends 'errors.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %} Logs Collector - CSRF error {% endblock title %}
|
||||||
|
|
||||||
|
{% block status_code %}403{% endblock status_code %}
|
||||||
|
{% block error_message %}
|
||||||
|
<p class="fs-3"> <span class="text-danger">Opps!</span> CSRF verification failed.</p>
|
||||||
|
<p class="lead">Request aborted</p>
|
||||||
|
{% endblock error_message %}
|
||||||
10
logs_collector/templates/404.html
Normal file
10
logs_collector/templates/404.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends 'errors.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %} Logs Collector - Not Found {% endblock title %}
|
||||||
|
|
||||||
|
{% block status_code %}404{% endblock status_code %}
|
||||||
|
{% block error_message %}
|
||||||
|
<p class="fs-3"> <span class="text-danger">Opps!</span> Page not found.</p>
|
||||||
|
<p class="lead">The content you're looking for doesn't exist.</p>
|
||||||
|
{% endblock error_message %}
|
||||||
14
logs_collector/templates/405.html
Normal file
14
logs_collector/templates/405.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
{% extends 'errors.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %} Logs Collector - Method not allowed {% endblock title %}
|
||||||
|
|
||||||
|
{% block status_code %}405{% endblock status_code %}
|
||||||
|
{% block error_message %}
|
||||||
|
<p class="fs-3"> <span class="text-danger">Opps!</span> Method not allowed</p>
|
||||||
|
<p class="lead">
|
||||||
|
Request method:
|
||||||
|
<span class="text-danger">{{ request.method }}</span>
|
||||||
|
isn't allowed for this URL
|
||||||
|
</p>
|
||||||
|
{% endblock error_message %}
|
||||||
10
logs_collector/templates/500.html
Normal file
10
logs_collector/templates/500.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{% extends 'errors.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %} Logs Collector - Server error {% endblock title %}
|
||||||
|
|
||||||
|
{% block status_code %}500{% endblock status_code %}
|
||||||
|
{% block error_message %}
|
||||||
|
<p class="fs-3"> <span class="text-danger">Opps!</span> Server error</p>
|
||||||
|
<p class="lead">Unexpected error, please try again or contact system admin </p>
|
||||||
|
{% endblock error_message %}
|
||||||
@@ -34,16 +34,21 @@
|
|||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
|
href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css"
|
||||||
>
|
>
|
||||||
|
{% block errors_head %}{% endblock errors_head %}
|
||||||
{% block collector_head %}{% endblock collector_head %}
|
{% block collector_head %}{% endblock collector_head %}
|
||||||
{% block account_head %}{% endblock account_head %}
|
{% block account_head %}{% endblock account_head %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="d-flex flex-column min-vh-100">
|
||||||
|
{% block http_errors %}{% endblock http_errors %}
|
||||||
{% block collector_content %}{% endblock collector_content %}
|
{% block collector_content %}{% endblock collector_content %}
|
||||||
{% block account_content %}{% endblock account_content %}
|
{% block account_content %}{% endblock account_content %}
|
||||||
|
<!-- BS dependences JS-->
|
||||||
<script src="{% static '/js/bootstrap.bundle.min.js' %}"></script>
|
<script src="{% static '/js/bootstrap.bundle.min.js' %}"></script>
|
||||||
|
<!-- Theme switcher JS-->
|
||||||
<script src="{% static '/js/bs.theme.mode.js' %}"></script>
|
<script src="{% static '/js/bs.theme.mode.js' %}"></script>
|
||||||
|
<!-- BS tooltip JS-->
|
||||||
|
<script src="{% static '/js/bs.tooltip.js' %}"></script>
|
||||||
|
<!-- Apps JS-->
|
||||||
{% block collector_scripts %}{% endblock collector_scripts %}
|
{% block collector_scripts %}{% endblock collector_scripts %}
|
||||||
{% block account_scripts %}{% endblock account_scripts %}
|
{% block account_scripts %}{% endblock account_scripts %}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
20
logs_collector/templates/errors.html
Normal file
20
logs_collector/templates/errors.html
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block errors_head %}
|
||||||
|
<title>{% block title %}{% endblock title %}</title>
|
||||||
|
{% endblock errors_head %}
|
||||||
|
|
||||||
|
{% block http_errors %}
|
||||||
|
<div class="d-flex align-items-center justify-content-center vh-100" >
|
||||||
|
<div class="text-center">
|
||||||
|
<h1 class="display-1 fw-bold">{% block status_code %}{% endblock status_code %}</h1>
|
||||||
|
{% block error_message %}{% endblock error_message %}
|
||||||
|
<a href="{% url 'collector:index' %}" class="btn btn-secondary">Go Home</a>
|
||||||
|
</div>
|
||||||
|
<!-- Theme switcher -->
|
||||||
|
<div class="dropdown position-fixed bottom-0 end-0 mb-3 me-3 bd-mode-toggle">
|
||||||
|
{% include 'includes/theme_switcher.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock http_errors %}
|
||||||
15
logs_collector/templates/includes/brand.html
Normal file
15
logs_collector/templates/includes/brand.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<a class="navbar-brand" href="{% url 'collector:index' %}">
|
||||||
|
Logs Collector
|
||||||
|
<i class="bi bi-file-earmark-zip-fill"></i>
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
class="navbar-toggler"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarSupportedContent"
|
||||||
|
aria-controls="navbarSupportedContent"
|
||||||
|
aria-expanded="false"
|
||||||
|
aria-label="collapse nav fields"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
57
logs_collector/templates/includes/extra_menu.html
Normal file
57
logs_collector/templates/includes/extra_menu.html
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
href="{% url 'account:show_profile' %}"
|
||||||
|
>
|
||||||
|
<i class="bi bi-person-circle"></i> {{ request.user }}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
>
|
||||||
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-end">
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" type="button" href="{% url 'admin:index' %}"
|
||||||
|
><i class="bi bi-shield-shaded"></i> Admin</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="{% url 'two_factor:profile' %}"
|
||||||
|
class="dropdown-item"
|
||||||
|
type="button">
|
||||||
|
<i class="bi bi-dice-5"></i> 2FA
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="{% url 'account:logout' %}"
|
||||||
|
class="dropdown-item"
|
||||||
|
type="button"><i class="bi bi-door-closed"></i> Logout
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||||
|
<div class="btn-group">
|
||||||
|
<a
|
||||||
|
type="button"
|
||||||
|
href="{% url 'two_factor:login' %}"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
>
|
||||||
|
<i class="bi bi-box-arrow-in-right"></i></i> Login
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
41
logs_collector/templates/includes/footer.html
Normal file
41
logs_collector/templates/includes/footer.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||||
|
<div class="container">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class="btn">
|
||||||
|
v{{ version }}
|
||||||
|
{% if environment != 'Production' %}
|
||||||
|
Staging: {{ environment }}
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<!-- Separator -->
|
||||||
|
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||||
|
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
||||||
|
<hr class="d-lg-none my-2 text-white-50">
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<button class='btn'> © {{ author }} </button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% if request.user.is_authenticated %}
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="btn"
|
||||||
|
type="button"
|
||||||
|
href="{% url 'swagger-ui' %}"
|
||||||
|
target="_blank"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-html="true"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
data-bs-title="Swagger"
|
||||||
|
>
|
||||||
|
<i class="bi bi-braces-asterisk"></i> API
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
47
logs_collector/templates/includes/menu.html
Normal file
47
logs_collector/templates/includes/menu.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
{% load collector_extras %}
|
||||||
|
{% get_platforms as platforms %}
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<button
|
||||||
|
class="nav-link dropdown-toggle"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false"
|
||||||
|
><i class="bi bi-filter-circle"></i> Tickets
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" aria-current="page" href="{% url 'collector:create' %}">
|
||||||
|
<i class="bi bi-pencil-square"></i> Create ticket
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
<a class="dropdown-item" aria-current="page" href="{% url 'collector:upload' %}">
|
||||||
|
<i class="bi bi-archive"></i>
|
||||||
|
Upload archive
|
||||||
|
</a>
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
{% for platform in platforms %}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="dropdown-item {% if request.resolver_match.kwargs.platform == platform.name %}active{% endif %}"
|
||||||
|
href="{{ platform.get_absolute_url }}"
|
||||||
|
>{{ platform.pretty_name}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="dropdown-item {% if request.GET.resolved %}active{% endif %}"
|
||||||
|
href="{% url 'collector:tickets' %}?resolved=true">
|
||||||
|
<i class="bi bi-check-circle"></i> Resolved
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li><hr class="dropdown-divider" /></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{% url 'collector:tickets' %}">
|
||||||
|
<i class="bi bi-funnel"></i> Reset filter
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
@@ -1,158 +1,22 @@
|
|||||||
{% load collector_extras %}
|
<nav class="navbar navbar-expand-xl bg-body-tertiary">
|
||||||
{% get_platforms as platforms %}
|
|
||||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<a
|
<!--Brand logo -->
|
||||||
class="navbar-brand"
|
{% include 'includes/brand.html' %}
|
||||||
href="{% url 'collector:index' %}">Logs Collector <i class="bi bi-file-earmark-zip-fill"></i>
|
<!-- Left fields -->
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
class="navbar-toggler"
|
|
||||||
type="button"
|
|
||||||
data-bs-toggle="collapse"
|
|
||||||
data-bs-target="#navbarSupportedContent"
|
|
||||||
aria-controls="navbarSupportedContent"
|
|
||||||
aria-expanded="false"
|
|
||||||
aria-label="Переключатель навигации"
|
|
||||||
>
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
<ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto">
|
<ul class="navbar-nav ml-auto mb-2 mb-lg-0 me-md-auto">
|
||||||
<li class="nav-item dropdown">
|
<!-- Menu -->
|
||||||
<button
|
{% include 'includes/menu.html' %}
|
||||||
class="nav-link dropdown-toggle"
|
<!-- Storage -->
|
||||||
role="button"
|
{% include 'includes/storage.html' %}
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>Archives</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="{% url 'collector:upload' %}">
|
|
||||||
<i class="bi bi-archive"></i> Upload archive
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<button
|
|
||||||
class="nav-link dropdown-toggle"
|
|
||||||
role="button"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>Tickets</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
{% for platform in platforms %}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="dropdown-item {% if request.resolver_match.kwargs.platform == platform.name %}active{% endif %}"
|
|
||||||
href="{{ platform.get_absolute_url }}"
|
|
||||||
>{{ platform.pretty_name}}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="dropdown-item {% if request.GET.resolved %}active{% endif %}"
|
|
||||||
href="{% url 'collector:tickets' %}?resolved=true">
|
|
||||||
<i class="bi bi-check-circle"></i> Resolved
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" href="{% url 'collector:tickets' %}">
|
|
||||||
<i class="bi bi-funnel"></i> Reset filter
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" aria-current="page" href="{% url 'collector:create' %}">
|
|
||||||
<i class="bi bi-pencil-square"></i> New
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<!-- Search -->
|
<!-- Search -->
|
||||||
<ul class="navbar-nav flex-row flex-wrap me-md-auto">
|
{% include 'includes/search.html' %}
|
||||||
<li class="nav-item py-2 col-12 col-lg-auto">
|
|
||||||
<form class="d-flex" role="search" action="{% url 'collector:tickets' %}">
|
|
||||||
<input
|
|
||||||
class="form-control me-2"
|
|
||||||
type="search"
|
|
||||||
placeholder="Search"
|
|
||||||
aria-label="Search"
|
|
||||||
name="search"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-success"
|
|
||||||
type="submit">
|
|
||||||
<i class="bi bi-search"></i>
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<!-- Right fields -->
|
<!-- Right fields -->
|
||||||
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
|
<ul class="navbar-nav flex-row flex-wrap ms-md-auto">
|
||||||
<!-- User settings -->
|
<!-- User settings -->
|
||||||
{% if request.user.is_authenticated %}
|
{% include 'includes/extra_menu.html' %}
|
||||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button type="button" class="btn btn-outline-secondary">
|
|
||||||
<i class="bi bi-person-square"></i> {{ request.user }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split"
|
|
||||||
data-bs-toggle="dropdown"
|
|
||||||
aria-expanded="false"
|
|
||||||
>
|
|
||||||
<span class="visually-hidden">Toggle Dropdown</span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu dropdown-menu-end">
|
|
||||||
{% if request.user.is_staff %}
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" type="button" href="{% url 'admin:index' %}"
|
|
||||||
><i class="bi bi-shield-shaded"></i> Admin</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<li>
|
|
||||||
<a class="dropdown-item" type="button" href="{% url 'swagger-ui' %}"
|
|
||||||
><i class="bi bi-braces-asterisk"></i> Swagger</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="{% url 'two_factor:profile' %}"
|
|
||||||
class="dropdown-item"
|
|
||||||
type="button">
|
|
||||||
<i class="bi bi-gear"></i> Settings
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li><hr class="dropdown-divider" /></li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="{% url 'account:logout' %}"
|
|
||||||
class="dropdown-item"
|
|
||||||
type="button"><i class="bi bi-door-closed"></i> Logout
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
|
||||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
|
||||||
<div class="btn-group">
|
|
||||||
<a
|
|
||||||
type="button"
|
|
||||||
href="{% url 'two_factor:login' %}"
|
|
||||||
class="btn btn-outline-secondary"
|
|
||||||
>
|
|
||||||
<i class="bi bi-box-arrow-in-right"></i></i> Login
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
<!-- Separator -->
|
<!-- Separator -->
|
||||||
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
<li class="nav-item py-2 py-lg-1 col-12 col-lg-auto">
|
||||||
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
<div class="vr d-none d-lg-flex h-100 mx-lg-2 text-white"></div>
|
||||||
|
|||||||
21
logs_collector/templates/includes/search.html
Normal file
21
logs_collector/templates/includes/search.html
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<ul class="navbar-nav flex-row flex-wrap me-md-auto">
|
||||||
|
<li class="nav-item py-2 col-12 col-lg-auto">
|
||||||
|
<form class="d-flex" role="search" action="{% url 'collector:tickets' %}">
|
||||||
|
<input
|
||||||
|
class="form-control me-2"
|
||||||
|
type="search"
|
||||||
|
placeholder="Search"
|
||||||
|
aria-label="Search"
|
||||||
|
name="search"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
data-bs-title="Type the ticket number or comma-separated numbers"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-success"
|
||||||
|
type="submit">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
55
logs_collector/templates/includes/storage.html
Normal file
55
logs_collector/templates/includes/storage.html
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{% if storage %}
|
||||||
|
<li class="nav-item col-lg-auto d-flex align-items-center">
|
||||||
|
<i
|
||||||
|
id="storage_icon"
|
||||||
|
class="nav-link me-1 bi bi-sd-card"
|
||||||
|
aria-current="page"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-html="true"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
data-bs-title="
|
||||||
|
<span><u>STORAGE</u><span>
|
||||||
|
<br>
|
||||||
|
Used: {{ storage.used_percent }}%
|
||||||
|
<br>
|
||||||
|
Status:
|
||||||
|
<span class={% if storage.status == 'error' %}text-danger{% else %}text-success{% endif %}>
|
||||||
|
{{ storage.status }}
|
||||||
|
<span>
|
||||||
|
"
|
||||||
|
>
|
||||||
|
</i>
|
||||||
|
<div
|
||||||
|
id="storage_progress_container"
|
||||||
|
class="progress"
|
||||||
|
role="progressbar"
|
||||||
|
aria-label="storage used"
|
||||||
|
aria-valuenow="{{ storage.used_percent }}"
|
||||||
|
aria-valuemin="0"
|
||||||
|
aria-valuemax="100"
|
||||||
|
style="width: 125px"
|
||||||
|
data-bs-toggle="tooltip"
|
||||||
|
data-bs-html="true"
|
||||||
|
data-bs-placement="bottom"
|
||||||
|
data-bs-title="
|
||||||
|
Total: {{ storage.total|filesizeformat }}
|
||||||
|
<br>
|
||||||
|
Used: {{ storage.used|filesizeformat }}
|
||||||
|
<br>
|
||||||
|
Free: {{ storage.free|filesizeformat }}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="storage_progress"
|
||||||
|
class="progress-bar
|
||||||
|
{% if storage.used_percent > 90 %} bg-danger
|
||||||
|
{% elif storage.used_percent > 80 %} bg-warning
|
||||||
|
{% else %} bg-success
|
||||||
|
{% endif %}"
|
||||||
|
style="width: {{ storage.used_percent }}%"
|
||||||
|
storage-url="{% url 'collector_api:storage-info' %}"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
{% extends "two_factor/_base.html" %}
|
{% extends "two_factor/_base.html" %}
|
||||||
|
|
||||||
{% block content_wrapper %}
|
{% block content_wrapper %}
|
||||||
<section>
|
<header>
|
||||||
<header>
|
<section>
|
||||||
{% block nav %}{% endblock nav %}
|
{% block nav %}{% endblock nav %}
|
||||||
</header>
|
</section>
|
||||||
</section>
|
</header>
|
||||||
<section>
|
|
||||||
<main>
|
<main>
|
||||||
<div class="container mt-3">
|
<section>
|
||||||
|
<div class="container mt-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Two-Factor Authentication settings:</h3>
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
@@ -17,6 +21,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</section>
|
||||||
</section>
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer mt-auto">
|
||||||
|
<section>
|
||||||
|
{% block footer_nav %}{% endblock footer_nav %}
|
||||||
|
</section>
|
||||||
|
</footer>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -33,3 +33,5 @@
|
|||||||
<button class="btn btn-outline-primary" type="submit">{% trans "Generate Tokens" %}</button>
|
<button class="btn btn-outline-primary" type="submit">{% trans "Generate Tokens" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||||
|
|||||||
@@ -65,3 +65,5 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||||
|
|||||||
@@ -24,3 +24,5 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||||
|
|||||||
@@ -15,5 +15,6 @@
|
|||||||
<button class="btn btn-danger" type="submit">{% trans "Disable" %}</button>
|
<button class="btn btn-danger" type="submit">{% trans "Disable" %}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||||
@@ -57,3 +57,5 @@
|
|||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block footer_nav %}{% include 'includes/footer.html' %}{% endblock footer_nav %}
|
||||||
|
|||||||
297
poetry.lock
generated
297
poetry.lock
generated
@@ -398,13 +398,100 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "phonenumberslite"
|
name = "phonenumberslite"
|
||||||
version = "8.13.19"
|
version = "8.13.20"
|
||||||
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
|
description = "Python version of Google's common library for parsing, formatting, storing and validating international phone numbers."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "phonenumberslite-8.13.19-py2.py3-none-any.whl", hash = "sha256:163a18e962e4a29ffd0bd523fa7aa60301bc7aa467bf0383bf3c4dac02097573"},
|
{file = "phonenumberslite-8.13.20-py2.py3-none-any.whl", hash = "sha256:f0840de9bfe5b8f10b6883d971dd81f3a53bed8210326a53266f11e8be3428c3"},
|
||||||
{file = "phonenumberslite-8.13.19.tar.gz", hash = "sha256:5d05d0f31f0838b1773c6093925c7c1ab3e98d98747db7c54a9385f8b6df5f71"},
|
{file = "phonenumberslite-8.13.20.tar.gz", hash = "sha256:09b07b32861f306dee10f17085afb3e058b4c179edf50deb94db4d4dd14235ff"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg"
|
||||||
|
version = "3.1.10"
|
||||||
|
description = "PostgreSQL database adapter for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "psycopg-3.1.10-py3-none-any.whl", hash = "sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52"},
|
||||||
|
{file = "psycopg-3.1.10.tar.gz", hash = "sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
psycopg-binary = {version = "3.1.10", optional = true, markers = "extra == \"binary\""}
|
||||||
|
typing-extensions = ">=4.1"
|
||||||
|
tzdata = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
binary = ["psycopg-binary (==3.1.10)"]
|
||||||
|
c = ["psycopg-c (==3.1.10)"]
|
||||||
|
dev = ["black (>=23.1.0)", "dnspython (>=2.1)", "flake8 (>=4.0)", "mypy (>=1.4.1)", "types-setuptools (>=57.4)", "wheel (>=0.37)"]
|
||||||
|
docs = ["Sphinx (>=5.0)", "furo (==2022.6.21)", "sphinx-autobuild (>=2021.3.14)", "sphinx-autodoc-typehints (>=1.12)"]
|
||||||
|
pool = ["psycopg-pool"]
|
||||||
|
test = ["anyio (>=3.6.2)", "mypy (>=1.4.1)", "pproxy (>=2.7)", "pytest (>=6.2.5)", "pytest-cov (>=3.0)", "pytest-randomly (>=3.5)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psycopg-binary"
|
||||||
|
version = "3.1.10"
|
||||||
|
description = "PostgreSQL database adapter for Python -- C optimisation distribution"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a529c203f6e0f4c67ba27cf8f9739eb3bc880ad70d6ad6c0e56c2230a66b5a09"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bd6e14d1aeb12754a43446c77a5ce819b68875cc25ae6538089ef90d7f6dd6f7"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1583ced5948cf88124212c4503dfe5b01ac3e2dd1a2833c083917f4c4aabe8b4"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2098721c486478987be700723b28ec7a48f134eba339de36af0e745f37dfe461"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7e61f7b412fca7b15dd043a0b22fd528d2ed8276e76b3764c3889e29fa65082b"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e0f33e33a072e3d5af51ee4d4a439e10dbe623fe87ef295d5d688180d529f13f"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f6f7738c59262d8d19154164d99c881ed58ed377fb6f1d685eb0dc43bbcd8022"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:511d38b1e1961d179d47d5103ba9634ecfc7ead431d19a9337ef82f3a2bca807"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:666e7acf2ffdb5e8a58e8b0c1759facdb9688c7e90ee8ca7aed675803b57404d"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:57b93c756fee5f7c7bd580c34cd5d244f7d5638f8b2cf25333f97b9b8b2ebfd1"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp310-cp310-win_amd64.whl", hash = "sha256:a1d61b7724c7215a8ea4495a5c6b704656f4b7bb6165f4cb9989b685886ebc48"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:36fff836a7823c9d71fa7faa333c74b2b081af216cebdbb0f481dce55ee2d974"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:32caf98cb00881bfcbbbae39a15f2a4e08b79ff983f1c0f13b60a888ef6e8431"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5565a6a86fee8d74f30de89e07f399567cdf59367aeb09624eb690d524339076"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9fb0d64520b29bd80a6731476ad8e1c20348dfdee00ab098899d23247b641675"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfc05ed4e74fa8615d7cc2bd57f00f97662f4e865a731dbd43da9a527e289c8c"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5b59c8cff887757ddf438ff9489d79c5e6b717112c96f5c68e16f367ff8724e"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a4cbaf12361136afefc5faab21a174a437e71c803b083f410e5140c7605bc66b"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ff72576061c774bcce5f5440b93e63d4c430032dd056d30f6cb1988e549dd92c"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:a4e91e1a8d61c60f592a1dfcebdf55e52a29fe4fdb650c5bd5414c848e77d029"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f7187269d825e84c945be7d93dd5088a4e0b6481a4bdaba3bf7069d4ac13703d"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp311-cp311-win_amd64.whl", hash = "sha256:ba7812a593c16d9d661844dc8dd4d81548fd1c2a0ee676f3e3d8638369f4c5e4"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88caa5859740507b3596c6c2e00ceaccee2c6ab5317bc535887801ad3cc7f3e1"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a3a7e99ba10c2e83a48d79431560e0d5ca7865f68f2bac3a462dc2b151e9926"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:848f4f4707dc73f4b4e844c92f3de795b2ddb728f75132602bda5e6ba55084fc"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:415961e839bb49cfd75cd961503fb8846c0768f247db1fa7171c1ac61d38711b"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0471869e658d0c6b8c3ed53153794739c18d7dad2dd5b8e6ff023a364c20f7df"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4290060ee0d856caa979ecf675c0e6959325f508272ccf27f64c3801c7bcbde7"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:abf04bc06c8f6a1ac3dc2106d3b79c8661352e9d8a57ca2934ffa6aae8fe600a"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:51fe70708243b83bf16710d8c11b61bd46562e6a24a6300d5434380b35911059"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8b658f7f8b49fb60a1c52e3f6692f690a85bdf1ad30aafe0f3f1fd74f6958cf8"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp37-cp37m-win_amd64.whl", hash = "sha256:ffc8c796194f23b9b07f6d25f927ec4df84a194bbc7a1f9e73316734eef512f9"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:74ce92122be34cf0e5f06d79869e1001c8421a68fa7ddf6fe38a717155cf3a64"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:75608a900984061c8898be68fbddc6f3da5eefdffce6e0624f5371645740d172"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6670d160d054466e8fdedfbc749ef8bf7dfdf69296048954d24645dd4d3d3c01"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d32026cfab7ba7ac687a42c33345026a2fb6fc5608a6144077f767af4386be0b"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:908fa388a5b75dfd17a937acb24708bd272e21edefca9a495004c6f70ec2636a"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e46b97073bd4de114f475249d681eaf054e950699c5d7af554d3684db39b82d"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9cf56bb4b115def3a18157f3b3b7d8322ee94a8dea30028db602c8f9ae34ad1e"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3b6c6f90241c4c5a6ca3f0d8827e37ef90fdc4deb9d8cfa5678baa0ea374b391"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:747176a6aeb058079f56c5397bd90339581ab7b3cc0d62e7445654e6a484c7e1"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41a415e78c457b06497fa0084e4ea7245ca1a377b55756dd757034210b64da7e"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp38-cp38-win_amd64.whl", hash = "sha256:a7bbe9017edd898d7b3a8747700ed045dda96a907dff87f45e642e28d8584481"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0f062f20256708929a58c41d44f350efced4c00a603323d1413f6dc0b84d95a5"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dea30f2704337ca2d0322fccfe1fa30f61ce9185de3937eb986321063114a51f"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9d88ac72531034ebf7ec09114e732b066a9078f4ce213cf65cc5e42eb538d30"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2bea0940d69c3e24a72530730952687912893b34c53aa39e79045e7b446174d"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a691dc8e2436d9c1e5cf93902d63e9501688fccc957eb22f952d37886257470"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa92661f99351765673835a4d936d79bd24dfbb358b29b084d83be38229a90e4"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:30eb731ed5525d8df892db6532cc8ffd8a163b73bc355127dee9c49334e16eee"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:50bf7a59d3a85a82d466fed341d352b44d09d6adc18656101d163a7cfc6509a0"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f48665947c55f8d6eb3f0be98de80411508e1ec329f354685329b57fced82c7f"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:caa771569da01fc0389ca34920c331a284425a68f92d1ba0a80cc08935f8356e"},
|
||||||
|
{file = "psycopg_binary-3.1.10-cp39-cp39-win_amd64.whl", hash = "sha256:b30887e631fd67affaed98f6cd2135b44f2d1a6d9bca353a69c3889c78bd7aa8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -459,13 +546,13 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2023.3"
|
version = "2023.3.post1"
|
||||||
description = "World timezone definitions, modern and historical"
|
description = "World timezone definitions, modern and historical"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "pytz-2023.3-py2.py3-none-any.whl", hash = "sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb"},
|
{file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"},
|
||||||
{file = "pytz-2023.3.tar.gz", hash = "sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588"},
|
{file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -557,108 +644,108 @@ rpds-py = ">=0.7.0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rpds-py"
|
name = "rpds-py"
|
||||||
version = "0.10.0"
|
version = "0.10.2"
|
||||||
description = "Python bindings to Rust's persistent data structures (rpds)"
|
description = "Python bindings to Rust's persistent data structures (rpds)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c1e0e9916301e3b3d970814b1439ca59487f0616d30f36a44cead66ee1748c31"},
|
{file = "rpds_py-0.10.2-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:9f00d54b18dd837f1431d66b076737deb7c29ce3ebb8412ceaf44d5e1954ac0c"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ce8caa29ebbdcde67e5fd652c811d34bc01f249dbc0d61e5cc4db05ae79a83b"},
|
{file = "rpds_py-0.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f4d561f4728f825e3b793a53064b606ca0b6fc264f67d09e54af452aafc5b82"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad277f74b1c164f7248afa968700e410651eb858d7c160d109fb451dc45a2f09"},
|
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:013d6c784150d10236a74b4094a79d96a256b814457e388fc5a4ba9efe24c402"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8e1c68303ccf7fceb50fbab79064a2636119fd9aca121f28453709283dbca727"},
|
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd1142d22fdb183a0fff66d79134bf644401437fed874f81066d314c67ee193c"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:780fcb855be29153901c67fc9c5633d48aebef21b90aa72812fa181d731c6b00"},
|
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a0536ed2b9297c75104e1a3da330828ba1b2639fa53b38d396f98bf7e3c68df"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbd7b24d108509a1b9b6679fcc1166a7dd031dbef1f3c2c73788f42e3ebb3beb"},
|
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:41bd430b7b63aa802c02964e331ac0b177148fef5f807d2c90d05ce71a52b4d4"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0700c2133ba203c4068aaecd6a59bda22e06a5e46255c9da23cbf68c6942215d"},
|
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e8474f7233fe1949ce4e03bea698a600c2d5d6b51dab6d6e6336dbe69acf23e"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:576da63eae7809f375932bfcbca2cf20620a1915bf2fedce4b9cc8491eceefe3"},
|
{file = "rpds_py-0.10.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d9d7efaad48b859053b90dedd69bc92f2095084251e732e4c57ac9726bcb1e64"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:23750a9b8a329844ba1fe267ca456bb3184984da2880ed17ae641c5af8de3fef"},
|
{file = "rpds_py-0.10.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5612b0b1de8d5114520094bd5fc3d04eb8af6f3e10d48ef05b7c8e77c1fd9545"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d08395595c42bcd82c3608762ce734504c6d025eef1c06f42326a6023a584186"},
|
{file = "rpds_py-0.10.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5d5eaf988951f6ecb6854ca3300b87123599c711183c83da7ce39717a7cbdbce"},
|
||||||
{file = "rpds_py-0.10.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1d7b7b71bcb82d8713c7c2e9c5f061415598af5938666beded20d81fa23e7640"},
|
{file = "rpds_py-0.10.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:75c8766734ac0053e1d683567e65e85306c4ec62631b0591caeb287ac8f72e08"},
|
||||||
{file = "rpds_py-0.10.0-cp310-none-win32.whl", hash = "sha256:97f5811df21703446b42303475b8b855ee07d6ab6cdf8565eff115540624f25d"},
|
{file = "rpds_py-0.10.2-cp310-none-win32.whl", hash = "sha256:8de9b88f0cbac73cfed34220d13c57849e62a7099a714b929142425e926d223a"},
|
||||||
{file = "rpds_py-0.10.0-cp310-none-win_amd64.whl", hash = "sha256:cdbed8f21204398f47de39b0a9b180d7e571f02dfb18bf5f1b618e238454b685"},
|
{file = "rpds_py-0.10.2-cp310-none-win_amd64.whl", hash = "sha256:2275f1a022e2383da5d2d101fe11ccdcbae799148c4b83260a4b9309fa3e1fc2"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:7a3a3d3e4f1e3cd2a67b93a0b6ed0f2499e33f47cc568e3a0023e405abdc0ff1"},
|
{file = "rpds_py-0.10.2-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:dd91a7d7a9ce7f4983097c91ce211f3e5569cc21caa16f2692298a07e396f82b"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fc72ae476732cdb7b2c1acb5af23b478b8a0d4b6fcf19b90dd150291e0d5b26b"},
|
{file = "rpds_py-0.10.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e82b4a70cc67094f3f3fd77579702f48fcf1de7bdc67d79b8f1e24d089a6162c"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c0583f69522732bdd79dca4cd3873e63a29acf4a299769c7541f2ca1e4dd4bc6"},
|
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e281b71922208e00886e4b7ffbfcf27874486364f177418ab676f102130e7ec9"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8b9a7cd381970e64849070aca7c32d53ab7d96c66db6c2ef7aa23c6e803f514"},
|
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b3eb1a0d2b6d232d1bcdfc3fcc5f7b004ab3fbd9203011a3172f051d4527c0b6"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d292cabd7c8335bdd3237ded442480a249dbcdb4ddfac5218799364a01a0f5c"},
|
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02945ae38fd78efc40900f509890de84cfd5ffe2cd2939eeb3a8800dc68b87cb"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6903cdca64f1e301af9be424798328c1fe3b4b14aede35f04510989fc72f012"},
|
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccfb77f6dc8abffa6f1c7e3975ed9070a41ce5fcc11154d2bead8c1baa940f09"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bed57543c99249ab3a4586ddc8786529fbc33309e5e8a1351802a06ca2baf4c2"},
|
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af52078719209bef33e38131486fd784832dd8d1dc9b85f00a44f6e7437dd021"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15932ec5f224b0e35764dc156514533a4fca52dcfda0dfbe462a1a22b37efd59"},
|
{file = "rpds_py-0.10.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56ba7c1100ed079527f2b995bf5486a2e557e6d5b733c52e8947476338815b69"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb2d59bc196e6d3b1827c7db06c1a898bfa0787c0574af398e65ccf2e97c0fbe"},
|
{file = "rpds_py-0.10.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:899b03a3be785a7e1ff84b237da71f0efa2f021512f147dd34ffdf7aa82cb678"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f99d74ddf9d3b6126b509e81865f89bd1283e3fc1b568b68cd7bd9dfa15583d7"},
|
{file = "rpds_py-0.10.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:22e6de18f00583f06928cc8d0993104ecc62f7c6da6478db2255de89a30e45d1"},
|
||||||
{file = "rpds_py-0.10.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f70bec8a14a692be6dbe7ce8aab303e88df891cbd4a39af091f90b6702e28055"},
|
{file = "rpds_py-0.10.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edd74b760a6bb950397e7a7bd2f38e6700f6525062650b1d77c6d851b82f02c2"},
|
||||||
{file = "rpds_py-0.10.0-cp311-none-win32.whl", hash = "sha256:5f7487be65b9c2c510819e744e375bd41b929a97e5915c4852a82fbb085df62c"},
|
{file = "rpds_py-0.10.2-cp311-none-win32.whl", hash = "sha256:18909093944727e068ebfc92e2e6ed1c4fa44135507c1c0555213ce211c53214"},
|
||||||
{file = "rpds_py-0.10.0-cp311-none-win_amd64.whl", hash = "sha256:748e472345c3a82cfb462d0dff998a7bf43e621eed73374cb19f307e97e08a83"},
|
{file = "rpds_py-0.10.2-cp311-none-win_amd64.whl", hash = "sha256:9568764e72d85cf7855ca78b48e07ed1be47bf230e2cea8dabda3c95f660b0ff"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:d4639111e73997567343df6551da9dd90d66aece1b9fc26c786d328439488103"},
|
{file = "rpds_py-0.10.2-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:0fc625059b83695fbb4fc8b7a8b66fa94ff9c7b78c84fb9986cd53ff88a28d80"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4760e1b02173f4155203054f77a5dc0b4078de7645c922b208d28e7eb99f3e2"},
|
{file = "rpds_py-0.10.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c86231c66e4f422e7c13ea6200bb4048b3016c8bfd11b4fd0dabd04d2c8e3501"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a6420a36975e0073acaeee44ead260c1f6ea56812cfc6c31ec00c1c48197173"},
|
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56777c57246e048908b550af9b81b0ec9cf804fd47cb7502ccd93238bd6025c2"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58fc4d66ee349a23dbf08c7e964120dc9027059566e29cf0ce6205d590ed7eca"},
|
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a4cb372e22e9c879bd9a9cc9b20b7c1fbf30a605ac953da45ecec05d8a6e1c77"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:063411228b852fb2ed7485cf91f8e7d30893e69b0acb207ec349db04cccc8225"},
|
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa3b3a43dabc4cc57a7800f526cbe03f71c69121e21b863fdf497b59b462b163"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65af12f70355de29e1092f319f85a3467f4005e959ab65129cb697169ce94b86"},
|
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:59d222086daa55421d599609b32d0ebe544e57654c4a0a1490c54a7ebaa67561"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:298e8b5d8087e0330aac211c85428c8761230ef46a1f2c516d6a2f67fb8803c5"},
|
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:529aab727f54a937085184e7436e1d0e19975cf10115eda12d37a683e4ee5342"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5b9bf77008f2c55dabbd099fd3ac87009471d223a1c7ebea36873d39511b780a"},
|
{file = "rpds_py-0.10.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:43e9b1531d6a898bdf086acb75c41265c7ec4331267d7619148d407efc72bd24"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c7853f27195598e550fe089f78f0732c66ee1d1f0eaae8ad081589a5a2f5d4af"},
|
{file = "rpds_py-0.10.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c2772bb95062e3f9774140205cd65d8997e39620715486cf5f843cf4ad8f744c"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:75dbfd41a61bc1fb0536bf7b1abf272dc115c53d4d77db770cd65d46d4520882"},
|
{file = "rpds_py-0.10.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ba1b28e44f611f3f2b436bd8290050a61db4b59a8e24be4465f44897936b3824"},
|
||||||
{file = "rpds_py-0.10.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b25136212a3d064a8f0b9ebbb6c57094c5229e0de76d15c79b76feff26aeb7b8"},
|
{file = "rpds_py-0.10.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5aba767e64b494483ad60c4873bec78d16205a21f8247c99749bd990d9c846c2"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:9affee8cb1ec453382c27eb9043378ab32f49cd4bc24a24275f5c39bf186c279"},
|
{file = "rpds_py-0.10.2-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:e1954f4b239d1a92081647eecfd51cbfd08ea16eb743b8af1cd0113258feea14"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4d55528ef13af4b4e074d067977b1f61408602f53ae4537dccf42ba665c2c7bd"},
|
{file = "rpds_py-0.10.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:de4a2fd524993578fe093044f291b4b24aab134390030b3b9b5f87fd41ab7e75"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7865df1fb564092bcf46dac61b5def25342faf6352e4bc0e61a286e3fa26a3d"},
|
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e69737bd56006a86fd5a78b2b85447580a6138c930a75eb9ef39fe03d90782b1"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3f5cc8c7bc99d2bbcd704cef165ca7d155cd6464c86cbda8339026a42d219397"},
|
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f40abbcc0a7d9a8a80870af839d317e6932533f98682aabd977add6c53beeb23"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cbae50d352e4717ffc22c566afc2d0da744380e87ed44a144508e3fb9114a3f4"},
|
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29ec8507664f94cc08457d98cfc41c3cdbddfa8952438e644177a29b04937876"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fccbf0cd3411719e4c9426755df90bf3449d9fc5a89f077f4a7f1abd4f70c910"},
|
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcde80aefe7054fad6277762fb7e9d35c72ea479a485ae1bb14629c640987b30"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d10c431073dc6ebceed35ab22948a016cc2b5120963c13a41e38bdde4a7212"},
|
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a65de5c02884760a14a58304fb6303f9ddfc582e630f385daea871e1bdb18686"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1b401e8b9aece651512e62c431181e6e83048a651698a727ea0eb0699e9f9b74"},
|
{file = "rpds_py-0.10.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e92e5817eb6bfed23aa5e45bfe30647b83602bdd6f9e25d63524d4e6258458b0"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7618a082c55cf038eede4a918c1001cc8a4411dfe508dc762659bcd48d8f4c6e"},
|
{file = "rpds_py-0.10.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2c8fc6c841ada60a86d29c9ebe2e8757c47eda6553f3596c560e59ca6e9b6fa1"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b3226b246facae14909b465061ddcfa2dfeadb6a64f407f24300d42d69bcb1a1"},
|
{file = "rpds_py-0.10.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:8557c807388e6617161fe51b1a4747ea8d1133f2d2ad8e79583439abebe58fbd"},
|
||||||
{file = "rpds_py-0.10.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a8edd467551c1102dc0f5754ab55cd0703431cd3044edf8c8e7d9208d63fa453"},
|
{file = "rpds_py-0.10.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:00e97d43a36811b78fa9ad9d3329bf34f76a31e891a7031a2ac01450c9b168ab"},
|
||||||
{file = "rpds_py-0.10.0-cp38-none-win32.whl", hash = "sha256:71333c22f7cf5f0480b59a0aef21f652cf9bbaa9679ad261b405b65a57511d1e"},
|
{file = "rpds_py-0.10.2-cp38-none-win32.whl", hash = "sha256:1ed3d5385d14be894e12a9033be989e012214a9811e7194849c94032ad69682a"},
|
||||||
{file = "rpds_py-0.10.0-cp38-none-win_amd64.whl", hash = "sha256:a8ab1adf04ae2d6d65835995218fd3f3eb644fe20655ca8ee233e2c7270ff53b"},
|
{file = "rpds_py-0.10.2-cp38-none-win_amd64.whl", hash = "sha256:02b4a2e28eb24dac4ef43dda4f6a6f7766e355179b143f7d0c76a1c5488a307b"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:87c93b25d538c433fb053da6228c6290117ba53ff6a537c133b0f2087948a582"},
|
{file = "rpds_py-0.10.2-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:2a55631b93e47956fbc97d69ba2054a8c6a4016f9a3064ec4e031f5f1030cb90"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5e7996aed3f65667c6dcc8302a69368435a87c2364079a066750a2eac75ea01e"},
|
{file = "rpds_py-0.10.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2ffbf1b38c88d0466de542e91b08225d51782282512f8e2b11715126c41fda48"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8856aa76839dc234d3469f1e270918ce6bec1d6a601eba928f45d68a15f04fc3"},
|
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213f9ef5c02ec2f883c1075d25a873149daadbaea50d18d622e9db55ec9849c2"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00215f6a9058fbf84f9d47536902558eb61f180a6b2a0fa35338d06ceb9a2e5a"},
|
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b00150a9a3fd0a8efaa90bc2696c105b04039d50763dd1c95a34c88c5966cb57"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:23a059143c1393015c68936370cce11690f7294731904bdae47cc3e16d0b2474"},
|
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ab0f7aabdbce4a202e013083eeab71afdb85efa405dc4a06fea98cde81204675"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e5c26905aa651cc8c0ddc45e0e5dea2a1296f70bdc96af17aee9d0493280a17"},
|
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2cd0c9fb5d40887500b4ed818770c68ab4fa6e0395d286f9704be6751b1b7d98"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c651847545422c8131660704c58606d841e228ed576c8f1666d98b3d318f89da"},
|
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8578fc6c8bdd0201327503720fa581000b4bd3934abbf07e2628d1ad3de157d"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80992eb20755701753e30a6952a96aa58f353d12a65ad3c9d48a8da5ec4690cf"},
|
{file = "rpds_py-0.10.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d27d08056fcd61ff47a0cd8407eff4d3e816c82cb6b9c6f0ce9a0ad49225f81"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ffcf18ad3edf1c170e27e88b10282a2c449aa0358659592462448d71b2000cfc"},
|
{file = "rpds_py-0.10.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c8f6526df47953b07c45b95c4d1da6b9a0861c0e5da0271db96bb1d807825412"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:08e08ccf5b10badb7d0a5c84829b914c6e1e1f3a716fdb2bf294e2bd01562775"},
|
{file = "rpds_py-0.10.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177c033e467a66a054dd3a9534167234a3d0b2e41445807b13b626e01da25d92"},
|
||||||
{file = "rpds_py-0.10.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7150b83b3e3ddaac81a8bb6a9b5f93117674a0e7a2b5a5b32ab31fdfea6df27f"},
|
{file = "rpds_py-0.10.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c74cbee9e532dc34371127f7686d6953e5153a1f22beab7f953d95ee4a0fe09"},
|
||||||
{file = "rpds_py-0.10.0-cp39-none-win32.whl", hash = "sha256:3455ecc46ea443b5f7d9c2f946ce4017745e017b0d0f8b99c92564eff97e97f5"},
|
{file = "rpds_py-0.10.2-cp39-none-win32.whl", hash = "sha256:05a1382905026bdd560f806c8c7c16e0f3e3fb359ba8868203ca6e5799884968"},
|
||||||
{file = "rpds_py-0.10.0-cp39-none-win_amd64.whl", hash = "sha256:afe6b5a04b2ab1aa89bad32ca47bf71358e7302a06fdfdad857389dca8fb5f04"},
|
{file = "rpds_py-0.10.2-cp39-none-win_amd64.whl", hash = "sha256:3fd503c27e7b7034128e30847ecdb4bff4ca5e60f29ad022a9f66ae8940d54ac"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:b1cb078f54af0abd835ca76f93a3152565b73be0f056264da45117d0adf5e99c"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4a96147791e49e84207dd1530109aa0e9eeaf1c8b7a59f150047fc0fcdf9bb64"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8e7e2b3577e97fa43c2c2b12a16139b2cedbd0770235d5179c0412b4794efd9b"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:203eb1532d51591d32e8dfafd60b5d31347ea7278c8da02b4b550287f6abe28b"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae46a50d235f1631d9ec4670503f7b30405103034830bc13df29fd947207f795"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2f416cdfe92f5fbb77177f5f3f7830059d1582db05f2c7119bf80069d1ab69b"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f869e34d2326e417baee430ae998e91412cc8e7fdd83d979277a90a0e79a5b47"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b2660000e1a113869c86eb5cc07f3343467490f3cd9d0299f81da9ddae7137b7"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d544a614055b131111bed6edfa1cb0fb082a7265761bcb03321f2dd7b5c6c48"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1adb04e4b4e41bf30aaa77eeb169c1b9ba9e5010e2e6ce8d6c17e1446edc9b68"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9c2f6ca9774c2c24bbf7b23086264e6b5fa178201450535ec0859739e6f78d"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2bca97521ee786087f0c5ef318fef3eef0266a9c3deff88205523cf353af7394"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2da4a8c6d465fde36cea7d54bf47b5cf089073452f0e47c8632ecb9dec23c07"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4969592e3cdeefa4cbb15a26cec102cbd4a1d6e5b695fac9fa026e19741138c8"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac00c41dd315d147b129976204839ca9de699d83519ff1272afbe4fb9d362d12"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df61f818edf7c8626bfa392f825860fb670b5f8336e238eb0ec7e2a5689cdded"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0155c33af0676fc38e1107679be882077680ad1abb6303956b97259c3177e85e"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b589d93a60e78fe55d5bc76ee8c2bf945dbdbb7cd16044c53e0307604e448de1"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:db6585b600b2e76e98131e0ac0e5195759082b51687ad0c94505970c90718f4a"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:73da69e1f612c3e682e34dcb971272d90d6f27b2c99acff444ca455a89978574"},
|
||||||
{file = "rpds_py-0.10.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:7b6975d3763d0952c111700c0634968419268e6bbc0b55fe71138987fa66f309"},
|
{file = "rpds_py-0.10.2-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:89438e8885a186c69fe31f7ef98bb2bf29688c466c3caf9060f404c0be89ae80"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:6388e4e95a26717b94a05ced084e19da4d92aca883f392dffcf8e48c8e221a24"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c4ecc4e9a5d73a816cae36ee6b5d8b7a0c72013cae1e101406e832887c3dc2d8"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:18f87baa20e02e9277ad8960cd89b63c79c05caf106f4c959a9595c43f2a34a5"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:907b214da5d2fcff0b6ddb83de1333890ca92abaf4bbf8d9c61dc1b95c87fd6e"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f05fc7d832e970047662b3440b190d24ea04f8d3c760e33e7163b67308c878"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb44644371eaa29a3aba7b69b1862d0d56f073bb7585baa32e4271a71a91ee82"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:291c9ce3929a75b45ce8ddde2aa7694fc8449f2bc8f5bd93adf021efaae2d10b"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:80c3cf46511653f94dfe07c7c79ab105c4164d6e1dfcb35b7214fb9af53eaef4"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:861d25ae0985a1dd5297fee35f476b60c6029e2e6e19847d5b4d0a43a390b696"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaba0613c759ebf95988a84f766ca6b7432d55ce399194f95dde588ad1be0878"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:668d2b45d62c68c7a370ac3dce108ffda482b0a0f50abd8b4c604a813a59e08f"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0527c97dcd8bb983822ee31d3760187083fd3ba18ac4dd22cf5347c89d5628f4"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:344b89384c250ba6a4ce1786e04d01500e4dac0f4137ceebcaad12973c0ac0b3"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9cdfd649011ce2d90cb0dd304c5aba1190fac0c266d19a9e2b96b81cfd150a09"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:885e023e73ce09b11b89ab91fc60f35d80878d2c19d6213a32b42ff36543c291"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75eea40355a8690459c7291ce6c8ce39c27bd223675c7da6619f510c728feb97"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:841128a22e6ac04070a0f84776d07e9c38c4dcce8e28792a95e45fc621605517"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1b804cfad04f862d6a84af9d1ad941b06f671878f0f7ecad6c92007d423de6"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:899b5e7e2d5a8bc92aa533c2d4e55e5ebba095c485568a5e4bedbc163421259a"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:bf77f9017fcfa1232f98598a637406e6c33982ccba8a5922339575c3e2b90ea5"},
|
||||||
{file = "rpds_py-0.10.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e7947d9a6264c727a556541b1630296bbd5d0a05068d21c38dde8e7a1c703ef0"},
|
{file = "rpds_py-0.10.2-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:46c4c550bf59ce05d6bff2c98053822549aaf9fbaf81103edea325e03350bca1"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:4992266817169997854f81df7f6db7bdcda1609972d8ffd6919252f09ec3c0f6"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:46af4a742b90c7460e94214f923452c2c1d050a9da1d2b8d4c70cbc045e692b7"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:26d9fd624649a10e4610fab2bc820e215a184d193e47d0be7fe53c1c8f67f370"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2a86d246a160d98d820ee7d02dc18c923c228de095be362e57b9fd8970b2c4a1"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0028eb0967942d0d2891eae700ae1a27b7fd18604cfcb16a1ef486a790fee99e"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae141c9017f8f473a6ee07a9425da021816a9f8c0683c2e5442f0ccf56b0fc62"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f9e7e493ded7042712a374471203dd43ae3fff5b81e3de1a0513fa241af9fd41"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e1147bc3d0dd1e549d991110d0a09557ec9f925dbc1ca62871fcdab2ec9d716b"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2d68a8e8a3a816629283faf82358d8c93fe5bd974dd2704152394a3de4cec22a"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fce7a8ee8d0f682c953c0188735d823f0fcb62779bf92cd6ba473a8e730e26ad"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d6d5f061f6a2aa55790b9e64a23dfd87b6664ab56e24cd06c78eb43986cb260b"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c7f9d70f99e1fbcbf57c75328b80e1c0a7f6cad43e75efa90a97221be5efe15"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c7c4266c1b61eb429e8aeb7d8ed6a3bfe6c890a1788b18dbec090c35c6b93fa"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b309908b6ff5ffbf6394818cb73b5a2a74073acee2c57fe8719046389aeff0d"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:80772e3bda6787510d9620bc0c7572be404a922f8ccdfd436bf6c3778119464c"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3ff1f585a0fdc1415bd733b804f33d386064a308672249b14828130dd43e7c31"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:b98e75b21fc2ba5285aef8efaf34131d16af1c38df36bdca2f50634bea2d3060"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:0188b580c490bccb031e9b67e9e8c695a3c44ac5e06218b152361eca847317c3"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:d63787f289944cc4bde518ad2b5e70a4f0d6e2ce76324635359c74c113fd188f"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:abe081453166e206e3a8c6d8ace57214c17b6d9477d7601ac14a365344dbc1f4"},
|
||||||
{file = "rpds_py-0.10.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:872f3dcaa8bf2245944861d7311179d2c0c9b2aaa7d3b464d99a7c2e401f01fa"},
|
{file = "rpds_py-0.10.2-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:9118de88c16947eaf5b92f749e65b0501ea69e7c2be7bd6aefc12551622360e1"},
|
||||||
{file = "rpds_py-0.10.0.tar.gz", hash = "sha256:e36d7369363d2707d5f68950a64c4e025991eb0177db01ccb6aa6facae48b69f"},
|
{file = "rpds_py-0.10.2.tar.gz", hash = "sha256:289073f68452b96e70990085324be7223944c7409973d13ddfe0eea1c1b5663b"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -727,4 +814,4 @@ brotli = ["Brotli"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "e67fb8ed8bd9086956f4c789fd42d8b0ebe140579bc2aa2f1c303bd1def9b2f7"
|
content-hash = "9c8a0b848ed1ebc1da6cafbf9b769f72f8a70e15ed4a226544c956ef69a4f750"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ drf-spectacular = "0.26.4"
|
|||||||
django-two-factor-auth = {extras = ["phonenumberslite"], version = "1.15.3"}
|
django-two-factor-auth = {extras = ["phonenumberslite"], version = "1.15.3"}
|
||||||
whitenoise = "6.5.0"
|
whitenoise = "6.5.0"
|
||||||
gunicorn = {extras = ["gthread"], version = "21.2.0"}
|
gunicorn = {extras = ["gthread"], version = "21.2.0"}
|
||||||
|
psycopg = {extras = ["binary"], version = "3.1.10"}
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
flake8 = "^6.0.0"
|
flake8 = "^6.0.0"
|
||||||
|
|||||||
266
requirements.txt
266
requirements.txt
@@ -64,18 +64,76 @@ markdown==3.4.4 ; python_version >= "3.10" and python_version < "4.0" \
|
|||||||
packaging==23.1 ; python_version >= "3.10" and python_version < "4.0" \
|
packaging==23.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
|
--hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \
|
||||||
--hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
|
--hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f
|
||||||
phonenumberslite==8.13.19 ; python_version >= "3.10" and python_version < "4.0" \
|
phonenumberslite==8.13.20 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:163a18e962e4a29ffd0bd523fa7aa60301bc7aa467bf0383bf3c4dac02097573 \
|
--hash=sha256:09b07b32861f306dee10f17085afb3e058b4c179edf50deb94db4d4dd14235ff \
|
||||||
--hash=sha256:5d05d0f31f0838b1773c6093925c7c1ab3e98d98747db7c54a9385f8b6df5f71
|
--hash=sha256:f0840de9bfe5b8f10b6883d971dd81f3a53bed8210326a53266f11e8be3428c3
|
||||||
|
psycopg-binary==3.1.10 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
|
--hash=sha256:0471869e658d0c6b8c3ed53153794739c18d7dad2dd5b8e6ff023a364c20f7df \
|
||||||
|
--hash=sha256:0f062f20256708929a58c41d44f350efced4c00a603323d1413f6dc0b84d95a5 \
|
||||||
|
--hash=sha256:1583ced5948cf88124212c4503dfe5b01ac3e2dd1a2833c083917f4c4aabe8b4 \
|
||||||
|
--hash=sha256:1e46b97073bd4de114f475249d681eaf054e950699c5d7af554d3684db39b82d \
|
||||||
|
--hash=sha256:2098721c486478987be700723b28ec7a48f134eba339de36af0e745f37dfe461 \
|
||||||
|
--hash=sha256:30eb731ed5525d8df892db6532cc8ffd8a163b73bc355127dee9c49334e16eee \
|
||||||
|
--hash=sha256:32caf98cb00881bfcbbbae39a15f2a4e08b79ff983f1c0f13b60a888ef6e8431 \
|
||||||
|
--hash=sha256:36fff836a7823c9d71fa7faa333c74b2b081af216cebdbb0f481dce55ee2d974 \
|
||||||
|
--hash=sha256:3b6c6f90241c4c5a6ca3f0d8827e37ef90fdc4deb9d8cfa5678baa0ea374b391 \
|
||||||
|
--hash=sha256:415961e839bb49cfd75cd961503fb8846c0768f247db1fa7171c1ac61d38711b \
|
||||||
|
--hash=sha256:41a415e78c457b06497fa0084e4ea7245ca1a377b55756dd757034210b64da7e \
|
||||||
|
--hash=sha256:4290060ee0d856caa979ecf675c0e6959325f508272ccf27f64c3801c7bcbde7 \
|
||||||
|
--hash=sha256:4a3a7e99ba10c2e83a48d79431560e0d5ca7865f68f2bac3a462dc2b151e9926 \
|
||||||
|
--hash=sha256:50bf7a59d3a85a82d466fed341d352b44d09d6adc18656101d163a7cfc6509a0 \
|
||||||
|
--hash=sha256:511d38b1e1961d179d47d5103ba9634ecfc7ead431d19a9337ef82f3a2bca807 \
|
||||||
|
--hash=sha256:51fe70708243b83bf16710d8c11b61bd46562e6a24a6300d5434380b35911059 \
|
||||||
|
--hash=sha256:5565a6a86fee8d74f30de89e07f399567cdf59367aeb09624eb690d524339076 \
|
||||||
|
--hash=sha256:57b93c756fee5f7c7bd580c34cd5d244f7d5638f8b2cf25333f97b9b8b2ebfd1 \
|
||||||
|
--hash=sha256:666e7acf2ffdb5e8a58e8b0c1759facdb9688c7e90ee8ca7aed675803b57404d \
|
||||||
|
--hash=sha256:6670d160d054466e8fdedfbc749ef8bf7dfdf69296048954d24645dd4d3d3c01 \
|
||||||
|
--hash=sha256:6a691dc8e2436d9c1e5cf93902d63e9501688fccc957eb22f952d37886257470 \
|
||||||
|
--hash=sha256:747176a6aeb058079f56c5397bd90339581ab7b3cc0d62e7445654e6a484c7e1 \
|
||||||
|
--hash=sha256:74ce92122be34cf0e5f06d79869e1001c8421a68fa7ddf6fe38a717155cf3a64 \
|
||||||
|
--hash=sha256:75608a900984061c8898be68fbddc6f3da5eefdffce6e0624f5371645740d172 \
|
||||||
|
--hash=sha256:7e61f7b412fca7b15dd043a0b22fd528d2ed8276e76b3764c3889e29fa65082b \
|
||||||
|
--hash=sha256:848f4f4707dc73f4b4e844c92f3de795b2ddb728f75132602bda5e6ba55084fc \
|
||||||
|
--hash=sha256:88caa5859740507b3596c6c2e00ceaccee2c6ab5317bc535887801ad3cc7f3e1 \
|
||||||
|
--hash=sha256:8b658f7f8b49fb60a1c52e3f6692f690a85bdf1ad30aafe0f3f1fd74f6958cf8 \
|
||||||
|
--hash=sha256:908fa388a5b75dfd17a937acb24708bd272e21edefca9a495004c6f70ec2636a \
|
||||||
|
--hash=sha256:9cf56bb4b115def3a18157f3b3b7d8322ee94a8dea30028db602c8f9ae34ad1e \
|
||||||
|
--hash=sha256:9fb0d64520b29bd80a6731476ad8e1c20348dfdee00ab098899d23247b641675 \
|
||||||
|
--hash=sha256:a1d61b7724c7215a8ea4495a5c6b704656f4b7bb6165f4cb9989b685886ebc48 \
|
||||||
|
--hash=sha256:a4cbaf12361136afefc5faab21a174a437e71c803b083f410e5140c7605bc66b \
|
||||||
|
--hash=sha256:a4e91e1a8d61c60f592a1dfcebdf55e52a29fe4fdb650c5bd5414c848e77d029 \
|
||||||
|
--hash=sha256:a529c203f6e0f4c67ba27cf8f9739eb3bc880ad70d6ad6c0e56c2230a66b5a09 \
|
||||||
|
--hash=sha256:a7bbe9017edd898d7b3a8747700ed045dda96a907dff87f45e642e28d8584481 \
|
||||||
|
--hash=sha256:abf04bc06c8f6a1ac3dc2106d3b79c8661352e9d8a57ca2934ffa6aae8fe600a \
|
||||||
|
--hash=sha256:b30887e631fd67affaed98f6cd2135b44f2d1a6d9bca353a69c3889c78bd7aa8 \
|
||||||
|
--hash=sha256:b9d88ac72531034ebf7ec09114e732b066a9078f4ce213cf65cc5e42eb538d30 \
|
||||||
|
--hash=sha256:ba7812a593c16d9d661844dc8dd4d81548fd1c2a0ee676f3e3d8638369f4c5e4 \
|
||||||
|
--hash=sha256:bd6e14d1aeb12754a43446c77a5ce819b68875cc25ae6538089ef90d7f6dd6f7 \
|
||||||
|
--hash=sha256:bfc05ed4e74fa8615d7cc2bd57f00f97662f4e865a731dbd43da9a527e289c8c \
|
||||||
|
--hash=sha256:c5b59c8cff887757ddf438ff9489d79c5e6b717112c96f5c68e16f367ff8724e \
|
||||||
|
--hash=sha256:caa771569da01fc0389ca34920c331a284425a68f92d1ba0a80cc08935f8356e \
|
||||||
|
--hash=sha256:d32026cfab7ba7ac687a42c33345026a2fb6fc5608a6144077f767af4386be0b \
|
||||||
|
--hash=sha256:dea30f2704337ca2d0322fccfe1fa30f61ce9185de3937eb986321063114a51f \
|
||||||
|
--hash=sha256:e0f33e33a072e3d5af51ee4d4a439e10dbe623fe87ef295d5d688180d529f13f \
|
||||||
|
--hash=sha256:f2bea0940d69c3e24a72530730952687912893b34c53aa39e79045e7b446174d \
|
||||||
|
--hash=sha256:f48665947c55f8d6eb3f0be98de80411508e1ec329f354685329b57fced82c7f \
|
||||||
|
--hash=sha256:f6f7738c59262d8d19154164d99c881ed58ed377fb6f1d685eb0dc43bbcd8022 \
|
||||||
|
--hash=sha256:f7187269d825e84c945be7d93dd5088a4e0b6481a4bdaba3bf7069d4ac13703d \
|
||||||
|
--hash=sha256:fa92661f99351765673835a4d936d79bd24dfbb358b29b084d83be38229a90e4 \
|
||||||
|
--hash=sha256:ff72576061c774bcce5f5440b93e63d4c430032dd056d30f6cb1988e549dd92c \
|
||||||
|
--hash=sha256:ffc8c796194f23b9b07f6d25f927ec4df84a194bbc7a1f9e73316734eef512f9
|
||||||
|
psycopg[binary]==3.1.10 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
|
--hash=sha256:15b25741494344c24066dc2479b0f383dd1b82fa5e75612fa4fa5bb30726e9b6 \
|
||||||
|
--hash=sha256:8bbeddae5075c7890b2fa3e3553440376d3c5e28418335dee3c3656b06fa2b52
|
||||||
pyjwt==2.8.0 ; python_version >= "3.10" and python_version < "4.0" \
|
pyjwt==2.8.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \
|
--hash=sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de \
|
||||||
--hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320
|
--hash=sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320
|
||||||
pypng==0.20220715.0 ; python_version >= "3.10" and python_version < "4.0" \
|
pypng==0.20220715.0 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c \
|
--hash=sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c \
|
||||||
--hash=sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1
|
--hash=sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1
|
||||||
pytz==2023.3 ; python_version >= "3.10" and python_version < "4.0" \
|
pytz==2023.3.post1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \
|
--hash=sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b \
|
||||||
--hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb
|
--hash=sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7
|
||||||
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" \
|
pyyaml==6.0.1 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
|
--hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
|
||||||
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
|
--hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
|
||||||
@@ -123,104 +181,104 @@ qrcode==7.4.2 ; python_version >= "3.10" and python_version < "4.0" \
|
|||||||
referencing==0.30.2 ; python_version >= "3.10" and python_version < "4.0" \
|
referencing==0.30.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf \
|
--hash=sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf \
|
||||||
--hash=sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0
|
--hash=sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0
|
||||||
rpds-py==0.10.0 ; python_version >= "3.10" and python_version < "4.0" \
|
rpds-py==0.10.2 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:00215f6a9058fbf84f9d47536902558eb61f180a6b2a0fa35338d06ceb9a2e5a \
|
--hash=sha256:00e97d43a36811b78fa9ad9d3329bf34f76a31e891a7031a2ac01450c9b168ab \
|
||||||
--hash=sha256:0028eb0967942d0d2891eae700ae1a27b7fd18604cfcb16a1ef486a790fee99e \
|
--hash=sha256:013d6c784150d10236a74b4094a79d96a256b814457e388fc5a4ba9efe24c402 \
|
||||||
--hash=sha256:0155c33af0676fc38e1107679be882077680ad1abb6303956b97259c3177e85e \
|
--hash=sha256:0188b580c490bccb031e9b67e9e8c695a3c44ac5e06218b152361eca847317c3 \
|
||||||
--hash=sha256:063411228b852fb2ed7485cf91f8e7d30893e69b0acb207ec349db04cccc8225 \
|
--hash=sha256:02945ae38fd78efc40900f509890de84cfd5ffe2cd2939eeb3a8800dc68b87cb \
|
||||||
--hash=sha256:0700c2133ba203c4068aaecd6a59bda22e06a5e46255c9da23cbf68c6942215d \
|
--hash=sha256:02b4a2e28eb24dac4ef43dda4f6a6f7766e355179b143f7d0c76a1c5488a307b \
|
||||||
--hash=sha256:08e08ccf5b10badb7d0a5c84829b914c6e1e1f3a716fdb2bf294e2bd01562775 \
|
--hash=sha256:0527c97dcd8bb983822ee31d3760187083fd3ba18ac4dd22cf5347c89d5628f4 \
|
||||||
--hash=sha256:0d292cabd7c8335bdd3237ded442480a249dbcdb4ddfac5218799364a01a0f5c \
|
--hash=sha256:05a1382905026bdd560f806c8c7c16e0f3e3fb359ba8868203ca6e5799884968 \
|
||||||
--hash=sha256:15932ec5f224b0e35764dc156514533a4fca52dcfda0dfbe462a1a22b37efd59 \
|
--hash=sha256:0b309908b6ff5ffbf6394818cb73b5a2a74073acee2c57fe8719046389aeff0d \
|
||||||
--hash=sha256:18f87baa20e02e9277ad8960cd89b63c79c05caf106f4c959a9595c43f2a34a5 \
|
--hash=sha256:0fc625059b83695fbb4fc8b7a8b66fa94ff9c7b78c84fb9986cd53ff88a28d80 \
|
||||||
--hash=sha256:1a6420a36975e0073acaeee44ead260c1f6ea56812cfc6c31ec00c1c48197173 \
|
--hash=sha256:177c033e467a66a054dd3a9534167234a3d0b2e41445807b13b626e01da25d92 \
|
||||||
--hash=sha256:1b401e8b9aece651512e62c431181e6e83048a651698a727ea0eb0699e9f9b74 \
|
--hash=sha256:18909093944727e068ebfc92e2e6ed1c4fa44135507c1c0555213ce211c53214 \
|
||||||
--hash=sha256:1d7b7b71bcb82d8713c7c2e9c5f061415598af5938666beded20d81fa23e7640 \
|
--hash=sha256:1adb04e4b4e41bf30aaa77eeb169c1b9ba9e5010e2e6ce8d6c17e1446edc9b68 \
|
||||||
--hash=sha256:23750a9b8a329844ba1fe267ca456bb3184984da2880ed17ae641c5af8de3fef \
|
--hash=sha256:1ed3d5385d14be894e12a9033be989e012214a9811e7194849c94032ad69682a \
|
||||||
--hash=sha256:23a059143c1393015c68936370cce11690f7294731904bdae47cc3e16d0b2474 \
|
--hash=sha256:203eb1532d51591d32e8dfafd60b5d31347ea7278c8da02b4b550287f6abe28b \
|
||||||
--hash=sha256:26d9fd624649a10e4610fab2bc820e215a184d193e47d0be7fe53c1c8f67f370 \
|
--hash=sha256:213f9ef5c02ec2f883c1075d25a873149daadbaea50d18d622e9db55ec9849c2 \
|
||||||
--hash=sha256:291c9ce3929a75b45ce8ddde2aa7694fc8449f2bc8f5bd93adf021efaae2d10b \
|
--hash=sha256:2275f1a022e2383da5d2d101fe11ccdcbae799148c4b83260a4b9309fa3e1fc2 \
|
||||||
--hash=sha256:298e8b5d8087e0330aac211c85428c8761230ef46a1f2c516d6a2f67fb8803c5 \
|
--hash=sha256:22e6de18f00583f06928cc8d0993104ecc62f7c6da6478db2255de89a30e45d1 \
|
||||||
--hash=sha256:2c7c4266c1b61eb429e8aeb7d8ed6a3bfe6c890a1788b18dbec090c35c6b93fa \
|
--hash=sha256:289073f68452b96e70990085324be7223944c7409973d13ddfe0eea1c1b5663b \
|
||||||
--hash=sha256:2d68a8e8a3a816629283faf82358d8c93fe5bd974dd2704152394a3de4cec22a \
|
--hash=sha256:29ec8507664f94cc08457d98cfc41c3cdbddfa8952438e644177a29b04937876 \
|
||||||
--hash=sha256:344b89384c250ba6a4ce1786e04d01500e4dac0f4137ceebcaad12973c0ac0b3 \
|
--hash=sha256:2a55631b93e47956fbc97d69ba2054a8c6a4016f9a3064ec4e031f5f1030cb90 \
|
||||||
--hash=sha256:3455ecc46ea443b5f7d9c2f946ce4017745e017b0d0f8b99c92564eff97e97f5 \
|
--hash=sha256:2a86d246a160d98d820ee7d02dc18c923c228de095be362e57b9fd8970b2c4a1 \
|
||||||
--hash=sha256:3d544a614055b131111bed6edfa1cb0fb082a7265761bcb03321f2dd7b5c6c48 \
|
--hash=sha256:2bca97521ee786087f0c5ef318fef3eef0266a9c3deff88205523cf353af7394 \
|
||||||
--hash=sha256:3e5c26905aa651cc8c0ddc45e0e5dea2a1296f70bdc96af17aee9d0493280a17 \
|
--hash=sha256:2c8fc6c841ada60a86d29c9ebe2e8757c47eda6553f3596c560e59ca6e9b6fa1 \
|
||||||
--hash=sha256:3f5cc8c7bc99d2bbcd704cef165ca7d155cd6464c86cbda8339026a42d219397 \
|
--hash=sha256:2cd0c9fb5d40887500b4ed818770c68ab4fa6e0395d286f9704be6751b1b7d98 \
|
||||||
--hash=sha256:4992266817169997854f81df7f6db7bdcda1609972d8ffd6919252f09ec3c0f6 \
|
--hash=sha256:2d27d08056fcd61ff47a0cd8407eff4d3e816c82cb6b9c6f0ce9a0ad49225f81 \
|
||||||
--hash=sha256:4d55528ef13af4b4e074d067977b1f61408602f53ae4537dccf42ba665c2c7bd \
|
--hash=sha256:2ffbf1b38c88d0466de542e91b08225d51782282512f8e2b11715126c41fda48 \
|
||||||
--hash=sha256:576da63eae7809f375932bfcbca2cf20620a1915bf2fedce4b9cc8491eceefe3 \
|
--hash=sha256:3fd503c27e7b7034128e30847ecdb4bff4ca5e60f29ad022a9f66ae8940d54ac \
|
||||||
--hash=sha256:58fc4d66ee349a23dbf08c7e964120dc9027059566e29cf0ce6205d590ed7eca \
|
--hash=sha256:3ff1f585a0fdc1415bd733b804f33d386064a308672249b14828130dd43e7c31 \
|
||||||
--hash=sha256:5b9bf77008f2c55dabbd099fd3ac87009471d223a1c7ebea36873d39511b780a \
|
--hash=sha256:41bd430b7b63aa802c02964e331ac0b177148fef5f807d2c90d05ce71a52b4d4 \
|
||||||
--hash=sha256:5e7996aed3f65667c6dcc8302a69368435a87c2364079a066750a2eac75ea01e \
|
--hash=sha256:43e9b1531d6a898bdf086acb75c41265c7ec4331267d7619148d407efc72bd24 \
|
||||||
--hash=sha256:5f7487be65b9c2c510819e744e375bd41b929a97e5915c4852a82fbb085df62c \
|
--hash=sha256:46af4a742b90c7460e94214f923452c2c1d050a9da1d2b8d4c70cbc045e692b7 \
|
||||||
--hash=sha256:6388e4e95a26717b94a05ced084e19da4d92aca883f392dffcf8e48c8e221a24 \
|
--hash=sha256:46c4c550bf59ce05d6bff2c98053822549aaf9fbaf81103edea325e03350bca1 \
|
||||||
--hash=sha256:65af12f70355de29e1092f319f85a3467f4005e959ab65129cb697169ce94b86 \
|
--hash=sha256:4969592e3cdeefa4cbb15a26cec102cbd4a1d6e5b695fac9fa026e19741138c8 \
|
||||||
--hash=sha256:668d2b45d62c68c7a370ac3dce108ffda482b0a0f50abd8b4c604a813a59e08f \
|
--hash=sha256:4a0536ed2b9297c75104e1a3da330828ba1b2639fa53b38d396f98bf7e3c68df \
|
||||||
--hash=sha256:71333c22f7cf5f0480b59a0aef21f652cf9bbaa9679ad261b405b65a57511d1e \
|
--hash=sha256:4a96147791e49e84207dd1530109aa0e9eeaf1c8b7a59f150047fc0fcdf9bb64 \
|
||||||
--hash=sha256:7150b83b3e3ddaac81a8bb6a9b5f93117674a0e7a2b5a5b32ab31fdfea6df27f \
|
--hash=sha256:4c7f9d70f99e1fbcbf57c75328b80e1c0a7f6cad43e75efa90a97221be5efe15 \
|
||||||
--hash=sha256:748e472345c3a82cfb462d0dff998a7bf43e621eed73374cb19f307e97e08a83 \
|
--hash=sha256:4e8474f7233fe1949ce4e03bea698a600c2d5d6b51dab6d6e6336dbe69acf23e \
|
||||||
--hash=sha256:75dbfd41a61bc1fb0536bf7b1abf272dc115c53d4d77db770cd65d46d4520882 \
|
--hash=sha256:4f1b804cfad04f862d6a84af9d1ad941b06f671878f0f7ecad6c92007d423de6 \
|
||||||
--hash=sha256:7618a082c55cf038eede4a918c1001cc8a4411dfe508dc762659bcd48d8f4c6e \
|
--hash=sha256:529aab727f54a937085184e7436e1d0e19975cf10115eda12d37a683e4ee5342 \
|
||||||
--hash=sha256:780fcb855be29153901c67fc9c5633d48aebef21b90aa72812fa181d731c6b00 \
|
--hash=sha256:5612b0b1de8d5114520094bd5fc3d04eb8af6f3e10d48ef05b7c8e77c1fd9545 \
|
||||||
--hash=sha256:78d10c431073dc6ebceed35ab22948a016cc2b5120963c13a41e38bdde4a7212 \
|
--hash=sha256:56777c57246e048908b550af9b81b0ec9cf804fd47cb7502ccd93238bd6025c2 \
|
||||||
--hash=sha256:7a3a3d3e4f1e3cd2a67b93a0b6ed0f2499e33f47cc568e3a0023e405abdc0ff1 \
|
--hash=sha256:56ba7c1100ed079527f2b995bf5486a2e557e6d5b733c52e8947476338815b69 \
|
||||||
--hash=sha256:7b6975d3763d0952c111700c0634968419268e6bbc0b55fe71138987fa66f309 \
|
--hash=sha256:59d222086daa55421d599609b32d0ebe544e57654c4a0a1490c54a7ebaa67561 \
|
||||||
--hash=sha256:80772e3bda6787510d9620bc0c7572be404a922f8ccdfd436bf6c3778119464c \
|
--hash=sha256:5aba767e64b494483ad60c4873bec78d16205a21f8247c99749bd990d9c846c2 \
|
||||||
--hash=sha256:80992eb20755701753e30a6952a96aa58f353d12a65ad3c9d48a8da5ec4690cf \
|
--hash=sha256:5d5eaf988951f6ecb6854ca3300b87123599c711183c83da7ce39717a7cbdbce \
|
||||||
--hash=sha256:841128a22e6ac04070a0f84776d07e9c38c4dcce8e28792a95e45fc621605517 \
|
--hash=sha256:73da69e1f612c3e682e34dcb971272d90d6f27b2c99acff444ca455a89978574 \
|
||||||
--hash=sha256:861d25ae0985a1dd5297fee35f476b60c6029e2e6e19847d5b4d0a43a390b696 \
|
--hash=sha256:75c8766734ac0053e1d683567e65e85306c4ec62631b0591caeb287ac8f72e08 \
|
||||||
--hash=sha256:872f3dcaa8bf2245944861d7311179d2c0c9b2aaa7d3b464d99a7c2e401f01fa \
|
--hash=sha256:75eea40355a8690459c7291ce6c8ce39c27bd223675c7da6619f510c728feb97 \
|
||||||
--hash=sha256:87c93b25d538c433fb053da6228c6290117ba53ff6a537c133b0f2087948a582 \
|
--hash=sha256:80c3cf46511653f94dfe07c7c79ab105c4164d6e1dfcb35b7214fb9af53eaef4 \
|
||||||
--hash=sha256:8856aa76839dc234d3469f1e270918ce6bec1d6a601eba928f45d68a15f04fc3 \
|
--hash=sha256:8557c807388e6617161fe51b1a4747ea8d1133f2d2ad8e79583439abebe58fbd \
|
||||||
--hash=sha256:885e023e73ce09b11b89ab91fc60f35d80878d2c19d6213a32b42ff36543c291 \
|
--hash=sha256:89438e8885a186c69fe31f7ef98bb2bf29688c466c3caf9060f404c0be89ae80 \
|
||||||
--hash=sha256:899b5e7e2d5a8bc92aa533c2d4e55e5ebba095c485568a5e4bedbc163421259a \
|
--hash=sha256:899b03a3be785a7e1ff84b237da71f0efa2f021512f147dd34ffdf7aa82cb678 \
|
||||||
--hash=sha256:8ce8caa29ebbdcde67e5fd652c811d34bc01f249dbc0d61e5cc4db05ae79a83b \
|
--hash=sha256:8de9b88f0cbac73cfed34220d13c57849e62a7099a714b929142425e926d223a \
|
||||||
--hash=sha256:8e1c68303ccf7fceb50fbab79064a2636119fd9aca121f28453709283dbca727 \
|
--hash=sha256:8f4d561f4728f825e3b793a53064b606ca0b6fc264f67d09e54af452aafc5b82 \
|
||||||
--hash=sha256:8e7e2b3577e97fa43c2c2b12a16139b2cedbd0770235d5179c0412b4794efd9b \
|
--hash=sha256:907b214da5d2fcff0b6ddb83de1333890ca92abaf4bbf8d9c61dc1b95c87fd6e \
|
||||||
--hash=sha256:92f05fc7d832e970047662b3440b190d24ea04f8d3c760e33e7163b67308c878 \
|
--hash=sha256:9118de88c16947eaf5b92f749e65b0501ea69e7c2be7bd6aefc12551622360e1 \
|
||||||
--hash=sha256:97f5811df21703446b42303475b8b855ee07d6ab6cdf8565eff115540624f25d \
|
--hash=sha256:9568764e72d85cf7855ca78b48e07ed1be47bf230e2cea8dabda3c95f660b0ff \
|
||||||
--hash=sha256:9affee8cb1ec453382c27eb9043378ab32f49cd4bc24a24275f5c39bf186c279 \
|
--hash=sha256:9c74cbee9e532dc34371127f7686d6953e5153a1f22beab7f953d95ee4a0fe09 \
|
||||||
--hash=sha256:a2da4a8c6d465fde36cea7d54bf47b5cf089073452f0e47c8632ecb9dec23c07 \
|
--hash=sha256:9cdfd649011ce2d90cb0dd304c5aba1190fac0c266d19a9e2b96b81cfd150a09 \
|
||||||
--hash=sha256:a6903cdca64f1e301af9be424798328c1fe3b4b14aede35f04510989fc72f012 \
|
--hash=sha256:9f00d54b18dd837f1431d66b076737deb7c29ce3ebb8412ceaf44d5e1954ac0c \
|
||||||
--hash=sha256:a8ab1adf04ae2d6d65835995218fd3f3eb644fe20655ca8ee233e2c7270ff53b \
|
--hash=sha256:a2f416cdfe92f5fbb77177f5f3f7830059d1582db05f2c7119bf80069d1ab69b \
|
||||||
--hash=sha256:a8edd467551c1102dc0f5754ab55cd0703431cd3044edf8c8e7d9208d63fa453 \
|
--hash=sha256:a4cb372e22e9c879bd9a9cc9b20b7c1fbf30a605ac953da45ecec05d8a6e1c77 \
|
||||||
--hash=sha256:ac00c41dd315d147b129976204839ca9de699d83519ff1272afbe4fb9d362d12 \
|
--hash=sha256:a65de5c02884760a14a58304fb6303f9ddfc582e630f385daea871e1bdb18686 \
|
||||||
--hash=sha256:ad277f74b1c164f7248afa968700e410651eb858d7c160d109fb451dc45a2f09 \
|
--hash=sha256:aa3b3a43dabc4cc57a7800f526cbe03f71c69121e21b863fdf497b59b462b163 \
|
||||||
--hash=sha256:ae46a50d235f1631d9ec4670503f7b30405103034830bc13df29fd947207f795 \
|
--hash=sha256:ab0f7aabdbce4a202e013083eeab71afdb85efa405dc4a06fea98cde81204675 \
|
||||||
--hash=sha256:afe6b5a04b2ab1aa89bad32ca47bf71358e7302a06fdfdad857389dca8fb5f04 \
|
--hash=sha256:abe081453166e206e3a8c6d8ace57214c17b6d9477d7601ac14a365344dbc1f4 \
|
||||||
--hash=sha256:b1cb078f54af0abd835ca76f93a3152565b73be0f056264da45117d0adf5e99c \
|
--hash=sha256:ae141c9017f8f473a6ee07a9425da021816a9f8c0683c2e5442f0ccf56b0fc62 \
|
||||||
--hash=sha256:b25136212a3d064a8f0b9ebbb6c57094c5229e0de76d15c79b76feff26aeb7b8 \
|
--hash=sha256:af52078719209bef33e38131486fd784832dd8d1dc9b85f00a44f6e7437dd021 \
|
||||||
--hash=sha256:b3226b246facae14909b465061ddcfa2dfeadb6a64f407f24300d42d69bcb1a1 \
|
--hash=sha256:b00150a9a3fd0a8efaa90bc2696c105b04039d50763dd1c95a34c88c5966cb57 \
|
||||||
--hash=sha256:b98e75b21fc2ba5285aef8efaf34131d16af1c38df36bdca2f50634bea2d3060 \
|
--hash=sha256:b2660000e1a113869c86eb5cc07f3343467490f3cd9d0299f81da9ddae7137b7 \
|
||||||
--hash=sha256:bbd7b24d108509a1b9b6679fcc1166a7dd031dbef1f3c2c73788f42e3ebb3beb \
|
--hash=sha256:b3eb1a0d2b6d232d1bcdfc3fcc5f7b004ab3fbd9203011a3172f051d4527c0b6 \
|
||||||
--hash=sha256:bed57543c99249ab3a4586ddc8786529fbc33309e5e8a1351802a06ca2baf4c2 \
|
--hash=sha256:b589d93a60e78fe55d5bc76ee8c2bf945dbdbb7cd16044c53e0307604e448de1 \
|
||||||
--hash=sha256:c0583f69522732bdd79dca4cd3873e63a29acf4a299769c7541f2ca1e4dd4bc6 \
|
--hash=sha256:b8578fc6c8bdd0201327503720fa581000b4bd3934abbf07e2628d1ad3de157d \
|
||||||
--hash=sha256:c1e0e9916301e3b3d970814b1439ca59487f0616d30f36a44cead66ee1748c31 \
|
--hash=sha256:ba1b28e44f611f3f2b436bd8290050a61db4b59a8e24be4465f44897936b3824 \
|
||||||
--hash=sha256:c651847545422c8131660704c58606d841e228ed576c8f1666d98b3d318f89da \
|
--hash=sha256:bb44644371eaa29a3aba7b69b1862d0d56f073bb7585baa32e4271a71a91ee82 \
|
||||||
--hash=sha256:c7853f27195598e550fe089f78f0732c66ee1d1f0eaae8ad081589a5a2f5d4af \
|
--hash=sha256:bcde80aefe7054fad6277762fb7e9d35c72ea479a485ae1bb14629c640987b30 \
|
||||||
--hash=sha256:cbae50d352e4717ffc22c566afc2d0da744380e87ed44a144508e3fb9114a3f4 \
|
--hash=sha256:bd1142d22fdb183a0fff66d79134bf644401437fed874f81066d314c67ee193c \
|
||||||
--hash=sha256:cdbed8f21204398f47de39b0a9b180d7e571f02dfb18bf5f1b618e238454b685 \
|
--hash=sha256:bf77f9017fcfa1232f98598a637406e6c33982ccba8a5922339575c3e2b90ea5 \
|
||||||
--hash=sha256:d08395595c42bcd82c3608762ce734504c6d025eef1c06f42326a6023a584186 \
|
--hash=sha256:c2772bb95062e3f9774140205cd65d8997e39620715486cf5f843cf4ad8f744c \
|
||||||
--hash=sha256:d4639111e73997567343df6551da9dd90d66aece1b9fc26c786d328439488103 \
|
--hash=sha256:c4ecc4e9a5d73a816cae36ee6b5d8b7a0c72013cae1e101406e832887c3dc2d8 \
|
||||||
--hash=sha256:d63787f289944cc4bde518ad2b5e70a4f0d6e2ce76324635359c74c113fd188f \
|
--hash=sha256:c86231c66e4f422e7c13ea6200bb4048b3016c8bfd11b4fd0dabd04d2c8e3501 \
|
||||||
--hash=sha256:d6d5f061f6a2aa55790b9e64a23dfd87b6664ab56e24cd06c78eb43986cb260b \
|
--hash=sha256:c8f6526df47953b07c45b95c4d1da6b9a0861c0e5da0271db96bb1d807825412 \
|
||||||
--hash=sha256:d7865df1fb564092bcf46dac61b5def25342faf6352e4bc0e61a286e3fa26a3d \
|
--hash=sha256:ccfb77f6dc8abffa6f1c7e3975ed9070a41ce5fcc11154d2bead8c1baa940f09 \
|
||||||
--hash=sha256:db6585b600b2e76e98131e0ac0e5195759082b51687ad0c94505970c90718f4a \
|
--hash=sha256:d9d7efaad48b859053b90dedd69bc92f2095084251e732e4c57ac9726bcb1e64 \
|
||||||
--hash=sha256:e36d7369363d2707d5f68950a64c4e025991eb0177db01ccb6aa6facae48b69f \
|
--hash=sha256:dd91a7d7a9ce7f4983097c91ce211f3e5569cc21caa16f2692298a07e396f82b \
|
||||||
--hash=sha256:e7947d9a6264c727a556541b1630296bbd5d0a05068d21c38dde8e7a1c703ef0 \
|
--hash=sha256:de4a2fd524993578fe093044f291b4b24aab134390030b3b9b5f87fd41ab7e75 \
|
||||||
--hash=sha256:eb2d59bc196e6d3b1827c7db06c1a898bfa0787c0574af398e65ccf2e97c0fbe \
|
--hash=sha256:df61f818edf7c8626bfa392f825860fb670b5f8336e238eb0ec7e2a5689cdded \
|
||||||
--hash=sha256:ee9c2f6ca9774c2c24bbf7b23086264e6b5fa178201450535ec0859739e6f78d \
|
--hash=sha256:e1147bc3d0dd1e549d991110d0a09557ec9f925dbc1ca62871fcdab2ec9d716b \
|
||||||
--hash=sha256:f4760e1b02173f4155203054f77a5dc0b4078de7645c922b208d28e7eb99f3e2 \
|
--hash=sha256:e1954f4b239d1a92081647eecfd51cbfd08ea16eb743b8af1cd0113258feea14 \
|
||||||
--hash=sha256:f70bec8a14a692be6dbe7ce8aab303e88df891cbd4a39af091f90b6702e28055 \
|
--hash=sha256:e281b71922208e00886e4b7ffbfcf27874486364f177418ab676f102130e7ec9 \
|
||||||
--hash=sha256:f869e34d2326e417baee430ae998e91412cc8e7fdd83d979277a90a0e79a5b47 \
|
--hash=sha256:e69737bd56006a86fd5a78b2b85447580a6138c930a75eb9ef39fe03d90782b1 \
|
||||||
--hash=sha256:f8b9a7cd381970e64849070aca7c32d53ab7d96c66db6c2ef7aa23c6e803f514 \
|
--hash=sha256:e82b4a70cc67094f3f3fd77579702f48fcf1de7bdc67d79b8f1e24d089a6162c \
|
||||||
--hash=sha256:f99d74ddf9d3b6126b509e81865f89bd1283e3fc1b568b68cd7bd9dfa15583d7 \
|
--hash=sha256:e92e5817eb6bfed23aa5e45bfe30647b83602bdd6f9e25d63524d4e6258458b0 \
|
||||||
--hash=sha256:f9e7e493ded7042712a374471203dd43ae3fff5b81e3de1a0513fa241af9fd41 \
|
--hash=sha256:eaba0613c759ebf95988a84f766ca6b7432d55ce399194f95dde588ad1be0878 \
|
||||||
--hash=sha256:fc72ae476732cdb7b2c1acb5af23b478b8a0d4b6fcf19b90dd150291e0d5b26b \
|
--hash=sha256:edd74b760a6bb950397e7a7bd2f38e6700f6525062650b1d77c6d851b82f02c2 \
|
||||||
--hash=sha256:fccbf0cd3411719e4c9426755df90bf3449d9fc5a89f077f4a7f1abd4f70c910 \
|
--hash=sha256:f40abbcc0a7d9a8a80870af839d317e6932533f98682aabd977add6c53beeb23 \
|
||||||
--hash=sha256:ffcf18ad3edf1c170e27e88b10282a2c449aa0358659592462448d71b2000cfc
|
--hash=sha256:fce7a8ee8d0f682c953c0188735d823f0fcb62779bf92cd6ba473a8e730e26ad
|
||||||
sqlparse==0.4.4 ; python_version >= "3.10" and python_version < "4.0" \
|
sqlparse==0.4.4 ; python_version >= "3.10" and python_version < "4.0" \
|
||||||
--hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \
|
--hash=sha256:5430a4fe2ac7d0f93e66f1efc6e1338a41884b7ddf2a350cedd20ccc4d9d28f3 \
|
||||||
--hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c
|
--hash=sha256:d446183e84b8349fa3061f0fe7f06ca94ba65b426946ffebe6e3e8295332420c
|
||||||
|
|||||||
104
scripts/entrypoint.sh
Executable file
104
scripts/entrypoint.sh
Executable file
@@ -0,0 +1,104 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
|
||||||
|
|
||||||
|
# set global variables:
|
||||||
|
_DB_URL="${DB_URL}"
|
||||||
|
|
||||||
|
|
||||||
|
# parses the database connection url and sets global variables:
|
||||||
|
#
|
||||||
|
# _DBMS: {psql, sqlite} default dbms used
|
||||||
|
# _DB_HOST {domain,ip_address} for connections
|
||||||
|
# _DB_PORT {5432,3306,etc} for connections
|
||||||
|
# _DB_USER {root,admin,etc} db username
|
||||||
|
# _DB_PASSWORD {super_secret} db user password
|
||||||
|
parse_db_url() {
|
||||||
|
local db_url=$1
|
||||||
|
echo "-------- Check used DBMS --------"
|
||||||
|
if [ -n "${db_url}" ]; then
|
||||||
|
# ! PARSING:
|
||||||
|
# -- -- --
|
||||||
|
# extract the protocol
|
||||||
|
local proto="$(echo "${db_url}" | grep '://' | sed -e's,^\(.*://\).*,\1,g')"
|
||||||
|
local dbms=${proto%:*}
|
||||||
|
# if dpms is "sqlite" we do not need parse url args
|
||||||
|
if [ "${dbms}" = "psql" ]; then
|
||||||
|
# remove the protocol (clean url)
|
||||||
|
local url=$(echo "${db_url}" | sed -e s,"${proto}",,g)
|
||||||
|
# extract the user and password (if any)
|
||||||
|
local userpass="$(echo "${url}" | grep @ | cut -d@ -f1)"
|
||||||
|
local password=$(echo "${userpass}" | grep : | cut -d: -f2)
|
||||||
|
# extract the host and port connection
|
||||||
|
local hostport=$(echo "${url}" | sed -e s,$userpass@,,g | cut -d/ -f1)
|
||||||
|
local port=$(echo "${hostport}" | grep : | cut -d: -f2)
|
||||||
|
# ! SET VARIABLES:
|
||||||
|
# -- -- -- -- --
|
||||||
|
# _DB_HOST or/and _DB_PORT for test db connection
|
||||||
|
if [ -n "${port}" ]; then
|
||||||
|
_DB_HOST=$(echo "${hostport}" | grep : | cut -d: -f1)
|
||||||
|
_DB_PORT="${port}"
|
||||||
|
else
|
||||||
|
_DB_HOST="${hostport}"
|
||||||
|
fi
|
||||||
|
# _DB_USER or/and _DB_PASSWORD for future features
|
||||||
|
if [ -n "${password}" ]; then
|
||||||
|
_DB_USER=$(echo "${userpass}" | grep : | cut -d: -f1)
|
||||||
|
_DB_PASSWORD="${password}"
|
||||||
|
else
|
||||||
|
_DB_USER="${userpass}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
# _DBMS (sqlite or psql)
|
||||||
|
_DBMS="${dbms}"
|
||||||
|
echo "${_DBMS} is set as the default DBMS"
|
||||||
|
else
|
||||||
|
# _DBMS (sqlite)
|
||||||
|
_DBMS="sqlite"
|
||||||
|
echo "${_DBMS} is set as the default DBMS"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# waits until the database becomes available for connection
|
||||||
|
wait_db() {
|
||||||
|
local dbms=$1
|
||||||
|
local db_host=$2
|
||||||
|
local db_port=$3
|
||||||
|
if [ "${dbms}" = "psql" ];then
|
||||||
|
echo "------- Waiting for database start -------"
|
||||||
|
while ! nc -z $db_host $db_port; do
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
echo "PostgreSQL started"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# app config performs database migration, collects static files
|
||||||
|
app_config() {
|
||||||
|
echo "-------- Apply migration --------"
|
||||||
|
python manage.py migrate --no-input
|
||||||
|
echo "----- Collect static files ------"
|
||||||
|
python manage.py collectstatic --no-input
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# run app wsgi web server
|
||||||
|
app_run() {
|
||||||
|
echo "--------- Run gunicorn ----------"
|
||||||
|
gunicorn logs_collector.wsgi:application
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# app entrypoint
|
||||||
|
main() {
|
||||||
|
parse_db_url $_DB_URL
|
||||||
|
wait_db $_DBMS $_DB_HOST $_DB_PORT
|
||||||
|
app_config
|
||||||
|
app_run
|
||||||
|
}
|
||||||
|
|
||||||
|
# RUN
|
||||||
|
main
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
159
scripts/uploader.sh
Executable file
159
scripts/uploader.sh
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
|
||||||
|
# INIT GLOBAL VARIABLES:
|
||||||
|
_VERSION="0.1.0"
|
||||||
|
_PACKGMGR="apt yum"
|
||||||
|
_SCRIPT_NAME="$0"
|
||||||
|
_CMD="curl"
|
||||||
|
_FILE=""
|
||||||
|
_TOKEN=""
|
||||||
|
_URL=""
|
||||||
|
|
||||||
|
|
||||||
|
# Colorize output
|
||||||
|
# Usage - $(colorize CYAN "Hello, friend!")
|
||||||
|
colorize() {
|
||||||
|
local RED="\033[0;31m"
|
||||||
|
local GREEN="\033[0;32m" # <-- [0 means not bold
|
||||||
|
local YELLOW="\033[1;33m" # <-- [1 means bold
|
||||||
|
local BLUE="\033[0;34m"
|
||||||
|
local MAGNETA="\033[0;35"
|
||||||
|
local CYAN="\033[1;36m"
|
||||||
|
# ... Add more colors if you like
|
||||||
|
|
||||||
|
local NC="\033[0m" # No Color
|
||||||
|
|
||||||
|
# printf "${(P)1}${2} ${NC}\n" # <-- zsh
|
||||||
|
# printf "${!1}${2} ${NC}\n" # <-- bash
|
||||||
|
echo -e "${!1}${2}${NC}" # <-- all-purpose
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# checks whether the utility is installed
|
||||||
|
# takes the util name as input $_CMD
|
||||||
|
check_util_exists() {
|
||||||
|
local request_util=$1
|
||||||
|
if ! command -v $request_util >/dev/null; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Print help message how used it script
|
||||||
|
help() {
|
||||||
|
local script=$(colorize GREEN "$_SCRIPT_NAME")
|
||||||
|
local required=$(colorize RED "required")
|
||||||
|
printf "Usage: $script [options [parameters]]\n"
|
||||||
|
printf "\n"
|
||||||
|
printf "Options:\n"
|
||||||
|
printf "\n"
|
||||||
|
printf " -f | --file full path to upload file $required\n"
|
||||||
|
printf " -t | --token access token $required\n"
|
||||||
|
printf " -u | --url target url $required\n"
|
||||||
|
printf " -v | --version print version\n"
|
||||||
|
printf " -h | --help print help\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# parse user arguments
|
||||||
|
argparser() {
|
||||||
|
# count user-passed arguments:
|
||||||
|
local count_arguments=$#
|
||||||
|
# run help if empty and exit:
|
||||||
|
if [[ count_arguments -eq 0 ]]; then
|
||||||
|
help
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
# parse args:
|
||||||
|
while [ ! -z "$1" ]; do
|
||||||
|
case "$1" in
|
||||||
|
--file|-f)
|
||||||
|
shift
|
||||||
|
_FILE="$1"
|
||||||
|
;;
|
||||||
|
--token|-t)
|
||||||
|
shift
|
||||||
|
_TOKEN="$1"
|
||||||
|
;;
|
||||||
|
--url|-u)
|
||||||
|
shift
|
||||||
|
_URL="$1"
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
help
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
--version|-v)
|
||||||
|
printf "$_VERSION\n"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
help
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# check curl is exists:
|
||||||
|
curl_is_exists() {
|
||||||
|
if ! check_util_exists $_CMD; then
|
||||||
|
local error_cmd=$(colorize RED "$_CMD")
|
||||||
|
printf "$(colorize RED "ERROR"): upload util doesn't exist, "
|
||||||
|
printf "please install $error_cmd before run $_SCRIPT_NAME\n"
|
||||||
|
# Print how install curl (support only apt/yum):
|
||||||
|
for pkgmgr in $_PACKGMGR; do
|
||||||
|
if check_util_exists $pkgmgr; then
|
||||||
|
printf "$(colorize GREEN "RUN"): $pkgmgr install $error_cmd"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
validate_args() {
|
||||||
|
if [[ -z $_URL ]]; then
|
||||||
|
printf "$(colorize RED "ERROR"): -u | --url argument is required\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z $_FILE ]]; then
|
||||||
|
printf "$(colorize RED "ERROR"): -f | --file argument is required\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [[ -z $_TOKEN ]]; then
|
||||||
|
printf "$(colorize RED "ERROR"): -t | --token argument is required\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Upload file used curl
|
||||||
|
# get $_URL $_FILE $_TOKEN
|
||||||
|
upload() {
|
||||||
|
local url=$1
|
||||||
|
local file=$2
|
||||||
|
local token=$3
|
||||||
|
# run:
|
||||||
|
curl --progress-bar -X 'POST' \
|
||||||
|
"${url}" \
|
||||||
|
-H 'accept: application/json' \
|
||||||
|
-H "Upload-Token: ${token} " \
|
||||||
|
-H 'Content-Type: multipart/form-data' \
|
||||||
|
-F "file=@${file}" | cat # cat required to show progress bar
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
main () {
|
||||||
|
argparser $@
|
||||||
|
curl_is_exists
|
||||||
|
validate_args
|
||||||
|
upload $_URL $_FILE $_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# RUN IT:
|
||||||
|
main $@
|
||||||
Reference in New Issue
Block a user