Compare commits
88 Commits
master
...
35-test-ta
| Author | SHA1 | Date | |
|---|---|---|---|
| 81be8abc50 | |||
| a5b19cfe2a | |||
| 4491b1b05a | |||
| a77df06bdd | |||
| 38c9c838ed | |||
| 17f5d81953 | |||
| 468eac45a2 | |||
| c0866ee863 | |||
| 5568c458c2 | |||
| e82586e08c | |||
| 520633f383 | |||
| 491d272304 | |||
| d6e1424d89 | |||
| 9bd3c3e45b | |||
| 7355552366 | |||
| 64cfccd353 | |||
| 97e0270550 | |||
| c4eb8b5568 | |||
| 0b5971c4af | |||
| 80b5a6a64e | |||
| bcf300e0c0 | |||
| 13f5bbc2dd | |||
| ce50a74510 | |||
| f77399b1d2 | |||
| 207a8737ba | |||
| c81ee3c4ec | |||
| d185aaf329 | |||
| fe33d5c7f6 | |||
| 0ee9e87b7f | |||
| cfdd6b72f0 | |||
| d0ffbeef0f | |||
| 8385eff636 | |||
| b0dd2eb5cf | |||
| f7abca1e1b | |||
| e73f57670a | |||
| f4831d5759 | |||
| 17bfb08e43 | |||
| dda9c841fd | |||
| 181a9a5ce9 | |||
| 7d6ce1eaee | |||
| 612d807bc4 | |||
| 25d5b34add | |||
| 0151e61cd6 | |||
| 687dc4bb9b | |||
| 814fefd18b | |||
| afdc7c17b6 | |||
| 3cde36d8a7 | |||
| 83ac7c3a66 | |||
| ad7bff67c4 | |||
| 83a09207d6 | |||
| 7103f3a089 | |||
| 10c012aba2 | |||
| 52df4b54d5 | |||
| 92a9f36acd | |||
| 070a63222c | |||
| 027475e4b3 | |||
| 13e2bff324 | |||
| 731b9d384a | |||
| c8370f96ff | |||
| 9aec75cdd7 | |||
| 38c0b9ba87 | |||
| ac23cc9397 | |||
| acd34f2ca5 | |||
| eb32f27bad | |||
| 60ef0e386d | |||
| 3f1e8c57ac | |||
| 18449382e1 | |||
| c42e39a8d5 | |||
| f8eb591b05 | |||
| d9f5c20557 | |||
| 4440e084b9 | |||
| 2dec1e33c2 | |||
| eb2d630dd0 | |||
| e4a02726e7 | |||
| a0a2248306 | |||
| fadfd8711c | |||
| d01386a4dc | |||
| c82107bad1 | |||
| 976576f8c6 | |||
| c2fecdd87c | |||
| a0f1654cf5 | |||
| 7fb9aae90e | |||
| 1e141ce6fa | |||
| 4da19bb053 | |||
| e7bc75f0d8 | |||
| d03154314c | |||
| 1a668159c7 | |||
| a33711cc49 |
@ -1,4 +1,3 @@
|
||||
*
|
||||
.*
|
||||
!d1/blank-app-nginx.conf
|
||||
!docker/checks
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
[**/*.py]
|
||||
vim_modeline = set noet ts=2 sts=2 sw=2 ai ci
|
||||
|
||||
[**/meson.build]
|
||||
vim_modeline = set noet ts=2 sts=2 sw=2 ai ci
|
||||
@ -1,3 +0,0 @@
|
||||
NGINX_EXPORTER_PORTS=127.0.0.1:9113
|
||||
CHECKS_PORTS=127.0.0.1:9097
|
||||
SUBNET=172.31.0
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@ -1,3 +1,2 @@
|
||||
releases/tar/** filter=lfs diff=lfs merge=lfs -text
|
||||
releases/whl/** filter=lfs diff=lfs merge=lfs -text
|
||||
docker/*/deps/whl/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -15,9 +15,3 @@ d2/book1/books
|
||||
python/build
|
||||
.*.kate-swp
|
||||
!releases/whl/*.whl
|
||||
.env
|
||||
!docker/*/.env
|
||||
.envs
|
||||
!docker/*/deps/whl/**
|
||||
|
||||
!dotfiles/.vim
|
||||
|
||||
51
Makefile
51
Makefile
@ -69,6 +69,7 @@ dotfiles_put:
|
||||
cp dotfiles/.vimrc ~/.vimrc
|
||||
cp dotfiles/.tmux.conf ~/.tmux.conf
|
||||
cp dotfiles/.py3.vimrc ~/.py3.vimrc
|
||||
cp dotfiles/.py3.vimrc ~/.py3.vimrc
|
||||
cp dotfiles/.gitconfig ~/.gitconfig
|
||||
cp -rp \
|
||||
dotfiles/.ipython/profile_default/ipython_config.py \
|
||||
@ -81,19 +82,6 @@ dotfiles_put:
|
||||
done
|
||||
#commands install -f -p dotfiles -s dotfiles/ -t ~/.config/
|
||||
|
||||
dotfiles_vim_put:
|
||||
mkdir -p $(INSTALL_ROOT)
|
||||
mkdir -p $(INSTALL_ROOT)/.vim
|
||||
|
||||
cp dotfiles/.vimrc ~/.vimrc
|
||||
cp dotfiles/.py3.vimrc ~/.py3.vimrc
|
||||
cp -rp dotfiles/.vim/online_fxreader_pr34_vim ~/.vim/
|
||||
|
||||
dotfiles_tmux_put:
|
||||
mkdir -p $(INSTALL_ROOT)
|
||||
|
||||
cp dotfiles/.tmux.conf ~/.tmux.conf
|
||||
|
||||
PLATFORM ?= macbook_air_2012
|
||||
PLATFORM_TMP ?= tmp/platform_dotfiles/$(PLATFORM)
|
||||
|
||||
@ -136,10 +124,14 @@ systemd:
|
||||
done
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
compose_env:
|
||||
for s in checks; do \
|
||||
cat docker/$$s/.env .envs/$$s.env > .envs/$$s.patched.env; \
|
||||
done
|
||||
venv:
|
||||
uv venv
|
||||
uv pip install -p .venv \
|
||||
-r requirements.txt
|
||||
|
||||
venv_compile:
|
||||
uv pip compile --generate-hashes \
|
||||
requirements.in > requirements.txt
|
||||
|
||||
MYPY_SOURCES ?= \
|
||||
d1/cpanel.py
|
||||
@ -147,28 +139,3 @@ mypy:
|
||||
. .venv/bin/activate && \
|
||||
mypy --strict --follow-imports silent \
|
||||
$(MYPY_SOURCES)
|
||||
|
||||
COMPOSE ?= sudo docker-compose
|
||||
|
||||
nginx_config_http:
|
||||
$(COMPOSE) exec app \
|
||||
python3 \
|
||||
d1/nginx_config.py \
|
||||
tmp/cache/forward.nginx.json \
|
||||
/etc/nginx/nginx.conf
|
||||
|
||||
nginx_config_https:
|
||||
$(COMPOSE) exec ssl-app \
|
||||
python3 \
|
||||
d1/nginx_config.py ssl \
|
||||
tmp/d1/ssl.nginx.json \
|
||||
/etc/nginx/nginx.conf
|
||||
|
||||
nginx_config: nginx_config_https nginx_config_http
|
||||
|
||||
nginx_reload_common:
|
||||
$(COMPOSE) exec $(NGINX_SERVICE) nginx -s reload
|
||||
|
||||
nginx_reload:
|
||||
make nginx_reload_common NGINX_SERVICE=ssl-app
|
||||
make nginx_reload_common NGINX_SERVICE=app
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
[Unit]
|
||||
Description=fxreader.online-certbot
|
||||
Requires=fxreader.online-gateway
|
||||
After=fxreader.online-gateway
|
||||
PartOf=fxreader.online-gateway
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
|
||||
@ -2,11 +2,10 @@
|
||||
Description=fxreader.online-service
|
||||
Requires=docker.service
|
||||
After=docker.service
|
||||
PartOf=docker.service
|
||||
|
||||
[Service]
|
||||
#Type=oneshot
|
||||
ExecStart=/usr/bin/docker compose up
|
||||
ExecStart=/usr/bin/docker compose up --force-recreate --remove-orphans
|
||||
ExecStop=/usr/bin/docker compose down
|
||||
WorkingDirectory={{PROJECT_ROOT}}
|
||||
StandardOutput=null
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import json
|
||||
import re
|
||||
import socket
|
||||
import os
|
||||
import io
|
||||
import sys
|
||||
@ -86,7 +84,6 @@ def forward(
|
||||
location_body_get = lambda target_endpoint: \
|
||||
r'''
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header X-Forwarded-For $t1;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@ -220,23 +217,6 @@ def ssl(input_json, output_conf):
|
||||
upstream_servers = []
|
||||
server_names = []
|
||||
|
||||
ssh_proxy_download_rate = ssl_nginx['stream_server'].get(
|
||||
'ssh_proxy_download_rate',
|
||||
128 * 1024,
|
||||
)
|
||||
ssh_proxy_upload_rate = ssl_nginx['stream_server'].get(
|
||||
'ssh_proxy_upload_rate',
|
||||
128 * 1024,
|
||||
)
|
||||
web_proxy_download_rate = ssl_nginx['stream_server'].get(
|
||||
'web_proxy_download_rate',
|
||||
128 * 1024 * 1024,
|
||||
)
|
||||
web_proxy_upload_rate = ssl_nginx['stream_server'].get(
|
||||
'web_proxy_upload_rate',
|
||||
128 * 1024 * 1024,
|
||||
)
|
||||
|
||||
if 'by_server_name' in ssl_nginx['stream_server']:
|
||||
for k, v in ssl_nginx['stream_server']['by_server_name'].items():
|
||||
upstream_servers.append(
|
||||
@ -277,15 +257,6 @@ stream {
|
||||
"TLSv1.3" $upstream_server_name;
|
||||
}
|
||||
|
||||
map $upstream_protocol $proxy_download_rate {
|
||||
web {web_proxy_download_rate};
|
||||
ssh {ssh_proxy_download_rate};
|
||||
}
|
||||
map $upstream_protocol $proxy_upload_rate {
|
||||
web {web_proxy_upload_rate};
|
||||
ssh {ssh_proxy_upload_rate};
|
||||
}
|
||||
|
||||
map $ssl_preread_server_name $upstream_server_name {
|
||||
default web;
|
||||
{server_names}
|
||||
@ -296,12 +267,7 @@ stream {
|
||||
listen 443;
|
||||
|
||||
ssl_preread on;
|
||||
|
||||
proxy_pass $upstream_protocol;
|
||||
|
||||
proxy_download_rate $proxy_download_rate;
|
||||
proxy_upload_rate $proxy_upload_rate;
|
||||
# proxy_upload_rate 10k;
|
||||
}
|
||||
}
|
||||
'''.replace(
|
||||
@ -311,14 +277,6 @@ stream {
|
||||
]),
|
||||
).replace(
|
||||
'{ssh_section}', ssh_section,
|
||||
).replace(
|
||||
'{web_proxy_download_rate}', '%d' % web_proxy_download_rate,
|
||||
).replace(
|
||||
'{ssh_proxy_download_rate}', '%d' % ssh_proxy_download_rate,
|
||||
).replace(
|
||||
'{web_proxy_upload_rate}', '%d' % web_proxy_upload_rate,
|
||||
).replace(
|
||||
'{ssh_proxy_upload_rate}', '%d' % ssh_proxy_upload_rate,
|
||||
).replace(
|
||||
'{server_names}', ''.join([
|
||||
' ' + o + '\n'
|
||||
@ -332,36 +290,8 @@ stream {
|
||||
if 'default_server' in ssl_nginx:
|
||||
server = ssl_nginx['default_server']
|
||||
|
||||
if 'metrics_allowed' in server:
|
||||
metrics_allowed_ip = socket.gethostbyname(server['metrics_allowed'])
|
||||
else:
|
||||
metrics_allowed_ip = '127.0.0.1'
|
||||
|
||||
servers.append(
|
||||
r'''
|
||||
server {
|
||||
server_name _;
|
||||
listen 80 default_server;
|
||||
|
||||
location = /_metrics {
|
||||
stub_status;
|
||||
access_log off;
|
||||
# allow 172.0.0.0/8;
|
||||
allow {metrics_allowed_ip};
|
||||
# allow 127.0.0.1;
|
||||
deny all;
|
||||
}
|
||||
|
||||
location ~ ^/.well-known/acme-challenge/ {
|
||||
alias /var/www/;
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location ~ {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
set $t1 $remote_addr;
|
||||
if ($http_x_forwarded_for)
|
||||
@ -387,57 +317,14 @@ server {
|
||||
'{domain_key}', server['domain_key'],
|
||||
).replace(
|
||||
'{ssl_port}', '%d' % ssl_port,
|
||||
).replace(
|
||||
'{metrics_allowed_ip}', metrics_allowed_ip
|
||||
)
|
||||
)
|
||||
|
||||
for server in ssl_nginx['servers']:
|
||||
location_proxy_app = r'''
|
||||
location ^~ / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://app:80;
|
||||
}
|
||||
'''
|
||||
|
||||
location_forward_ssl = r'''
|
||||
location ~ {
|
||||
#return 444;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
'''
|
||||
|
||||
if server.get('allow_http') in [True]:
|
||||
http_location = location_proxy_app
|
||||
else:
|
||||
http_location = location_forward_ssl
|
||||
|
||||
drop_by_user_agent = ''
|
||||
|
||||
if not server.get('drop_by_user_agent') is None:
|
||||
r = re.compile('^([a-zA-Z0-9\s\.\,\(\)]+)$')
|
||||
user_agent_list = [
|
||||
r.match(o)[1]
|
||||
for o in server.get('drop_by_user_agent')
|
||||
]
|
||||
drop_by_user_agent = r'''
|
||||
if ( $http_user_agent ~ ({user_agent_list}) ) {
|
||||
return 444;
|
||||
}
|
||||
'''.replace(
|
||||
'{user_agent_list}',
|
||||
'|'.join(user_agent_list)
|
||||
)
|
||||
|
||||
servers.append(
|
||||
r'''
|
||||
|
||||
|
||||
server {
|
||||
set $t1 $remote_addr;
|
||||
if ($http_x_forwarded_for)
|
||||
@ -454,7 +341,10 @@ server {
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
{http_location}
|
||||
location ~ {
|
||||
#return 444;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
@ -472,8 +362,6 @@ server {
|
||||
ssl_certificate {signed_chain_cert};
|
||||
ssl_certificate_key {domain_key};
|
||||
|
||||
{drop_by_user_agent}
|
||||
|
||||
location ^~ / {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
@ -482,7 +370,6 @@ server {
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_redirect off;
|
||||
proxy_buffering off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://app:80;
|
||||
}
|
||||
}
|
||||
@ -494,12 +381,8 @@ server {
|
||||
'{client_max_body_size}', server['client_max_body_size'],
|
||||
).replace(
|
||||
'{domain_key}', server['domain_key'],
|
||||
).replace(
|
||||
'{drop_by_user_agent}', drop_by_user_agent,
|
||||
).replace(
|
||||
'{ssl_port}', '%d' % ssl_port,
|
||||
).replace(
|
||||
'{http_location}', http_location
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
5
deps/test-task-2025-06-30-v1/.dockerignore
vendored
Normal file
5
deps/test-task-2025-06-30-v1/.dockerignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.venv
|
||||
tmp
|
||||
.git
|
||||
.env
|
||||
build
|
||||
1
deps/test-task-2025-06-30-v1/.gitattributes
vendored
Normal file
1
deps/test-task-2025-06-30-v1/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
releases/whl/** filter=lfs diff=lfs merge=lfs -text
|
||||
6
deps/test-task-2025-06-30-v1/.gitignore
vendored
Normal file
6
deps/test-task-2025-06-30-v1/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
!.tmuxp/
|
||||
!python
|
||||
.env/
|
||||
releases/tar
|
||||
build
|
||||
!releases/whl/**
|
||||
9
deps/test-task-2025-06-30-v1/.tmuxp/v1.yaml
vendored
Normal file
9
deps/test-task-2025-06-30-v1/.tmuxp/v1.yaml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
session_name: test-task-2025-06-30-v1
|
||||
start_directory: ${PWD}/deps/test-task-2025-06-30-v1
|
||||
windows:
|
||||
- focus: 'true'
|
||||
layout: 5687,98x12,0,0,18
|
||||
options: {}
|
||||
panes:
|
||||
- pane
|
||||
window_name: zsh
|
||||
62
deps/test-task-2025-06-30-v1/Makefile
vendored
Normal file
62
deps/test-task-2025-06-30-v1/Makefile
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
ENV_PATH ?= .venv
|
||||
PYTHON_PATH = $(ENV_PATH)/bin/python3
|
||||
PYTHON_VERSION ?= 3.10
|
||||
UV_ARGS ?= --offline
|
||||
DOCKER ?= podman
|
||||
COMPOSE ?= podman compose
|
||||
|
||||
venv_extract_requirements:
|
||||
$(ENV_PATH)/bin/tomlq \
|
||||
-r '.project.dependencies | join("\n")' \
|
||||
pyproject.toml > requirements.in
|
||||
|
||||
venv_compile:
|
||||
uv pip compile \
|
||||
$(UV_ARGS) \
|
||||
-p $(PYTHON_VERSION) \
|
||||
--generate-hashes \
|
||||
requirements.in > \
|
||||
requirements.txt
|
||||
|
||||
|
||||
venv:
|
||||
uv \
|
||||
venv \
|
||||
-p 3.13 \
|
||||
$(UV_ARGS) \
|
||||
--seed \
|
||||
$(ENV_PATH)
|
||||
uv \
|
||||
pip install \
|
||||
$(UV_ARGS) \
|
||||
-p $(ENV_PATH) \
|
||||
-r requirements.txt
|
||||
|
||||
pyright:
|
||||
$(ENV_PATH)/bin/python3 -m pyright \
|
||||
-p pyproject.toml \
|
||||
--pythonpath $(PYTHON_PATH)
|
||||
|
||||
|
||||
compose_env:
|
||||
cat docker/postgresql/.env .env/postgresql.env > .env/postgresql.patched.env
|
||||
cat docker/web/.env .env/web.env > .env/web.patched.env
|
||||
|
||||
compose_build_web:
|
||||
$(COMPOSE) build web
|
||||
|
||||
git-release:
|
||||
git archive \
|
||||
--format=tar \
|
||||
-o "releases/tar/repo-$$(git describe --tags).tar" \
|
||||
HEAD
|
||||
|
||||
ALEMBIC_CMD ?= --help
|
||||
alembic:
|
||||
$(ENV_PATH)/bin/alembic \
|
||||
-c pyproject.toml \
|
||||
$(ALEMBIC_CMD)
|
||||
|
||||
deploy_wheel:
|
||||
make pyright
|
||||
$(PYTHON_PATH) -m build -o releases/whl -w -n
|
||||
76
deps/test-task-2025-06-30-v1/docker-compose.yml
vendored
Normal file
76
deps/test-task-2025-06-30-v1/docker-compose.yml
vendored
Normal file
@ -0,0 +1,76 @@
|
||||
services:
|
||||
redis:
|
||||
image: docker.io/redis:latest-alpine@sha256:e71b4cb00ea461ac21114cff40ff12fb8396914238e1e9ec41520b2d5a4d3423
|
||||
ports:
|
||||
- 127.0.0.1:9004:6379
|
||||
|
||||
web: &web
|
||||
image: online.fxreader.pr34.test_task_2025_06_30_v1:dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/web/Dockerfile
|
||||
target: web
|
||||
env_file: .env/web.env
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 128M
|
||||
|
||||
web-dev:
|
||||
<<: *web
|
||||
volumes:
|
||||
- .:/app:ro
|
||||
- ./tmp/cache:/app/tmp/cache:rw
|
||||
|
||||
emcont_worker:
|
||||
<<: *web
|
||||
image: online.fxreader.pr34.test_task_2025_06_30_v1:dev
|
||||
environment:
|
||||
command:
|
||||
- python3
|
||||
- -m
|
||||
- online.fxreader.pr34.test_task_2025_06_30_v1.async_api.app
|
||||
|
||||
postgresql:
|
||||
image: docker.io/postgres:14.18-bookworm@sha256:c0aab7962b283cf24a0defa5d0d59777f5045a7be59905f21ba81a20b1a110c9
|
||||
# restart: always
|
||||
# set shared memory limit when using docker compose
|
||||
shm_size: 128mb
|
||||
volumes:
|
||||
- postgresql_data:/var/lib/postgresql/data/:rw
|
||||
# or set shared memory limit when deploy via swarm stack
|
||||
#volumes:
|
||||
# - type: tmpfs
|
||||
# target: /dev/shm
|
||||
# tmpfs:
|
||||
# size: 134217728 # 128*2^20 bytes = 128Mb
|
||||
env_file: .env/postgresql.patched.env
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD: example
|
||||
ports:
|
||||
- 127.0.0.1:9002:5432
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 128M
|
||||
|
||||
adminer:
|
||||
image: docker.io/adminer:standalone@sha256:730215fe535daca9a2f378c48321bc615c8f0d88668721e0eff530fa35b6e8f6
|
||||
ports:
|
||||
- 127.0.0.1:9001:8080
|
||||
|
||||
|
||||
volumes:
|
||||
postgresql_data:
|
||||
3
deps/test-task-2025-06-30-v1/docker/postgresql/.env
vendored
Normal file
3
deps/test-task-2025-06-30-v1/docker/postgresql/.env
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
PGDATA=/var/lib/postgresql/data/pgdata
|
||||
POSTGRES_USER=tickers
|
||||
POSTGRES_DB=tickers
|
||||
1
deps/test-task-2025-06-30-v1/docker/web/.env
vendored
Normal file
1
deps/test-task-2025-06-30-v1/docker/web/.env
vendored
Normal file
@ -0,0 +1 @@
|
||||
# DB_URL=
|
||||
50
deps/test-task-2025-06-30-v1/docker/web/Dockerfile
vendored
Normal file
50
deps/test-task-2025-06-30-v1/docker/web/Dockerfile
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
FROM docker.io/library/python:3.12@sha256:6121c801703ec330726ebf542faab113efcfdf2236378c03df8f49d80e7b4180 AS base
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY docker/web/apt.requirements.txt docker/web/apt.requirements.txt
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y $(cat docker/web/apt.requirements.txt)
|
||||
|
||||
RUN \
|
||||
pip3 install \
|
||||
--break-system-packages uv
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
|
||||
RUN \
|
||||
--mount=type=bind,source=releases/whl,target=/app/releases/whl \
|
||||
--mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip \
|
||||
install \
|
||||
--system \
|
||||
--break-system-packages \
|
||||
-f releases/whl \
|
||||
-r requirements.txt
|
||||
|
||||
WORKDIR /app
|
||||
RUN apt-get update -yy && apt-get install -yy tini
|
||||
|
||||
FROM base as web
|
||||
|
||||
RUN \
|
||||
--mount=type=bind,source=releases/whl,target=/app/releases/whl \
|
||||
--mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip \
|
||||
install \
|
||||
--system \
|
||||
--break-system-packages \
|
||||
--no-index \
|
||||
-f releases/whl \
|
||||
'online.fxreader.pr34.test_task_2025_06_30_v1==0.1'
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD [ \
|
||||
"python3", \
|
||||
"-m", \
|
||||
"online.fxreader.pr34.test_task_2025_06_30_v1.async_api.app" \
|
||||
]
|
||||
1
deps/test-task-2025-06-30-v1/docker/web/apt.requirements.txt
vendored
Normal file
1
deps/test-task-2025-06-30-v1/docker/web/apt.requirements.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
wget tar git curl
|
||||
89
deps/test-task-2025-06-30-v1/docs/readme.md
vendored
Normal file
89
deps/test-task-2025-06-30-v1/docs/readme.md
vendored
Normal file
@ -0,0 +1,89 @@
|
||||
# Requirements
|
||||
|
||||
Tickers of interest:
|
||||
- EURUSD
|
||||
- USDJPY
|
||||
- GBPUSD
|
||||
- AUDUSD
|
||||
- USDCAD
|
||||
|
||||
Rest API - https://rates.emcont.com
|
||||
|
||||
Scrape every second;
|
||||
|
||||
Schema:
|
||||
Ticker:
|
||||
id: foreign_key market
|
||||
timestamp: datetime
|
||||
# (ask + bid) / 2
|
||||
value: decimal
|
||||
|
||||
Store up to 30 minutes of recent tickers;
|
||||
|
||||
Return via websocket up to 30 minutes of recent tickers;
|
||||
|
||||
# AsyncAPI
|
||||
|
||||
```yaml
|
||||
AsyncAPI:
|
||||
Endpoints:
|
||||
subscribe:
|
||||
Request: SubscribeAction
|
||||
Response: AssetHistoryResponse | AssetTickerResponse
|
||||
list:
|
||||
Request: AssetsAction
|
||||
Response: AssetsResponse
|
||||
Schema:
|
||||
SubscribeAction:
|
||||
action: Literal['subscribe']
|
||||
message:
|
||||
assetId: 1
|
||||
AssetHistoryResponse:
|
||||
action: Literal['asset_history']
|
||||
message:
|
||||
points:
|
||||
- assetName: EURUSD
|
||||
time: 1455883484
|
||||
assetId: 1
|
||||
value: 1.110481
|
||||
- assetName: EURUSD
|
||||
time: 1455883485
|
||||
assetId: 1
|
||||
value: 1.110948
|
||||
- assetName: EURUSD
|
||||
time: 1455883486
|
||||
assetId: 1
|
||||
value: 1.111122
|
||||
AssetTickerResponse:
|
||||
action: Literal['point']
|
||||
message:
|
||||
assetName: EURUSD
|
||||
time: 1455883484
|
||||
assetId: 1
|
||||
value: 1.110481
|
||||
AssetsAction:
|
||||
action: Literal['assets']
|
||||
message: {}
|
||||
AssetsResponse:
|
||||
action: Literal['assets']
|
||||
message:
|
||||
assets:
|
||||
- id: 1
|
||||
name: EURUSD
|
||||
- id: 2
|
||||
name: USDJPY
|
||||
- id: 3
|
||||
name: GBPUSD
|
||||
- id: 4
|
||||
name: AUDUSD
|
||||
- id: 5
|
||||
name: USDCAD
|
||||
```
|
||||
|
||||
# Services:
|
||||
|
||||
``` yaml
|
||||
web:
|
||||
ports:
|
||||
- 8080:80
|
||||
```
|
||||
@ -1,5 +1,5 @@
|
||||
[project]
|
||||
description = 'checks service'
|
||||
description = 'test task for websocket with crypto tickers'
|
||||
requires-python = '>= 3.10'
|
||||
maintainers = [
|
||||
{ name = 'Siarhei Siniak', email = 'siarheisiniak@gmail.com' },
|
||||
@ -8,15 +8,64 @@ classifiers = [
|
||||
'Programming Language :: Python',
|
||||
]
|
||||
|
||||
name = 'online.fxreader.pr34.checks'
|
||||
name = 'online.fxreader.pr34.test_task_2025_06_30_v1'
|
||||
version = '0.1.1'
|
||||
|
||||
dependencies = [
|
||||
'alembic',
|
||||
'fastapi',
|
||||
'uvicorn',
|
||||
'websockets',
|
||||
'uvloop',
|
||||
'tomlq',
|
||||
'mypy',
|
||||
'marisa-trie',
|
||||
'pydantic',
|
||||
'asyncpg',
|
||||
'pydantic-settings',
|
||||
'tomlkit',
|
||||
'tomlq',
|
||||
'numpy',
|
||||
'cryptography',
|
||||
'mypy',
|
||||
'pyright',
|
||||
'ruff',
|
||||
'ipython',
|
||||
'ipdb',
|
||||
'requests',
|
||||
'types-requests',
|
||||
'aiohttp',
|
||||
'build',
|
||||
'wheel',
|
||||
'setuptools',
|
||||
'setuptools-scm',
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ['build', 'wheel', 'setuptools', 'setuptools-scm']
|
||||
build-backend = 'setuptools.build_meta'
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = false
|
||||
[tool.setuptools.package-dir]
|
||||
'online.fxreader.pr34.test_task_2025_06_30_v1' = 'python/online/fxreader/pr34/test_task_2025_06_30_v1'
|
||||
|
||||
[tool.alembic]
|
||||
script_location = 'python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic'
|
||||
prepend_sys_path = ['python']
|
||||
|
||||
# sqlalchemy.url = 'asdfasdf:/asdfasdfa'
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 160
|
||||
target-version = 'py310'
|
||||
# builtins = ['_', 'I', 'P']
|
||||
include = [
|
||||
'*.py',
|
||||
'*/**/*.py',
|
||||
'*/**/*.pyi',
|
||||
# 'follow_the_leader/**/*.py',
|
||||
#'*.py',
|
||||
# '*.recipe',
|
||||
'python/**/*.py',
|
||||
'python/**/*.pyi',
|
||||
]
|
||||
exclude = [
|
||||
'.venv',
|
||||
@ -50,6 +99,9 @@ select = ['E', 'F', 'I', 'W', 'INT']
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
detect-same-package = true
|
||||
# extra-standard-library = ["aes", "elementmaker", "encodings"]
|
||||
# known-first-party = ["calibre_extensions", "calibre_plugins", "polyglot"]
|
||||
# known-third-party = ["odf", "qt", "templite", "tinycss", "css_selectors"]
|
||||
relative-imports-order = "closest-to-furthest"
|
||||
split-on-trailing-comma = true
|
||||
section-order = [
|
||||
@ -67,11 +119,16 @@ enabled = false
|
||||
|
||||
[tool.pyright]
|
||||
include = [
|
||||
'*/**/*.py',
|
||||
#'../../../../../follow_the_leader/views2/payments.py',
|
||||
#'../../../../../follow_the_leader/logic/payments.py',
|
||||
#'../../../../../follow_the_leader/logic/paypal.py',
|
||||
'python/**/*.py',
|
||||
'python/**/*.pyi',
|
||||
]
|
||||
# stubPath = '../mypy-stubs'
|
||||
extraPaths = [
|
||||
'.',
|
||||
]
|
||||
#strict = ["src"]
|
||||
|
||||
analyzeUnannotatedFunctions = true
|
||||
disableBytesTypePromotions = true
|
||||
@ -170,4 +227,3 @@ reportShadowedImports = "none"
|
||||
reportUninitializedInstanceVariable = "none"
|
||||
reportUnnecessaryTypeIgnoreComment = "none"
|
||||
reportUnusedCallResult = "none"
|
||||
|
||||
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/__init__.py
vendored
Normal file
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/__init__.py
vendored
Normal file
68
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/app.py
vendored
Normal file
68
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/app.py
vendored
Normal file
@ -0,0 +1,68 @@
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
# import os
|
||||
from ..tickers_retrieval.emcont import Emcont
|
||||
from ..tickers.models import Ticker
|
||||
from ..tickers.logic import ticker_store_multiple, markets_get_by_symbol
|
||||
from .db import create_engine
|
||||
import sqlalchemy.ext.asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from typing import Any
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
async def run() -> None:
|
||||
async_session = create_engine()
|
||||
|
||||
async def store_cb(
|
||||
rates: list[Emcont.rates_get_t.data_t.rate_t],
|
||||
timestamp: datetime.datetime,
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
) -> None:
|
||||
logger.info(dict(
|
||||
msg='before markets',
|
||||
))
|
||||
|
||||
markets = await markets_get_by_symbol(
|
||||
session,
|
||||
set([
|
||||
rate.symbol
|
||||
for rate in rates
|
||||
]),
|
||||
)
|
||||
|
||||
logger.info(dict(
|
||||
msg='after markets',
|
||||
))
|
||||
|
||||
await ticker_store_multiple(
|
||||
session,
|
||||
[
|
||||
Ticker(
|
||||
id=markets[rate.symbol],
|
||||
timestamp=timestamp,
|
||||
value=rate.value,
|
||||
)
|
||||
for rate in rates
|
||||
]
|
||||
)
|
||||
|
||||
logger.info(dict(
|
||||
rates=rates,
|
||||
timestamp=timestamp.isoformat()
|
||||
))
|
||||
|
||||
await Emcont.worker(
|
||||
only_symbols={'EURUSD', 'USDJPY', 'GBPUSD', 'AUDUSD', 'USDCAD'},
|
||||
session=async_session,
|
||||
store_cb=store_cb,
|
||||
request_timeout=2,
|
||||
store_timeout=0.5,
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(run())
|
||||
15
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/db.py
vendored
Normal file
15
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/db.py
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
from ..tickers.settings import Settings as ModelsSettings
|
||||
|
||||
import sqlalchemy.ext.asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
def create_engine() -> 'async_sessionmaker[AsyncSession]':
|
||||
engine = sqlalchemy.ext.asyncio.create_async_engine(
|
||||
ModelsSettings.singleton().db_url
|
||||
)
|
||||
async_session = sqlalchemy.ext.asyncio.async_sessionmaker(
|
||||
engine
|
||||
)
|
||||
|
||||
return async_session
|
||||
71
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/fastapi.py
vendored
Normal file
71
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/fastapi.py
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
import fastapi
|
||||
import pydantic
|
||||
import functools
|
||||
import logging
|
||||
import copy
|
||||
import uvicorn
|
||||
import uvicorn.config
|
||||
import sys
|
||||
|
||||
from .settings import Settings as APISettings
|
||||
from .db import create_engine
|
||||
from .websocket_api import WebsocketAPI
|
||||
|
||||
from typing import (Any, Optional, Literal, Annotated,)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def websocket_tickers(
|
||||
websocket: fastapi.WebSocket,
|
||||
websocket_api: WebsocketAPI,
|
||||
) -> None:
|
||||
try:
|
||||
await websocket_api.connect(websocket)
|
||||
|
||||
while True:
|
||||
msg = await websocket.receive_text()
|
||||
await websocket_api.on_message(websocket, msg)
|
||||
except fastapi.WebSocketDisconnect:
|
||||
pass
|
||||
# websocket_api.disconnect(websocket)
|
||||
except:
|
||||
logger.exception('')
|
||||
raise
|
||||
finally:
|
||||
await websocket_api.disconnect(websocket)
|
||||
|
||||
def create_app() -> fastapi.FastAPI:
|
||||
async_session = create_engine()
|
||||
|
||||
websocket_api = WebsocketAPI(
|
||||
session=async_session,
|
||||
)
|
||||
|
||||
app = fastapi.FastAPI()
|
||||
|
||||
app.websocket(
|
||||
'/tickers/',
|
||||
)(
|
||||
functools.partial(
|
||||
websocket_tickers,
|
||||
websocket_api=fastapi.Depends(lambda : websocket_api),
|
||||
)
|
||||
)
|
||||
|
||||
return app
|
||||
|
||||
def run(args: list[str]):
|
||||
log_config = copy.deepcopy(uvicorn.config.LOGGING_CONFIG)
|
||||
|
||||
uvicorn.run(
|
||||
create_app(),
|
||||
host=APISettings.singleton().uvicorn_host,
|
||||
port=APISettings.singleton().uvicorn_port,
|
||||
loop='uvloop',
|
||||
log_config=log_config,
|
||||
log_level=logging.INFO,
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
run(sys.argv[1:])
|
||||
67
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/schema.py
vendored
Normal file
67
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/schema.py
vendored
Normal file
@ -0,0 +1,67 @@
|
||||
import pydantic
|
||||
import decimal
|
||||
|
||||
from typing import (Literal, Annotated,)
|
||||
|
||||
class SubscribeAction(pydantic.BaseModel):
|
||||
action: Literal['subscribe']
|
||||
class message_t(pydantic.BaseModel):
|
||||
asset_id: Annotated[
|
||||
int,
|
||||
pydantic.Field(alias='assetId')
|
||||
]
|
||||
|
||||
message: message_t
|
||||
|
||||
class AssetsAction(pydantic.BaseModel):
|
||||
action: Literal['assets']
|
||||
class message_t(pydantic.BaseModel):
|
||||
pass
|
||||
|
||||
message: Annotated[
|
||||
message_t,
|
||||
pydantic.Field(
|
||||
default_factory=message_t,
|
||||
)
|
||||
]
|
||||
|
||||
Action = pydantic.RootModel[
|
||||
AssetsAction | SubscribeAction
|
||||
]
|
||||
|
||||
class AssetHistoryResponse(pydantic.BaseModel):
|
||||
action: Literal['asset_history'] = 'asset_history'
|
||||
|
||||
class message_t(pydantic.BaseModel):
|
||||
class point_t(pydantic.BaseModel):
|
||||
asset_name : Annotated[
|
||||
str,
|
||||
pydantic.Field(
|
||||
alias='assetName',
|
||||
)
|
||||
]
|
||||
time: int
|
||||
asset_id : Annotated[
|
||||
int,
|
||||
pydantic.Field(alias='assetId')
|
||||
]
|
||||
value: decimal.Decimal
|
||||
|
||||
points: list[point_t]
|
||||
message: message_t
|
||||
|
||||
class AssetTickerResponse(pydantic.BaseModel):
|
||||
action: Literal['point'] = 'point'
|
||||
|
||||
message: 'AssetHistoryResponse.message_t.point_t'
|
||||
|
||||
class AssetsResponse(pydantic.BaseModel):
|
||||
action: Literal['assets'] = 'assets'
|
||||
|
||||
class message_t(pydantic.BaseModel):
|
||||
class asset_t(pydantic.BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
|
||||
assets: list[asset_t]
|
||||
message: message_t
|
||||
18
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/settings.py
vendored
Normal file
18
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/settings.py
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
import pydantic
|
||||
import pydantic_settings
|
||||
|
||||
from typing import (ClassVar, Optional,)
|
||||
|
||||
|
||||
class Settings(pydantic_settings.BaseSettings):
|
||||
uvicorn_port : int = 80
|
||||
uvicorn_host : str = '127.0.0.1'
|
||||
|
||||
_singleton : ClassVar[Optional['Settings']] = None
|
||||
|
||||
@classmethod
|
||||
def singleton(cls) -> 'Settings':
|
||||
if cls._singleton is None:
|
||||
cls._singleton = Settings.model_validate({})
|
||||
|
||||
return cls._singleton
|
||||
126
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/websocket_api.py
vendored
Normal file
126
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/async_api/websocket_api.py
vendored
Normal file
@ -0,0 +1,126 @@
|
||||
import fastapi
|
||||
import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from . import schema
|
||||
from ..tickers.logic import tickers_get_by_period, markets_all
|
||||
|
||||
from typing import (Optional, Literal)
|
||||
|
||||
class WebsocketAPI:
|
||||
def __init__(
|
||||
self,
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
) -> None:
|
||||
self.connections : set[
|
||||
fastapi.WebSocket,
|
||||
] = set()
|
||||
self.subscriptions_by_asset_id : dict[
|
||||
int, set[fastapi.WebSocket]
|
||||
] = dict()
|
||||
self.subscriptions_by_client : dict[
|
||||
fastapi.WebSocket,
|
||||
int,
|
||||
] = dict()
|
||||
self.session = session
|
||||
|
||||
async def connect(self, client: fastapi.WebSocket) -> None:
|
||||
assert not client in self.connections
|
||||
|
||||
await client.accept()
|
||||
|
||||
self.connections.add(client)
|
||||
|
||||
|
||||
async def subscribe(
|
||||
self,
|
||||
client: fastapi.WebSocket,
|
||||
asset_id: int
|
||||
) -> None:
|
||||
if client in self.subscriptions_by_client:
|
||||
last_asset_id = self.subscriptions_by_client[client]
|
||||
del self.subscriptions_by_asset_id[last_asset_id]
|
||||
del self.subscriptions_by_client[client]
|
||||
|
||||
if not asset_id in self.subscriptions_by_asset_id:
|
||||
self.subscriptions_by_asset_id[asset_id] = set()
|
||||
|
||||
self.subscriptions_by_asset_id[asset_id].add(client)
|
||||
self.subscriptions_by_client[client] = asset_id
|
||||
|
||||
await self.asset_last_period(client, asset_id)
|
||||
|
||||
async def asset_last_period(
|
||||
self,
|
||||
client: fastapi.WebSocket,
|
||||
asset_id: int,
|
||||
) -> None:
|
||||
tickers = await tickers_get_by_period(
|
||||
self.session,
|
||||
period=datetime.timedelta(minutes=30),
|
||||
market_id=asset_id,
|
||||
)
|
||||
|
||||
await client.send_text(
|
||||
schema.AssetHistoryResponse(
|
||||
message=schema.AssetHistoryResponse.message_t(
|
||||
points=[
|
||||
schema.AssetHistoryResponse.message_t.point_t.model_construct(
|
||||
asset_name=o.market.name,
|
||||
asset_id=o.market.id,
|
||||
time=int(o.timestamp.timestamp()),
|
||||
value=o.value,
|
||||
)
|
||||
for o in tickers
|
||||
]
|
||||
)
|
||||
).json(by_alias=True,),
|
||||
)
|
||||
|
||||
async def assets_index(
|
||||
self,
|
||||
client: fastapi.WebSocket,
|
||||
) -> None:
|
||||
markets = await markets_all(
|
||||
self.session,
|
||||
)
|
||||
|
||||
await client.send_text(
|
||||
schema.AssetsResponse(
|
||||
message=schema.AssetsResponse.message_t(
|
||||
assets=[
|
||||
schema.AssetsResponse.message_t.asset_t.model_construct(
|
||||
name=o.name,
|
||||
id=o.id,
|
||||
)
|
||||
for o in markets
|
||||
]
|
||||
)
|
||||
).json(by_alias=True,),
|
||||
)
|
||||
|
||||
async def on_message(
|
||||
self,
|
||||
client: fastapi.WebSocket,
|
||||
msg_raw: str
|
||||
) -> None:
|
||||
msg = schema.Action.model_validate_json(
|
||||
msg_raw
|
||||
).root
|
||||
|
||||
if isinstance(msg, schema.SubscribeAction):
|
||||
await self.subscribe(
|
||||
client,
|
||||
msg.message.asset_id
|
||||
)
|
||||
elif isinstance(msg, schema.AssetsAction):
|
||||
await self.assets_index(
|
||||
client,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
async def disconnect(self, client: fastapi.WebSocket) -> None:
|
||||
assert client in self.connections
|
||||
|
||||
self.connections.remove(client)
|
||||
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/py.typed
vendored
Normal file
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/py.typed
vendored
Normal file
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/__init__.py
vendored
Normal file
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/__init__.py
vendored
Normal file
117
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/env.py
vendored
Normal file
117
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/env.py
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from sqlalchemy.engine.base import Connection
|
||||
|
||||
from alembic import context
|
||||
|
||||
from online.fxreader.pr34.test_task_2025_06_30_v1.tickers.settings import Settings
|
||||
from online.fxreader.pr34.test_task_2025_06_30_v1.tickers.models import (
|
||||
Base,
|
||||
Market,
|
||||
)
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
config.set_main_option(
|
||||
'sqlalchemy.url',
|
||||
Settings.singleton().db_url
|
||||
)
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
# if config.config_file_name is not None:
|
||||
# fileConfig(config.config_file_name)
|
||||
# else:
|
||||
if True:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
# target_metadata = None
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def do_run_migrations(
|
||||
connection: Connection,
|
||||
):
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
async def run_async_migrations():
|
||||
"""In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
logger.info(dict(msg='started'))
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
logger.info(dict(msg='done'))
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode."""
|
||||
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
raise NotImplementedError
|
||||
# run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
28
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/script.py.mako
vendored
Normal file
28
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/script.py.mako
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
36
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/versions/335b4c4f052c_add_market_table.py
vendored
Normal file
36
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/versions/335b4c4f052c_add_market_table.py
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
"""add Market table
|
||||
|
||||
Revision ID: 335b4c4f052c
|
||||
Revises:
|
||||
Create Date: 2025-07-04 11:31:10.983947
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '335b4c4f052c'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tickers_market',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('name', sa.String(length=32), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('tickers_market')
|
||||
# ### end Alembic commands ###
|
||||
38
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/versions/729afc7194c9_add_timezone.py
vendored
Normal file
38
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/versions/729afc7194c9_add_timezone.py
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
"""add timezone
|
||||
|
||||
Revision ID: 729afc7194c9
|
||||
Revises: eb63f793db3a
|
||||
Create Date: 2025-07-11 11:30:06.246152
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '729afc7194c9'
|
||||
down_revision: Union[str, Sequence[str], None] = 'eb63f793db3a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('tickers_ticker', 'timestamp',
|
||||
existing_type=postgresql.TIMESTAMP(),
|
||||
type_=sa.DateTime(timezone=True),
|
||||
existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.alter_column('tickers_ticker', 'timestamp',
|
||||
existing_type=sa.DateTime(timezone=True),
|
||||
type_=postgresql.TIMESTAMP(),
|
||||
existing_nullable=False)
|
||||
# ### end Alembic commands ###
|
||||
38
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/versions/eb63f793db3a_add_ticker_table.py
vendored
Normal file
38
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/alembic/versions/eb63f793db3a_add_ticker_table.py
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
"""add Ticker table
|
||||
|
||||
Revision ID: eb63f793db3a
|
||||
Revises: 335b4c4f052c
|
||||
Create Date: 2025-07-07 10:32:49.812738
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'eb63f793db3a'
|
||||
down_revision: Union[str, Sequence[str], None] = '335b4c4f052c'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('tickers_ticker',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('timestamp', sa.DateTime(), nullable=False),
|
||||
sa.Column('value', sa.Numeric(precision=32, scale=6), nullable=False),
|
||||
sa.ForeignKeyConstraint(['id'], ['tickers_market.id'], ondelete='CASCADE'),
|
||||
sa.UniqueConstraint('id', 'timestamp')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('tickers_ticker')
|
||||
# ### end Alembic commands ###
|
||||
83
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/logic.py
vendored
Normal file
83
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/logic.py
vendored
Normal file
@ -0,0 +1,83 @@
|
||||
import datetime
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from sqlalchemy.orm import selectinload, make_transient
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from .models import Ticker, Market
|
||||
from .utils import get_or_create
|
||||
|
||||
async def markets_get_by_symbol(
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
symbols: set[str],
|
||||
) -> dict[str, int]:
|
||||
res : dict[str, int] = dict()
|
||||
|
||||
async with session() as active_session:
|
||||
async with active_session.begin() as transaction:
|
||||
for o in symbols:
|
||||
m = (await get_or_create(
|
||||
active_session,
|
||||
Market,
|
||||
name=o,
|
||||
))[0]
|
||||
res[o] = m.id
|
||||
|
||||
return res
|
||||
|
||||
async def ticker_store_multiple(
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
tickers: list[Ticker],
|
||||
) -> None:
|
||||
async with session() as active_session:
|
||||
async with active_session.begin() as transaction:
|
||||
active_session.add_all(
|
||||
tickers,
|
||||
)
|
||||
|
||||
async def tickers_get_by_period(
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
market_id: int,
|
||||
period: datetime.timedelta,
|
||||
) -> list[Ticker]:
|
||||
async with session() as active_session:
|
||||
async with active_session.begin() as transaction:
|
||||
q = select(
|
||||
Ticker
|
||||
).join(Ticker.market).where(
|
||||
Market.id == market_id,
|
||||
Ticker.timestamp >= datetime.datetime.now(
|
||||
tz=datetime.timezone.utc
|
||||
) - period
|
||||
).order_by(Ticker.timestamp.desc()).options(
|
||||
selectinload(Ticker.market)
|
||||
)
|
||||
|
||||
res = await active_session.execute(q)
|
||||
|
||||
rows = [o[0] for o in res]
|
||||
|
||||
for o in rows:
|
||||
active_session.expunge(o)
|
||||
make_transient(o.market)
|
||||
|
||||
return rows
|
||||
|
||||
async def markets_all(
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
) -> list[Market]:
|
||||
async with session() as active_session:
|
||||
async with active_session.begin() as transaction:
|
||||
q = select(
|
||||
Market
|
||||
)
|
||||
|
||||
res = await active_session.execute(q)
|
||||
|
||||
rows = [o[0] for o in res]
|
||||
|
||||
for o in rows:
|
||||
active_session.expunge(o)
|
||||
|
||||
return rows
|
||||
63
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/models.py
vendored
Normal file
63
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/models.py
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
import datetime
|
||||
import decimal
|
||||
|
||||
from sqlalchemy.orm import (
|
||||
mapped_column,
|
||||
Mapped,
|
||||
DeclarativeBase,
|
||||
relationship,
|
||||
)
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
ForeignKey,
|
||||
Numeric,
|
||||
DateTime,
|
||||
UniqueConstraint,
|
||||
)
|
||||
|
||||
from typing import (Optional,)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class Market(Base):
|
||||
__tablename__ = 'tickers_market'
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
name: Mapped[str] = mapped_column(String(32))
|
||||
|
||||
tickers: Mapped[list['Ticker']] = relationship(
|
||||
back_populates='market',
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Market(id={self.id!r}, name={self.name!r})"
|
||||
|
||||
class Ticker(Base):
|
||||
__tablename__ = 'tickers_ticker'
|
||||
|
||||
id: Mapped[int] = mapped_column(ForeignKey(
|
||||
'tickers_market.id',
|
||||
ondelete='CASCADE',
|
||||
))
|
||||
market: Mapped['Market'] = relationship(
|
||||
back_populates='tickers'
|
||||
)
|
||||
|
||||
timestamp: Mapped[datetime.datetime] = mapped_column(
|
||||
DateTime(timezone=True,)
|
||||
)
|
||||
value: Mapped[decimal.Decimal] = mapped_column(Numeric(
|
||||
precision=32, scale=6,
|
||||
))
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('id', 'timestamp'),
|
||||
)
|
||||
|
||||
__mapper_args__ = dict(
|
||||
primary_key=('id', 'timestamp',)
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Ticker(id={self.id!r}, timestamp={self.timestamp!r}, value={self.value!r})"
|
||||
17
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/settings.py
vendored
Normal file
17
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/settings.py
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
import pydantic
|
||||
import pydantic_settings
|
||||
|
||||
from typing import (ClassVar, Optional,)
|
||||
|
||||
|
||||
class Settings(pydantic_settings.BaseSettings):
|
||||
db_url : str
|
||||
|
||||
_singleton : ClassVar[Optional['Settings']] = None
|
||||
|
||||
@classmethod
|
||||
def singleton(cls) -> 'Settings':
|
||||
if cls._singleton is None:
|
||||
cls._singleton = Settings.model_validate({})
|
||||
|
||||
return cls._singleton
|
||||
50
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/utils.py
vendored
Normal file
50
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers/utils.py
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
from typing import (TypeVar, Optional, Any, cast,)
|
||||
from sqlalchemy.ext.asyncio import AsyncSessionTransaction, AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.exc import NoResultFound, IntegrityError
|
||||
|
||||
M = TypeVar('M', bound='DeclarativeBase')
|
||||
|
||||
async def get_or_create(
|
||||
session: AsyncSession,
|
||||
model: type[M],
|
||||
create_method: Optional[str] = None,
|
||||
create_method_kwargs: Optional[dict[str, Any]] = None,
|
||||
**kwargs: Any
|
||||
) -> tuple[M, bool]:
|
||||
async def select_row() -> M:
|
||||
res = await session.execute(
|
||||
select(model).where(
|
||||
*[
|
||||
getattr(model, k) == v
|
||||
for k, v in kwargs.items()
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
row = res.one()[0]
|
||||
|
||||
assert isinstance(row, model)
|
||||
|
||||
return row
|
||||
|
||||
try:
|
||||
res = await select_row()
|
||||
return res, False
|
||||
except NoResultFound:
|
||||
if create_method_kwargs:
|
||||
kwargs.update(create_method_kwargs)
|
||||
|
||||
if not create_method:
|
||||
created = model(**kwargs)
|
||||
else:
|
||||
created = getattr(model, create_method)(**kwargs)
|
||||
|
||||
try:
|
||||
session.add(created)
|
||||
await session.flush()
|
||||
return created, True
|
||||
except IntegrityError:
|
||||
await session.rollback()
|
||||
return await select_row(), False
|
||||
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers_retrieval/__init__.py
vendored
Normal file
0
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers_retrieval/__init__.py
vendored
Normal file
165
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers_retrieval/emcont.py
vendored
Normal file
165
deps/test-task-2025-06-30-v1/python/online/fxreader/pr34/test_task_2025_06_30_v1/tickers_retrieval/emcont.py
vendored
Normal file
@ -0,0 +1,165 @@
|
||||
import aiohttp
|
||||
import asyncio
|
||||
import decimal
|
||||
import logging
|
||||
import datetime
|
||||
# import datetime.timezone
|
||||
import pydantic
|
||||
import json
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from typing import (
|
||||
Any, Annotated, Optional, Awaitable, Callable,
|
||||
Protocol,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class Emcont:
|
||||
class rates_get_t:
|
||||
class data_t(pydantic.BaseModel):
|
||||
class rate_t(pydantic.BaseModel):
|
||||
symbol: Annotated[
|
||||
str,
|
||||
pydantic.Field(
|
||||
alias='Symbol',
|
||||
)
|
||||
]
|
||||
bid: Annotated[
|
||||
decimal.Decimal,
|
||||
pydantic.Field(
|
||||
alias='Bid',
|
||||
)
|
||||
]
|
||||
ask: Annotated[
|
||||
decimal.Decimal,
|
||||
pydantic.Field(
|
||||
alias='Ask',
|
||||
)
|
||||
]
|
||||
|
||||
@pydantic.computed_field
|
||||
def value(self) -> decimal.Decimal:
|
||||
return (self.ask + self.bid) / 2
|
||||
|
||||
product_type: Annotated[
|
||||
str,
|
||||
pydantic.Field(
|
||||
alias='ProductType',
|
||||
)
|
||||
]
|
||||
|
||||
rates: Annotated[
|
||||
list[rate_t],
|
||||
pydantic.Field(
|
||||
alias='Rates',
|
||||
)
|
||||
]
|
||||
|
||||
@classmethod
|
||||
async def rates_get(
|
||||
cls,
|
||||
only_symbols: Optional[set[str]] = None,
|
||||
) -> Any:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get('https://rates.emcont.com') as response:
|
||||
data_json = await response.text()
|
||||
data = cls.rates_get_t.data_t.model_validate_json(
|
||||
data_json[5:-3],
|
||||
)
|
||||
|
||||
if only_symbols:
|
||||
data.rates = [
|
||||
o
|
||||
for o in data.rates
|
||||
if o.symbol in only_symbols
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
class store_cb_t(Protocol):
|
||||
async def __call__(
|
||||
self,
|
||||
rates: list['Emcont.rates_get_t.data_t.rate_t'],
|
||||
timestamp: datetime.datetime,
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
) -> None: ...
|
||||
|
||||
@classmethod
|
||||
async def worker(
|
||||
cls,
|
||||
session: 'async_sessionmaker[AsyncSession]',
|
||||
store_cb: 'Emcont.store_cb_t',
|
||||
only_symbols: Optional[set[str]] = None,
|
||||
request_timeout: float | int = 0.5,
|
||||
store_timeout: float | int = 0.5,
|
||||
request_period: float | int = 1,
|
||||
) -> None:
|
||||
last_retrieval = datetime.datetime.now(
|
||||
tz=datetime.timezone.utc,
|
||||
)
|
||||
|
||||
assert request_timeout >= 0
|
||||
assert store_timeout >= 0
|
||||
|
||||
request_period_timedelta = datetime.timedelta(
|
||||
seconds=request_period,
|
||||
)
|
||||
|
||||
while True:
|
||||
logger.info(dict(msg='started'))
|
||||
|
||||
entries : Optional['Emcont.rates_get_t.data_t'] = None
|
||||
|
||||
try:
|
||||
try:
|
||||
async with asyncio.timeout(request_timeout):
|
||||
entries = await cls.rates_get(
|
||||
only_symbols=only_symbols,
|
||||
)
|
||||
except TimeoutError:
|
||||
logger.exception('request timeout')
|
||||
|
||||
try:
|
||||
async with asyncio.timeout(store_timeout):
|
||||
if entries:
|
||||
await store_cb(
|
||||
rates=entries.rates,
|
||||
timestamp=last_retrieval,
|
||||
session=session,
|
||||
)
|
||||
except TimeoutError:
|
||||
logger.exception('store timeout')
|
||||
except:
|
||||
logger.exception('')
|
||||
|
||||
next_retrieval = last_retrieval
|
||||
|
||||
def wait_interval():
|
||||
nonlocal next_retrieval
|
||||
|
||||
return (
|
||||
next_retrieval - datetime.datetime.now(
|
||||
tz=datetime.timezone.utc,
|
||||
)
|
||||
).total_seconds()
|
||||
|
||||
while True:
|
||||
next_retrieval += request_period_timedelta
|
||||
|
||||
if (
|
||||
wait_interval() > 0 or
|
||||
wait_interval() > -request_period_timedelta.total_seconds() / 4
|
||||
):
|
||||
break
|
||||
else:
|
||||
logger.warning(dict(
|
||||
msg='skip period due to huge lag',
|
||||
))
|
||||
|
||||
if wait_interval() > 0:
|
||||
await asyncio.sleep(wait_interval())
|
||||
|
||||
last_retrieval = next_retrieval
|
||||
BIN
deps/test-task-2025-06-30-v1/releases/whl/online_fxreader_pr34_test_task_2025_06_30_v1-0.1-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-06-30-v1/releases/whl/online_fxreader_pr34_test_task_2025_06_30_v1-0.1-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-06-30-v1/releases/whl/online_fxreader_pr34_test_task_2025_06_30_v1-0.1.1-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-06-30-v1/releases/whl/online_fxreader_pr34_test_task_2025_06_30_v1-0.1.1-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
27
deps/test-task-2025-06-30-v1/requirements.in
vendored
Normal file
27
deps/test-task-2025-06-30-v1/requirements.in
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
alembic
|
||||
fastapi
|
||||
uvicorn
|
||||
websockets
|
||||
uvloop
|
||||
tomlq
|
||||
mypy
|
||||
marisa-trie
|
||||
pydantic
|
||||
asyncpg
|
||||
pydantic-settings
|
||||
tomlkit
|
||||
tomlq
|
||||
numpy
|
||||
cryptography
|
||||
mypy
|
||||
pyright
|
||||
ruff
|
||||
ipython
|
||||
ipdb
|
||||
requests
|
||||
types-requests
|
||||
aiohttp
|
||||
build
|
||||
wheel
|
||||
setuptools
|
||||
setuptools-scm
|
||||
1723
deps/test-task-2025-06-30-v1/requirements.txt
vendored
Normal file
1723
deps/test-task-2025-06-30-v1/requirements.txt
vendored
Normal file
File diff suppressed because it is too large
Load Diff
5
deps/test-task-2025-07-17-v2/.dockerignore
vendored
Normal file
5
deps/test-task-2025-07-17-v2/.dockerignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
.venv
|
||||
tmp
|
||||
.git
|
||||
.env
|
||||
build
|
||||
1
deps/test-task-2025-07-17-v2/.gitattributes
vendored
Normal file
1
deps/test-task-2025-07-17-v2/.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
releases/whl/** filter=lfs diff=lfs merge=lfs -text
|
||||
6
deps/test-task-2025-07-17-v2/.gitignore
vendored
Normal file
6
deps/test-task-2025-07-17-v2/.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
!.tmuxp/
|
||||
!python
|
||||
.env/
|
||||
releases/tar
|
||||
build
|
||||
!releases/whl/**
|
||||
9
deps/test-task-2025-07-17-v2/.tmuxp/v1.yaml
vendored
Normal file
9
deps/test-task-2025-07-17-v2/.tmuxp/v1.yaml
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
session_name: test-task-2025-07-17-v2
|
||||
start_directory: ${PWD}/deps/test-task-2025-07-17-v2
|
||||
windows:
|
||||
- focus: 'true'
|
||||
layout: 5687,98x12,0,0,18
|
||||
options: {}
|
||||
panes:
|
||||
- pane
|
||||
window_name: zsh
|
||||
99
deps/test-task-2025-07-17-v2/Makefile
vendored
Normal file
99
deps/test-task-2025-07-17-v2/Makefile
vendored
Normal file
@ -0,0 +1,99 @@
|
||||
ENV_PATH ?= .venv
|
||||
PYTHON_PATH = $(ENV_PATH)/bin/python3
|
||||
PYTHON_VERSION ?= 3.12.9
|
||||
UV_ARGS ?= --offline
|
||||
DOCKER ?= podman
|
||||
COMPOSE ?= podman compose
|
||||
|
||||
venv_extract_requirements:
|
||||
$(ENV_PATH)/bin/tomlq \
|
||||
-r '.project.dependencies | join("\n")' \
|
||||
pyproject.toml > requirements.in
|
||||
|
||||
venv_compile:
|
||||
for requirements_name in requirements requirements.torch; do\
|
||||
uv pip compile \
|
||||
$(UV_ARGS) \
|
||||
-p $(PYTHON_VERSION) \
|
||||
--generate-hashes \
|
||||
$$requirements_name.in > \
|
||||
$$requirements_name.txt; \
|
||||
cat $$requirements_name.in | grep 'index-url' >> $$requirements_name.txt; \
|
||||
done
|
||||
|
||||
|
||||
venv:
|
||||
uv \
|
||||
venv \
|
||||
-p 3.13 \
|
||||
$(UV_ARGS) \
|
||||
--seed \
|
||||
$(ENV_PATH)
|
||||
uv \
|
||||
pip install \
|
||||
$(UV_ARGS) \
|
||||
-p $(ENV_PATH) \
|
||||
-r requirements.txt
|
||||
|
||||
PYRIGHT_ARGS ?= --threads 3
|
||||
|
||||
pyright:
|
||||
$(ENV_PATH)/bin/python3 -m pyright \
|
||||
-p pyproject.toml \
|
||||
--pythonpath $(PYTHON_PATH) \
|
||||
$(PYRIGHT_ARGS)
|
||||
|
||||
pyright_watch:
|
||||
make \
|
||||
PYRIGHT_ARGS=-w \
|
||||
pyright
|
||||
|
||||
ruff_check:
|
||||
$(ENV_PATH)/bin/python3 -m ruff \
|
||||
check
|
||||
|
||||
ruff_format_check:
|
||||
$(ENV_PATH)/bin/python3 -m ruff \
|
||||
format --check
|
||||
|
||||
ruff_format:
|
||||
$(ENV_PATH)/bin/python3 -m ruff \
|
||||
format
|
||||
|
||||
ruff: ruff_format_check ruff_check
|
||||
|
||||
compose_env:
|
||||
cat docker/postgresql/.env .env/postgresql.env > .env/postgresql.patched.env
|
||||
cat docker/web/.env .env/web.env > .env/web.patched.env
|
||||
for app in summarizer payloads; do \
|
||||
cat docker/web/$$app.env .env/$$app.env > .env/$$app.patched.env; \
|
||||
done
|
||||
|
||||
compose_build_web:
|
||||
$(COMPOSE) build web
|
||||
|
||||
compose_build_summarizer:
|
||||
$(COMPOSE) build summarizer
|
||||
|
||||
compose_build_payloads:
|
||||
$(COMPOSE) build payloads
|
||||
|
||||
git-release:
|
||||
mkdir -p releases/tar
|
||||
git archive \
|
||||
--format=tar \
|
||||
-o "releases/tar/repo-$$(git describe --tags).tar" \
|
||||
HEAD
|
||||
|
||||
ALEMBIC_CMD ?= --help
|
||||
alembic:
|
||||
$(ENV_PATH)/bin/alembic \
|
||||
-c pyproject.toml \
|
||||
$(ALEMBIC_CMD)
|
||||
|
||||
deploy_wheel:
|
||||
make pyright
|
||||
make deploy_wheel_unsafe
|
||||
|
||||
deploy_wheel_unsafe:
|
||||
$(PYTHON_PATH) -m build -o releases/whl -w -n
|
||||
88
deps/test-task-2025-07-17-v2/docker-compose.yml
vendored
Normal file
88
deps/test-task-2025-07-17-v2/docker-compose.yml
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
services:
|
||||
redis:
|
||||
image: docker.io/redis:latest-alpine@sha256:e71b4cb00ea461ac21114cff40ff12fb8396914238e1e9ec41520b2d5a4d3423
|
||||
ports:
|
||||
- 127.0.0.1:9004:6379
|
||||
|
||||
web: &web
|
||||
image: online.fxreader.pr34.test_task_2025_07_17_v2.web:dev
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./docker/web/Dockerfile
|
||||
target: web
|
||||
env_file: .env/web.env
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 128M
|
||||
|
||||
web-dev:
|
||||
<<: *web
|
||||
volumes:
|
||||
- .:/app:ro
|
||||
- ./tmp/cache:/app/tmp/cache:rw
|
||||
|
||||
payloads:
|
||||
<<: *web
|
||||
image: online.fxreader.pr34.test_task_2025_07_17_v2.payloads:dev
|
||||
env_file: .env/payloads.patched.env
|
||||
ports:
|
||||
- 127.0.0.1:9003:80
|
||||
|
||||
summarizer:
|
||||
<<: *web
|
||||
image: online.fxreader.pr34.test_task_2025_07_17_v2.summarizer:dev
|
||||
env_file: .env/summarizer.patched.env
|
||||
# ports:
|
||||
# - 127.0.0.1:9003:80
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '4'
|
||||
memory: 1500M
|
||||
volumes:
|
||||
- ~/.cache/huggingface/hub:/root/.cache/huggingface/hub:ro
|
||||
|
||||
postgresql:
|
||||
image: docker.io/postgres:14.18-bookworm@sha256:c0aab7962b283cf24a0defa5d0d59777f5045a7be59905f21ba81a20b1a110c9
|
||||
# restart: always
|
||||
# set shared memory limit when using docker compose
|
||||
shm_size: 128mb
|
||||
volumes:
|
||||
- postgresql_data:/var/lib/postgresql/data/:rw
|
||||
# or set shared memory limit when deploy via swarm stack
|
||||
#volumes:
|
||||
# - type: tmpfs
|
||||
# target: /dev/shm
|
||||
# tmpfs:
|
||||
# size: 134217728 # 128*2^20 bytes = 128Mb
|
||||
env_file: .env/postgresql.patched.env
|
||||
# environment:
|
||||
# POSTGRES_PASSWORD: example
|
||||
ports:
|
||||
- 127.0.0.1:9002:5432
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: 10m
|
||||
max-file: "3"
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '0.5'
|
||||
memory: 128M
|
||||
|
||||
adminer:
|
||||
image: docker.io/adminer:standalone@sha256:730215fe535daca9a2f378c48321bc615c8f0d88668721e0eff530fa35b6e8f6
|
||||
ports:
|
||||
- 127.0.0.1:9001:8080
|
||||
|
||||
|
||||
volumes:
|
||||
postgresql_data:
|
||||
3
deps/test-task-2025-07-17-v2/docker/postgresql/.env
vendored
Normal file
3
deps/test-task-2025-07-17-v2/docker/postgresql/.env
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
PGDATA=/var/lib/postgresql/data/pgdata
|
||||
POSTGRES_USER=payloads
|
||||
POSTGRES_DB=payloads
|
||||
1
deps/test-task-2025-07-17-v2/docker/web/.env
vendored
Normal file
1
deps/test-task-2025-07-17-v2/docker/web/.env
vendored
Normal file
@ -0,0 +1 @@
|
||||
# DB_URL=
|
||||
55
deps/test-task-2025-07-17-v2/docker/web/Dockerfile
vendored
Normal file
55
deps/test-task-2025-07-17-v2/docker/web/Dockerfile
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
FROM docker.io/library/python:3.12@sha256:6121c801703ec330726ebf542faab113efcfdf2236378c03df8f49d80e7b4180 AS base
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY docker/web/apt.requirements.txt docker/web/apt.requirements.txt
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y $(cat docker/web/apt.requirements.txt)
|
||||
|
||||
RUN \
|
||||
pip3 install \
|
||||
--break-system-packages uv
|
||||
|
||||
RUN apt-get update -yy && apt-get install -yy tini
|
||||
|
||||
COPY requirements.txt .
|
||||
COPY requirements.torch.txt .
|
||||
|
||||
RUN \
|
||||
# --mount=type=bind,source=releases/whl,target=/app/releases/whl \
|
||||
--mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
( \
|
||||
for requirements in requirements*.txt; do \
|
||||
uv pip \
|
||||
install \
|
||||
--system \
|
||||
--break-system-packages \
|
||||
-r $requirements; \
|
||||
done; \
|
||||
)
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
FROM base as web
|
||||
|
||||
RUN \
|
||||
--mount=type=bind,source=releases/whl,target=/app/releases/whl \
|
||||
--mount=type=cache,target=/root/.cache/pip \
|
||||
--mount=type=cache,target=/root/.cache/uv \
|
||||
uv pip \
|
||||
install \
|
||||
--system \
|
||||
--break-system-packages \
|
||||
--no-index \
|
||||
-f releases/whl \
|
||||
'online.fxreader.pr34.test_task_2025_07_17_v2==0.1.15'
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD [ \
|
||||
"python3", \
|
||||
"-m", \
|
||||
"online.fxreader.pr34.test_task_2025_07_17_v2.async_api.fastapi" \
|
||||
]
|
||||
1
deps/test-task-2025-07-17-v2/docker/web/apt.requirements.txt
vendored
Normal file
1
deps/test-task-2025-07-17-v2/docker/web/apt.requirements.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
wget tar git curl
|
||||
5
deps/test-task-2025-07-17-v2/docker/web/payloads.env
vendored
Normal file
5
deps/test-task-2025-07-17-v2/docker/web/payloads.env
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
APPS=["online.fxreader.pr34.test_task_2025_07_17_v2.payloads.app:get_app_router:"]
|
||||
UVICORN_HOST=0.0.0.0
|
||||
UVICORN_PORT=80
|
||||
SUMMARIZER_DOMAIN=summarizer
|
||||
SUMMARIZER_PROTOCOL=http
|
||||
5
deps/test-task-2025-07-17-v2/docker/web/summarizer.env
vendored
Normal file
5
deps/test-task-2025-07-17-v2/docker/web/summarizer.env
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
APPS=["online.fxreader.pr34.test_task_2025_07_17_v2.transform.app:get_app_router:"]
|
||||
UVICORN_HOST=0.0.0.0
|
||||
UVICORN_PORT=80
|
||||
SUMMARIZER_DOMAIN=summarizer
|
||||
SUMMARIZER_PROTOCOL=http
|
||||
34
deps/test-task-2025-07-17-v2/docs/readme.md
vendored
Normal file
34
deps/test-task-2025-07-17-v2/docs/readme.md
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
# Requirements
|
||||
|
||||
1. FastAPI Microservice // Caching service
|
||||
|
||||
1.1. endpoints
|
||||
1.1.1 POST:/payload `payload_create`
|
||||
1.1.2. GET:/payload/<payload_id> `payload_read`
|
||||
1.2. tech specs
|
||||
1.2.1. fastapi for rest api
|
||||
1.2.2. sqlite/postgresql for DB that caches LLM replies;
|
||||
1.2.3. LLM transform can be stubbed (idk, maybe try to find something simple);
|
||||
1.2.4. docker-compose for services
|
||||
1.2.5. add pytest based tests;
|
||||
1.2.6. add some linters for code style, and type checking;
|
||||
|
||||
# Schemas
|
||||
```yaml
|
||||
endpoints:
|
||||
payload:
|
||||
create:
|
||||
request:
|
||||
list_1: list[str]
|
||||
list_2: list[str]
|
||||
response:
|
||||
payload:
|
||||
id: int
|
||||
output: list[str]
|
||||
read:
|
||||
request:
|
||||
id: int
|
||||
response:
|
||||
id: int
|
||||
output: list[str]
|
||||
```
|
||||
230
deps/test-task-2025-07-17-v2/pyproject.toml
vendored
Normal file
230
deps/test-task-2025-07-17-v2/pyproject.toml
vendored
Normal file
@ -0,0 +1,230 @@
|
||||
[project]
|
||||
description = 'test task for LLM replies caching'
|
||||
requires-python = '>= 3.10'
|
||||
maintainers = [
|
||||
{ name = 'Siarhei Siniak', email = 'siarheisiniak@gmail.com' },
|
||||
]
|
||||
classifiers = [
|
||||
'Programming Language :: Python',
|
||||
]
|
||||
|
||||
name = 'online.fxreader.pr34.test_task_2025_07_17_v2'
|
||||
version = '0.1.15'
|
||||
|
||||
dependencies = [
|
||||
'alembic',
|
||||
'fastapi',
|
||||
'uvicorn',
|
||||
'websockets',
|
||||
'uvloop',
|
||||
'tomlq',
|
||||
'mypy',
|
||||
'marisa-trie',
|
||||
'pydantic',
|
||||
'asyncpg',
|
||||
'pydantic-settings',
|
||||
'tomlkit',
|
||||
'tomlq',
|
||||
'numpy',
|
||||
'cryptography',
|
||||
'mypy',
|
||||
'pyright',
|
||||
'ruff',
|
||||
'ipython',
|
||||
'ipdb',
|
||||
'requests',
|
||||
'types-requests',
|
||||
'aiohttp',
|
||||
'build',
|
||||
'wheel',
|
||||
'setuptools',
|
||||
'setuptools-scm',
|
||||
'transformers',
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ['build', 'wheel', 'setuptools', 'setuptools-scm']
|
||||
build-backend = 'setuptools.build_meta'
|
||||
|
||||
[tool.setuptools]
|
||||
include-package-data = false
|
||||
[tool.setuptools.package-dir]
|
||||
'online.fxreader.pr34.test_task_2025_07_17_v2' = 'python/online/fxreader/pr34/test_task_2025_07_17_v2'
|
||||
|
||||
[tool.alembic]
|
||||
script_location = 'python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/alembic'
|
||||
prepend_sys_path = ['python']
|
||||
|
||||
# sqlalchemy.url = 'asdfasdf:/asdfasdfa'
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 160
|
||||
target-version = 'py310'
|
||||
# builtins = ['_', 'I', 'P']
|
||||
include = [
|
||||
# 'follow_the_leader/**/*.py',
|
||||
#'*.py',
|
||||
# '*.recipe',
|
||||
'python/**/*.py',
|
||||
'python/**/*.pyi',
|
||||
]
|
||||
exclude = [
|
||||
'.venv',
|
||||
]
|
||||
|
||||
[tool.ruff.format]
|
||||
quote-style = 'single'
|
||||
indent-style = 'tab'
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
|
||||
[tool.ruff.lint]
|
||||
ignore = [
|
||||
'E402', 'E722', 'E741', 'W191', 'E101', 'E501', 'I001', 'F401', 'E714',
|
||||
'E713',
|
||||
# remove lambdas later on
|
||||
'E731',
|
||||
# fix this too
|
||||
'E712',
|
||||
'E703',
|
||||
# remove unused variables, or fix a bug
|
||||
'F841',
|
||||
# fix * imports
|
||||
'F403',
|
||||
# don't care about trailing new lines
|
||||
'W292',
|
||||
|
||||
]
|
||||
select = ['E', 'F', 'I', 'W', 'INT']
|
||||
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
detect-same-package = true
|
||||
# extra-standard-library = ["aes", "elementmaker", "encodings"]
|
||||
# known-first-party = ["calibre_extensions", "calibre_plugins", "polyglot"]
|
||||
# known-third-party = ["odf", "qt", "templite", "tinycss", "css_selectors"]
|
||||
relative-imports-order = "closest-to-furthest"
|
||||
split-on-trailing-comma = true
|
||||
section-order = [
|
||||
# '__python__',
|
||||
"future",
|
||||
"standard-library", "third-party", "first-party", "local-folder"
|
||||
]
|
||||
force-wrap-aliases = true
|
||||
|
||||
# [tool.ruff.lint.isort.sections]
|
||||
# '__python__' = ['__python__']
|
||||
|
||||
[tool.pylsp-mypy]
|
||||
enabled = false
|
||||
|
||||
[tool.pyright]
|
||||
include = [
|
||||
#'../../../../../follow_the_leader/views2/payments.py',
|
||||
#'../../../../../follow_the_leader/logic/payments.py',
|
||||
#'../../../../../follow_the_leader/logic/paypal.py',
|
||||
'python/**/*.py',
|
||||
'python/**/*.pyi',
|
||||
]
|
||||
# stubPath = '../mypy-stubs'
|
||||
extraPaths = [
|
||||
]
|
||||
#strict = ["src"]
|
||||
|
||||
analyzeUnannotatedFunctions = true
|
||||
disableBytesTypePromotions = true
|
||||
strictParameterNoneValue = true
|
||||
enableTypeIgnoreComments = true
|
||||
enableReachabilityAnalysis = true
|
||||
strictListInference = true
|
||||
strictDictionaryInference = true
|
||||
strictSetInference = true
|
||||
deprecateTypingAliases = false
|
||||
enableExperimentalFeatures = false
|
||||
reportMissingTypeStubs ="error"
|
||||
reportMissingModuleSource = "warning"
|
||||
reportInvalidTypeForm = "error"
|
||||
reportMissingImports = "error"
|
||||
reportUndefinedVariable = "error"
|
||||
reportAssertAlwaysTrue = "error"
|
||||
reportInvalidStringEscapeSequence = "error"
|
||||
reportInvalidTypeVarUse = "error"
|
||||
reportSelfClsParameterName = "error"
|
||||
reportUnsupportedDunderAll = "error"
|
||||
reportUnusedExpression = "error"
|
||||
reportWildcardImportFromLibrary = "error"
|
||||
reportAbstractUsage = "error"
|
||||
reportArgumentType = "error"
|
||||
reportAssertTypeFailure = "error"
|
||||
reportAssignmentType = "error"
|
||||
reportAttributeAccessIssue = "error"
|
||||
reportCallIssue = "error"
|
||||
reportGeneralTypeIssues = "error"
|
||||
reportInconsistentOverload = "error"
|
||||
reportIndexIssue = "error"
|
||||
reportInvalidTypeArguments = "error"
|
||||
reportNoOverloadImplementation = "error"
|
||||
reportOperatorIssue = "error"
|
||||
reportOptionalSubscript = "error"
|
||||
reportOptionalMemberAccess = "error"
|
||||
reportOptionalCall = "error"
|
||||
reportOptionalIterable = "error"
|
||||
reportOptionalContextManager = "error"
|
||||
reportOptionalOperand = "error"
|
||||
reportRedeclaration = "error"
|
||||
reportReturnType = "error"
|
||||
reportTypedDictNotRequiredAccess = "error"
|
||||
reportPrivateImportUsage = "error"
|
||||
reportUnboundVariable = "error"
|
||||
reportUnhashable = "error"
|
||||
reportUnusedCoroutine = "error"
|
||||
reportUnusedExcept = "error"
|
||||
reportFunctionMemberAccess = "error"
|
||||
reportIncompatibleMethodOverride = "error"
|
||||
reportIncompatibleVariableOverride = "error"
|
||||
reportOverlappingOverload = "error"
|
||||
reportPossiblyUnboundVariable = "error"
|
||||
reportConstantRedefinition = "error"
|
||||
#reportDeprecated = "error"
|
||||
reportDeprecated = "warning"
|
||||
reportDuplicateImport = "error"
|
||||
reportIncompleteStub = "error"
|
||||
reportInconsistentConstructor = "error"
|
||||
reportInvalidStubStatement = "error"
|
||||
reportMatchNotExhaustive = "error"
|
||||
reportMissingParameterType = "error"
|
||||
reportMissingTypeArgument = "error"
|
||||
reportPrivateUsage = "error"
|
||||
reportTypeCommentUsage = "error"
|
||||
reportUnknownArgumentType = "error"
|
||||
reportUnknownLambdaType = "error"
|
||||
reportUnknownMemberType = "error"
|
||||
reportUnknownParameterType = "error"
|
||||
reportUnknownVariableType = "error"
|
||||
#reportUnknownVariableType = "warning"
|
||||
reportUnnecessaryCast = "error"
|
||||
reportUnnecessaryComparison = "error"
|
||||
reportUnnecessaryContains = "error"
|
||||
#reportUnnecessaryIsInstance = "error"
|
||||
reportUnnecessaryIsInstance = "warning"
|
||||
reportUnusedClass = "error"
|
||||
#reportUnusedImport = "error"
|
||||
reportUnusedImport = "none"
|
||||
# reportUnusedFunction = "error"
|
||||
reportUnusedFunction = "warning"
|
||||
#reportUnusedVariable = "error"
|
||||
reportUnusedVariable = "warning"
|
||||
reportUntypedBaseClass = "error"
|
||||
reportUntypedClassDecorator = "error"
|
||||
reportUntypedFunctionDecorator = "error"
|
||||
reportUntypedNamedTuple = "error"
|
||||
reportCallInDefaultInitializer = "none"
|
||||
reportImplicitOverride = "none"
|
||||
reportImplicitStringConcatenation = "none"
|
||||
reportImportCycles = "none"
|
||||
reportMissingSuperCall = "none"
|
||||
reportPropertyTypeMismatch = "none"
|
||||
reportShadowedImports = "none"
|
||||
reportUninitializedInstanceVariable = "none"
|
||||
reportUnnecessaryTypeIgnoreComment = "none"
|
||||
reportUnusedCallResult = "none"
|
||||
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/__init__.py
vendored
Normal file
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/__init__.py
vendored
Normal file
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/async_api/__init__.py
vendored
Normal file
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/async_api/__init__.py
vendored
Normal file
25
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/async_api/db.py
vendored
Normal file
25
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/async_api/db.py
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
import fastapi
|
||||
|
||||
from ..payloads.settings import Settings as ModelsSettings
|
||||
|
||||
import sqlalchemy.ext.asyncio
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
|
||||
from typing import (Annotated, Generator,)
|
||||
|
||||
|
||||
def create_engine() -> Generator[
|
||||
'async_sessionmaker[AsyncSession]',
|
||||
None,
|
||||
None,
|
||||
]:
|
||||
engine = sqlalchemy.ext.asyncio.create_async_engine(ModelsSettings.singleton().db_url)
|
||||
async_session = sqlalchemy.ext.asyncio.async_sessionmaker(engine)
|
||||
|
||||
yield async_session
|
||||
|
||||
AsyncSessionDep = Annotated[
|
||||
'async_sessionmaker[AsyncSession]',
|
||||
fastapi.Depends(create_engine)
|
||||
]
|
||||
@ -9,6 +9,9 @@ import uvicorn.config
|
||||
import sys
|
||||
|
||||
from .settings import Settings as APISettings
|
||||
# from .db import create_engine, async_session
|
||||
from ..payloads.views import router as payloads_router
|
||||
# from .websocket_api import WebsocketAPI
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
@ -22,16 +25,51 @@ from typing import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# async def websocket_tickers(
|
||||
# websocket: fastapi.WebSocket,
|
||||
# websocket_api: WebsocketAPI,
|
||||
# ) -> None:
|
||||
# try:
|
||||
# await websocket_api.connect(websocket)
|
||||
#
|
||||
# while True:
|
||||
# msg = await websocket.receive_text()
|
||||
# await websocket_api.on_message(websocket, msg)
|
||||
# except fastapi.WebSocketDisconnect:
|
||||
# pass
|
||||
# # websocket_api.disconnect(websocket)
|
||||
# except:
|
||||
# logger.exception('')
|
||||
# raise
|
||||
# finally:
|
||||
# await websocket_api.disconnect(websocket)
|
||||
|
||||
|
||||
def create_app() -> fastapi.FastAPI:
|
||||
app = fastapi.FastAPI()
|
||||
# async_session = create_engine()
|
||||
|
||||
# websocket_api = WebsocketAPI(
|
||||
# session=async_session,
|
||||
# )
|
||||
|
||||
app = fastapi.FastAPI(
|
||||
# dependencies=[
|
||||
# fastapi.Depends(async_session),
|
||||
# ]
|
||||
)
|
||||
|
||||
logger.info(dict(msg='started loading apps'))
|
||||
|
||||
for app_config in APISettings.singleton().apps:
|
||||
logger.info(dict(msg='start loading app = {}'.format(app_config)))
|
||||
app_module, app_method, app_prefix = app_config.split(':')
|
||||
|
||||
app_router = cast(Callable[[], Any], getattr(importlib.import_module(app_module), app_method))()
|
||||
app_router = cast(
|
||||
Callable[[], Any],
|
||||
getattr(
|
||||
importlib.import_module(app_module),
|
||||
app_method
|
||||
)
|
||||
)()
|
||||
|
||||
assert isinstance(app_router, fastapi.APIRouter)
|
||||
|
||||
@ -43,6 +81,15 @@ def create_app() -> fastapi.FastAPI:
|
||||
logger.info(dict(msg='done loading app = {}'.format(app_config)))
|
||||
logger.info(dict(msg='done loading apps'))
|
||||
|
||||
# app.websocket(
|
||||
# '/tickers/',
|
||||
# )(
|
||||
# functools.partial(
|
||||
# websocket_tickers,
|
||||
# websocket_api=fastapi.Depends(lambda : websocket_api),
|
||||
# )
|
||||
# )
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@ -52,6 +99,7 @@ def run(args: list[str]):
|
||||
log_config = copy.deepcopy(uvicorn.config.LOGGING_CONFIG)
|
||||
|
||||
uvicorn.run(
|
||||
# create_app(),
|
||||
create_app,
|
||||
host=APISettings.singleton().uvicorn_host,
|
||||
port=APISettings.singleton().uvicorn_port,
|
||||
71
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/async_api/schema.py
vendored
Normal file
71
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/async_api/schema.py
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
import pydantic
|
||||
import decimal
|
||||
|
||||
from typing import (
|
||||
Literal,
|
||||
Annotated,
|
||||
)
|
||||
|
||||
# class SubscribeAction(pydantic.BaseModel):
|
||||
# action: Literal['subscribe']
|
||||
# class message_t(pydantic.BaseModel):
|
||||
# asset_id: Annotated[
|
||||
# int,
|
||||
# pydantic.Field(alias='assetId')
|
||||
# ]
|
||||
#
|
||||
# message: message_t
|
||||
#
|
||||
# class AssetsAction(pydantic.BaseModel):
|
||||
# action: Literal['assets']
|
||||
# class message_t(pydantic.BaseModel):
|
||||
# pass
|
||||
#
|
||||
# message: Annotated[
|
||||
# message_t,
|
||||
# pydantic.Field(
|
||||
# default_factory=message_t,
|
||||
# )
|
||||
# ]
|
||||
#
|
||||
# Action = pydantic.RootModel[
|
||||
# AssetsAction | SubscribeAction
|
||||
# ]
|
||||
#
|
||||
# class AssetHistoryResponse(pydantic.BaseModel):
|
||||
# action: Literal['asset_history'] = 'asset_history'
|
||||
#
|
||||
# class message_t(pydantic.BaseModel):
|
||||
# class point_t(pydantic.BaseModel):
|
||||
# asset_name : Annotated[
|
||||
# str,
|
||||
# pydantic.Field(
|
||||
# alias='assetName',
|
||||
# )
|
||||
# ]
|
||||
# time: int
|
||||
# asset_id : Annotated[
|
||||
# int,
|
||||
# pydantic.Field(alias='assetId')
|
||||
# ]
|
||||
# value: decimal.Decimal
|
||||
#
|
||||
# points: list[point_t]
|
||||
# message: message_t
|
||||
#
|
||||
# class AssetTickerResponse(pydantic.BaseModel):
|
||||
# action: Literal['point'] = 'point'
|
||||
#
|
||||
# message: 'AssetHistoryResponse.message_t.point_t'
|
||||
#
|
||||
# class AssetsResponse(pydantic.BaseModel):
|
||||
# action: Literal['assets'] = 'assets'
|
||||
#
|
||||
# class message_t(pydantic.BaseModel):
|
||||
# class asset_t(pydantic.BaseModel):
|
||||
# id: int
|
||||
# name: str
|
||||
#
|
||||
# assets: list[asset_t]
|
||||
# message: message_t
|
||||
#
|
||||
@ -13,7 +13,7 @@ class Settings(pydantic_settings.BaseSettings):
|
||||
list[str],
|
||||
pydantic.Field(
|
||||
default_factory=list,
|
||||
),
|
||||
)
|
||||
]
|
||||
|
||||
uvicorn_port: int = 80
|
||||
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/__init__.py
vendored
Normal file
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/__init__.py
vendored
Normal file
114
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/alembic/env.py
vendored
Normal file
114
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/alembic/env.py
vendored
Normal file
@ -0,0 +1,114 @@
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from logging.config import fileConfig
|
||||
|
||||
from sqlalchemy.ext.asyncio import async_engine_from_config
|
||||
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
|
||||
from sqlalchemy.engine.base import Connection
|
||||
|
||||
from alembic import context
|
||||
|
||||
from online.fxreader.pr34.test_task_2025_07_17_v2.payloads.settings import Settings
|
||||
from online.fxreader.pr34.test_task_2025_07_17_v2.payloads.models import (
|
||||
Base,
|
||||
# Market,
|
||||
)
|
||||
|
||||
# this is the Alembic Config object, which provides
|
||||
# access to the values within the .ini file in use.
|
||||
config = context.config
|
||||
|
||||
config.set_main_option('sqlalchemy.url', Settings.singleton().db_url)
|
||||
|
||||
# Interpret the config file for Python logging.
|
||||
# This line sets up loggers basically.
|
||||
# if config.config_file_name is not None:
|
||||
# fileConfig(config.config_file_name)
|
||||
# else:
|
||||
if True:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# add your model's MetaData object here
|
||||
# for 'autogenerate' support
|
||||
# from myapp import mymodel
|
||||
# target_metadata = mymodel.Base.metadata
|
||||
# target_metadata = None
|
||||
target_metadata = Base.metadata
|
||||
|
||||
# other values from the config, defined by the needs of env.py,
|
||||
# can be acquired:
|
||||
# my_important_option = config.get_main_option("my_important_option")
|
||||
# ... etc.
|
||||
|
||||
|
||||
def do_run_migrations(
|
||||
connection: Connection,
|
||||
):
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_async_migrations():
|
||||
"""In this scenario we need to create an Engine
|
||||
and associate a connection with the context.
|
||||
|
||||
"""
|
||||
|
||||
logger.info(dict(msg='started'))
|
||||
connectable = async_engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix='sqlalchemy.',
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
|
||||
await connectable.dispose()
|
||||
|
||||
logger.info(dict(msg='done'))
|
||||
|
||||
|
||||
def run_migrations_offline():
|
||||
"""Run migrations in 'offline' mode.
|
||||
|
||||
This configures the context with just a URL
|
||||
and not an Engine, though an Engine is acceptable
|
||||
here as well. By skipping the Engine creation
|
||||
we don't even need a DBAPI to be available.
|
||||
|
||||
Calls to context.execute() here emit the given string to the
|
||||
script output.
|
||||
|
||||
"""
|
||||
url = config.get_main_option('sqlalchemy.url')
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={'paramstyle': 'named'},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online():
|
||||
"""Run migrations in 'online' mode."""
|
||||
|
||||
asyncio.run(run_async_migrations())
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
raise NotImplementedError
|
||||
# run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
||||
28
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/alembic/script.py.mako
vendored
Normal file
28
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/alembic/script.py.mako
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
42
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/alembic/versions/f7fa90d3339d_add_payloads_models.py
vendored
Normal file
42
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/alembic/versions/f7fa90d3339d_add_payloads_models.py
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
"""add payloads models
|
||||
|
||||
Revision ID: f7fa90d3339d
|
||||
Revises:
|
||||
Create Date: 2025-07-18 09:58:54.099010
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'f7fa90d3339d'
|
||||
down_revision: Union[str, Sequence[str], None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
'payloads_payload',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('output', sa.JSON(), nullable=False),
|
||||
sa.Column('list_1', sa.JSON(), nullable=False),
|
||||
sa.Column('list_2', sa.JSON(), nullable=False),
|
||||
sa.Column('input_hash', sa.String(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('input_hash'),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('payloads_payload')
|
||||
# ### end Alembic commands ###
|
||||
11
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/app.py
vendored
Normal file
11
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/app.py
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
import logging
|
||||
import fastapi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from . import views
|
||||
|
||||
from typing import (Annotated,)
|
||||
|
||||
def get_app_router() -> fastapi.APIRouter:
|
||||
return views.router
|
||||
112
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/logic.py
vendored
Normal file
112
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/logic.py
vendored
Normal file
@ -0,0 +1,112 @@
|
||||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from sqlalchemy.orm import selectinload, make_transient
|
||||
from sqlalchemy.future import select
|
||||
|
||||
from .models import Payload
|
||||
from .utils import get_or_create
|
||||
|
||||
from typing import (Optional, Any,)
|
||||
|
||||
async def payload_get_or_create(
|
||||
session: AsyncSession,
|
||||
output: list[str],
|
||||
list_1: list[str],
|
||||
list_2: list[str],
|
||||
input_hash: Optional[str] = None,
|
||||
):
|
||||
if input_hash is None:
|
||||
input_hash = hashlib.sha256(json.dumps(dict(
|
||||
list_1=list_1,
|
||||
list_2=list_2,
|
||||
)).encode('utf-8')).digest().hex()
|
||||
|
||||
return await get_or_create(
|
||||
session,
|
||||
Payload,
|
||||
create_method_kwargs=dict(
|
||||
output=output,
|
||||
list_1=list_1,
|
||||
list_2=list_2,
|
||||
),
|
||||
input_hash=input_hash,
|
||||
)
|
||||
|
||||
# async def markets_get_by_symbol(
|
||||
# session: 'async_sessionmaker[AsyncSession]',
|
||||
# symbols: set[str],
|
||||
# ) -> dict[str, int]:
|
||||
# res : dict[str, int] = dict()
|
||||
#
|
||||
# async with session() as active_session:
|
||||
# async with active_session.begin() as transaction:
|
||||
# for o in symbols:
|
||||
# m = (await get_or_create(
|
||||
# active_session,
|
||||
# Market,
|
||||
# name=o,
|
||||
# ))[0]
|
||||
# res[o] = m.id
|
||||
#
|
||||
# return res
|
||||
#
|
||||
# async def ticker_store_multiple(
|
||||
# session: 'async_sessionmaker[AsyncSession]',
|
||||
# tickers: list[Ticker],
|
||||
# ) -> None:
|
||||
# async with session() as active_session:
|
||||
# async with active_session.begin() as transaction:
|
||||
# active_session.add_all(
|
||||
# tickers,
|
||||
# )
|
||||
#
|
||||
# async def tickers_get_by_period(
|
||||
# session: 'async_sessionmaker[AsyncSession]',
|
||||
# market_id: int,
|
||||
# period: datetime.timedelta,
|
||||
# ) -> list[Ticker]:
|
||||
# async with session() as active_session:
|
||||
# async with active_session.begin() as transaction:
|
||||
# q = select(
|
||||
# Ticker
|
||||
# ).join(Ticker.market).where(
|
||||
# Market.id == market_id,
|
||||
# Ticker.timestamp >= datetime.datetime.now(
|
||||
# tz=datetime.timezone.utc
|
||||
# ) - period
|
||||
# ).order_by(Ticker.timestamp.desc()).options(
|
||||
# selectinload(Ticker.market)
|
||||
# )
|
||||
#
|
||||
# res = await active_session.execute(q)
|
||||
#
|
||||
# rows = [o[0] for o in res]
|
||||
#
|
||||
# for o in rows:
|
||||
# active_session.expunge(o)
|
||||
# make_transient(o.market)
|
||||
#
|
||||
# return rows
|
||||
#
|
||||
# async def markets_all(
|
||||
# session: 'async_sessionmaker[AsyncSession]',
|
||||
# ) -> list[Market]:
|
||||
# async with session() as active_session:
|
||||
# async with active_session.begin() as transaction:
|
||||
# q = select(
|
||||
# Market
|
||||
# )
|
||||
#
|
||||
# res = await active_session.execute(q)
|
||||
#
|
||||
# rows = [o[0] for o in res]
|
||||
#
|
||||
# for o in rows:
|
||||
# active_session.expunge(o)
|
||||
#
|
||||
# return rows
|
||||
#
|
||||
50
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/models.py
vendored
Normal file
50
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/models.py
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
|
||||
from sqlalchemy.orm import (
|
||||
mapped_column,
|
||||
Mapped,
|
||||
DeclarativeBase,
|
||||
relationship,
|
||||
)
|
||||
from sqlalchemy import (
|
||||
String,
|
||||
ForeignKey,
|
||||
Numeric,
|
||||
DateTime,
|
||||
UniqueConstraint,
|
||||
JSON,
|
||||
)
|
||||
|
||||
from typing import (
|
||||
Optional,
|
||||
)
|
||||
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
|
||||
class Payload(Base):
|
||||
__tablename__ = 'payloads_payload'
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
output: Mapped[list[str]] = mapped_column(JSON())
|
||||
list_1: Mapped[list[str]] = mapped_column(JSON())
|
||||
list_2: Mapped[list[str]] = mapped_column(JSON())
|
||||
input_hash: Mapped[str] = mapped_column()
|
||||
|
||||
__table_args__ = (UniqueConstraint('input_hash'),)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return json.dumps(
|
||||
dict(
|
||||
model=str(type(self)),
|
||||
id=self.id,
|
||||
output=self.output,
|
||||
list_1=self.list_1,
|
||||
list_2=self.list_2,
|
||||
input_hash=self.input_hash,
|
||||
)
|
||||
)
|
||||
5
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/schema.py
vendored
Normal file
5
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/schema.py
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
import pydantic
|
||||
|
||||
class Payload(pydantic.BaseModel):
|
||||
id: int
|
||||
output: list[str]
|
||||
24
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/settings.py
vendored
Normal file
24
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/settings.py
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
import pydantic
|
||||
import pydantic_settings
|
||||
|
||||
from typing import (
|
||||
ClassVar,
|
||||
Optional,
|
||||
Literal,
|
||||
)
|
||||
|
||||
|
||||
class Settings(pydantic_settings.BaseSettings):
|
||||
db_url: str
|
||||
|
||||
_singleton: ClassVar[Optional['Settings']] = None
|
||||
|
||||
summarizer_domain: str
|
||||
summarizer_protocol: Literal['http', 'https']
|
||||
|
||||
@classmethod
|
||||
def singleton(cls) -> 'Settings':
|
||||
if cls._singleton is None:
|
||||
cls._singleton = Settings.model_validate({})
|
||||
|
||||
return cls._singleton
|
||||
18
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/summarizer.py
vendored
Normal file
18
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/summarizer.py
vendored
Normal file
@ -0,0 +1,18 @@
|
||||
from ..transform import rest_client as summarizer_rest_client
|
||||
from ..transform.rest_client import SummaryRequest
|
||||
from .settings import Settings as ModelSettings
|
||||
|
||||
from typing import (ClassVar, Optional,)
|
||||
|
||||
class SummarizerClient:
|
||||
_summarizer_client : ClassVar[Optional[summarizer_rest_client.SummarizerClient]] = None
|
||||
|
||||
@classmethod
|
||||
def singleton(cls) -> summarizer_rest_client.SummarizerClient:
|
||||
if cls._summarizer_client is None:
|
||||
cls._summarizer_client = summarizer_rest_client.SummarizerClient(
|
||||
domain=ModelSettings.singleton().summarizer_domain,
|
||||
protocol=ModelSettings.singleton().summarizer_protocol,
|
||||
)
|
||||
|
||||
return cls._summarizer_client
|
||||
45
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/utils.py
vendored
Normal file
45
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/utils.py
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
from typing import (
|
||||
TypeVar,
|
||||
Optional,
|
||||
Any,
|
||||
cast,
|
||||
)
|
||||
from sqlalchemy.ext.asyncio import AsyncSessionTransaction, AsyncSession
|
||||
from sqlalchemy.future import select
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.exc import NoResultFound, IntegrityError
|
||||
|
||||
M = TypeVar('M', bound='DeclarativeBase')
|
||||
|
||||
|
||||
async def get_or_create(
|
||||
session: AsyncSession, model: type[M], create_method: Optional[str] = None, create_method_kwargs: Optional[dict[str, Any]] = None, **kwargs: Any
|
||||
) -> tuple[M, bool]:
|
||||
async def select_row() -> M:
|
||||
res = await session.execute(select(model).where(*[getattr(model, k) == v for k, v in kwargs.items()]))
|
||||
|
||||
row = res.one()[0]
|
||||
|
||||
assert isinstance(row, model)
|
||||
|
||||
return row
|
||||
|
||||
try:
|
||||
res = await select_row()
|
||||
return res, False
|
||||
except NoResultFound:
|
||||
if create_method_kwargs:
|
||||
kwargs.update(create_method_kwargs)
|
||||
|
||||
if not create_method:
|
||||
created = model(**kwargs)
|
||||
else:
|
||||
created = getattr(model, create_method)(**kwargs)
|
||||
|
||||
try:
|
||||
session.add(created)
|
||||
await session.flush()
|
||||
return created, True
|
||||
except IntegrityError:
|
||||
await session.rollback()
|
||||
return await select_row(), False
|
||||
70
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/views.py
vendored
Normal file
70
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/payloads/views.py
vendored
Normal file
@ -0,0 +1,70 @@
|
||||
import fastapi
|
||||
import itertools
|
||||
|
||||
from typing import (Annotated, Any, cast, Optional,)
|
||||
from . import schema
|
||||
from .summarizer import SummarizerClient, SummaryRequest
|
||||
from ..async_api.db import AsyncSessionDep
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker
|
||||
from . import logic
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
@router.post('/payload')
|
||||
async def payload_create(
|
||||
list_1: Annotated[
|
||||
list[str],
|
||||
fastapi.Body(),
|
||||
],
|
||||
list_2: Annotated[
|
||||
list[str],
|
||||
fastapi.Body(),
|
||||
],
|
||||
session: AsyncSessionDep
|
||||
) -> schema.Payload:
|
||||
data_1 = (await SummarizerClient.singleton().summarize_post(
|
||||
request=SummaryRequest(
|
||||
data=list_1
|
||||
)
|
||||
)).data
|
||||
|
||||
data_2 = (await SummarizerClient.singleton().summarize_post(
|
||||
request=SummaryRequest(
|
||||
data=list_2
|
||||
)
|
||||
)).data
|
||||
|
||||
output_len = max(len(data_1), len(data_2))
|
||||
|
||||
def filter_none(o: Any) -> bool:
|
||||
return o is not None
|
||||
|
||||
output = list(
|
||||
filter(
|
||||
filter_none,
|
||||
sum(
|
||||
list(
|
||||
itertools.zip_longest(data_1, data_2),
|
||||
),
|
||||
cast(tuple[str], tuple()),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async with session() as active_session:
|
||||
async with active_session.begin() as transaction:
|
||||
payload, is_created = await logic.payload_get_or_create(
|
||||
active_session,
|
||||
list_1=list_1,
|
||||
list_2=list_2,
|
||||
output=output,
|
||||
)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
@router.get('/payload/{paylaod_id}')
|
||||
async def payload_read(
|
||||
payload_id: int,
|
||||
) -> schema.Payload:
|
||||
raise NotImplementedError
|
||||
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/py.typed
vendored
Normal file
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/py.typed
vendored
Normal file
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/__init__.py
vendored
Normal file
0
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/__init__.py
vendored
Normal file
27
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/app.py
vendored
Normal file
27
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/app.py
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
import logging
|
||||
import fastapi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from . import views
|
||||
|
||||
from .dependencies import summarizer_dependency
|
||||
|
||||
from typing import (Annotated,)
|
||||
|
||||
def get_app_router() -> fastapi.APIRouter:
|
||||
logger.info(dict(msg='started'))
|
||||
router = fastapi.APIRouter(
|
||||
# dependencies=[
|
||||
# fastapi.Depends(summarizer_dependency,)
|
||||
# ]
|
||||
)
|
||||
|
||||
router.include_router(
|
||||
views.router,
|
||||
prefix='',
|
||||
)
|
||||
|
||||
logger.info(dict(msg='done'))
|
||||
|
||||
return router
|
||||
22
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/dependencies.py
vendored
Normal file
22
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/dependencies.py
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import logging
|
||||
import fastapi
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .worker import Summarizer
|
||||
|
||||
from typing import (Annotated,)
|
||||
|
||||
async def create_summarizer(
|
||||
) -> Summarizer:
|
||||
# return Summarizer()
|
||||
return Summarizer.singleton()
|
||||
|
||||
AnnotatedSummarizer = Annotated[
|
||||
Summarizer, fastapi.Depends(create_summarizer)
|
||||
]
|
||||
|
||||
async def summarizer_dependency(
|
||||
summarizer: AnnotatedSummarizer
|
||||
) -> None:
|
||||
pass
|
||||
12
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/logic.py
vendored
Normal file
12
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/logic.py
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
import asyncio
|
||||
|
||||
async def transform(
|
||||
list_1: list[str],
|
||||
list_2: list[str]
|
||||
) -> list[str]:
|
||||
raise NotImplementedError
|
||||
|
||||
async def transform_api_post(
|
||||
data: list[str],
|
||||
) -> list[str]:
|
||||
raise NotImplementedError
|
||||
36
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/rest_client.py
vendored
Normal file
36
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/rest_client.py
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
import asyncio
|
||||
import json
|
||||
import aiohttp
|
||||
import logging
|
||||
|
||||
from .schema import Summary, SummaryRequest
|
||||
|
||||
from typing import (Literal,)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SummarizerClient:
|
||||
def __init__(
|
||||
self,
|
||||
domain: str,
|
||||
protocol: Literal['http', 'https'] = 'http',
|
||||
) -> None:
|
||||
self.domain = domain
|
||||
self.protocol = protocol
|
||||
|
||||
async def summarize_post(
|
||||
self,
|
||||
request: SummaryRequest,
|
||||
) -> Summary:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(
|
||||
'{}://{}/summarize'.format(self.protocol, self.domain),
|
||||
json=json.loads(request.json()),
|
||||
) as response:
|
||||
data_json = await response.text()
|
||||
data = Summary.model_validate_json(
|
||||
data_json,
|
||||
)
|
||||
|
||||
return data
|
||||
7
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/schema.py
vendored
Normal file
7
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/schema.py
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
import pydantic
|
||||
|
||||
class Summary(pydantic.BaseModel):
|
||||
data: list[str]
|
||||
|
||||
class SummaryRequest(pydantic.BaseModel):
|
||||
data: list[str]
|
||||
25
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/views.py
vendored
Normal file
25
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/views.py
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
import fastapi
|
||||
|
||||
from typing import (Annotated, Any,)
|
||||
from . import schema
|
||||
# from .worker import Summarizer
|
||||
from .dependencies import AnnotatedSummarizer
|
||||
|
||||
router = fastapi.APIRouter()
|
||||
|
||||
@router.post(
|
||||
'/summarize',
|
||||
# response_model=schema.Summary,
|
||||
)
|
||||
async def summarize(
|
||||
request: Annotated[
|
||||
schema.SummaryRequest,
|
||||
fastapi.Body(),
|
||||
],
|
||||
summarizer: AnnotatedSummarizer
|
||||
) -> schema.Summary:
|
||||
return schema.Summary(
|
||||
data=summarizer.summarize(
|
||||
request.data,
|
||||
)
|
||||
)
|
||||
80
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/worker.py
vendored
Normal file
80
deps/test-task-2025-07-17-v2/python/online/fxreader/pr34/test_task_2025_07_17_v2/transform/worker.py
vendored
Normal file
@ -0,0 +1,80 @@
|
||||
import transformers
|
||||
import transformers.pipelines
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from typing import (
|
||||
Any, cast, Callable, Protocol, Literal, TypedDict,
|
||||
TypeAlias, ClassVar, Optional,
|
||||
)
|
||||
|
||||
class SummarizerPipeline(Protocol):
|
||||
class predict_t:
|
||||
class output_t(TypedDict):
|
||||
summary_text: str
|
||||
|
||||
res_t : TypeAlias = list[output_t]
|
||||
|
||||
def predict(self, data: str) -> predict_t.res_t: ...
|
||||
|
||||
class Pipeline(Protocol):
|
||||
def __call__(
|
||||
self,
|
||||
task: Literal['summarization'],
|
||||
model: Any,
|
||||
tokenizer: Any,
|
||||
) -> 'SummarizerPipeline': ...
|
||||
|
||||
|
||||
class Summarizer:
|
||||
def __init__(self) -> None:
|
||||
logger.info(dict(msg='started loading bart'))
|
||||
|
||||
self.tokenizer = cast(
|
||||
Callable[[str], Any],
|
||||
getattr(transformers.AutoTokenizer, 'from_pretrained')
|
||||
)(
|
||||
'sshleifer/distilbart-cnn-12-6',
|
||||
)
|
||||
self.model = cast(
|
||||
Callable[[str], Any],
|
||||
getattr(transformers.AutoModelForSeq2SeqLM, 'from_pretrained')
|
||||
)(
|
||||
'sshleifer/distilbart-cnn-12-6',
|
||||
)
|
||||
|
||||
logger.info(dict(msg='done loading bart'))
|
||||
|
||||
self.summarizer = cast(
|
||||
Pipeline,
|
||||
# getattr(transformers.pipelines, 'pipeline')
|
||||
getattr(transformers, 'pipeline')
|
||||
)(
|
||||
'summarization',
|
||||
model=self.model,
|
||||
tokenizer=self.tokenizer,
|
||||
# framework='pt',
|
||||
)
|
||||
|
||||
logger.info(dict(msg='created pipeline'))
|
||||
|
||||
def summarize(
|
||||
self,
|
||||
data: list[str]
|
||||
) -> list[str]:
|
||||
res = self.summarizer.predict(
|
||||
' '.join(data)
|
||||
)
|
||||
assert len(res) == 1
|
||||
|
||||
return res[0]['summary_text'].split()
|
||||
|
||||
_singleton: ClassVar[Optional['Summarizer']] = None
|
||||
|
||||
@classmethod
|
||||
def singleton(cls) -> 'Summarizer':
|
||||
if cls._singleton is None:
|
||||
cls._singleton = Summarizer()
|
||||
|
||||
return cls._singleton
|
||||
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.1-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.1-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.10-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.10-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.11-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.11-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.12-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.12-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.13-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.13-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.14-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.14-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.15-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.15-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.2-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.2-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.3-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.3-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.4-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.4-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.5-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.5-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.6-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.6-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.7-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
BIN
deps/test-task-2025-07-17-v2/releases/whl/online_fxreader_pr34_test_task_2025_07_17_v2-0.1.7-py3-none-any.whl
(Stored with Git LFS)
vendored
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user