Skip to content

Commit 1a664ea

Browse files
authored
Merge pull request #62 from AitoDotAI/feature/estimate-and-modify-endpoints
Add estimate, aggregate, modify, and batch API endpoints
2 parents f161d95 + dea7617 commit 1a664ea

14 files changed

Lines changed: 542 additions & 14 deletions

File tree

.circleci/config.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,15 +167,15 @@ commonYAMLStructure:
167167
(E'Johnson\'s Automotive', 2344, NULL);"
168168
- &run_sdk_and_cli_tests
169169
run:
170-
name: run all tests execluding sql functions tests
170+
name: run all tests excluding sql functions tests
171171
command: |
172172
[ "$CIRCLE_BRANCH" != master ] && git diff --quiet HEAD origin/master -- . ":!docs" && echo "NO CHANGE - SKIP" && exit 0
173173
. venv/bin/activate
174174
python -m tests -v suite sdk
175175
python -m tests -v suite cli
176176
- &run_sdk_and_cli_tests_windows
177177
run:
178-
name: run all tests execluding sql functions tests
178+
name: run all tests excluding sql functions tests
179179
command: |
180180
if ($Env:CIRCLE_BRANCH -ne "master") {
181181
(git diff --quiet HEAD origin/master -- . ":!docs") -and (echo "NO CHANGE - SKIP") -and (exit 0)

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ aito-python-tools.iml
117117
# vscode
118118
.vscode/
119119

120+
# AI assistant files
121+
.ai/
122+
120123
# test ouput file
121124
*_out.*
122125

aito/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.5.1"
1+
__version__ = "0.5.2"

aito/api.py

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,3 +1167,153 @@ def generic_query(
11671167
if use_job:
11681168
return job_request(client=client, request_obj=req)
11691169
return client.request(request_obj=req, raise_for_status=raise_for_status)
1170+
1171+
1172+
def estimate(
1173+
client: AitoClient, query: Dict, raise_for_status: Optional[bool] = None, use_job: bool = False
1174+
) -> Union[aito_responses.EstimateResponse, RequestError]:
1175+
"""send a query to the `Estimate API <https://aito.ai/docs/api/#post-api-v1-estimate>`__
1176+
1177+
The Estimate operation predicts numeric values using K-NN with transformation pipelines.
1178+
1179+
:param client: the AitoClient instance
1180+
:type client: AitoClient
1181+
:param query: the estimate query
1182+
:type query: Dict
1183+
:param raise_for_status: raise :class:`.RequestError` if the request fails instead of returning the error
1184+
If set to None, value from Client will be used. Defaults to None
1185+
:type raise_for_status: Optional[bool]
1186+
:param use_job: use job fo request that takes longer than 30 seconds, defaults to False
1187+
:type use_job: bool
1188+
:return: :class:`.EstimateResponse` or :class:`.RequestError` if an error occurred and not raise_for_status
1189+
:rtype: Union[EstimateResponse, RequestError]
1190+
"""
1191+
req = aito_requests.EstimateRequest(query)
1192+
if use_job:
1193+
return job_request(client=client, request_obj=req)
1194+
return client.request(request_obj=req, raise_for_status=raise_for_status)
1195+
1196+
1197+
def aggregate(
1198+
client: AitoClient, query: Dict, raise_for_status: Optional[bool] = None, use_job: bool = False
1199+
) -> Union[aito_responses.AggregateResponse, RequestError]:
1200+
"""send a query to the `Aggregate API <https://aito.ai/docs/api/#post-api-v1-aggregate>`__
1201+
1202+
Returns aggregated values like averages, sums, counts, etc.
1203+
1204+
:param client: the AitoClient instance
1205+
:type client: AitoClient
1206+
:param query: the aggregate query
1207+
:type query: Dict
1208+
:param raise_for_status: raise :class:`.RequestError` if the request fails instead of returning the error
1209+
If set to None, value from Client will be used. Defaults to None
1210+
:type raise_for_status: Optional[bool]
1211+
:param use_job: use job fo request that takes longer than 30 seconds, defaults to False
1212+
:type use_job: bool
1213+
:return: :class:`.AggregateResponse` or :class:`.RequestError` if an error occurred and not raise_for_status
1214+
:rtype: Union[AggregateResponse, RequestError]
1215+
"""
1216+
req = aito_requests.AggregateRequest(query)
1217+
if use_job:
1218+
return job_request(client=client, request_obj=req)
1219+
return client.request(request_obj=req, raise_for_status=raise_for_status)
1220+
1221+
1222+
def modify(
1223+
client: AitoClient,
1224+
query: Union[Dict, 'aito_requests.ModifyOperation', List['aito_requests.ModifyOperation']],
1225+
raise_for_status: Optional[bool] = None,
1226+
use_job: bool = False
1227+
) -> Union[aito_responses.ModifyResponse, RequestError]:
1228+
"""perform atomic modifications on table data using the
1229+
`Modify API <https://aito.ai/docs/api/#post-api-v1-data-modify>`__
1230+
1231+
The modify endpoint allows individual modifications or a sequence
1232+
of modifications in one atomic operation.
1233+
1234+
Can accept a raw query dict, a single ModifyOperation, or a list of ModifyOperations::
1235+
1236+
from aito.client.requests import Insert, Update, Delete
1237+
1238+
# Insert a single entry
1239+
api.modify(client, Insert("products", {"id": "1", "name": "Apple"}))
1240+
1241+
# Update entries matching a condition
1242+
api.modify(client, Update("products").where({"id": "1"}).set({"name": "New Name"}))
1243+
1244+
# Delete entries matching a condition
1245+
api.modify(client, Delete("products", {"id": "1"}))
1246+
1247+
# Multiple operations atomically
1248+
api.modify(client, [
1249+
Insert("products", {"id": "1", "name": "Apple"}),
1250+
Update("products").where({"id": "2"}).set({"name": "Banana"}),
1251+
Delete("products", {"id": "3"})
1252+
])
1253+
1254+
:param client: the AitoClient instance
1255+
:type client: AitoClient
1256+
:param query: the modify query - can be a Dict, a ModifyOperation (Insert/Update/Delete),
1257+
or a list of ModifyOperations
1258+
:type query: Union[Dict, ModifyOperation, List[ModifyOperation]]
1259+
:param raise_for_status: raise :class:`.RequestError` if the request fails instead of returning the error
1260+
If set to None, value from Client will be used. Defaults to None
1261+
:type raise_for_status: Optional[bool]
1262+
:param use_job: use job fo request that takes longer than 30 seconds, defaults to False
1263+
:type use_job: bool
1264+
:return: :class:`.ModifyResponse` or :class:`.RequestError` if an error occurred and not raise_for_status
1265+
:rtype: Union[ModifyResponse, RequestError]
1266+
"""
1267+
# Convert ModifyOperation(s) to query dict
1268+
if isinstance(query, aito_requests.ModifyOperation):
1269+
query = query.to_query()
1270+
elif isinstance(query, list) and len(query) > 0 and isinstance(query[0], aito_requests.ModifyOperation):
1271+
# Multiple operations must be wrapped in {"operations": [...]}
1272+
query = {"operations": [op.to_query() for op in query]}
1273+
elif isinstance(query, list):
1274+
# Raw list of dicts also needs wrapping
1275+
query = {"operations": query}
1276+
1277+
req = aito_requests.ModifyRequest(query)
1278+
if use_job:
1279+
return job_request(client=client, request_obj=req)
1280+
return client.request(request_obj=req, raise_for_status=raise_for_status)
1281+
1282+
1283+
def batch(
1284+
client: AitoClient,
1285+
queries: List[Dict],
1286+
raise_for_status: Optional[bool] = None
1287+
) -> Union[aito_responses.BatchResponse, RequestError]:
1288+
"""execute multiple queries in a single request using the
1289+
`Batch API <https://aito.ai/docs/api/#post-api-v1-batch>`__
1290+
1291+
The batch endpoint allows executing multiple queries (predict, similarity,
1292+
search, etc.) in a single HTTP request for improved performance.
1293+
1294+
Example::
1295+
1296+
import aito.api as api
1297+
1298+
# Execute multiple queries in one request
1299+
results = api.batch(client, [
1300+
{"from": "products", "where": {"name": "rye bread"}, "predict": "category"},
1301+
{"from": "products", "similarity": {"name": "rye bread"}, "limit": 5}
1302+
])
1303+
1304+
# Access individual results
1305+
predict_result = results[0] # First query result
1306+
similarity_result = results[1] # Second query result
1307+
1308+
:param client: the AitoClient instance
1309+
:type client: AitoClient
1310+
:param queries: a list of query dictionaries to execute in batch
1311+
:type queries: List[Dict]
1312+
:param raise_for_status: raise :class:`.RequestError` if the request fails instead of returning the error
1313+
If set to None, value from Client will be used. Defaults to None
1314+
:type raise_for_status: Optional[bool]
1315+
:return: :class:`.BatchResponse` containing results for each query, or :class:`.RequestError` if an error occurred
1316+
:rtype: Union[BatchResponse, RequestError]
1317+
"""
1318+
req = aito_requests.BatchRequest(queries)
1319+
return client.request(request_obj=req, raise_for_status=raise_for_status)

aito/cli/main_parser.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
UploadEntriesSubCommand, UploadBatchSubCommand, UploadFileSubCommand, UploadDataFromSQLSubCommand, OptimizeTableSubCommand, \
1818
QuickAddTableFromSQLSubCommand, GetDatabaseSubCommand, GetTableSubCommand, QuickPredictSubCommand, SearchSubCommand, \
1919
PredictSubCommand, RecommendSubCommand, EvaluateSubCommand, SimilaritySubCommand, MatchSubCommand, RelateSubCommand, \
20-
GenericQuerySubCommand, CreateDatabaseSubCommand
20+
GenericQuerySubCommand, CreateDatabaseSubCommand, EstimateSubCommand, AggregateSubCommand
2121
from .sub_commands.infer_table_schema_sub_command import InferTableSchemaSubCommand
2222
from .sub_commands.sub_command import SubCommand
2323

@@ -51,7 +51,9 @@ class MainParser(ArgParser):
5151
SimilaritySubCommand(),
5252
MatchSubCommand(),
5353
RelateSubCommand(),
54-
GenericQuerySubCommand()
54+
GenericQuerySubCommand(),
55+
EstimateSubCommand(),
56+
AggregateSubCommand()
5557
]
5658

5759
def __init__(self, commands: List[SubCommand] = None):

aito/cli/sub_commands/database_sub_command.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -445,3 +445,5 @@ def parse_and_execute(self, parsed_args: Dict):
445445
GenericQuerySubCommand = type(
446446
'GenericQuerySubCommand', (QueryToEndpointSubCommand,), {'api_method_name': 'generic_query'}
447447
)
448+
EstimateSubCommand = type('EstimateSubCommand', (QueryToEndpointSubCommand,), {'api_method_name': 'estimate'})
449+
AggregateSubCommand = type('AggregateSubCommand', (QueryToEndpointSubCommand,), {'api_method_name': 'aggregate'})

aito/client/requests/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
"""
44

55
from .data_api_request import DataAPIRequest, UploadEntriesRequest, DeleteEntriesRequest, InitiateFileUploadRequest, \
6-
TriggerFileProcessingRequest, GetFileProcessingRequest
6+
TriggerFileProcessingRequest, GetFileProcessingRequest, ModifyRequest
7+
from .modify_operations import ModifyOperation, Insert, Update, Delete
78
from .job_api_request import CreateJobRequest, GetJobResultRequest, GetJobStatusRequest
89
from .query_api_request import QueryAPIRequest, SearchRequest, PredictRequest, RecommendRequest, EvaluateRequest, \
9-
SimilarityRequest, MatchRequest, RelateRequest, GenericQueryRequest
10+
SimilarityRequest, MatchRequest, RelateRequest, GenericQueryRequest, EstimateRequest, AggregateRequest, \
11+
BatchRequest
1012
from .schema_api_request import GetDatabaseSchemaRequest, GetTableSchemaRequest, GetColumnSchemaRequest, \
1113
CreateDatabaseSchemaRequest, CreateTableSchemaRequest, CreateColumnSchemaRequest, DeleteDatabaseSchemaRequest, \
1214
DeleteTableSchemaRequest, DeleteColumnSchemaRequest

aito/client/requests/data_api_request.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from aito.client import responses as aito_resp
88
from .aito_request import AitoRequest, _FinalRequest, _PatternEndpoint, _GetRequest, _PostRequest
99
from aito.schema import AitoSchema
10+
from ..responses.query_api_response import ModifyResponse
1011

1112

1213
class DataAPIRequest(AitoRequest, ABC):
@@ -174,3 +175,28 @@ def path_matches(cls, path_to_match):
174175

175176
class GetFileProcessingRequest(_GetRequest, TriggerFileProcessingRequest, DataAPIRequest):
176177
"""Request to `Initiate File Upload <https://aito.ai/docs/api/#post-api-v1-data-table-file>`__"""
178+
179+
180+
class ModifyRequest(_PostRequest, _FinalRequest, DataAPIRequest):
181+
"""Request to `Modify data atomically <https://aito.ai/docs/api/#post-api-v1-data-modify>`__
182+
183+
Allows individual modifications or a sequence of modifications in one atomic operation.
184+
"""
185+
_path_suffix = '_modify'
186+
endpoint = f'{DataAPIRequest.endpoint_prefix}/{_path_suffix}'
187+
response_cls = ModifyResponse
188+
189+
def __init__(self, query: Dict):
190+
"""
191+
:param query: The modify query with operations
192+
:type query: Dict
193+
"""
194+
super().__init__(query=query)
195+
196+
@classmethod
197+
def _endpoint_pattern(cls):
198+
return re.compile(f'^{cls.endpoint}$')
199+
200+
@classmethod
201+
def path_matches(cls, path_to_match):
202+
return path_to_match.endswith(f'/{cls._path_suffix}')

0 commit comments

Comments
 (0)