Skip to content

Commit 434b8ae

Browse files
authored
feat: add python client (#428)
* feat: add script to build python package * feat: build python files with git commit version * ci: add action file to build python package * ci: use buf setup action * ci: update buf gen version * ci: update buf gen file version * ci: update project license decprecated warning * chore: use variable for folders * ci: add action to publish to testpy * ci: fix packages-dir * ci: use calver for version * fix: add missing dependencies * fix: include dependencies files * docs: update readme * ci: remove test repo url * ci: remove current branch * docs: update readme * docs: change path of js readme
1 parent fba3992 commit 434b8ae

File tree

11 files changed

+365
-3
lines changed

11 files changed

+365
-3
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Build and Publish Python Package
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
8+
jobs:
9+
build-and-publish:
10+
runs-on: ubuntu-latest
11+
permissions:
12+
id-token: write
13+
contents: read
14+
defaults:
15+
run:
16+
working-directory: python
17+
18+
steps:
19+
- uses: actions/checkout@v4
20+
with:
21+
fetch-depth: 0 # Fetch all history for git hash
22+
23+
- name: Set up Python
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version: "3.11"
27+
28+
- name: Install buf
29+
uses: bufbuild/[email protected]
30+
with:
31+
version: "1.47.2"
32+
github_token: ${{ secrets.GITHUB_TOKEN }}
33+
34+
- name: Install Python dependencies
35+
run: |
36+
python -m pip install --upgrade pip
37+
pip install -e .
38+
39+
- name: Build Python package
40+
run: proton-build
41+
42+
- name: Build wheel
43+
run: proton-publish
44+
45+
- name: Publish to PyPI
46+
uses: pypa/gh-action-pypi-publish@release/v1
47+
with:
48+
password: ${{ secrets.PYPI_API_TOKEN }}
49+
packages-dir: python/dist/dist/

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ buf generate --path raystack/assets
4747
4848
Check out Compass [implementation](https://github.com/raystack/compass) for reference.
4949
50+
## Python Client
51+
52+
A pre-built Python client package is available:
53+
54+
### Installation
55+
56+
```bash
57+
pip install raystack-proton
58+
```
59+
60+
**PyPI:** https://pypi.org/project/raystack-proton/
61+
62+
### Usage
63+
64+
See the [Python README](python/README.md) for detailed usage examples.
65+
5066
## JavaScript/TypeScript Client
5167

5268
A pre-built JavaScript/TypeScript client package is available for browser and Node.js environments:
@@ -57,6 +73,12 @@ A pre-built JavaScript/TypeScript client package is available for browser and No
5773
npm install @raystack/proton
5874
```
5975

76+
**NPM:** https://npmjs.com/package/@raystack/proton
77+
78+
### Usage
79+
80+
See the [JavaScript README](js/README.md) for detailed usage examples.
81+
6082
For browser applications using TanStack Query:
6183
```bash
6284
npm install @raystack/proton @tanstack/react-query
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
TypeScript/JavaScript client library for Raystack APIs generated from Protocol Buffer definitions.
44

5+
**NPM:** https://npmjs.com/package/@raystack/proton
6+
57
## Installation
68

79
```bash

js/scripts/generate.mjs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ const __dirname = path.dirname(__filename);
1010
const outDir = path.resolve(__dirname, "../dist");
1111
const bufGenFile = path.join(__dirname, "../", "buf.gen.yaml");
1212
const packageTemplatePath = path.join(__dirname, "package.template.json");
13-
const readmeTemplatePath = path.join(__dirname, "README.template.md");
13+
const readmePath = path.join(__dirname, "../README.md");
1414
const nodeModulesBin = path.join(__dirname, "../node_modules/.bin");
1515
const raystackDir = path.join(outDir, "raystack");
1616
const packagePath = path.join(outDir, "package.json");
17-
const readmePath = path.join(outDir, "README.md");
17+
const readmeDistPath = path.join(outDir, "README.md");
1818
const protonRoot = path.join(__dirname, "../..");
1919

2020
// Parse command line arguments
@@ -73,7 +73,7 @@ async function createPackageJson(services) {
7373

7474
async function copyReadme() {
7575
try {
76-
await copyFile(readmeTemplatePath, readmePath);
76+
await copyFile(readmePath, readmeDistPath);
7777
} catch (error) {
7878
console.warn("Warning: Could not copy README:", error.message);
7979
}

python/.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
dist/
2+
3+
4+
# Python
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
*.egg-info/
9+
.eggs/
10+
python/dist/

python/README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Raystack Proton Python
2+
3+
Python client library for Raystack services, generated from Protocol Buffer definitions.
4+
5+
**PyPI:** https://pypi.org/project/raystack-proton/
6+
7+
## Installation
8+
9+
```bash
10+
pip install raystack-proton
11+
```
12+
13+
This will automatically install the required dependencies:
14+
- `connect-python>=0.5.0` - Connect RPC client
15+
- `googleapis-common-protos>=1.50.0` - Common Google API types
16+
- `grpcio>=1.50.0` - gRPC runtime
17+
- `protobuf>=4.21.0,<7.0.0` - Protocol Buffer runtime
18+
19+
## Usage
20+
21+
This library uses [Connect RPC](https://github.com/connectrpc/connect-python) for communication with Raystack services.
22+
23+
### Synchronous Client
24+
25+
```python
26+
from raystack.frontier.v1beta1 import admin_connect, admin_pb2
27+
28+
# Create client
29+
admin_client = admin_connect.AdminServiceClientSync("http://localhost:8082")
30+
31+
# Make request with authentication headers
32+
request = admin_pb2.CheckFederatedResourcePermissionRequest(
33+
subject="user:<user-id>",
34+
resource="app/organization:<org-id>",
35+
permission="get"
36+
)
37+
38+
response = admin_client.check_federated_resource_permission(
39+
request,
40+
headers={"Authorization": "<auth-token>"}
41+
)
42+
print(f"Has permission: {response.status}")
43+
```
44+
45+
### Async Client
46+
47+
```python
48+
import asyncio
49+
from raystack.frontier.v1beta1 import admin_connect, admin_pb2
50+
51+
async def check_permission():
52+
admin_client = admin_connect.AdminServiceClient("http://localhost:8082")
53+
54+
request = admin_pb2.CheckFederatedResourcePermissionRequest(
55+
subject="user:<user-id>",
56+
resource="app/organization:<org-id>",
57+
permission="get"
58+
)
59+
60+
response = await admin_client.check_federated_resource_permission(
61+
request,
62+
headers={"Authorization": "<auth-token>"}
63+
)
64+
print(f"Has permission: {response.status}")
65+
66+
# Run the async function
67+
asyncio.run(check_permission())
68+
```
69+
70+
## Development
71+
72+
### Building
73+
74+
```bash
75+
make build
76+
```
77+
78+
Or using Python:
79+
80+
```bash
81+
python3 build.py build
82+
```
83+
84+
## License
85+
86+
Apache-2.0

python/pyproject.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[build-system]
2+
requires = ["setuptools>=61.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "proton-python-dev"
7+
version = "0.0.1"
8+
description = "Development dependencies for Proton Python package"
9+
requires-python = ">=3.8"
10+
11+
dependencies = [
12+
"build>=1.0.0",
13+
"twine>=4.0.0",
14+
]
15+
16+
[project.scripts]
17+
proton-build = "scripts.build:build_cli"
18+
proton-clean = "scripts.build:clean"
19+
proton-publish = "scripts.build:publish"
20+
21+
[tool.setuptools.packages.find]
22+
where = ["."]
23+
include = ["scripts*"]

python/scripts/__init__.py

Whitespace-only changes.

python/scripts/buf.gen.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: v2
2+
plugins:
3+
- remote: buf.build/protocolbuffers/python
4+
out: python/dist
5+
- remote: buf.build/protocolbuffers/pyi
6+
out: python/dist
7+
- remote: buf.build/connectrpc/python
8+
out: python/dist

python/scripts/build.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env python3
2+
"""Build script for generating Python protobuf code."""
3+
4+
import subprocess
5+
import shutil
6+
import sys
7+
from pathlib import Path
8+
9+
10+
# Get the proton root directory
11+
ROOT_DIR = Path(__file__).parent.parent.parent
12+
PYTHON_DIR = Path(__file__).parent.parent
13+
DIST_DIR = PYTHON_DIR / "dist"
14+
15+
16+
def clean():
17+
"""Remove the dist directory."""
18+
if DIST_DIR.exists():
19+
print(f"Cleaning {DIST_DIR}")
20+
shutil.rmtree(DIST_DIR)
21+
22+
23+
def publish():
24+
"""Build wheel for publishing."""
25+
if not DIST_DIR.exists():
26+
print("Error: dist directory not found. Run 'build' first.", file=sys.stderr)
27+
sys.exit(1)
28+
29+
# Check if build package is installed
30+
check_build = subprocess.run(
31+
[sys.executable, "-m", "build", "--version"], capture_output=True
32+
)
33+
if check_build.returncode != 0:
34+
print("Error: 'build' package not installed. Install it with:")
35+
print(" pip install build")
36+
sys.exit(1)
37+
38+
print("Building wheel...")
39+
result = subprocess.run([sys.executable, "-m", "build", "--wheel"], cwd=DIST_DIR)
40+
41+
if result.returncode != 0:
42+
print("Wheel build failed!", file=sys.stderr)
43+
sys.exit(1)
44+
45+
print("Wheel built successfully!")
46+
print(f"Output: {DIST_DIR}/dist/")
47+
48+
49+
def build():
50+
"""Generate Python code from proto files."""
51+
clean()
52+
53+
cmd = [
54+
"buf",
55+
"generate",
56+
"--template",
57+
"python/scripts/buf.gen.yaml",
58+
"--include-imports",
59+
"--path",
60+
"raystack",
61+
".",
62+
]
63+
64+
print(f"Running: {' '.join(cmd)}")
65+
result = subprocess.run(cmd, cwd=ROOT_DIR)
66+
67+
if result.returncode != 0:
68+
print("Build failed!", file=sys.stderr)
69+
sys.exit(1)
70+
71+
# Create __init__.py files in all directories
72+
if DIST_DIR.exists():
73+
print("Creating __init__.py files...")
74+
for dir_path in DIST_DIR.rglob("*"):
75+
if dir_path.is_dir():
76+
init_file = dir_path / "__init__.py"
77+
if not init_file.exists():
78+
init_file.touch()
79+
80+
# Copy pyproject.toml template to dist
81+
template_file = Path(__file__).parent / "pyproject.template.toml"
82+
if template_file.exists():
83+
print("Copying pyproject.toml to dist...")
84+
content = template_file.read_text()
85+
86+
# Generate CalVer format: YYYY.MM.DD.HHMMSS
87+
from datetime import datetime
88+
89+
now = datetime.utcnow()
90+
version_str = now.strftime("%Y.%m.%d.%H%M%S")
91+
content = content.replace('version = "0.1.0"', f'version = "{version_str}"')
92+
print(f"Set version to: {version_str}")
93+
(DIST_DIR / "pyproject.toml").write_text(content)
94+
95+
# Copy README to dist
96+
readme_file = PYTHON_DIR / "README.md"
97+
if readme_file.exists():
98+
print("Copying README.md to dist...")
99+
shutil.copy(readme_file, DIST_DIR / "README.md")
100+
101+
# Copy LICENSE to dist
102+
license_file = ROOT_DIR / "LICENSE"
103+
if license_file.exists():
104+
print("Copying LICENSE to dist...")
105+
shutil.copy(license_file, DIST_DIR / "LICENSE")
106+
107+
print("Build successful!")
108+
109+
110+
def build_cli():
111+
"""CLI entry point for build command."""
112+
build()
113+
114+
115+
def main():
116+
import argparse
117+
118+
parser = argparse.ArgumentParser(description="Build Python protobuf package")
119+
parser.add_argument(
120+
"command", choices=["clean", "build", "publish"], help="Command to run"
121+
)
122+
123+
args = parser.parse_args()
124+
125+
if args.command == "clean":
126+
clean()
127+
elif args.command == "build":
128+
build()
129+
elif args.command == "publish":
130+
publish()
131+
132+
133+
if __name__ == "__main__":
134+
main()

0 commit comments

Comments
 (0)