docs
This commit is contained in:
parent
f52bad680c
commit
04e8ba559e
15
Dockerfile
Normal file
15
Dockerfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
FROM python:3.10
|
||||||
|
WORKDIR /app/
|
||||||
|
RUN apt-get update
|
||||||
|
RUN apt-get install -y python3-sphinx node.js
|
||||||
|
RUN apt-get install -y npm
|
||||||
|
RUN npm install -g http-server
|
||||||
|
RUN pip install pydata-sphinx-theme
|
||||||
|
COPY docs/Makefile Makefile
|
||||||
|
COPY setup.py setup.py
|
||||||
|
COPY docs/source source
|
||||||
|
COPY ptvp35 ptvp35
|
||||||
|
RUN make html
|
||||||
|
WORKDIR /app/build/html/
|
||||||
|
CMD [ "http-server", "-p", "80" ]
|
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Minimal makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line, and also
|
||||||
|
# from the environment for the first two.
|
||||||
|
SPHINXOPTS ?=
|
||||||
|
SPHINXBUILD ?= sphinx-build
|
||||||
|
SOURCEDIR = source
|
||||||
|
BUILDDIR = build
|
||||||
|
|
||||||
|
# Put it first so that "make" without argument is like "make help".
|
||||||
|
help:
|
||||||
|
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||||
|
|
||||||
|
.PHONY: help Makefile
|
||||||
|
|
||||||
|
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||||
|
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||||
|
%: Makefile
|
||||||
|
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
pushd %~dp0
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set SOURCEDIR=source
|
||||||
|
set BUILDDIR=build
|
||||||
|
|
||||||
|
%SPHINXBUILD% >NUL 2>NUL
|
||||||
|
if errorlevel 9009 (
|
||||||
|
echo.
|
||||||
|
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
|
||||||
|
echo.installed, then set the SPHINXBUILD environment variable to point
|
||||||
|
echo.to the full path of the 'sphinx-build' executable. Alternatively you
|
||||||
|
echo.may add the Sphinx directory to PATH.
|
||||||
|
echo.
|
||||||
|
echo.If you don't have Sphinx installed, grab it from
|
||||||
|
echo.https://www.sphinx-doc.org/
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
goto end
|
||||||
|
|
||||||
|
:help
|
||||||
|
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
|
||||||
|
|
||||||
|
:end
|
||||||
|
popd
|
36
docs/source/conf.py
Normal file
36
docs/source/conf.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Configuration file for the Sphinx documentation builder.
|
||||||
|
#
|
||||||
|
# For the full list of built-in configuration values, see the documentation:
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||||
|
|
||||||
|
# -- Project information -----------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||||
|
|
||||||
|
project = 'ptvp35'
|
||||||
|
copyright = '2022, PARRRATE TNV'
|
||||||
|
author = 'PARRRATE TNV'
|
||||||
|
release = '1.0rc2'
|
||||||
|
|
||||||
|
# -- General configuration ---------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||||
|
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
]
|
||||||
|
|
||||||
|
templates_path = ['_templates']
|
||||||
|
exclude_patterns = []
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for HTML output -------------------------------------------------
|
||||||
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
|
||||||
|
|
||||||
|
html_theme = 'pydata_sphinx_theme'
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath('..'))
|
25
docs/source/index.rst
Normal file
25
docs/source/index.rst
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
.. ptvp35 documentation master file, created by
|
||||||
|
sphinx-quickstart on Sat Nov 19 20:02:24 2022.
|
||||||
|
You can adapt this file completely to your liking, but it should at least
|
||||||
|
contain the root `toctree` directive.
|
||||||
|
|
||||||
|
Welcome to ptvp35's documentation!
|
||||||
|
==================================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
:caption: Contents:
|
||||||
|
motivation
|
||||||
|
usage
|
||||||
|
modules
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :doc:`motivation`
|
||||||
|
* :doc:`usage`
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
7
docs/source/modules.rst
Normal file
7
docs/source/modules.rst
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
ptvp35
|
||||||
|
======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 4
|
||||||
|
|
||||||
|
ptvp35
|
28
docs/source/motivation.rst
Normal file
28
docs/source/motivation.rst
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
Motivation
|
||||||
|
==========
|
||||||
|
|
||||||
|
General structure
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
* We're making a key-value database.
|
||||||
|
* Keys and values are python objects.
|
||||||
|
* While the DB is offline (after a correct shutdown), all its KVPs are stored as lines in one editable file.
|
||||||
|
* DB should be able to survive powercut at any point (D in ACID).
|
||||||
|
|
||||||
|
AP/LP get
|
||||||
|
------
|
||||||
|
|
||||||
|
The central idea for this database is the zero-latency (as far as python goes) reads. Therefore, the DB has all its data stored as a :code:`dict`.
|
||||||
|
|
||||||
|
CAP/CLP set
|
||||||
|
-------
|
||||||
|
|
||||||
|
The database allows both availability-(zero)latency and consistency (note: it's not ACID consistency, it's CAP consistency and ACID durability) variants for its writes.
|
||||||
|
All zero-latency writes are also consistent with respect to (zero-latency) reads.
|
||||||
|
|
||||||
|
Consistent get
|
||||||
|
------
|
||||||
|
|
||||||
|
Later versions of the database will include remote connections, and thus also consistent reads.
|
||||||
|
These will probably be implemented as requests (same as writes).
|
||||||
|
Some connection variants may not even include availability reads/writes.
|
10
docs/source/ptvp35.rst
Normal file
10
docs/source/ptvp35.rst
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
ptvp35 package
|
||||||
|
==============
|
||||||
|
|
||||||
|
Module contents
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. automodule:: ptvp35
|
||||||
|
:members:
|
||||||
|
:undoc-members:
|
||||||
|
:show-inheritance:
|
54
docs/source/usage.rst
Normal file
54
docs/source/usage.rst
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
Usage
|
||||||
|
=====
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
Default installation option is to use pip+git
|
||||||
|
|
||||||
|
.. code-block:: console
|
||||||
|
|
||||||
|
(.venv) $ pip install git+https://gitea.parrrate.ru/PTV/ptvp35.git
|
||||||
|
|
||||||
|
Basic functionality
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
.. autoclass:: ptvp35.DbFactory
|
||||||
|
|
||||||
|
:code:`DbFactory` class provides context management for database connections (:code:`DbConnection`) via :code:`async with` statement.
|
||||||
|
The connection isn't just a "connection", it's also the MMDB itself, so **using two connections to one database is an undefined behaviour**.
|
||||||
|
Also, that means that each connection start/shutdown is quite time expensive.
|
||||||
|
These two facts together tell that, if you intend on using the connection, you should probably wrap the main program in an :code:`async with` block.
|
||||||
|
|
||||||
|
.. code-block:: python3
|
||||||
|
|
||||||
|
import pathlib
|
||||||
|
from ptvp35 import DbFactory, KVJson
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
async with DbFactory(pathlib.Path('example.db', kvfactory=KVJson())) as connection:
|
||||||
|
await _main(connection)
|
||||||
|
|
||||||
|
.. autoclass:: ptvp35.DbConnection
|
||||||
|
|
||||||
|
.. automethod:: get
|
||||||
|
|
||||||
|
this method is instant.
|
||||||
|
|
||||||
|
.. automethod:: set
|
||||||
|
|
||||||
|
this method may take time to run.
|
||||||
|
ordering may not be guaranteed (depends on event loop implementation).
|
||||||
|
|
||||||
|
.. automethod:: set_nowait
|
||||||
|
|
||||||
|
this method is instant.
|
||||||
|
ordering is guaranteed.
|
||||||
|
|
||||||
|
.. automethod:: commit
|
||||||
|
|
||||||
|
this method may take time to run.
|
||||||
|
respects the ordering of previously called :code:`set_nowait` methods.
|
||||||
|
will, under most circumstances, also execute later changes.
|
||||||
|
|
||||||
|
.. automethod:: transaction
|
@ -9,7 +9,15 @@ from io import StringIO, UnsupportedOperation
|
|||||||
from typing import Any, Optional, IO, Hashable
|
from typing import Any, Optional, IO, Hashable
|
||||||
|
|
||||||
|
|
||||||
__all__ = ('KVRequest', 'KVJson', 'DbConnection', 'DbFactory', 'Db',)
|
__all__ = (
|
||||||
|
'KVFactory',
|
||||||
|
'KVJson',
|
||||||
|
'DbConnection',
|
||||||
|
'DbFactory',
|
||||||
|
'Db',
|
||||||
|
'Transaction',
|
||||||
|
'FallbackMapping',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Request:
|
class Request:
|
||||||
@ -37,18 +45,27 @@ class Request:
|
|||||||
|
|
||||||
|
|
||||||
class KVFactory:
|
class KVFactory:
|
||||||
|
"""note: unstable signature."""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def line(self, key: Any, value: Any, /) -> str:
|
def line(self, key: Any, value: Any, /) -> str:
|
||||||
|
"""line must contain exactly one '\\n' at exactly the end if the line is not empty."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def fromline(self, line: str, /) -> 'KVRequest':
|
def fromline(self, line: str, /) -> 'KVRequest':
|
||||||
|
"""inverse of line(). should use free() method to construct the request."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def request(self, key: Any, value: Any, /, *, future: Optional[asyncio.Future]) -> 'KVRequest':
|
def request(self, key: Any, value: Any, /, *, future: Optional[asyncio.Future]) -> 'KVRequest':
|
||||||
|
"""form request with Future.
|
||||||
|
low-level API.
|
||||||
|
note: unstable signature."""
|
||||||
return KVRequest(key, value, future=future, factory=self)
|
return KVRequest(key, value, future=future, factory=self)
|
||||||
|
|
||||||
def free(self, key: Any, value: Any, /) -> 'KVRequest':
|
def free(self, key: Any, value: Any, /) -> 'KVRequest':
|
||||||
|
"""result free from Future.
|
||||||
|
note: unstable signature."""
|
||||||
return self.request(key, value, future=None)
|
return self.request(key, value, future=None)
|
||||||
|
|
||||||
|
|
||||||
@ -81,6 +98,8 @@ class UnknownRequestType(TypeError):
|
|||||||
|
|
||||||
|
|
||||||
class KVJson(KVFactory):
|
class KVJson(KVFactory):
|
||||||
|
"""note: unstable signature."""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def line(self, key: Any, value: Any, /) -> str:
|
def line(self, key: Any, value: Any, /) -> str:
|
||||||
@ -118,6 +137,8 @@ class TransactionRequest(Request):
|
|||||||
|
|
||||||
|
|
||||||
class DbConnection:
|
class DbConnection:
|
||||||
|
"""note: unstable constructor signature."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'__factory',
|
'__factory',
|
||||||
'__path',
|
'__path',
|
||||||
@ -182,7 +203,8 @@ class DbConnection:
|
|||||||
self._queue_error(line).result()
|
self._queue_error(line).result()
|
||||||
|
|
||||||
def io2db(self, io: IO[str], db: dict, /) -> int:
|
def io2db(self, io: IO[str], db: dict, /) -> int:
|
||||||
"""there are no guarantees about .error file if error occurs here"""
|
"""there are no guarantees about .error file if an error occurs here.
|
||||||
|
note: unstable signature."""
|
||||||
size = 0
|
size = 0
|
||||||
for line in io:
|
for line in io:
|
||||||
try:
|
try:
|
||||||
@ -196,6 +218,8 @@ class DbConnection:
|
|||||||
return size
|
return size
|
||||||
|
|
||||||
def db2io(self, db: dict, io: IO[str], /) -> int:
|
def db2io(self, db: dict, io: IO[str], /) -> int:
|
||||||
|
"""does not handle any errors.
|
||||||
|
note: unstable signature."""
|
||||||
size = 0
|
size = 0
|
||||||
for key, value in db.items():
|
for key, value in db.items():
|
||||||
size += io.write(self.__factory.kvfactory.free(key, value).line())
|
size += io.write(self.__factory.kvfactory.free(key, value).line())
|
||||||
@ -211,9 +235,11 @@ class DbConnection:
|
|||||||
return self.db2io(db, file)
|
return self.db2io(db, file)
|
||||||
|
|
||||||
def get(self, key: Any, default: Any, /):
|
def get(self, key: Any, default: Any, /):
|
||||||
|
"""dict-like get with mandatory default parametre."""
|
||||||
return self.__mmdb.get(key, default)
|
return self.__mmdb.get(key, default)
|
||||||
|
|
||||||
async def set(self, key: Any, value: Any, /) -> None:
|
async def set(self, key: Any, value: Any, /) -> None:
|
||||||
|
"""set the value and wait until it's written to disk."""
|
||||||
self.__mmdb[key] = value
|
self.__mmdb[key] = value
|
||||||
future = self._create_future()
|
future = self._create_future()
|
||||||
self.__queue.put_nowait(
|
self.__queue.put_nowait(
|
||||||
@ -221,6 +247,7 @@ class DbConnection:
|
|||||||
await future
|
await future
|
||||||
|
|
||||||
def set_nowait(self, key: Any, value: Any, /) -> None:
|
def set_nowait(self, key: Any, value: Any, /) -> None:
|
||||||
|
"""set value and add write-to-disk request to queue."""
|
||||||
self.__mmdb[key] = value
|
self.__mmdb[key] = value
|
||||||
self.__queue.put_nowait(self.__factory.kvfactory.free(key, value))
|
self.__queue.put_nowait(self.__factory.kvfactory.free(key, value))
|
||||||
|
|
||||||
@ -448,11 +475,15 @@ intended for heavy tasks."""
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create(cls, factory: 'DbFactory', /) -> 'DbConnection':
|
async def create(cls, factory: 'DbFactory', /) -> 'DbConnection':
|
||||||
|
"""connect to the factory.
|
||||||
|
note: unstable signature."""
|
||||||
dbconnection = DbConnection(factory)
|
dbconnection = DbConnection(factory)
|
||||||
await dbconnection._initialize()
|
await dbconnection._initialize()
|
||||||
return dbconnection
|
return dbconnection
|
||||||
|
|
||||||
async def aclose(self, /) -> None:
|
async def aclose(self, /) -> None:
|
||||||
|
"""close the connection.
|
||||||
|
note: unstable signature."""
|
||||||
if not self.__task.done():
|
if not self.__task.done():
|
||||||
await self.__queue.join()
|
await self.__queue.join()
|
||||||
self.__task.cancel()
|
self.__task.cancel()
|
||||||
@ -473,6 +504,7 @@ intended for heavy tasks."""
|
|||||||
self.__not_running = True
|
self.__not_running = True
|
||||||
|
|
||||||
async def complete_transaction(self, delta: dict, /) -> None:
|
async def complete_transaction(self, delta: dict, /) -> None:
|
||||||
|
"""hybrid of set() and dict.update()."""
|
||||||
if not delta:
|
if not delta:
|
||||||
return
|
return
|
||||||
buffer = StringIO()
|
buffer = StringIO()
|
||||||
@ -482,12 +514,19 @@ intended for heavy tasks."""
|
|||||||
self.__queue.put_nowait(TransactionRequest(buffer, future=future))
|
self.__queue.put_nowait(TransactionRequest(buffer, future=future))
|
||||||
await future
|
await future
|
||||||
|
|
||||||
|
def submit_transaction(self, delta: dict, /) -> asyncio.Future | None:
|
||||||
|
"""not implemented.
|
||||||
|
low-level API."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
async def commit(self, /) -> None:
|
async def commit(self, /) -> None:
|
||||||
|
"""wait until all requests queued before are completed."""
|
||||||
future = self._create_future()
|
future = self._create_future()
|
||||||
self.__queue.put_nowait(DumpRequest(future))
|
self.__queue.put_nowait(DumpRequest(future))
|
||||||
await future
|
await future
|
||||||
|
|
||||||
def transaction(self, /) -> 'Transaction':
|
def transaction(self, /) -> 'Transaction':
|
||||||
|
"""open new transaction."""
|
||||||
return Transaction(self)
|
return Transaction(self)
|
||||||
|
|
||||||
|
|
||||||
@ -496,23 +535,28 @@ class DbFactory:
|
|||||||
'path',
|
'path',
|
||||||
'kvfactory',
|
'kvfactory',
|
||||||
'buffersize',
|
'buffersize',
|
||||||
'db',
|
'__db',
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, path: pathlib.Path, /, *, kvfactory: KVFactory, buffersize=1048576) -> None:
|
def __init__(self, path: pathlib.Path, /, *, kvfactory: KVFactory, buffersize=1048576) -> None:
|
||||||
self.path = path
|
self.path = path
|
||||||
|
"""note: unstable signature."""
|
||||||
self.kvfactory = kvfactory
|
self.kvfactory = kvfactory
|
||||||
|
"""note: unstable signature."""
|
||||||
self.buffersize = buffersize
|
self.buffersize = buffersize
|
||||||
|
"""note: unstable signature."""
|
||||||
|
|
||||||
async def __aenter__(self) -> DbConnection:
|
async def __aenter__(self) -> DbConnection:
|
||||||
self.db = await DbConnection.create(self)
|
self.__db = await DbConnection.create(self)
|
||||||
return self.db
|
return self.__db
|
||||||
|
|
||||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
await self.db.aclose()
|
await self.__db.aclose()
|
||||||
|
|
||||||
|
|
||||||
class Db(DbConnection):
|
class Db(DbConnection):
|
||||||
|
"""simplified usecase combining the factory and the connection in one class."""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
def __init__(self, path: str | pathlib.Path, /, *, kvfactory: KVFactory, buffersize=1048576):
|
def __init__(self, path: str | pathlib.Path, /, *, kvfactory: KVFactory, buffersize=1048576):
|
||||||
@ -532,6 +576,8 @@ class Db(DbConnection):
|
|||||||
|
|
||||||
|
|
||||||
class FallbackMapping:
|
class FallbackMapping:
|
||||||
|
"""note: unstable constructor signature."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'__delta',
|
'__delta',
|
||||||
'__shadow',
|
'__shadow',
|
||||||
@ -554,13 +600,28 @@ class FallbackMapping:
|
|||||||
self.__delta[key] = value
|
self.__delta[key] = value
|
||||||
|
|
||||||
async def commit(self, /) -> None:
|
async def commit(self, /) -> None:
|
||||||
|
"""bulk analog of DbConnection.set method."""
|
||||||
delta = self.__delta.copy()
|
delta = self.__delta.copy()
|
||||||
self.__shadow |= delta
|
self.__shadow |= delta
|
||||||
self.__delta.clear()
|
self.__delta.clear()
|
||||||
await self.__connection.complete_transaction(delta)
|
await self.__connection.complete_transaction(delta)
|
||||||
|
|
||||||
|
async def commit_submitted(self, /) -> None:
|
||||||
|
"""not implemented.
|
||||||
|
commit previously submitted changes."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def submit(self, /) -> None:
|
||||||
|
"""not implemented.
|
||||||
|
submit changes.
|
||||||
|
_nowait analog of commit.
|
||||||
|
bulk analog of DbConnection.set_nowait method."""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class Transaction:
|
class Transaction:
|
||||||
|
"""note: unstable signature."""
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'__connection',
|
'__connection',
|
||||||
'__delta',
|
'__delta',
|
||||||
|
Loading…
Reference in New Issue
Block a user