Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
aa0bba7
chore: add datasource_composite
jbarreau Nov 6, 2024
eef7c43
chore: add tests for composite datasource
jbarreau Nov 6, 2024
e58fa78
chore: use datasource composite
jbarreau Nov 6, 2024
db910e9
chore: add name in datasources
jbarreau Nov 6, 2024
3fbc694
chore: add native query to datasources
jbarreau Nov 19, 2024
3924ec1
chore: add native_query route
jbarreau Nov 19, 2024
e628fe6
chore(datasources): change datasource name to native query connections
jbarreau Nov 21, 2024
ed3a43d
chore(datasources): native query connections is no longer in schema
jbarreau Nov 21, 2024
e94c627
chore(datasources): adapt native query and capabilities route
jbarreau Nov 21, 2024
0be8e22
chore: refactor context variable injector
jbarreau Nov 25, 2024
1e12628
chore: fix typo
jbarreau Nov 25, 2024
bd4e75c
chore: ineject context variables in chart queries
jbarreau Nov 25, 2024
e0f6126
chore: draft permissions for segments live queries
jbarreau Nov 25, 2024
873763d
chore: draft of live query segments
jbarreau Nov 25, 2024
1cb7070
chore(example): update example projects
jbarreau Nov 25, 2024
3abfb99
chore: add parameters to execute native query and handle it
jbarreau Nov 27, 2024
7830f63
chore: add comments for query formating
jbarreau Nov 27, 2024
24533e4
chore: add error handling
jbarreau Nov 27, 2024
44919e5
chore: fix existing tests
jbarreau Nov 27, 2024
b90ac03
chore: fix linting
jbarreau Nov 27, 2024
3f32b27
chore: fix linting
jbarreau Nov 27, 2024
f51ee5e
chore: fix test
jbarreau Nov 27, 2024
c726991
chore: add tests for capability route
jbarreau Nov 28, 2024
f9ce5a5
chore: add tests for django datasource
jbarreau Nov 28, 2024
51f710a
chore: add last test on django datasource
jbarreau Nov 28, 2024
a89f2d6
chore: remove useless verification
jbarreau Nov 29, 2024
2a1cfa3
chore: update example project
jbarreau Nov 29, 2024
eda63fc
chore: add tests on sqlalchemy datasource
jbarreau Nov 29, 2024
ad6510a
chore: fix concurency issue with test database
jbarreau Nov 29, 2024
84e6d80
chore: fixes after front and backend pluging
jbarreau Dec 2, 2024
8ce15fa
chore: handle every type of chart
jbarreau Dec 3, 2024
ce1f66f
chore: improve error handling
jbarreau Dec 3, 2024
c764794
chore: add native_query tests on composite datasource
jbarreau Dec 3, 2024
4245b1b
chore: add tests on native query resource
jbarreau Dec 3, 2024
180d932
chore: fix linting
jbarreau Dec 3, 2024
c577da6
chore: add tests for segments
jbarreau Dec 4, 2024
5c13d66
chore: details on pk field for segments
jbarreau Dec 4, 2024
cdbf016
chore: fix linting
jbarreau Dec 4, 2024
324c22f
chore: add tests on permissions
jbarreau Dec 4, 2024
1a3084d
chore: add test on django routes
jbarreau Dec 5, 2024
e005930
chore: add test on flask routes
jbarreau Dec 5, 2024
5f3f93b
chore: adapt error messages
jbarreau Dec 5, 2024
ab41081
chore: can link add datasource methods
jbarreau Dec 9, 2024
481423b
chore: add sql query checker
jbarreau Dec 10, 2024
3e6f394
chore: try to improve ci speed on test py3.12
jbarreau Dec 10, 2024
a25d815
chore: try to improve ci speed on test py3.12
jbarreau Dec 10, 2024
715485b
chore: try to improve ci speed on test py3.12
jbarreau Dec 10, 2024
d099793
chore: try to improve ci speed on test py3.12
jbarreau Dec 10, 2024
8dfa13e
chore: try to improve ci speed on test py3.12
jbarreau Dec 10, 2024
11284c4
chore: try to improve ci speed on test py3.12
jbarreau Dec 10, 2024
b4ee9a1
chore(ci): try to cache poetry
jbarreau Dec 10, 2024
02d53b9
chore(ci): try to cache poetry
jbarreau Dec 10, 2024
b9083f7
chore(ci): try to cache poetry
jbarreau Dec 10, 2024
4bf3330
chore(ci): try to cache poetry
jbarreau Dec 10, 2024
df08234
chore(CI): use cache v4
jbarreau Dec 16, 2024
1f08a1d
chore: handle sse cache invalidation for segments query
jbarreau Dec 16, 2024
21dc926
chore: add link to doc
jbarreau Dec 16, 2024
e4603bf
chore: fix linting
jbarreau Dec 16, 2024
ffb52b2
chore: refactor permission for key rendering
jbarreau Dec 16, 2024
d0657de
chore(ci): disable cache on poetry install
jbarreau Dec 16, 2024
8e7a895
chore(ci): try to improve poetry setup
jbarreau Dec 16, 2024
b4ab040
chore(ci): restore old way to install poetry
jbarreau Dec 16, 2024
66f437d
chore(ci): another try to improve poetry setup
jbarreau Dec 16, 2024
62d8107
chore(ci): disable setup python cache on poetry because of concurrenc…
jbarreau Dec 16, 2024
d3f41b8
chore: don't show stacktrace on 403 forbidden
jbarreau Dec 17, 2024
35949f9
chore: update example
jbarreau Dec 17, 2024
36de6a9
chore: theses are in a different PR
jbarreau Dec 17, 2024
4b9fdc8
chore: permission refactor wasn't complete
jbarreau Dec 18, 2024
35e4777
chore: fix for review
jbarreau Dec 19, 2024
df36131
chore: remove conflict
jbarreau Dec 19, 2024
cf138b9
chore: remove test related to removed log
jbarreau Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/_example/django/django_demo/.forestadmin-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2726,7 +2726,7 @@
"enums": null,
"field": "body",
"inverseOf": null,
"isFilterable": false,
"isFilterable": true,
"isPrimaryKey": false,
"isReadOnly": false,
"isRequired": false,
Expand All @@ -2740,7 +2740,7 @@
"enums": null,
"field": "email",
"inverseOf": null,
"isFilterable": false,
"isFilterable": true,
"isPrimaryKey": false,
"isReadOnly": false,
"isRequired": false,
Expand Down Expand Up @@ -2768,7 +2768,7 @@
"enums": null,
"field": "name",
"inverseOf": null,
"isFilterable": false,
"isFilterable": true,
"isPrimaryKey": false,
"isReadOnly": false,
"isRequired": false,
Expand Down Expand Up @@ -3486,7 +3486,7 @@
"enums": null,
"field": "body",
"inverseOf": null,
"isFilterable": false,
"isFilterable": true,
"isPrimaryKey": false,
"isReadOnly": false,
"isRequired": false,
Expand Down Expand Up @@ -3514,7 +3514,7 @@
"enums": null,
"field": "title",
"inverseOf": null,
"isFilterable": false,
"isFilterable": true,
"isPrimaryKey": false,
"isReadOnly": false,
"isRequired": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,17 @@ def __init__(self, datasource: Datasource[Self]):
"name": {
"type": FieldType.COLUMN,
"column_type": "String",
"filter_operators": set([Operator.EQUAL]),
},
"email": {
"type": FieldType.COLUMN,
"column_type": "String",
"filter_operators": set([Operator.EQUAL]),
},
"body": {
"type": FieldType.COLUMN,
"column_type": "String",
"filter_operators": set([Operator.EQUAL]),
},
}
)
Expand All @@ -140,10 +143,12 @@ def __init__(self, datasource: Datasource[Self]):
"title": {
"type": FieldType.COLUMN,
"column_type": "String",
"filter_operators": set([Operator.EQUAL]),
},
"body": {
"type": FieldType.COLUMN,
"column_type": "String",
"filter_operators": set([Operator.EQUAL]),
},
}
)
Expand Down
2 changes: 1 addition & 1 deletion src/_example/django/django_demo/app/forest/customer.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ async def get_customer_spending_values(records: List[RecordsDataAlias], context:

def customer_full_name() -> ComputedDefinition:
async def _get_customer_fullname_values(records: List[RecordsDataAlias], context: CollectionCustomizationContext):
return [f"{record['first_name']} - {record['last_name']}" for record in records]
return [f"{record.get('first_name', '')} - {record.get('last_name', '')}" for record in records]

return {
"column_type": "String",
Expand Down
11 changes: 9 additions & 2 deletions src/_example/django/django_demo/app/forest_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,16 @@

def customize_forest(agent: DjangoAgent):
# customize_forest_logging()
agent.add_datasource(DjangoDatasource(support_polymorphic_relations=True))

agent.add_datasource(
DjangoDatasource(
support_polymorphic_relations=True, live_query_connection={"django": "default", "dj_sqlachemy": "other"}
)
)
agent.add_datasource(TypicodeDatasource())
agent.add_datasource(SqlAlchemyDatasource(Base, DB_URI))
agent.add_datasource(
SqlAlchemyDatasource(Base, DB_URI, live_query_connection="sqlalchemy"),
)

agent.customize_collection("address").add_segment("France", segment_addr_fr("address"))
agent.customize_collection("app_address").add_segment("France", segment_addr_fr("app_address"))
Expand Down
8 changes: 4 additions & 4 deletions src/_example/django/django_demo/django_demo/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
"ATOMIC_REQUESTS": True,
},
# "other": {
# "ENGINE": "django.db.backends.sqlite3",
# "NAME": os.path.join(BASE_DIR, "db_flask_example.sqlite"),
# },
"other": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db_sqlalchemy.sql"),
},
}

