Skip to content

Commit b3e0330

Browse files
committed
Initial commit
0 parents  commit b3e0330

67 files changed

Lines changed: 4080 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/run-tests.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: "Run tests"
2+
3+
on: ["pull_request", "push", "workflow_dispatch"]
4+
5+
permissions:
6+
contents: read
7+
8+
jobs:
9+
tests:
10+
runs-on: ubuntu-latest
11+
services:
12+
postgres:
13+
image: postgres:latest
14+
env:
15+
POSTGRES_USER: user
16+
POSTGRES_PASSWORD: password
17+
18+
ports:
19+
- 5432:5432
20+
21+
options: >-
22+
--health-cmd pg_isready
23+
--health-interval 10s
24+
--health-timeout 5s
25+
--health-retries 5
26+
27+
steps:
28+
- name: "Checkout repo"
29+
uses: actions/checkout@v4
30+
31+
- name: Install uv
32+
uses: astral-sh/setup-uv@v5.4.0
33+
with:
34+
python-version: ${{matrix.python-version}}
35+
36+
- name: Install dependencies
37+
run: |
38+
uv sync --dev
39+
40+
- name: "Make settings.toml"
41+
run: |
42+
cp docs/settings.example.toml settings.toml
43+
44+
- name: "Run tests"
45+
run: |
46+
uv run pytest
47+
env:
48+
ENV_FOR_DYNACONF: testing

