Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Packaging python client #6

Merged
merged 1 commit into from
Nov 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ on:
- build.rs
- Cargo.toml
- Cargo.lock
- Dockerfile
- .dockerignore
push:
tags:
- '*'
Expand Down
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