DATABASE_ROUTERS = ["django_demo.db_router.DBRouter"]
Expand Down
19 changes: 17 additions & 2 deletions src/agent_toolkit/forestadmin/agent_toolkit/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from forestadmin.agent_toolkit.resources.collections.charts_datasource import ChartsDatasourceResource
from forestadmin.agent_toolkit.resources.collections.crud import CrudResource
from forestadmin.agent_toolkit.resources.collections.crud_related import CrudRelatedResource
from forestadmin.agent_toolkit.resources.collections.native_query import NativeQueryResource
from forestadmin.agent_toolkit.resources.collections.stats import StatsResource
from forestadmin.agent_toolkit.resources.security.resources import Authentication
from forestadmin.agent_toolkit.services.permissions.ip_whitelist_service import IpWhiteListService
Expand Down Expand Up @@ -37,6 +38,7 @@ class Resources(TypedDict):
actions: ActionResource
collection_charts: ChartsCollectionResource
datasource_charts: ChartsDatasourceResource
native_query: NativeQueryResource


class Agent:
Expand Down Expand Up @@ -73,10 +75,13 @@ def __del__(self):
async def __mk_resources(self):
self._resources: Resources = {
"capabilities": CapabilitiesResource(
await self.customizer.get_datasource(), self._ip_white_list_service, self.options
self.customizer.composite_datasource,
self._ip_white_list_service,
self.options,
),
"authentication": Authentication(self._ip_white_list_service, self.options),
"crud": CrudResource(
self.customizer.composite_datasource,
await self.customizer.get_datasource(),
self._permission_service,
self._ip_white_list_service,
Expand Down Expand Up @@ -112,14 +117,23 @@ async def __mk_resources(self):
self._ip_white_list_service,
self.options,
),
"native_query": NativeQueryResource(
self.customizer.composite_datasource,
await self.customizer.get_datasource(),
self._permission_service,
self._ip_white_list_service,
self.options,
),
}

