ouro-py 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ouro/__init__.py ADDED
@@ -0,0 +1,89 @@
1
+ VERSION = "0.0.1"
2
+
3
+
4
+ import logging
5
+ import os
6
+
7
+
8
+ import requests
9
+ from dotenv import load_dotenv
10
+ from supabase.client import ClientOptions
11
+
12
+ from supabase import Client, create_client
13
+ from ouro.air import Air
14
+ from ouro.earth import Earth
15
+
16
+ logger = logging.getLogger(__name__)
17
+ logger.setLevel(logging.INFO)
18
+ console_handler = logging.StreamHandler()
19
+ log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
20
+ formatter = logging.Formatter(log_format)
21
+ console_handler.setFormatter(formatter)
22
+ logger.addHandler(console_handler)
23
+
24
+ postgres_logger = logging.getLogger("httpx")
25
+ postgres_logger.setLevel(logging.WARNING)
26
+
27
+
28
+ load_dotenv()
29
+
30
+
31
+ class Ouro:
32
+ def __init__(self):
33
+ self.client = None
34
+ self.public_client = None
35
+ self.user = None
36
+ self.token = None
37
+
38
+ self.earth = None
39
+ self.water = None
40
+ self.air = None
41
+ self.fire = None
42
+
43
+ # Class Instances
44
+ # self.MakeAirPost = MakeAirPost
45
+
46
+ def login(self, api_key: str):
47
+ url: str = os.environ.get("SUPABASE_URL")
48
+ key: str = os.environ.get("SUPABASE_ANON_KEY")
49
+
50
+ if not api_key:
51
+ raise Exception("No API key found")
52
+
53
+ # Send a request to Ouro Backend to get an access token
54
+ req = requests.post(
55
+ f"{os.environ.get('OURO_BACKEND_URL')}/users/get-token",
56
+ json={"pat": api_key},
57
+ )
58
+ json = req.json()
59
+ self.token = json["token"]
60
+ self.client: Client = create_client(
61
+ url,
62
+ key,
63
+ options=ClientOptions(
64
+ schema="datasets",
65
+ auto_refresh_token=True,
66
+ persist_session=False,
67
+ ),
68
+ )
69
+ self.public_client: Client = create_client(
70
+ url,
71
+ key,
72
+ options=ClientOptions(
73
+ auto_refresh_token=True,
74
+ persist_session=False,
75
+ ),
76
+ )
77
+
78
+ if not self.token:
79
+ raise Exception("No user found for this API key")
80
+
81
+ self.client.postgrest.auth(self.token)
82
+ self.public_client.postgrest.auth(self.token)
83
+
84
+ self.user = self.client.auth.get_user(self.token).user
85
+ print(f"Successfully logged in as {self.user.email}.")
86
+
87
+ # Instanciate classes
88
+ self.earth = Earth(config=self)
89
+ self.air = Air(config=self)
ouro/air/__init__.py ADDED
@@ -0,0 +1,182 @@
1
+
2
+
3
+
4
+ from supabase import Client
5
+ import time
6
+ import logging
7
+ import json
8
+ import os
9
+ import requests
10
+ import pandas as pd
11
+
12
+ logger = logging.getLogger(__name__)
13
+ logger.setLevel(logging.INFO)
14
+ console_handler = logging.StreamHandler()
15
+ log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
16
+ formatter = logging.Formatter(log_format)
17
+ console_handler.setFormatter(formatter)
18
+ logger.addHandler(console_handler)
19
+
20
+
21
+ class Post:
22
+ """Create a new Air post. Formats the data to be viewed with the AirViewer.
23
+
24
+ Inspired by https://github.com/didix21/mdutils
25
+ """
26
+
27
+ def __init__(self, data: dict = {}, content: dict = {}):
28
+ self.data = data
29
+ self.content = {
30
+ "type": "doc",
31
+ "content": [],
32
+ }
33
+
34
+ def new_header(self, level: int, title: str):
35
+ element = {
36
+ "type": "heading",
37
+ "attrs": {"level": level},
38
+ "content": [{"text": title, "type": "text"}],
39
+ }
40
+ self.content["content"].append(element)
41
+
42
+ def new_paragraph(self, text: str):
43
+ element = {
44
+ "type": "paragraph",
45
+ "content": [{"text": text, "type": "text"}],
46
+ }
47
+ self.content["content"].append(element)
48
+
49
+ def new_line(self):
50
+ element = {
51
+ "type": "paragraph",
52
+ "content": [{"text": "", "type": "text"}],
53
+ }
54
+ self.content["content"].append(element)
55
+
56
+ def new_code_block(self, code: str, language: str = None):
57
+ element = {
58
+ "type": "codeBlock",
59
+ "attrs": {"language": language},
60
+ "content": [{"text": code, "type": "text"}],
61
+ }
62
+ self.content["content"].append(element)
63
+
64
+ def new_table(self, data: pd.DataFrame):
65
+ element = {
66
+ "type": "table",
67
+ "content": [],
68
+ }
69
+
70
+ # Generate the header row
71
+ header_row = {
72
+ "type": "tableRow",
73
+ "content": list(
74
+ map(
75
+ (
76
+ lambda x: {
77
+ "type": "tableHeader",
78
+ "attrs": {"colspan": 1, "rowspan": 1, "colwidth": None},
79
+ "content": [
80
+ {
81
+ "type": "paragraph",
82
+ "content": [{"text": str(x), "type": "text"}],
83
+ }
84
+ ],
85
+ }
86
+ ),
87
+ data.columns,
88
+ )
89
+ ),
90
+ }
91
+ # Generate the rows
92
+ rows = list(
93
+ map(
94
+ (
95
+ lambda x: {
96
+ "type": "tableRow",
97
+ "content": list(
98
+ map(
99
+ (
100
+ lambda y: {
101
+ "type": "tableCell",
102
+ "attrs": {
103
+ "colspan": 1,
104
+ "rowspan": 1,
105
+ "colwidth": None,
106
+ },
107
+ "content": [
108
+ {
109
+ "type": "paragraph",
110
+ "content": [
111
+ {
112
+ "text": str(y),
113
+ "type": "text",
114
+ }
115
+ ],
116
+ }
117
+ ],
118
+ }
119
+ ),
120
+ x[1].values,
121
+ )
122
+ ),
123
+ }
124
+ ),
125
+ data.iterrows(),
126
+ )
127
+ )
128
+ # Add the header row and rows to the table
129
+ element["content"] = [header_row, *rows]
130
+
131
+ self.content["content"].append(element)
132
+
133
+ def new_inline_image(self, src: str, alt: str):
134
+ element = {
135
+ "type": "image",
136
+ "attrs": {"src": src, "alt": alt},
137
+ }
138
+ self.content["content"].append(element)
139
+
140
+ def new_inline_asset(
141
+ self,
142
+ id: str,
143
+ asset_type: str,
144
+ filters: dict = None,
145
+ view_mode: str = "default",
146
+ ):
147
+ element = {
148
+ # "type": "paragraph",
149
+ # "content": [
150
+ # {
151
+ "type": "assetComponent",
152
+ "attrs": {
153
+ "id": id,
154
+ "assetType": asset_type,
155
+ "filters": filters,
156
+ "viewMode": view_mode,
157
+ },
158
+ # }
159
+ # ],
160
+ }
161
+ self.content["content"].append(element)
162
+
163
+
164
+ class Air:
165
+ def __init__(self, config):
166
+ self.config = config
167
+ self.Post = Post
168
+
169
+ def create_post(self, post: Post):
170
+ request = requests.post(f"{os.environ.get('OURO_BACKEND_URL')}/elements/air/create",
171
+ headers={
172
+ "Authorization": f"{self.config.token}",
173
+ "Content-Type": "application/json",
174
+ },
175
+ json={
176
+ "content": {"json": post.content, "text": ""},
177
+ "post": post.data
178
+ },
179
+ )
180
+ request.raise_for_status()
181
+
182
+ return request.json()
ouro/earth/__init__.py ADDED
@@ -0,0 +1,161 @@
1
+ from supabase import Client
2
+ import time
3
+ import logging
4
+ import httpx
5
+ import os
6
+ import pandas as pd
7
+ import numpy as np
8
+
9
+
10
+ logger = logging.getLogger(__name__)
11
+ logger.setLevel(logging.INFO)
12
+ # console_handler = logging.StreamHandler()
13
+ log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
14
+ formatter = logging.Formatter(log_format)
15
+ # console_handler.setFormatter(formatter)
16
+ # logger.addHandler(console_handler)
17
+
18
+
19
+ class Earth:
20
+ def __init__(self, config):
21
+ self.config = config
22
+
23
+ def create_dataset(self, dataset: dict, data: pd.DataFrame):
24
+ df = data.copy()
25
+ # Get a sql safe table name from the name
26
+ table_name = dataset["name"].replace(" ", "_").lower()
27
+
28
+ # Reset the index if it exists to use as the primary key
29
+ index_name = df.index.name
30
+ if index_name:
31
+ df.reset_index(inplace=True)
32
+
33
+ create_table_sql = pd.io.sql.get_schema(
34
+ df, name=table_name, schema="datasets", keys=index_name
35
+ )
36
+
37
+ create_table_sql = create_table_sql.replace(
38
+ "TIMESTAMP", "TIMESTAMP WITH TIME ZONE"
39
+ )
40
+ create_table_sql = create_table_sql.replace(
41
+ "CREATE TABLE", "CREATE TABLE IF NOT EXISTS"
42
+ )
43
+
44
+ logger.info(f"{create_table_sql}")
45
+
46
+ request = httpx.post(
47
+ f"{os.environ.get('OURO_BACKEND_URL')}/elements/earth/create/from-schema",
48
+ headers={
49
+ "Authorization": f"{self.config.token}",
50
+ "Content-Type": "application/json",
51
+ },
52
+ json={"dataset": {**dataset, "schema": create_table_sql}},
53
+ )
54
+ request.raise_for_status()
55
+ response = request.json()
56
+
57
+ logger.info(response)
58
+
59
+ if response["error"]:
60
+ logger.error(response["error"])
61
+ raise Exception(response["error"])
62
+
63
+ # If we've created the dataset, we can now insert the data
64
+ # if response["data"] and not response["error"]:
65
+ created = response["data"]
66
+ table_name = created["metadata"]["table_name"]
67
+
68
+ # Format the DataFrame to be inserted
69
+ # Manually format any dates to be ISO 8601
70
+
71
+ # insert_data = data.to_json(orient="records")
72
+ # insert_data = pd.read_json(insert_data)
73
+
74
+ for column in df.columns:
75
+ if df[column].dtype == "datetime64[ns]":
76
+ df[column] = df[column].dt.strftime("%Y-%m-%d")
77
+
78
+ # Fill NaN values with None
79
+ df = df.where(pd.notnull(df), None)
80
+ df = df.map(lambda x: None if pd.isna(x) or x == "" else x)
81
+
82
+ insert_data = df.to_dict(orient="records")
83
+
84
+ # Ensure that we're not inserting any NaN values by converting them to None
85
+ insert_data = [
86
+ {k: v if not pd.isna(v) else None for k, v in row.items()}
87
+ for row in insert_data
88
+ ]
89
+
90
+ insert = self.config.client.table(table_name).insert(insert_data).execute()
91
+ if len(insert.data) > 0:
92
+ logger.info(f"Inserted {len(insert.data)} rows into {table_name}")
93
+
94
+ def get_dataset(self, dataset_id: str):
95
+ dataset = (
96
+ self.public_client.table("datasets")
97
+ .select("*")
98
+ .eq("id", dataset_id)
99
+ .limit(1)
100
+ .single()
101
+ .execute()
102
+ ).data
103
+ return dataset
104
+
105
+ def get_dataset_from_name(self, name: str):
106
+ dataset = (
107
+ self.public_client.table("datasets")
108
+ .select("*")
109
+ .eq("name", name)
110
+ .limit(1)
111
+ .single()
112
+ .execute()
113
+ ).data
114
+ return dataset
115
+
116
+ def get_dataset_schema(self, dataset_id: str):
117
+ dataset = (
118
+ self.public_client.table("datasets")
119
+ .select("*")
120
+ .eq("id", dataset_id)
121
+ .limit(1)
122
+ .single()
123
+ .execute()
124
+ ).data
125
+ # Get the schema with an RPC call to the database
126
+ schema = (
127
+ self.public_client.rpc(
128
+ "get_table_schema",
129
+ {"table_schema_name": "datasets", "table_name": dataset["table_name"]},
130
+ ).execute()
131
+ ).data
132
+ return schema
133
+
134
+ def load_dataset(self, table_name: str, schema: str = "datasets"):
135
+ start = time.time()
136
+
137
+ row_count = self.client.table(table_name).select("*", count="exact").execute()
138
+ row_count = row_count.count
139
+
140
+ logger.info(f"Loading {row_count} rows from {schema}.{table_name}...")
141
+ # Batch load the data if it's too big
142
+ if row_count > 1_000_000:
143
+ data = []
144
+ for i in range(0, row_count, 1_000_000):
145
+ logger.debug(f"Loading rows {i} to {i+1_000_000}")
146
+ res = (
147
+ self.client.table(table_name)
148
+ .select("*")
149
+ .range(i, i + 1_000_000)
150
+ .execute()
151
+ )
152
+ data.extend(res.data)
153
+ else:
154
+ res = self.client.table(table_name).select("*").limit(1_000_000).execute()
155
+ data = res.data
156
+
157
+ end = time.time()
158
+ logger.info(f"Finished loading data in {round(end - start, 2)} seconds.")
159
+
160
+ self.data = data
161
+ return data
ouro/ouro.py ADDED
File without changes
ouro/utils/__init__.py ADDED
@@ -0,0 +1,37 @@
1
+
2
+ def ouro_field(key, value):
3
+ def decorator(func):
4
+ if not hasattr(func, "ouro_fields"):
5
+ func.ouro_fields = {}
6
+ func.ouro_fields[key] = value
7
+ return func
8
+ return decorator
9
+
10
+ def get_custom_openapi(app, get_openapi):
11
+ def custom_openapi():
12
+ if app.openapi_schema:
13
+ return app.openapi_schema
14
+
15
+ openapi_schema = get_openapi(
16
+ title=app.title,
17
+ version=app.version,
18
+ summary=app.summary,
19
+ description=app.description,
20
+ routes=app.routes,
21
+ )
22
+
23
+ for path, path_item in openapi_schema["paths"].items():
24
+ for method, operation in path_item.items():
25
+ endpoint = app.routes[0]
26
+ for route in app.routes:
27
+ if route.path == path and method.upper() in route.methods:
28
+ endpoint = route.endpoint
29
+ break
30
+
31
+ if hasattr(endpoint, "ouro_fields"):
32
+ operation.update(endpoint.ouro_fields)
33
+
34
+ app.openapi_schema = openapi_schema
35
+ return app.openapi_schema
36
+
37
+ return custom_openapi
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.3
2
+ Name: ouro-py
3
+ Version: 0.0.1
4
+ Summary: Python wrapper for the Ouro API
5
+ Project-URL: Homepage, https://github.com/ourofoundation/ouro-py
6
+ Project-URL: PyPI, https://pypi.org/project/ouro-py
7
+ Author-email: Matt Moderwell <matt@ouro.foundation>
8
+ License-Expression: MIT
9
+ Keywords: ouro
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.7
14
+ Requires-Dist: httpx
15
+ Requires-Dist: numpy
16
+ Requires-Dist: pandas
17
+ Requires-Dist: postgrest
18
+ Requires-Dist: python-dotenv
19
+ Requires-Dist: supabase
20
+ Description-Content-Type: text/markdown
21
+
22
+ # `ouro-py`
23
+
24
+ Python client for [Ouro](https://ouro.foundation)
25
+
26
+ - Documentation: [ouro.foundation/docs](https://ouro.foundation/docs)
27
+
28
+ ## Usage
29
+
30
+ Generate an API key from your account settings by going to [ouro.foundation/app/settings/api-keys](https://ouro.foundation/app/settings/api-keys).
31
+
32
+ Set your Ouro environment variables in a dotenv file, or using the shell:
33
+
34
+ ```bash
35
+ export USER_API_KEY="your_ouro_api_key"
36
+ ```
37
+
38
+ Init client:
39
+
40
+ ```python
41
+ import os
42
+ from ouro import Ouro
43
+
44
+ api_key = os.environ.get("USER_API_KEY")
45
+ ouro = Ouro()
46
+ ouro.login(api_key)
47
+ ```
48
+
49
+ Use the client to interface with the Ouro framework.
50
+
51
+ ### Create a dataset
52
+
53
+ ```python
54
+ data = pd.DataFrame([
55
+ {"name": "Bob", "age": 30},
56
+ {"name": "Alice", "age": 27},
57
+ {"name": "Matt", "age": 26},
58
+
59
+ ])
60
+
61
+ dataset = ouro.earth.create_dataset({
62
+ "name": "unique_dataset_name",
63
+ "visibility": "private",
64
+ },
65
+ data
66
+ )
67
+ ```
68
+
69
+ ## Contributing
70
+
71
+ Contributing to the Python library is a great way to get involved with the Ouro community. Reach out to us on our [Github Discussions](https://github.com/orgs/ourofoundation/discussions) page if you want to get involved.
72
+
73
+ ## Set up a Local Development Environment
74
+
75
+ ### Clone the Repository
76
+
77
+ ```bash
78
+ git clone git@github.com:ourofoundation/ouro-py.git
79
+ cd ouro-py
80
+ ```
81
+
82
+ ### Create and Activate a Virtual Environment
83
+
84
+ We recommend activating your virtual environment. Click [here](https://docs.python.org/3/library/venv.html) for more about Python virtual environments and working with [conda](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#activating-an-environment) and [poetry](https://python-poetry.org/docs/basic-usage/).
85
+
86
+ Using venv (Python 3 built-in):
87
+
88
+ ```bash
89
+ python3 -m venv env
90
+ source env/bin/activate # On Windows, use .\env\Scripts\activate
91
+ ```
92
+
93
+ Using conda:
94
+
95
+ ```bash
96
+ conda create --name ouro-py
97
+ conda activate ouro-py
98
+ ```
99
+
100
+ ### PyPi installation
101
+
102
+ Install the package (for > Python 3.7):
103
+
104
+ ```bash
105
+ # with pip
106
+ pip install ouro
107
+ ```
108
+
109
+ ### Local installation
110
+
111
+ You can also install locally after cloning this repo. Install Development mode with `pip install -e`, which makes it so when you edit the source code the changes will be reflected in your python module.
112
+
113
+ ## Badges
114
+
115
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?label=license)](https://opensource.org/licenses/MIT)
116
+ [![CI](https://github.com/ourofoundation/ouro-py/actions/workflows/ci.yml/badge.svg)](https://github.com/ourofoundation/ouro-py/actions/workflows/ci.yml)
117
+ [![Python](https://img.shields.io/pypi/pyversions/ouro-py)](https://pypi.org/project/ouro-py)
118
+ [![Version](https://img.shields.io/pypi/v/ouro-py?color=%2334D058)](https://pypi.org/project/ouro-py)
119
+ [![Codecov](https://codecov.io/gh/ourofoundation/ouro-py/branch/develop/graph/badge.svg)](https://codecov.io/gh/ourofoundation/ouro-py)
120
+ [![Last commit](https://img.shields.io/github/last-commit/ourofoundation/ouro-py.svg?style=flat)](https://github.com/ourofoundation/ouro-py/commits)
121
+ [![GitHub commit activity](https://img.shields.io/github/commit-activity/m/ourofoundation/ouro-py)](https://github.com/ourofoundation/ouro-py/commits)
122
+ [![Github Stars](https://img.shields.io/github/stars/ourofoundation/ouro-py?style=flat&logo=github)](https://github.com/ourofoundation/ouro-py/stargazers)
123
+ [![Github Forks](https://img.shields.io/github/forks/ourofoundation/ouro-py?style=flat&logo=github)](https://github.com/ourofoundation/ouro-py/network/members)
124
+ [![Github Watchers](https://img.shields.io/github/watchers/ourofoundation/ouro-py?style=flat&logo=github)](https://github.com/ourofoundation/ouro-py)
125
+ [![GitHub contributors](https://img.shields.io/github/contributors/ourofoundation/ouro-py)](https://github.com/ourofoundation/ouro-py/graphs/contributors)
@@ -0,0 +1,8 @@
1
+ ouro/__init__.py,sha256=4Hg5ciwZrF1JtFnO2v6lFjGUgNgaxKWddHTQLaVtY34,2342
2
+ ouro/ouro.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ ouro/air/__init__.py,sha256=SWrcJ93qtssU_6Llga51QRX1bxNqZqalCGmSinifugM,5665
4
+ ouro/earth/__init__.py,sha256=482sMs0g_IAtL8YEy2wLfw51LZK_Ic6n7fJv3b1ibAM,5198
5
+ ouro/utils/__init__.py,sha256=6B2o7RUus6G7MWy4y3eYbTxRZFiQJWjbC7CL0EekjjU,1174
6
+ ouro_py-0.0.1.dist-info/METADATA,sha256=rk_6_Y_nuaFoL21sKLFFxMWXUDNSMPuHa-Otn5Nos-A,4406
7
+ ouro_py-0.0.1.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87
8
+ ouro_py-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.24.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any