cecil 0.0.7__tar.gz

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.

Potentially problematic release.


This version of cecil might be problematic. Click here for more details.

cecil-0.0.7/.gitignore ADDED
@@ -0,0 +1,6 @@
1
+ .idea
2
+ .venv
3
+ __pycache__
4
+ dist/*
5
+ venv
6
+ *.ipynb
@@ -0,0 +1,21 @@
1
+ ## Development installation
2
+
3
+ Install packaging/distribution tools:
4
+
5
+ ```shell
6
+ pip install hatch twine
7
+ ```
8
+
9
+ Install linter
10
+
11
+ ```shell
12
+ pip install black
13
+ ```
14
+
15
+ From top-level repo directory, install the package in editable mode:
16
+
17
+ ```shell
18
+ pip install -e .
19
+ ```
20
+
21
+ Local edits to the package will immediately take effect.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 cecilearth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cecil-0.0.7/Makefile ADDED
@@ -0,0 +1,33 @@
1
+ HL = @printf "\033[36m>> $1\033[0m\n"
2
+
3
+ default: help
4
+
5
+ .PHONY: help
6
+ help:
7
+ @echo "Usage: make <target>\n"
8
+ @grep -E ".+:\s.*?##" $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?##"}; {printf "\033[36m%-24s\033[0m %s\n", $$1, $$2}'
9
+
10
+ .PHONY: build
11
+ build: clean test ## Build package
12
+ $(call HL,build)
13
+ hatch build
14
+
15
+ .PHONY: clean
16
+ clean: ## Clean dist
17
+ $(call HL,clean)
18
+ hatch clean
19
+
20
+ .PHONY: publish-test
21
+ publish-test: build ## Publish package to testpypi
22
+ $(call HL,publish-test)
23
+ twine upload --repository testpypi dist/*
24
+
25
+ .PHONY: publish-prod
26
+ publish-prod: build ## Publish package to pypi
27
+ $(call HL,publish-prod)
28
+ twine upload --repository pypi dist/*
29
+
30
+ .PHONY: test
31
+ test: ## Run tests
32
+ $(call HL,test)
33
+ hatch test -v
cecil-0.0.7/PKG-INFO ADDED
@@ -0,0 +1,119 @@
1
+ Metadata-Version: 2.3
2
+ Name: cecil
3
+ Version: 0.0.7
4
+ Summary: Python SDK for Cecil Earth
5
+ License-Expression: MIT
6
+ License-File: LICENSE.txt
7
+ Classifier: Development Status :: 4 - Beta
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Requires-Python: >=3.8
12
+ Requires-Dist: pydantic
13
+ Requires-Dist: requests
14
+ Requires-Dist: snowflake-connector-python[pandas]
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Cecil SDK
18
+
19
+ [![PyPI - Version](https://img.shields.io/pypi/v/cecil-sdk.svg)](https://pypi.org/project/cecil-sdk)
20
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cecil-sdk.svg)](https://pypi.org/project/cecil-sdk)
21
+
22
+ -----
23
+
24
+ ## Table of Contents
25
+
26
+ - [Installation](#installation)
27
+ - [Authentication](#authentication)
28
+ - [License](#license)
29
+ - [Examples](#examples)
30
+
31
+ ## Installation
32
+
33
+ ```shell
34
+ pip install cecil
35
+ ```
36
+
37
+ ## Authentication
38
+
39
+ Set `CECIL_API_KEY` environment variable to your Cecil API key.
40
+
41
+ ## Examples
42
+
43
+ ### Create an AOI and data request using the Cecil client
44
+
45
+ ```python
46
+ import cecil
47
+
48
+ client = cecil.Client()
49
+
50
+ my_aoi = client.create_aoi(
51
+ name="My AOI",
52
+ geometry={
53
+ "type": "Polygon",
54
+ "coordinates": [
55
+ [
56
+ [145.410408835, -42.004083838],
57
+ [145.410408835, -42.004203978],
58
+ [145.410623191, -42.004203978],
59
+ [145.410623191, -42.004083838],
60
+ [145.410408835, -42.004083838],
61
+ ]
62
+ ],
63
+ },
64
+ )
65
+
66
+ # Get dataset ID from docs.cecil.earth -> Datasets
67
+ planet_forest_carbon_diligence_id = "c2dd4f55-56f6-4d05-aae3-ba7c1dcd812f"
68
+
69
+ my_data_request = client.create_data_request(
70
+ aoi_id=my_aoi.id,
71
+ dataset_id=planet_forest_carbon_diligence_id,
72
+ )
73
+
74
+ print(client.get_data_request(my_data_request.id).status)
75
+ ```
76
+
77
+ ### Create a reprojection using the Cecil client (once data request is completed)
78
+
79
+ ```python
80
+ my_reprojection = client.create_reprojection(
81
+ data_request_id=my_data_request.id,
82
+ crs="EPSG:4326",
83
+ resolution=0.005,
84
+ )
85
+
86
+ print(client.get_reprojection(my_reprojection.id).status)
87
+ ```
88
+
89
+ ### Query data (once reprojection is completed)
90
+
91
+ ```python
92
+ df = client.query(f'''
93
+ SELECT *
94
+ FROM
95
+ planet.forest_carbon_diligence
96
+ WHERE
97
+ reprojection_id = '{my_reprojection.id}'
98
+ ''')
99
+ ```
100
+
101
+ ### Other client methods:
102
+
103
+ ```python
104
+ client.list_aois()
105
+
106
+ client.get_aoi(my_aoi.id)
107
+
108
+ client.list_data_requests()
109
+
110
+ client.get_data_request(my_data_request.id)
111
+
112
+ client.list_reprojections()
113
+
114
+ client.get_reprojection(my_reprojection.id)
115
+ ```
116
+
117
+ ## License
118
+
119
+ `cecil` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
cecil-0.0.7/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Cecil SDK
2
+
3
+ [![PyPI - Version](https://img.shields.io/pypi/v/cecil-sdk.svg)](https://pypi.org/project/cecil-sdk)
4
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/cecil-sdk.svg)](https://pypi.org/project/cecil-sdk)
5
+
6
+ -----
7
+
8
+ ## Table of Contents
9
+
10
+ - [Installation](#installation)
11
+ - [Authentication](#authentication)
12
+ - [License](#license)
13
+ - [Examples](#examples)
14
+
15
+ ## Installation
16
+
17
+ ```shell
18
+ pip install cecil
19
+ ```
20
+
21
+ ## Authentication
22
+
23
+ Set `CECIL_API_KEY` environment variable to your Cecil API key.
24
+
25
+ ## Examples
26
+
27
+ ### Create an AOI and data request using the Cecil client
28
+
29
+ ```python
30
+ import cecil
31
+
32
+ client = cecil.Client()
33
+
34
+ my_aoi = client.create_aoi(
35
+ name="My AOI",
36
+ geometry={
37
+ "type": "Polygon",
38
+ "coordinates": [
39
+ [
40
+ [145.410408835, -42.004083838],
41
+ [145.410408835, -42.004203978],
42
+ [145.410623191, -42.004203978],
43
+ [145.410623191, -42.004083838],
44
+ [145.410408835, -42.004083838],
45
+ ]
46
+ ],
47
+ },
48
+ )
49
+
50
+ # Get dataset ID from docs.cecil.earth -> Datasets
51
+ planet_forest_carbon_diligence_id = "c2dd4f55-56f6-4d05-aae3-ba7c1dcd812f"
52
+
53
+ my_data_request = client.create_data_request(
54
+ aoi_id=my_aoi.id,
55
+ dataset_id=planet_forest_carbon_diligence_id,
56
+ )
57
+
58
+ print(client.get_data_request(my_data_request.id).status)
59
+ ```
60
+
61
+ ### Create a reprojection using the Cecil client (once data request is completed)
62
+
63
+ ```python
64
+ my_reprojection = client.create_reprojection(
65
+ data_request_id=my_data_request.id,
66
+ crs="EPSG:4326",
67
+ resolution=0.005,
68
+ )
69
+
70
+ print(client.get_reprojection(my_reprojection.id).status)
71
+ ```
72
+
73
+ ### Query data (once reprojection is completed)
74
+
75
+ ```python
76
+ df = client.query(f'''
77
+ SELECT *
78
+ FROM
79
+ planet.forest_carbon_diligence
80
+ WHERE
81
+ reprojection_id = '{my_reprojection.id}'
82
+ ''')
83
+ ```
84
+
85
+ ### Other client methods:
86
+
87
+ ```python
88
+ client.list_aois()
89
+
90
+ client.get_aoi(my_aoi.id)
91
+
92
+ client.list_data_requests()
93
+
94
+ client.get_data_request(my_data_request.id)
95
+
96
+ client.list_reprojections()
97
+
98
+ client.get_reprojection(my_reprojection.id)
99
+ ```
100
+
101
+ ## License
102
+
103
+ `cecil` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
@@ -0,0 +1 @@
1
+ __version__ = "0.0.7"
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "cecil"
7
+ dynamic = ["version"]
8
+ description = 'Python SDK for Cecil Earth'
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = "MIT"
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ dependencies = [
19
+ "pydantic",
20
+ "requests",
21
+ "snowflake-connector-python[pandas]"
22
+ ]
23
+
24
+ [tool.hatch.version]
25
+ path = "__about__.py"
26
+
27
+ [tool.hatch.envs.hatch-test]
28
+ extra-dependencies = [
29
+ "responses"
30
+ ]
@@ -0,0 +1 @@
1
+ from .client import Client
@@ -0,0 +1,131 @@
1
+ import os
2
+ from pydantic import BaseModel
3
+ from typing import Dict, List
4
+ import requests
5
+
6
+ from requests import auth
7
+
8
+ import snowflake.connector
9
+
10
+ from .models import (
11
+ AOI,
12
+ AOICreate,
13
+ DataRequest,
14
+ DataRequestCreate,
15
+ Reprojection,
16
+ ReprojectionCreate,
17
+ SnowflakeCredentials,
18
+ )
19
+
20
+ # TODO: Documentation (Google style)
21
+ # TODO: Add HTTP retries
22
+
23
+
24
+ class Client:
25
+ def __init__(self, env="dev"):
26
+ self._api_auth = None
27
+ self._base_url = f"https://{env}-api.cecil.earth" # TODO: hard-code to prod URL
28
+ self._snowflake_creds = None
29
+
30
+ def create_aoi(self, name: str, geometry: Dict) -> AOI:
31
+ # TODO: validate geometry
32
+ res = self._post(url="/v0/aois", model=AOICreate(name=name, geometry=geometry))
33
+ return AOI(**res)
34
+
35
+ def get_aoi(self, id: str) -> AOI:
36
+ res = self._get(url=f"/v0/aois/{id}")
37
+ return AOI(**res)
38
+
39
+ def list_aois(self) -> List[AOI]:
40
+ res = self._get(url="/v0/aois")
41
+ return [AOI(**record) for record in res["records"]]
42
+
43
+ def create_data_request(self, aoi_id: str, dataset_id: str) -> DataRequest:
44
+ res = self._post(
45
+ url="/v0/data-requests",
46
+ model=DataRequestCreate(aoi_id=aoi_id, dataset_id=dataset_id),
47
+ )
48
+ return DataRequest(**res)
49
+
50
+ def get_data_request(self, id: str) -> DataRequest:
51
+ res = self._get(url=f"/v0/data-requests/{id}")
52
+ return DataRequest(**res)
53
+
54
+ def list_data_requests(self):
55
+ res = self._get(url="/v0/data-requests")
56
+ return [DataRequest(**record) for record in res["records"]]
57
+
58
+ def create_reprojection(
59
+ self, data_request_id: str, crs: str, resolution: float
60
+ ) -> Reprojection:
61
+ # TODO: check if data request is completed before creating reprojection
62
+ res = self._post(
63
+ url="/v0/reprojections",
64
+ model=ReprojectionCreate(
65
+ data_request_id=data_request_id,
66
+ crs=crs,
67
+ resolution=resolution,
68
+ ),
69
+ )
70
+ return Reprojection(**res)
71
+
72
+ def get_reprojection(self, id: str) -> Reprojection:
73
+ res = self._get(url=f"/v0/reprojections/{id}")
74
+ return Reprojection(**res)
75
+
76
+ def list_reprojections(self) -> List[Reprojection]:
77
+ res = self._get(url="/v0/reprojections")
78
+ return [Reprojection(**record) for record in res["records"]]
79
+
80
+ def query(self, sql):
81
+ if self._snowflake_creds is None:
82
+ res = self._get(url="/v0/data-access-credentials")
83
+ self._snowflake_creds = SnowflakeCredentials(**res)
84
+
85
+ with snowflake.connector.connect(
86
+ account=self._snowflake_creds.account.get_secret_value(),
87
+ user=self._snowflake_creds.user.get_secret_value(),
88
+ password=self._snowflake_creds.password.get_secret_value(),
89
+ ) as conn:
90
+ df = conn.cursor().execute(sql).fetch_pandas_all()
91
+ df.columns = [x.lower() for x in df.columns]
92
+
93
+ return df
94
+
95
+ def _request(self, method: str, url: str, **kwargs) -> Dict:
96
+
97
+ self._set_auth()
98
+
99
+ try:
100
+ r = requests.request(
101
+ method=method,
102
+ url=self._base_url + url,
103
+ auth=self._api_auth,
104
+ timeout=None,
105
+ **kwargs,
106
+ )
107
+ r.raise_for_status()
108
+ return r.json()
109
+
110
+ except requests.exceptions.ConnectionError as err:
111
+ raise ValueError("Connection error") from err
112
+ except requests.exceptions.HTTPError as err:
113
+ if err.response.status_code == 403:
114
+ raise ValueError("Authentication error") from err
115
+ else:
116
+ raise
117
+
118
+ def _get(self, url: str, **kwargs) -> Dict:
119
+ return self._request(method="get", url=url, **kwargs)
120
+
121
+ def _post(self, url: str, model: BaseModel, **kwargs) -> Dict:
122
+ return self._request(
123
+ method="post", url=url, json=model.model_dump(by_alias=True), **kwargs
124
+ )
125
+
126
+ def _set_auth(self) -> None:
127
+ try:
128
+ api_key = os.environ["CECIL_API_KEY"]
129
+ self._api_auth = auth.HTTPBasicAuth(username=api_key, password="")
130
+ except KeyError:
131
+ raise ValueError("environment variable CECIL_API_KEY not set") from None
@@ -0,0 +1,80 @@
1
+ import datetime
2
+ from enum import Enum
3
+ from typing import Dict, List, Optional
4
+
5
+ from pydantic import BaseModel, ConfigDict, SecretStr
6
+ from pydantic.alias_generators import to_camel
7
+
8
+
9
+ class SubRequestStatus(str, Enum):
10
+ COMPLETED = "completed"
11
+ FAILED = "failed"
12
+ PROCESSING = "processing"
13
+
14
+
15
+ class DataRequestStatus(str, Enum):
16
+ COMPLETED = "completed"
17
+ FAILED = "failed"
18
+ PROCESSING = "processing"
19
+
20
+
21
+ class AOI(BaseModel):
22
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
23
+ id: str
24
+ name: str
25
+ geometry: Dict
26
+ hectares: float
27
+ created: datetime.datetime
28
+
29
+
30
+ class AOICreate(BaseModel):
31
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
32
+ name: str
33
+ geometry: Dict
34
+
35
+
36
+ class SubRequest(BaseModel):
37
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
38
+ external_id: str
39
+ description: str
40
+ status: SubRequestStatus
41
+ error_message: Optional[str]
42
+
43
+
44
+ class DataRequest(BaseModel):
45
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
46
+ id: str
47
+ aoi_id: str
48
+ dataset_id: str
49
+ sub_requests: List[SubRequest]
50
+ status: DataRequestStatus
51
+ created: datetime.datetime
52
+
53
+
54
+ class DataRequestCreate(BaseModel):
55
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
56
+ aoi_id: str
57
+ dataset_id: str
58
+
59
+
60
+ class Reprojection(BaseModel):
61
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
62
+ id: str
63
+ data_request_id: str
64
+ crs: str
65
+ resolution: float
66
+ created: datetime.datetime
67
+
68
+
69
+ class ReprojectionCreate(BaseModel):
70
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
71
+ data_request_id: str
72
+ crs: str
73
+ resolution: float
74
+
75
+
76
+ class SnowflakeCredentials(BaseModel):
77
+ model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)
78
+ account: SecretStr
79
+ user: SecretStr
80
+ password: SecretStr
File without changes
@@ -0,0 +1,90 @@
1
+ import responses
2
+
3
+ from src.cecil.client import Client
4
+ from src.cecil.models import DataRequest, DataRequestStatus
5
+
6
+ FROZEN_TIME = "2024-01-01T00:00:00.000Z"
7
+
8
+
9
+ def test_client_class():
10
+ client = Client()
11
+ assert client._base_url == "https://dev-api.cecil.earth"
12
+
13
+
14
+ @responses.activate
15
+ def test_client_create_data_request():
16
+ responses.add(
17
+ responses.POST,
18
+ "https://dev-api.cecil.earth/v0/data-requests",
19
+ json={
20
+ "id": "id",
21
+ "aoiId": "aoi_id",
22
+ "datasetId": "dataset_id",
23
+ "subRequests": [],
24
+ "status": "processing",
25
+ "created": FROZEN_TIME,
26
+ },
27
+ status=201,
28
+ )
29
+
30
+ client = Client()
31
+ res = client.create_data_request("aoi_id", "dataset_id")
32
+
33
+ assert res == DataRequest(
34
+ id="id",
35
+ aoiId="aoi_id",
36
+ datasetId="dataset_id",
37
+ subRequests=[],
38
+ status="processing",
39
+ created="2024-01-01T00:00:00.000Z",
40
+ )
41
+
42
+
43
+ @responses.activate
44
+ def test_client_list_data_requests():
45
+ responses.add(
46
+ responses.GET,
47
+ "https://dev-api.cecil.earth/v0/data-requests",
48
+ json={
49
+ "records": [
50
+ {
51
+ "id": "data_request_id_1",
52
+ "aoiId": "aoi_id",
53
+ "datasetId": "dataset_id",
54
+ "subRequests": [], # TODO: Add some SubRequests
55
+ "status": "processing",
56
+ "created": "2024-09-19T04:45:57.561Z",
57
+ },
58
+ {
59
+ "id": "data_request_id_2",
60
+ "aoiId": "aoi_id",
61
+ "datasetId": "dataset_id",
62
+ "subRequests": [], # TODO: Add some SubRequests
63
+ "status": "completed",
64
+ "created": "2024-09-19T04:54:38.252Z",
65
+ },
66
+ ]
67
+ },
68
+ )
69
+
70
+ client = Client()
71
+ data_requests = client.list_data_requests()
72
+
73
+ assert data_requests == [
74
+ DataRequest(
75
+ id="data_request_id_1",
76
+ aoiId="aoi_id",
77
+ datasetId="dataset_id",
78
+ subRequests=[],
79
+ status=DataRequestStatus.PROCESSING,
80
+ created="2024-09-19T04:45:57.561Z",
81
+ ),
82
+ DataRequest(
83
+ id="data_request_id_2",
84
+ aoiId="aoi_id",
85
+ datasetId="dataset_id",
86
+ subRequests=[],
87
+ status=DataRequestStatus.COMPLETED,
88
+ created="2024-09-19T04:54:38.252Z",
89
+ ),
90
+ ]