async def get_resources(self):
if self._resources is None:
await self.__mk_resources()
return self._resources

def add_datasource(self, datasource: Datasource[BoundCollection], options: Optional[DataSourceOptions] = None):
def add_datasource(
self, datasource: Datasource[BoundCollection], options: Optional[DataSourceOptions] = None
) -> Self:
"""Add a datasource

Args:
Expand All @@ -130,6 +144,7 @@ def add_datasource(self, datasource: Datasource[BoundCollection], options: Optio
options = {}
self.customizer.add_datasource(datasource, options)
self._resources = None
return self

def use(self, plugin: type, options: Optional[Dict] = {}) -> Self:
"""Load a plugin across all collections
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from forestadmin.agent_toolkit.services.permissions.ip_whitelist_service import IpWhiteListService
from forestadmin.agent_toolkit.utils.context import HttpResponseBuilder, Request, RequestMethod, Response
from forestadmin.agent_toolkit.utils.forest_schema.generator_field import SchemaFieldGenerator
from forestadmin.datasource_toolkit.datasource_customizer.datasource_composite import CompositeDatasource
from forestadmin.datasource_toolkit.datasource_customizer.datasource_customizer import DatasourceCustomizer
from forestadmin.datasource_toolkit.exceptions import BusinessError
from forestadmin.datasource_toolkit.interfaces.fields import Column, is_column
Expand All @@ -19,12 +20,12 @@
class CapabilitiesResource(IpWhitelistResource):
def __init__(
self,
datasource: DatasourceAlias,
composite_datasource: CompositeDatasource,
ip_white_list_service: IpWhiteListService,
options: Options,
):
super().__init__(ip_white_list_service, options)
self.datasource = datasource
self.composite_datasource: CompositeDatasource = composite_datasource

