Skip to content

Commit

Permalink
Merge pull request #7 from ceramicstudio/olas/wrapper
Browse files Browse the repository at this point in the history
Olas/wrapper
  • Loading branch information
mzkrasner authored Oct 8, 2024
2 parents d295e7b + a9ee5cd commit dc31eb9
Show file tree
Hide file tree
Showing 21 changed files with 344 additions and 255 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- dev
- main
paths:
- "ceramic-client/**"
- "ceramicsdk/**"
workflow_dispatch:
jobs:
publish-to-pypi:
Expand Down
3 changes: 0 additions & 3 deletions ceramic_client/.gitignore

This file was deleted.

6 changes: 0 additions & 6 deletions ceramic_client/ceramic_python/__init__.py

This file was deleted.

5 changes: 5 additions & 0 deletions ceramicsdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
venv
__pycache__
.DS_Store
dist
build
8 changes: 3 additions & 5 deletions ceramic_client/README.md → ceramicsdk/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# Ceramic Python client
# Ceramic and OrbisDB Python Client

This Ceramic client implements the payload building, encoding, and signing needed to interact with the [Ceramic Network](https://ceramic.network/). It currently supports `ModelInstanceDocument`.
These Orbis and Ceramic clients implements the payload building, encoding, and signing needed to interact with the [Ceramic Network](https://ceramic.network/). It currently supports `ModelInstanceDocument`.

## Features

- Implements payload building, encoding, and signing for Ceramic interactions
- Currently supports `ModelInstanceDocument`

## Working with Ceramic streams

### Install the Ceramic client using pip
## Install the Ceramic client using pip

```bash
pip3 install ceramic_python
Expand Down
2 changes: 2 additions & 0 deletions ceramicsdk/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .orbis_python.orbis_db import OrbisDB
from .ceramic_python.ceramic_client import CeramicClient
3 changes: 3 additions & 0 deletions ceramicsdk/ceramic_python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .ceramic_client import CeramicClient
from .did import DID
from .model_instance_document import ModelInstanceDocument, ModelInstanceDocumentMetadata, ModelInstanceDocumentMetadataArgs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def create_stream_from_genesis(
"opts": opts,
}
try:
response = requests.post(f"{self.url}/api/v0/streams", json=payload, timeout=2)
response = requests.post(f"{self.url}/api/v0/streams", json=payload, timeout=5)
logging.debug(f"Request URL: {f'{self.url}/api/v0/streams'}")
logging.debug(f"Request Data: {payload}")
logging.debug(f"Response Status Code: {response.status_code}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import json
from base64 import urlsafe_b64encode, b64encode, b64decode
import hashlib

from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519
import os
from key_did_provider_ed25519.src.key_did_provider_ed25519.utils import encode_did
from .helper import sign_ed25519
from multiformats import CID

Expand Down Expand Up @@ -48,15 +51,40 @@ def decode_linked_block(linked_block: str) -> dict:
return dag_cbor.decode(encoded_bytes)

class DID:
def __init__(self, id: str, private_key: str):
self.id = id
self.private_key = private_key

def __init__(self, private_key = None) -> None:
self._private_key = private_key or os.urandom(32).hex()
self.ed25519_private_key = ed25519.Ed25519PrivateKey.from_private_bytes(bytes.fromhex(self._private_key))
self.ed25519_public_key = self.ed25519_private_key.public_key()
self._public_key = encode_did(self.ed25519_public_key.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
))
self._id = self._public_key # Store the ID in a private attribute,

@property
def private_key(self):
return self._private_key

@property
def public_key(self):
return self._public_key

@property
def id(self):
return self._id

@property
def did(self):
return {
"id": self.id,
"private_key": self._private_key
}

def as_controller(self):
return self.id

def create_dag_jws(self, payload: dict) -> dict:

encoded_bytes = dag_cbor.encode(data=payload)
linked_block = b64encode(encoded_bytes).decode("utf-8")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# ceramic/helper.py

import hashlib
import os
from datetime import datetime, timezone, UTC
from datetime import datetime, timezone
from multiformats.multibase import base36
from base64 import urlsafe_b64encode, b64encode, b64decode
from base64 import urlsafe_b64encode
from jwcrypto import jwk, jws
from jwcrypto.common import json_encode, base64url_encode, base64url_decode
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
Expand Down
File renamed without changes.
29 changes: 29 additions & 0 deletions ceramicsdk/example-orbis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from orbis_python.orbis_db import OrbisDB
import os

CONTEXT_ID = os.getenv("CONTEXT_ID")

# Setup a table stream and a private key for the DID
table_stream = "kjzl6hvfrbw6c6adsnzvbyr6itmf0igfy25xu0mqzei2pe2xw1hlusqyuknb9ky"
did_pkey = os.urandom(32).hex()

# Instantiate a read-only db
db = OrbisDB.from_stream(table_stream)

# Read the whole db
print(db.read())

# Instantiate a read and write db
db = OrbisDB(context_stream=CONTEXT_ID, table_stream=table_stream, controller_private_key=did_pkey)

# Add a new row
db.add_row({"user_id": 2, "user_name": "test_user_3", "user_points": 1000})

# Select some rows
print(db.filter({"user_points": 1000}))

# Update a row batch
db.update_rows(filters={"user_name": "test_user_3"}, new_content={"user_points": 2000})

# Dump the db to a local json file
db.dump()
1 change: 1 addition & 0 deletions ceramicsdk/orbis_python/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .orbis_db import OrbisDB
148 changes: 148 additions & 0 deletions ceramicsdk/orbis_python/orbis_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
from ceramicsdk.ceramic_python.did import DID
from ceramicsdk.ceramic_python.ceramic_client import CeramicClient
from ceramicsdk.ceramic_python.model_instance_document import ModelInstanceDocument, ModelInstanceDocumentMetadataArgs
import requests
from typing import Optional
from pathlib import Path
import json
from ceramicsdk.ceramic_python.model_instance_document import ModelInstanceDocument, ModelInstanceDocumentMetadataArgs

class OrbisDB:
"""A relational database stored on OrbisDB/Ceramic"""

def __init__(
self,
c_endpoint: str,
o_endpoint: str,
context_stream: Optional[str] = None,
table_stream: Optional[str] = None,
controller_private_key: Optional[str] = None
) -> None:

if not table_stream and not controller_private_key:
raise ValueError("Either the table stream or the controller needs to be specified when instantiating an OrbisDB class")
self.o_endpoint = o_endpoint
self.context_stream = context_stream
self.table_stream = table_stream
self.controller = DID(private_key=controller_private_key)
self.ceramic_client = CeramicClient(c_endpoint, self.controller if self.controller else "")


@classmethod
def from_stream(cls, table_stream: Optional[str] = None):
"""Load a read-only db from a stream"""
return cls(
context_stream=None,
table_stream=table_stream,
controller_private_key=None
)


def read(self, env_id: str):
"""Read the db from Ceramic"""
if not self.table_stream:
raise ValueError("OrbisDB table stream has not being specified. Cannot read the database.")
return self.query(env_id, f"SELECT * FROM {self.table_stream}")


def dump(self, file_path: Path = Path("orbis_db.json")):
"""Dump to json"""
table = self.read()
with open(file_path, "w", encoding="utf-8") as file:
json.dump(table, file, indent=4)


def add_row(self, entry_data):
"""Add a new row to the table"""

if not self.controller:
raise ValueError("Read-only database. OrbisDB controller has not being specified. Cannot write to the database.")

metadata_args = ModelInstanceDocumentMetadataArgs(
controller=self.controller.public_key,
model=self.table_stream,
context=self.context_stream
)

doc = ModelInstanceDocument.create(self.ceramic_client, entry_data, metadata_args)
return doc.stream_id


def update_rows(self, filters, new_content: dict):
"""Update rows"""

if not self.controller:
raise ValueError("Read-only database. OrbisDB controller has not being specified. Cannot write to the database.")

document_ids = [row["stream_id"] for row in self.filter(filters)]

metadata_args = ModelInstanceDocumentMetadataArgs(
controller=self.controller.public_key,
model=self.table_stream,
context=self.context_stream,
)

for document_id in document_ids:
patch = []

modelInstance = ModelInstanceDocument.load(self.ceramic_client, stream_id=document_id)
new_doc = modelInstance.content.copy()

for key, value in new_content.items():
new_doc[key] = value
patch.append({
"op": "replace",
"path": f"/{key}",
"value": value
})

modelInstance.patch(json_patch=patch, metadata_args=metadata_args, opts={'anchor': True, 'publish': True, 'sync': 0})

return len(document_ids)


def query(self, env_id: str, query: str):
"""Query the database
Example: SELECT * FROM {TABLE_ID}
"""

body = {
"jsonQuery": {
"$raw": {
"query": query,
"params": []
}
},
"env": env_id
}
headers = {
"Content-Type": "application/json"
}
response = requests.post(url=self.o_endpoint, headers=headers, json=body)
return response.json()["data"]


def filter(self, env_id: str, filters):
"""Filter"""

filter_list = [f'{key} = {f"'{value}'" if isinstance(value, str) else value}' for key, value in filters.items()] # strings need to be wrapped around single quotes
joined_filters = " AND ".join(filter_list)
query = f"SELECT * FROM {self.table_stream} WHERE {joined_filters}"

body = {
"jsonQuery": {
"$raw": {
"query": query,
"params": []
}
},
"env": env_id
}
headers = {
"Content-Type": "application/json"
}
response = requests.post(url=self.o_endpoint, headers=headers, json=body)
return response.json().get("data", [])


Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,8 @@ cryptography==41.0.1
jwcrypto==1.5.0
multiformats==0.3.1
dag-cbor==0.3.2
base58==2.1.1
base58==2.1.1
web3==7.2.0
cbor2==5.6.4
bip44==0.1.4

19 changes: 11 additions & 8 deletions ceramic_client/setup.py → ceramicsdk/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@
long_description = fh.read()

setup(
name="ceramic_python",
version="0.1.7",
author='Index',
author_email='[email protected]',
description="This Ceramic client implements the payload building, encoding, and signing needed to interact with the Ceramic Network. It currently supports ModelInstanceDocument.",
name="ceramicsdk",
version="0.1.0",
author='Ceramic Ecosystem Developers',
description="This Ceramic client implements the payload building, encoding, and signing needed to interact with the Ceramic Network. It currently supports ModelInstanceDocument and OrbisDB.",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/indexnetwork/ceramic-python/tree/main/ceramic-client",
packages=find_packages(),
url="https://github.com/ceramicstudio/orbis-python-starter/tree/main/py_lib",
packages=find_packages(include=['ceramicsdk', 'ceramicsdk.*']),
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
Expand All @@ -24,7 +23,7 @@
],
python_requires=">=3.7",
install_requires=[
"requests==2.32.2",
"requests==2.31.0",
"python-dateutil==2.8.2",
"pytz==2023.3",
"jsonpatch==1.33",
Expand All @@ -33,5 +32,9 @@
"multiformats==0.3.1",
"dag-cbor==0.3.2",
"base58==2.1.1",
"web3==7.2.0",
"cbor2==5.6.4",
"bip44==0.1.4",
"varint",
],
)
Loading

0 comments on commit dc31eb9

Please sign in to comment.