.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
*/**/__pycache__/*
2+
__pycache__/
3+
venv/*
4+
config.py
5+
alembic.ini
6+
settings.toml
7+
.venv
8+
playground.py
9+
10+
# Linters
11+
.pytest_cache
12+
.ruff_cache
13+
14+
# misc
15+
.DS_STORE
16+
*.db
17+
18+
# PyRight
19+
pyrightconfig.json
20+
21+
# PyCharm
22+
.idea
23+
24+
# VsCode
25+
.vscode/*
26+

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 Volodymyr Biloshytskyi
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

alembic/README

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Generic single-database configuration with an async dbapi.

alembic/env.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import asyncio
2+
from logging.config import fileConfig
3+
4+
from sqlalchemy import pool
5+
from sqlalchemy.engine import Connection
6+
from sqlalchemy.ext.asyncio import async_engine_from_config
7+
8+
from alembic import context
9+
10+
from app.models import Base
11+
12+
# this is the Alembic Config object, which provides
13+
# access to the values within the .ini file in use.
14+
config = context.config
15+
16+
# Interpret the config file for Python logging.
17+
# This line sets up loggers basically.
18+
if config.config_file_name is not None:
19+
fileConfig(config.config_file_name)
20+
21+
# add your model's MetaData object here
22+
# for 'autogenerate' support
23+
# from myapp import mymodel
24+
# target_metadata = mymodel.Base.metadata
25+
target_metadata = Base.metadata
26+
27+
# other values from the config, defined by the needs of env.py,
28+
# can be acquired:
29+
# my_important_option = config.get_main_option("my_important_option")
30+
# ... etc.
31+
32+
33+
def run_migrations_offline() -> None:
34+
"""Run migrations in 'offline' mode.
35+
36+
This configures the context with just a URL
37+
and not an Engine, though an Engine is acceptable
38+
here as well. By skipping the Engine creation
39+
we don't even need a DBAPI to be available.
40+
41+
Calls to context.execute() here emit the given string to the
42+
script output.
43+
44+
"""
45+
url = config.get_main_option("sqlalchemy.url")
46+
context.configure(
47+
url=url,
48+
target_metadata=target_metadata,
49+
literal_binds=True,
50+
dialect_opts={"paramstyle": "named"},
51+
)
52+
53+
with context.begin_transaction():
54+
context.run_migrations()
55+
56+
57+
def do_run_migrations(connection: Connection) -> None:
58+
context.configure(connection=connection, target_metadata=target_metadata)
59+
60+
with context.begin_transaction():
61+
context.run_migrations()
62+
63+
64+
async def run_async_migrations() -> None:
65+
"""In this scenario we need to create an Engine
66+
and associate a connection with the context.
67+
68+
"""
69+
70+
connectable = async_engine_from_config(
71+
config.get_section(config.config_ini_section, {}),
72+
prefix="sqlalchemy.",
73+
poolclass=pool.NullPool,
74+
)
75+
76+
async with connectable.connect() as connection:
77+
await connection.run_sync(do_run_migrations)
78+
79+
await connectable.dispose()
80+
81+
82+
def run_migrations_online() -> None:
83+
"""Run migrations in 'online' mode."""
84+
85+
asyncio.run(run_async_migrations())
86+
87+
88+
if context.is_offline_mode():
89+
run_migrations_offline()
90+
else:
91+
run_migrations_online()

alembic/script.py.mako

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""${message}
2+
3+
Revision ID: ${up_revision}
4+
Revises: ${down_revision | comma,n}
5+
Create Date: ${create_date}
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
${imports if imports else ""}
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = ${repr(up_revision)}
16+
down_revision: Union[str, None] = ${repr(down_revision)}
17+
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
18+
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
19+
20+
21+
def upgrade() -> None:
22+
${upgrades if upgrades else "pass"}
23+
24+
25+
def downgrade() -> None:
26+
${downgrades if downgrades else "pass"}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
"""Initial
2+
3+
Revision ID: f14d9f9ad604
4+
Revises:
5+
Create Date: 2025-07-13 12:04:12.097472
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
from sqlalchemy.dialects import postgresql
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = 'f14d9f9ad604'
16+
down_revision: Union[str, None] = None
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.create_table('service_addresses',
24+
sa.Column('address', sa.String(length=70), nullable=False),
25+
sa.Column('id', sa.Uuid(), nullable=False),
26+
sa.PrimaryKeyConstraint('id')
27+
)
28+
op.create_index(op.f('ix_service_addresses_address'), 'service_addresses', ['address'], unique=True)
29+
op.create_table('service_blocks',
30+
sa.Column('blockhash', sa.String(length=64), nullable=False),
31+
sa.Column('transactions', postgresql.ARRAY(sa.String()), nullable=False),
32+
sa.Column('height', sa.Integer(), nullable=False),
33+
sa.Column('movements', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
34+
sa.Column('created', sa.DateTime(), nullable=False),
35+
sa.Column('timestamp', sa.Integer(), nullable=False),
36+
sa.Column('prev_blockhash', sa.String(length=64), nullable=True),
37+
sa.Column('id', sa.Uuid(), nullable=False),
38+
sa.PrimaryKeyConstraint('id')
39+
)
40+
op.create_index(op.f('ix_service_blocks_blockhash'), 'service_blocks', ['blockhash'], unique=True)
41+
op.create_index(op.f('ix_service_blocks_height'), 'service_blocks', ['height'], unique=False)
42+
op.create_index(op.f('ix_service_blocks_prev_blockhash'), 'service_blocks', ['prev_blockhash'], unique=False)
43+
op.create_table('service_inputs',
44+
sa.Column('shortcut', sa.String(length=70), nullable=False),
45+
sa.Column('blockhash', sa.String(length=64), nullable=False),
46+
sa.Column('txid', sa.String(length=64), nullable=False),
47+
sa.Column('source_txid', sa.String(length=64), nullable=False),
48+
sa.Column('index', sa.Integer(), nullable=False),
49+
sa.Column('id', sa.Uuid(), nullable=False),
50+
sa.PrimaryKeyConstraint('id')
51+
)
52+
op.create_index(op.f('ix_service_inputs_blockhash'), 'service_inputs', ['blockhash'], unique=False)
53+
op.create_index(op.f('ix_service_inputs_shortcut'), 'service_inputs', ['shortcut'], unique=True)
54+
op.create_index(op.f('ix_service_inputs_source_txid'), 'service_inputs', ['source_txid'], unique=False)
55+
op.create_index(op.f('ix_service_inputs_txid'), 'service_inputs', ['txid'], unique=False)
56+
op.create_table('service_mempool',
57+
sa.Column('raw', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
58+
sa.Column('id', sa.Uuid(), nullable=False),
59+
sa.PrimaryKeyConstraint('id')
60+
)
61+
op.create_table('service_outputs',
62+
sa.Column('currency', sa.String(length=64), nullable=False),
63+
sa.Column('shortcut', sa.String(length=70), nullable=False),
64+
sa.Column('blockhash', sa.String(length=64), nullable=False),
65+
sa.Column('address', sa.String(length=70), nullable=False),
66+
sa.Column('txid', sa.String(length=64), nullable=False),
67+
sa.Column('amount', sa.Numeric(precision=28, scale=8), nullable=False),
68+
sa.Column('timelock', sa.Integer(), nullable=False),
69+
sa.Column('type', sa.String(length=64), nullable=False),
70+
sa.Column('script', sa.String(), nullable=False),
71+
sa.Column('asm', sa.String(), nullable=False),
72+
sa.Column('spent', sa.Boolean(), nullable=False),
73+
sa.Column('index', sa.Integer(), nullable=False),
74+
sa.Column('meta', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
75+
sa.Column('id', sa.Uuid(), nullable=False),
76+
sa.PrimaryKeyConstraint('id')
77+
)
78+
op.create_index(op.f('ix_service_outputs_address'), 'service_outputs', ['address'], unique=False)
79+
op.create_index(op.f('ix_service_outputs_blockhash'), 'service_outputs', ['blockhash'], unique=False)
80+
op.create_index(op.f('ix_service_outputs_currency'), 'service_outputs', ['currency'], unique=False)
81+
op.create_index(op.f('ix_service_outputs_shortcut'), 'service_outputs', ['shortcut'], unique=True)
82+
op.create_index(op.f('ix_service_outputs_txid'), 'service_outputs', ['txid'], unique=False)
83+
op.create_index(op.f('ix_service_outputs_type'), 'service_outputs', ['type'], unique=False)
84+
op.create_table('service_transactions',
85+
sa.Column('currencies', postgresql.ARRAY(sa.String(length=64)), nullable=False),
86+
sa.Column('txid', sa.String(length=64), nullable=False),
87+
sa.Column('blockhash', sa.String(length=64), nullable=False),
88+
sa.Column('addresses', postgresql.ARRAY(sa.String()), nullable=False),
89+
sa.Column('created', sa.DateTime(), nullable=False),
90+
sa.Column('timestamp', sa.Integer(), nullable=False),
91+
sa.Column('size', sa.Integer(), nullable=False),
92+
sa.Column('height', sa.Integer(), nullable=False),
93+
sa.Column('locktime', sa.Integer(), nullable=False),
94+
sa.Column('version', sa.Integer(), nullable=False),
95+
sa.Column('amount', postgresql.JSONB(astext_type=sa.Text()), nullable=False),
96+
sa.Column('id', sa.Uuid(), nullable=False),
97+
sa.PrimaryKeyConstraint('id')
98+
)
99+
op.create_index(op.f('ix_service_transactions_addresses'), 'service_transactions', ['addresses'], unique=False)
100+
op.create_index(op.f('ix_service_transactions_blockhash'), 'service_transactions', ['blockhash'], unique=False)
101+
op.create_index(op.f('ix_service_transactions_currencies'), 'service_transactions', ['currencies'], unique=False)
102+
op.create_index(op.f('ix_service_transactions_txid'), 'service_transactions', ['txid'], unique=True)
103+
op.create_table('service_address_balances',
104+
sa.Column('balance', sa.Numeric(precision=28, scale=8), nullable=False),
105+
sa.Column('currency', sa.String(length=64), nullable=False),
106+
sa.Column('address_id', sa.Uuid(), nullable=False),
107+
sa.Column('id', sa.Uuid(), nullable=False),
108+
sa.ForeignKeyConstraint(['address_id'], ['service_addresses.id'], ),
109+
sa.PrimaryKeyConstraint('address_id', 'id')
110+
)
111+
op.create_index(op.f('ix_service_address_balances_currency'), 'service_address_balances', ['currency'], unique=False)
112+
# ### end Alembic commands ###
113+
114+
115+
def downgrade() -> None:
116+
# ### commands auto generated by Alembic - please adjust! ###
117+
op.drop_index(op.f('ix_service_address_balances_currency'), table_name='service_address_balances')
118+
op.drop_table('service_address_balances')
119+
op.drop_index(op.f('ix_service_transactions_txid'), table_name='service_transactions')
120+
op.drop_index(op.f('ix_service_transactions_currencies'), table_name='service_transactions')
121+
op.drop_index(op.f('ix_service_transactions_blockhash'), table_name='service_transactions')
122+
op.drop_index(op.f('ix_service_transactions_addresses'), table_name='service_transactions')
123+
op.drop_table('service_transactions')
124+
op.drop_index(op.f('ix_service_outputs_type'), table_name='service_outputs')
125+
op.drop_index(op.f('ix_service_outputs_txid'), table_name='service_outputs')
126+
op.drop_index(op.f('ix_service_outputs_shortcut'), table_name='service_outputs')
127+
op.drop_index(op.f('ix_service_outputs_currency'), table_name='service_outputs')
128+
op.drop_index(op.f('ix_service_outputs_blockhash'), table_name='service_outputs')
129+
op.drop_index(op.f('ix_service_outputs_address'), table_name='service_outputs')
130+
op.drop_table('service_outputs')
131+
op.drop_table('service_mempool')
132+
op.drop_index(op.f('ix_service_inputs_txid'), table_name='service_inputs')
133+
op.drop_index(op.f('ix_service_inputs_source_txid'), table_name='service_inputs')
134+
op.drop_index(op.f('ix_service_inputs_shortcut'), table_name='service_inputs')
135+
op.drop_index(op.f('ix_service_inputs_blockhash'), table_name='service_inputs')
136+
op.drop_table('service_inputs')
137+
op.drop_index(op.f('ix_service_blocks_prev_blockhash'), table_name='service_blocks')
138+
op.drop_index(op.f('ix_service_blocks_height'), table_name='service_blocks')
139+
op.drop_index(op.f('ix_service_blocks_blockhash'), table_name='service_blocks')
140+
op.drop_table('service_blocks')
141+
op.drop_index(op.f('ix_service_addresses_address'), table_name='service_addresses')
142+
op.drop_table('service_addresses')
143+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)