@ip_white_list
async def dispatch(self, request: Request, method_name: LiteralMethod) -> Response:
Expand All @@ -40,14 +41,18 @@ async def dispatch(self, request: Request, method_name: LiteralMethod) -> Respon
@check_method(RequestMethod.POST)
@authenticate
async def capabilities(self, request: Request) -> Response:
ret = {"collections": []}
ret = {"collections": [], "nativeQueryConnections": []}
requested_collections = request.body.get("collectionNames", [])
for collection_name in requested_collections:
ret["collections"].append(self._get_collection_capability(collection_name))

ret["nativeQueryConnections"] = [
{"name": connection} for connection in self.composite_datasource.get_native_query_connections()
]
return HttpResponseBuilder.build_success_response(ret)

def _get_collection_capability(self, collection_name: str) -> Dict[str, Any]:
collection = self.datasource.get_collection(collection_name)
collection = self.composite_datasource.get_collection(collection_name)
fields = []
for field_name, field_schema in collection.schema["fields"].items():
if is_column(field_schema):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import asyncio
from typing import Any, Awaitable, Dict, List, Literal, Tuple, Union, cast
from typing import Any, Awaitable, Dict, List, Literal, Optional, Tuple, Union, cast
from uuid import UUID

from forestadmin.agent_toolkit.forest_logger import ForestLogger
from forestadmin.agent_toolkit.options import Options
from forestadmin.agent_toolkit.resources.collections.base_collection_resource import BaseCollectionResource
from forestadmin.agent_toolkit.resources.collections.decorators import (
authenticate,
Expand All @@ -21,15 +22,21 @@
parse_timezone,
)
from forestadmin.agent_toolkit.resources.collections.requests import RequestCollection, RequestCollectionException
from forestadmin.agent_toolkit.resources.context_variable_injector_mixin import ContextVariableInjectorResourceMixin
from forestadmin.agent_toolkit.services.permissions.ip_whitelist_service import IpWhiteListService
from forestadmin.agent_toolkit.services.permissions.permission_service import PermissionService
from forestadmin.agent_toolkit.services.serializers import add_search_metadata
from forestadmin.agent_toolkit.services.serializers.json_api import JsonApiException, JsonApiSerializer
from forestadmin.agent_toolkit.utils.context import HttpResponseBuilder, Request, RequestMethod, Response, User
from forestadmin.agent_toolkit.utils.csv import Csv, CsvException
from forestadmin.agent_toolkit.utils.id import unpack_id
from forestadmin.agent_toolkit.utils.sql_query_checker import SqlQueryChecker
from forestadmin.datasource_toolkit.collections import Collection
from forestadmin.datasource_toolkit.datasource_customizer.collection_customizer import CollectionCustomizer
from forestadmin.datasource_toolkit.datasources import DatasourceException
from forestadmin.datasource_toolkit.exceptions import ForbiddenError
from forestadmin.datasource_toolkit.datasource_customizer.datasource_composite import CompositeDatasource
from forestadmin.datasource_toolkit.datasource_customizer.datasource_customizer import DatasourceCustomizer
from forestadmin.datasource_toolkit.datasources import Datasource, DatasourceException
from forestadmin.datasource_toolkit.exceptions import ForbiddenError, NativeQueryException
from forestadmin.datasource_toolkit.interfaces.fields import (
ManyToOne,
OneToOne,
Expand All @@ -55,14 +62,26 @@
from forestadmin.datasource_toolkit.interfaces.query.projections.factory import ProjectionFactory
from forestadmin.datasource_toolkit.interfaces.records import CompositeIdAlias, RecordsDataAlias
from forestadmin.datasource_toolkit.utils.collections import CollectionUtils
from forestadmin.datasource_toolkit.utils.records import RecordUtils
from forestadmin.datasource_toolkit.utils.schema import SchemaUtils
from forestadmin.datasource_toolkit.validations.field import FieldValidatorException
from forestadmin.datasource_toolkit.validations.records import RecordValidator, RecordValidatorException

LiteralMethod = Literal["list", "count", "add", "get", "delete_list", "csv"]


class CrudResource(BaseCollectionResource):
class CrudResource(BaseCollectionResource, ContextVariableInjectorResourceMixin):
def __init__(
self,
datasource_composite: CompositeDatasource,
datasource: Union[Datasource, DatasourceCustomizer],
permission: PermissionService,
ip_white_list_service: IpWhiteListService,
options: Options,
):
self._datasource_composite = datasource_composite
super().__init__(datasource, permission, ip_white_list_service, options)

@ip_white_list
async def dispatch(self, request: Request, method_name: LiteralMethod) -> Response:
method = getattr(self, method_name)
Expand Down Expand Up @@ -159,6 +178,9 @@ async def list(self, request: RequestCollection) -> Response:
scope_tree = await self.permission.get_scope(request.user, request.collection)
try:
paginated_filter = build_paginated_filter(request, scope_tree)
condition_tree = await self._handle_live_query_segment(request, paginated_filter.condition_tree)
paginated_filter = paginated_filter.override({"condition_tree": condition_tree})

except FilterException as e:
ForestLogger.log("exception", e)
return HttpResponseBuilder.build_client_error_response([e])
Expand Down Expand Up @@ -191,6 +213,8 @@ async def csv(self, request: RequestCollection) -> Response:
scope_tree = await self.permission.get_scope(request.user, request.collection)
try:
paginated_filter = build_paginated_filter(request, scope_tree)
condition_tree = await self._handle_live_query_segment(request, paginated_filter.condition_tree)
paginated_filter = paginated_filter.override({"condition_tree": condition_tree})
paginated_filter.page = None
except FilterException as e:
ForestLogger.log("exception", e)
Expand Down Expand Up @@ -220,9 +244,12 @@ async def count(self, request: RequestCollection) -> Response:
return HttpResponseBuilder.build_success_response({"meta": {"count": "deactivated"}})

scope_tree = await self.permission.get_scope(request.user, request.collection)
filter = build_filter(request, scope_tree)
filter_ = build_filter(request, scope_tree)
filter_ = filter_.override(
{"condition_tree": await self._handle_live_query_segment(request, filter_.condition_tree)}
)
aggregation = Aggregation({"operation": "Count"})
result = await request.collection.aggregate(request.user, filter, aggregation)
result = await request.collection.aggregate(request.user, filter_, aggregation)
try:
count = result[0]["value"]
except IndexError:
Expand Down Expand Up @@ -424,3 +451,33 @@ def _serialize_records_with_relationships(

schema = JsonApiSerializer.get(collection)
return schema(projections=projection).dump(records if many is True else records[0], many=many)

async def _handle_live_query_segment(
self, request: RequestCollection, condition_tree: Optional[ConditionTree]
) -> Optional[ConditionTree]:
if request.query.get("segmentQuery") is not None:
if request.query.get("connectionName") in ["", None]:
raise NativeQueryException("Missing native query connection attribute")

await self.permission.can_live_query_segment(request)
SqlQueryChecker.check_query(request.query["segmentQuery"])
variables = await self.inject_and_get_context_variables_in_live_query_segment(request)
native_query_result = await self._datasource_composite.execute_native_query(
request.query["connectionName"], request.query["segmentQuery"], variables
)

pk_field = SchemaUtils.get_primary_keys(request.collection.schema)[0]
if len(native_query_result) > 0 and pk_field not in native_query_result[0]:
raise NativeQueryException(f"Live query must return the primary key field ('{pk_field}').")

trees = []
if condition_tree:
trees.append(condition_tree)
trees.append(
ConditionTreeFactory.match_ids(
request.collection.schema,
[RecordUtils.get_primary_key(request.collection.schema, r) for r in native_query_result],
)
)
return ConditionTreeFactory.intersect(trees)
return condition_tree
Loading
Loading