Skip to content

Commit

Permalink
packaging python client
Browse files Browse the repository at this point in the history
  • Loading branch information
saint1991 committed Nov 4, 2024
1 parent 73410e3 commit b08884e
Show file tree
Hide file tree
Showing 16 changed files with 375 additions and 118 deletions.
28 changes: 28 additions & 0 deletions .github/workflows/python-client.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: build-python-client

on:
pull_request:
paths:
- .github/workflows/lint-python.yml
- client/**.py

env:
CARGO_TERM_COLOR: always

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: astral-sh/setup-uv@v3
with:
version: "0.4.29"
- name: Install dependencies
run: uv sync
working-directory: ./client
- name: Build wheel file
run: ./build.sh
working-directory: ./client
6 changes: 5 additions & 1 deletion client/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
gen
gduck/proto
build
dist
*.egg-info
__pycache__
.venv
.mypy_cache
23 changes: 0 additions & 23 deletions client/Dockerfile

This file was deleted.

14 changes: 13 additions & 1 deletion client/README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,14 @@
## Python client
# gduck Python client

Client implementation to utilize remove gduck server.

## Usage

```python
from gduck import Connection

conn = Connection("localhost:50051")

with conn.transaction(database_file="database.duckdb", mode="read_write") as trans:
counts = trans.query_value("SELECT COUNT(*) FROM videos WHERE comments > ?", 10)
```
5 changes: 5 additions & 0 deletions client/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/bin/bash -eu

./codegen.sh

uv build --wheel
24 changes: 22 additions & 2 deletions client/codegen.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,26 @@
#!/bin/bash -eu

SCRIPT_DIR=$(cd $(dirname $0); pwd)
PARENT_DIR=$(dirname $SCRIPT_DIR)
PROJECT_DIR=$(dirname $SCRIPT_DIR)
PROTO_DIR="${PROJECT_DIR}/proto"
GEN_DIR="${SCRIPT_DIR}/gduck/proto"

docker build -f ${SCRIPT_DIR}/Dockerfile --output=type=local,dest=$SCRIPT_DIR/gen $PARENT_DIR
cleanup() {
rm -rf ${GEN_DIR}
mkdir -p ${GEN_DIR}
}

codegen() {
uv run python -m grpc_tools.protoc \
-I ${PROTO_DIR} \
--python_out=${GEN_DIR} \
--grpc_python_out=${GEN_DIR} \
${PROTO_DIR}/*.proto
}

if [ "${1:-}" = "--clean" ]; then
cleanup
else
cleanup
codegen
fi
10 changes: 10 additions & 0 deletions client/gduck/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# flake8: noqa: F401,F402,F403
import sys
from pathlib import Path

PROTO_PATH = (Path(__file__).parent / "proto").absolute()
sys.path.append(str(PROTO_PATH))

from .client import *
from .request import *
from .types import *
14 changes: 8 additions & 6 deletions client/gduck/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
from pathlib import Path
from queue import SimpleQueue
from types import TracebackType
from typing import Self
from typing import Generator, Self

import grpc
from error_pb2 import Error
from grpc._channel import _MultiThreadedRendezvous
from query_pb2 import Query
from service_pb2 import Request, Response
from service_pb2_grpc import DbServiceStub

from .proto.error_pb2 import Error
from .proto.query_pb2 import Query
from .proto.service_pb2 import Request, Response
from .proto.service_pb2_grpc import DbServiceStub
from .request import ConnectionMode, Value, connect, ctas, execute, local_file, parquet, request, rows, value
from .response import parse_location, parse_rows, parse_value

__all__ = ["Addr", "Connection", "DuckDbTransaction"]


@dataclass(frozen=True)
class Addr:
Expand Down Expand Up @@ -75,7 +77,7 @@ def __init__(self, addr: Addr, database_file: str, mode: ConnectionMode) -> None
self._results = SimpleQueue()

# This block executed in other thread
def _request_generator(self):
def _request_generator(self) -> Generator[Request, None, None]:
yield self._connect_request()

while (request := self._requests.get()) != self._END_STREAM:
Expand Down
12 changes: 6 additions & 6 deletions client/gduck/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@
from pathlib import Path
from typing import Literal

from database_pb2 import Connect, Date
from database_pb2 import Decimal as ProtoDecimal
from database_pb2 import Interval, Params, ScalarValue, Time
from dateutil.relativedelta import relativedelta
from google.protobuf.struct_pb2 import NULL_VALUE
from google.protobuf.timestamp_pb2 import Timestamp
from location_pb2 import Location
from query_pb2 import Query
from service_pb2 import Request

from .proto.database_pb2 import Connect, Date
from .proto.database_pb2 import Decimal as ProtoDecimal
from .proto.database_pb2 import Interval, Params, ScalarValue, Time
from .proto.location_pb2 import Location
from .proto.query_pb2 import Query
from .proto.service_pb2 import Request
from .types import Value

__all__ = ["ConnectionMode", "connect", "local_file", "execute", "value", "rows", "ctas", "parquet", "request"]
Expand Down
4 changes: 2 additions & 2 deletions client/gduck/response.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
from pathlib import Path
from typing import Callable

from database_pb2 import DataType, Rows, ScalarValue
from dateutil.relativedelta import relativedelta
from location_pb2 import Location

from .proto.database_pb2 import DataType, Rows, ScalarValue
from .proto.location_pb2 import Location
from .types import ParquetLocation, Schema, Value

__all__ = ["parse_value", "parse_rows", "parse_location"]
Expand Down
9 changes: 6 additions & 3 deletions client/gduck/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
from pathlib import Path
from typing import Iterator, Self, TypeAlias

from database_pb2 import Column as ProtoColumn
from database_pb2 import DataType
from database_pb2 import Schema as ProtoSchema
from dateutil.relativedelta import relativedelta

from .proto.database_pb2 import Column as ProtoColumn
from .proto.database_pb2 import DataType
from .proto.database_pb2 import Schema as ProtoSchema

__all__ = ["Value", "Column", "Schema", "Rows", "ParquetLocation"]

Value: TypeAlias = bool | int | float | Decimal | str | datetime | date | time | relativedelta | None


Expand Down
7 changes: 0 additions & 7 deletions client/main.py

This file was deleted.

8 changes: 4 additions & 4 deletions client/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[project]
name = "client"
name = "gduck-client"
version = "0.1.0"
requires-python = ">=3.13"
requires-python = ">=3.10"
dependencies = [
"grpcio-tools>=1.67.0",
"grpcio>=1.67.0",
"grpcio-tools>=1.67.1",
"grpcio>=1.67.1",
"python-dateutil>=2.9.0.post0",
]

Expand Down
24 changes: 24 additions & 0 deletions client/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Generator

import pytest
from gduck.client import Addr, Connection
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for

IMAGE_NAME = "gduck:latest"
CONTAINER_PORT = 50051


@pytest.fixture(scope="function")
def gduck_container(image_name: str = IMAGE_NAME) -> Generator[DockerContainer, None, None]:
with DockerContainer(image_name).with_exposed_ports(CONTAINER_PORT) as dc:
wait_for(lambda: dc.get_wrapped_container() is not None and dc.get_wrapped_container().health == "healthy")
print("container becomes healthy")
yield dc


@pytest.fixture(scope="function")
def gduck_connection(gduck_container: DockerContainer) -> Connection:
host = gduck_container.get_container_host_ip()
port = gduck_container.get_exposed_port(CONTAINER_PORT)
return Connection(addr=Addr(host=host, port=port))
7 changes: 7 additions & 0 deletions client/tests/gduck/test_gduck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from gduck.client import Connection


def test_query_value(gduck_connection: Connection) -> None:
with gduck_connection.transaction(":memory:", "read_write") as trans:
result = trans.query_value("SELECT 1;")
assert result == 1
Loading

0 comments on commit b08884e

Please sign in to comment.