dex-connector-algolia 0.1.0__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.
- dex_connector_algolia-0.1.0/.gitignore +6 -0
- dex_connector_algolia-0.1.0/PKG-INFO +53 -0
- dex_connector_algolia-0.1.0/README.md +42 -0
- dex_connector_algolia-0.1.0/examples/index_search/__init__.py +3 -0
- dex_connector_algolia-0.1.0/examples/index_search/flow.py +88 -0
- dex_connector_algolia-0.1.0/examples/index_search/worker.py +51 -0
- dex_connector_algolia-0.1.0/pyproject.toml +37 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/__init__.py +51 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/_generated/__init__.py +2 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/_generated/config.py +26 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/_generated/connection.py +134 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/_generated/definitions.py +55 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/_generated/mock.py +107 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/_generated/models.py +82 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/_generated/steps.py +131 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/algoliamock/__init__.py +54 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/client.py +387 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/connection.py +49 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/dex-connector.json +77 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/models.py +34 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/py.typed +0 -0
- dex_connector_algolia-0.1.0/src/dex_connectors/algolia/steps.py +38 -0
- dex_connector_algolia-0.1.0/tests/test_manifest_contract.py +15 -0
- dex_connector_algolia-0.1.0/tests/test_operations.py +229 -0
- dex_connector_algolia-0.1.0/uv.lock +932 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dex-connector-algolia
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Algolia connector for Dex Flows
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: dex-connectors-sdk==0.1.0
|
|
8
|
+
Requires-Dist: dex-python-sdk>=1.28.0
|
|
9
|
+
Requires-Dist: httpx>=0.27
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# Algolia connector for Python
|
|
13
|
+
|
|
14
|
+
Install with `pip install dex-connector-algolia==0.1.0`. The package requires
|
|
15
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
16
|
+
manifest operations `search` (Query) and `saveObject` (Mutation) with the same
|
|
17
|
+
branches, defaults, and failure classification as the Go module.
|
|
18
|
+
|
|
19
|
+
## Operations
|
|
20
|
+
|
|
21
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `search` | `new_search_step` | `searched` | `providerRejected`, `invalidResponse`, `defect` |
|
|
24
|
+
| `saveObject` | `new_save_object_step` | `saved` | `providerRejected`, `invalidResponse`, `defect` |
|
|
25
|
+
|
|
26
|
+
`search` returns Algolia's hit count and each hit's `objectID` and optional
|
|
27
|
+
`name`; `hitsPerPage` ranges from 0 (Algolia's default) through 1000.
|
|
28
|
+
`saveObject` replaces the whole record at its stable `objectID`, so a retry
|
|
29
|
+
writes the same absolute value; only a response echoing the `objectID` with a
|
|
30
|
+
positive `taskID` selects `saved`. Indexing completes asynchronously.
|
|
31
|
+
|
|
32
|
+
Both operations retry on 408, 409, 429, 5xx, and transport failures; 401 and
|
|
33
|
+
403 select `providerRejected` with authentication and authorization kinds.
|
|
34
|
+
With the default base URL, each call targets `https://<application ID>.algolia.net/1`.
|
|
35
|
+
|
|
36
|
+
## Authorization
|
|
37
|
+
|
|
38
|
+
Create a Dex Web connection named `algolia` as described in the
|
|
39
|
+
[connector README](../README.md#authorization). The API key travels only in
|
|
40
|
+
the `x-algolia-api-key` header and is resolved for every attempt.
|
|
41
|
+
|
|
42
|
+
## Example
|
|
43
|
+
|
|
44
|
+
[`examples/index_search`](examples/index_search/flow.py) searches one index
|
|
45
|
+
for up to ten hits. Run the Worker with
|
|
46
|
+
`uv run python -m examples.index_search.worker` after `dexcli dev` has loaded
|
|
47
|
+
a project containing the `algolia` connection.
|
|
48
|
+
|
|
49
|
+
## Tests
|
|
50
|
+
|
|
51
|
+
`algoliamock.Mock` scripts both operations and defaults to the manifest
|
|
52
|
+
answers. Run `uv run pytest tests` for provider parity and manifest contract
|
|
53
|
+
checks.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Algolia connector for Python
|
|
2
|
+
|
|
3
|
+
Install with `pip install dex-connector-algolia==0.1.0`. The package requires
|
|
4
|
+
`dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and implements the
|
|
5
|
+
manifest operations `search` (Query) and `saveObject` (Mutation) with the same
|
|
6
|
+
branches, defaults, and failure classification as the Go module.
|
|
7
|
+
|
|
8
|
+
## Operations
|
|
9
|
+
|
|
10
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| `search` | `new_search_step` | `searched` | `providerRejected`, `invalidResponse`, `defect` |
|
|
13
|
+
| `saveObject` | `new_save_object_step` | `saved` | `providerRejected`, `invalidResponse`, `defect` |
|
|
14
|
+
|
|
15
|
+
`search` returns Algolia's hit count and each hit's `objectID` and optional
|
|
16
|
+
`name`; `hitsPerPage` ranges from 0 (Algolia's default) through 1000.
|
|
17
|
+
`saveObject` replaces the whole record at its stable `objectID`, so a retry
|
|
18
|
+
writes the same absolute value; only a response echoing the `objectID` with a
|
|
19
|
+
positive `taskID` selects `saved`. Indexing completes asynchronously.
|
|
20
|
+
|
|
21
|
+
Both operations retry on 408, 409, 429, 5xx, and transport failures; 401 and
|
|
22
|
+
403 select `providerRejected` with authentication and authorization kinds.
|
|
23
|
+
With the default base URL, each call targets `https://<application ID>.algolia.net/1`.
|
|
24
|
+
|
|
25
|
+
## Authorization
|
|
26
|
+
|
|
27
|
+
Create a Dex Web connection named `algolia` as described in the
|
|
28
|
+
[connector README](../README.md#authorization). The API key travels only in
|
|
29
|
+
the `x-algolia-api-key` header and is resolved for every attempt.
|
|
30
|
+
|
|
31
|
+
## Example
|
|
32
|
+
|
|
33
|
+
[`examples/index_search`](examples/index_search/flow.py) searches one index
|
|
34
|
+
for up to ten hits. Run the Worker with
|
|
35
|
+
`uv run python -m examples.index_search.worker` after `dexcli dev` has loaded
|
|
36
|
+
a project containing the `algolia` connection.
|
|
37
|
+
|
|
38
|
+
## Tests
|
|
39
|
+
|
|
40
|
+
`algoliamock.Mock` scripts both operations and defaults to the manifest
|
|
41
|
+
answers. Run `uv run pytest tests` for provider parity and manifest contract
|
|
42
|
+
checks.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Runnable Algolia Flow that searches one index."""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field, replace
|
|
6
|
+
|
|
7
|
+
import dex
|
|
8
|
+
from dex_connectors.sdk import QueryResult
|
|
9
|
+
|
|
10
|
+
from dex_connectors.algolia import Connection, SearchInput, SearchPage, new_search_step
|
|
11
|
+
|
|
12
|
+
FLOW_TYPE = "AlgoliaIndexSearch"
|
|
13
|
+
CONNECTION_NAME = "algolia"
|
|
14
|
+
_SEARCH_STEP_TYPE = "SearchAlgoliaIndex"
|
|
15
|
+
_HITS_PER_PAGE = 10
|
|
16
|
+
_OUTCOME = dex.Attribute("algolia-index-search-outcome", SearchPage)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class IndexSearchRequest:
|
|
21
|
+
"""An existing index in the connected application and a search phrase; blank matches all records."""
|
|
22
|
+
|
|
23
|
+
index_name: str = field(metadata={"dex_json_name": "indexName"})
|
|
24
|
+
query: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def map_to_search_input(request: IndexSearchRequest) -> SearchInput:
|
|
28
|
+
"""Search the validated index for one bounded page."""
|
|
29
|
+
return SearchInput(index_name=request.index_name, query=request.query, hits_per_page=_HITS_PER_PAGE)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# dex:group group-id:index-search group-label:"Index search"
|
|
33
|
+
# dex:explanation text:"Validate the index name before searching Algolia."
|
|
34
|
+
class _Start(dex.Step[IndexSearchRequest]):
|
|
35
|
+
def get_step_type(self) -> str:
|
|
36
|
+
return "StartAlgoliaIndexSearch"
|
|
37
|
+
|
|
38
|
+
def execute(self, context: dex.Context, input: IndexSearchRequest) -> dex.StepDecision:
|
|
39
|
+
request = replace(input, index_name=input.index_name.strip())
|
|
40
|
+
if not request.index_name:
|
|
41
|
+
return dex.force_fail("indexName is required")
|
|
42
|
+
return dex.go_to(dex.StepRef(_SEARCH_STEP_TYPE), request)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# dex:group group-id:index-search group-label:"Index search"
|
|
46
|
+
# dex:explanation text:"Complete with the matching record identities."
|
|
47
|
+
class _Searched(dex.Step[QueryResult[SearchPage]]):
|
|
48
|
+
def get_step_type(self) -> str:
|
|
49
|
+
return "AlgoliaHitsFound"
|
|
50
|
+
|
|
51
|
+
def execute(self, context: dex.Context, input: QueryResult[SearchPage]) -> dex.StepDecision:
|
|
52
|
+
_OUTCOME.set(context, input.value)
|
|
53
|
+
return dex.graceful_complete(input.value)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class IndexSearchFlow(dex.Flow[IndexSearchRequest]):
|
|
57
|
+
"""Search one Algolia index through the static algolia connection."""
|
|
58
|
+
|
|
59
|
+
def __init__(self, connection: Connection) -> None:
|
|
60
|
+
if connection.reference.name != CONNECTION_NAME:
|
|
61
|
+
raise ValueError("index search requires the static algolia connection")
|
|
62
|
+
self._start = _Start()
|
|
63
|
+
self._searched = _Searched()
|
|
64
|
+
self._search = new_search_step(
|
|
65
|
+
step_type=_SEARCH_STEP_TYPE,
|
|
66
|
+
step_input_type=IndexSearchRequest,
|
|
67
|
+
annotations={
|
|
68
|
+
"groupId": "index-search",
|
|
69
|
+
"groupLabel": "Index search",
|
|
70
|
+
"explanation": "Search the connected Algolia index for matching records.",
|
|
71
|
+
},
|
|
72
|
+
connection=connection,
|
|
73
|
+
connection_name=CONNECTION_NAME,
|
|
74
|
+
map_to_operation_input=map_to_search_input,
|
|
75
|
+
searched=self._searched,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def get_flow_type(self) -> str:
|
|
79
|
+
"""Return the stable Flow identity used in Dex Web."""
|
|
80
|
+
return FLOW_TYPE
|
|
81
|
+
|
|
82
|
+
def get_steps(self) -> dex.StepList[IndexSearchRequest]:
|
|
83
|
+
"""Register the start, search, and completion Steps."""
|
|
84
|
+
return dex.StepList.start_step(self._start).other_steps(self._search, self._searched)
|
|
85
|
+
|
|
86
|
+
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
87
|
+
"""Persist the hit page."""
|
|
88
|
+
return dex.PersistenceSchema.of(_OUTCOME)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Worker entrypoint for the Algolia index search Flow."""
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import dex
|
|
11
|
+
from dex_connectors.sdk.projectconfig import load_from_environment
|
|
12
|
+
|
|
13
|
+
from dex_connectors.algolia import new_project_connection
|
|
14
|
+
|
|
15
|
+
from .flow import CONNECTION_NAME, IndexSearchFlow
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run_worker() -> None:
|
|
19
|
+
"""Load the named Dex Web connection and search indexes."""
|
|
20
|
+
project = await load_from_environment()
|
|
21
|
+
connection = None
|
|
22
|
+
cache = None
|
|
23
|
+
try:
|
|
24
|
+
connection = new_project_connection(project, CONNECTION_NAME)
|
|
25
|
+
registry = dex.Registry([IndexSearchFlow(connection)], allow_async_handlers=True)
|
|
26
|
+
cache_directory = os.environ.get(
|
|
27
|
+
"DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-algolia-index-search-blobs")
|
|
28
|
+
)
|
|
29
|
+
cache = dex.open_blob_cache(dex.BlobCacheConfig(cache_directory, 1 << 30))
|
|
30
|
+
worker = dex.AsyncWorker(
|
|
31
|
+
registry,
|
|
32
|
+
cache,
|
|
33
|
+
dex.WorkerOptions(server_address=os.environ.get("DEX_FLOW_SERVICE_ADDRESS", "localhost:8801")),
|
|
34
|
+
)
|
|
35
|
+
async with worker:
|
|
36
|
+
await worker.start()
|
|
37
|
+
finally:
|
|
38
|
+
if connection is not None:
|
|
39
|
+
await connection.aclose()
|
|
40
|
+
if cache is not None:
|
|
41
|
+
cache.close()
|
|
42
|
+
await project.close()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def main() -> None:
|
|
46
|
+
"""Run until the Worker stops or the process receives an interrupt."""
|
|
47
|
+
asyncio.run(run_worker())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dex-connector-algolia"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Algolia connector for Dex Flows"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["dex-connectors-sdk==0.1.0", "dex-python-sdk>=1.28.0", "httpx>=0.27"]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = ["pytest>=8", "pytest-asyncio>=0.24", "mypy>=1.13", "ruff>=0.7", "pyyaml>=6", "types-PyYAML>=6"]
|
|
16
|
+
|
|
17
|
+
[tool.hatch.build.targets.wheel]
|
|
18
|
+
packages = ["src/dex_connectors"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
pythonpath = [".", "src"]
|
|
23
|
+
asyncio_mode = "auto"
|
|
24
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
25
|
+
|
|
26
|
+
[tool.mypy]
|
|
27
|
+
python_version = "3.11"
|
|
28
|
+
strict = true
|
|
29
|
+
mypy_path = "src"
|
|
30
|
+
explicit_package_bases = true
|
|
31
|
+
|
|
32
|
+
[tool.ruff]
|
|
33
|
+
target-version = "py311"
|
|
34
|
+
line-length = 110
|
|
35
|
+
|
|
36
|
+
[tool.ruff.lint]
|
|
37
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Algolia index search and record replacement operations for Dex connector Steps."""
|
|
4
|
+
|
|
5
|
+
from .client import (
|
|
6
|
+
CONNECTOR_ID,
|
|
7
|
+
DEFECT,
|
|
8
|
+
INVALID_RESPONSE,
|
|
9
|
+
PROVIDER_REJECTED,
|
|
10
|
+
PUBLIC_HOST_TEMPLATE,
|
|
11
|
+
SAVE_OBJECT_DEFINITION,
|
|
12
|
+
SAVED,
|
|
13
|
+
SEARCH_DEFINITION,
|
|
14
|
+
SEARCHED,
|
|
15
|
+
Client,
|
|
16
|
+
Config,
|
|
17
|
+
Credentials,
|
|
18
|
+
SaveObjectOperation,
|
|
19
|
+
SearchOperation,
|
|
20
|
+
)
|
|
21
|
+
from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
|
|
22
|
+
from .models import SavedObject, SaveObjectInput, SearchHit, SearchInput, SearchPage
|
|
23
|
+
from .steps import new_save_object_step, new_search_step
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"Client",
|
|
27
|
+
"Config",
|
|
28
|
+
"Connection",
|
|
29
|
+
"Credentials",
|
|
30
|
+
"SaveObjectInput",
|
|
31
|
+
"SaveObjectOperation",
|
|
32
|
+
"SavedObject",
|
|
33
|
+
"SearchHit",
|
|
34
|
+
"SearchInput",
|
|
35
|
+
"SearchOperation",
|
|
36
|
+
"SearchPage",
|
|
37
|
+
"new_connection",
|
|
38
|
+
"new_connection_with_operations",
|
|
39
|
+
"new_project_connection",
|
|
40
|
+
"new_save_object_step",
|
|
41
|
+
"new_search_step",
|
|
42
|
+
"CONNECTOR_ID",
|
|
43
|
+
"DEFECT",
|
|
44
|
+
"INVALID_RESPONSE",
|
|
45
|
+
"PROVIDER_REJECTED",
|
|
46
|
+
"PUBLIC_HOST_TEMPLATE",
|
|
47
|
+
"SAVE_OBJECT_DEFINITION",
|
|
48
|
+
"SAVED",
|
|
49
|
+
"SEARCH_DEFINITION",
|
|
50
|
+
"SEARCHED",
|
|
51
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import SecretString
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class Config:
|
|
12
|
+
|
|
13
|
+
base_url: str = "https://algolia.net/1"
|
|
14
|
+
timeout_seconds: float = 15
|
|
15
|
+
max_response_bytes: int = 2097152
|
|
16
|
+
|
|
17
|
+
setattr(Config, '__module__', 'dex_connectors.algolia.client')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Credentials:
|
|
22
|
+
|
|
23
|
+
application_id: str
|
|
24
|
+
api_key: SecretString
|
|
25
|
+
|
|
26
|
+
setattr(Credentials, '__module__', 'dex_connectors.algolia.client')
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
from dex_connectors.sdk import ConnectionRef, Mutation, Query, Call, SecretString
|
|
9
|
+
from dex_connectors.sdk.projectconfig import ConnectionKey, LoadedProject
|
|
10
|
+
|
|
11
|
+
from ..client import Client, Config, Credentials
|
|
12
|
+
from .definitions import CONNECTOR_ID, SAVE_OBJECT_DEFINITION, SEARCH_DEFINITION
|
|
13
|
+
from ..models import SaveObjectInput, SavedObject, SearchInput, SearchPage
|
|
14
|
+
|
|
15
|
+
class Connection:
|
|
16
|
+
|
|
17
|
+
__slots__ = ('_save_object', '_search', '_client', 'reference')
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
reference: ConnectionRef,
|
|
21
|
+
save_object: Mutation[SaveObjectInput, SavedObject],
|
|
22
|
+
search: Query[SearchInput, SearchPage],
|
|
23
|
+
client: Client | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
reference.validate()
|
|
26
|
+
if reference.provider != CONNECTOR_ID or save_object is None or search is None:
|
|
27
|
+
raise ValueError("Algolia connection provider and operations are required")
|
|
28
|
+
if save_object.definition().operation != SAVE_OBJECT_DEFINITION.operation:
|
|
29
|
+
raise ValueError("Algolia connection operation identity differs")
|
|
30
|
+
if search.definition().operation != SEARCH_DEFINITION.operation:
|
|
31
|
+
raise ValueError("Algolia connection operation identity differs")
|
|
32
|
+
self.reference = reference
|
|
33
|
+
self._save_object = save_object
|
|
34
|
+
self._search = search
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
def save_object(self) -> Mutation[SaveObjectInput, SavedObject]:
|
|
38
|
+
return self._save_object
|
|
39
|
+
|
|
40
|
+
def search(self) -> Query[SearchInput, SearchPage]:
|
|
41
|
+
return self._search
|
|
42
|
+
|
|
43
|
+
async def aclose(self) -> None:
|
|
44
|
+
if self._client is not None:
|
|
45
|
+
await self._client.aclose()
|
|
46
|
+
|
|
47
|
+
def __repr__(self) -> str:
|
|
48
|
+
return "algolia.Connection([REDACTED])"
|
|
49
|
+
|
|
50
|
+
def __reduce__(self) -> tuple[object, ...]:
|
|
51
|
+
raise TypeError("Algolia connections cannot be serialized")
|
|
52
|
+
|
|
53
|
+
setattr(Connection, '__module__', 'dex_connectors.algolia.connection')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def new_connection(client: Client, reference: ConnectionRef) -> Connection:
|
|
57
|
+
if client is None:
|
|
58
|
+
raise ValueError("Algolia client is required")
|
|
59
|
+
return Connection(reference, client.save_object(), client.search(), client)
|
|
60
|
+
|
|
61
|
+
setattr(new_connection, '__module__', 'dex_connectors.algolia.connection')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def new_connection_with_operations(
|
|
65
|
+
save_object: Mutation[SaveObjectInput, SavedObject],
|
|
66
|
+
search: Query[SearchInput, SearchPage],
|
|
67
|
+
reference: ConnectionRef,
|
|
68
|
+
) -> Connection:
|
|
69
|
+
return Connection(reference, save_object, search)
|
|
70
|
+
|
|
71
|
+
setattr(new_connection_with_operations, '__module__', 'dex_connectors.algolia.connection')
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class _ProjectCredentialSource:
|
|
75
|
+
|
|
76
|
+
def __init__(self, project: LoadedProject, key: ConnectionKey) -> None:
|
|
77
|
+
self.project = project
|
|
78
|
+
self.key = key
|
|
79
|
+
|
|
80
|
+
async def resolve(self, call: Call) -> Credentials:
|
|
81
|
+
material = await self.project.resolve_credential(self.key, str(call.id))
|
|
82
|
+
application_id = material.get("application_id")
|
|
83
|
+
if not isinstance(application_id, str) or not application_id:
|
|
84
|
+
raise ValueError("Algolia credential application_id is required")
|
|
85
|
+
api_key = material.get("api_key")
|
|
86
|
+
if not isinstance(api_key, str) or not api_key:
|
|
87
|
+
raise ValueError("Algolia credential api_key is required")
|
|
88
|
+
return Credentials(application_id, SecretString(api_key))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def new_project_connection(
|
|
92
|
+
project: LoadedProject, connection_name: str, http_client: httpx.AsyncClient | None = None
|
|
93
|
+
) -> Connection:
|
|
94
|
+
if project is None:
|
|
95
|
+
raise ValueError("Algolia connection requires loaded project configuration")
|
|
96
|
+
reference = ConnectionRef(CONNECTOR_ID, connection_name)
|
|
97
|
+
reference.validate()
|
|
98
|
+
key = ConnectionKey(CONNECTOR_ID, connection_name)
|
|
99
|
+
matching = [
|
|
100
|
+
entry
|
|
101
|
+
for entry in project.configuration.connections
|
|
102
|
+
if entry.connector_id == CONNECTOR_ID and entry.connection_name == connection_name
|
|
103
|
+
]
|
|
104
|
+
if len(matching) != 1:
|
|
105
|
+
raise ValueError("Algolia project connection is missing or duplicated")
|
|
106
|
+
if matching[0].provider != CONNECTOR_ID:
|
|
107
|
+
raise ValueError("Algolia project connection provider differs")
|
|
108
|
+
config = _decode_config(matching[0].configuration)
|
|
109
|
+
client = Client(config, _ProjectCredentialSource(project, key), http_client)
|
|
110
|
+
return new_connection(client, reference)
|
|
111
|
+
|
|
112
|
+
setattr(new_project_connection, '__module__', 'dex_connectors.algolia.connection')
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _decode_config(value: dict[str, Any]) -> Config:
|
|
116
|
+
if set(value) - {"baseUrl", "timeout", "maxResponseBytes"}:
|
|
117
|
+
raise ValueError("Algolia configuration has unknown fields")
|
|
118
|
+
config_arguments: dict[str, Any] = {}
|
|
119
|
+
if "baseUrl" in value:
|
|
120
|
+
base_url = value["baseUrl"]
|
|
121
|
+
if not (isinstance(base_url, str)):
|
|
122
|
+
raise ValueError("Algolia configuration baseUrl has an invalid type")
|
|
123
|
+
config_arguments['base_url'] = base_url
|
|
124
|
+
if "timeout" in value:
|
|
125
|
+
timeout = value["timeout"]
|
|
126
|
+
if not (type(timeout) is int):
|
|
127
|
+
raise ValueError("Algolia configuration timeout has an invalid type")
|
|
128
|
+
config_arguments['timeout_seconds'] = timeout / 1_000_000_000
|
|
129
|
+
if "maxResponseBytes" in value:
|
|
130
|
+
max_response_bytes = value["maxResponseBytes"]
|
|
131
|
+
if not (type(max_response_bytes) is int):
|
|
132
|
+
raise ValueError("Algolia configuration maxResponseBytes has an invalid type")
|
|
133
|
+
config_arguments['max_response_bytes'] = max_response_bytes
|
|
134
|
+
return Config(**config_arguments)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import (
|
|
8
|
+
BranchDefinition,
|
|
9
|
+
MutationDefinition,
|
|
10
|
+
OperationRef,
|
|
11
|
+
QueryDefinition,
|
|
12
|
+
RetryPolicy,
|
|
13
|
+
StepDefaults,
|
|
14
|
+
default_connector_step_execute_retry_policy,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
CONNECTOR_ID = "algolia"
|
|
18
|
+
PROVIDER = "algolia"
|
|
19
|
+
SAVE_OBJECT_OPERATION_ID = "saveObject"
|
|
20
|
+
SEARCH_OPERATION_ID = "search"
|
|
21
|
+
DEFECT = "defect"
|
|
22
|
+
INVALID_RESPONSE = "invalidResponse"
|
|
23
|
+
PROVIDER_REJECTED = "providerRejected"
|
|
24
|
+
SAVED = "saved"
|
|
25
|
+
SEARCHED = "searched"
|
|
26
|
+
|
|
27
|
+
SAVE_OBJECT_DEFINITION = MutationDefinition(
|
|
28
|
+
OperationRef(CONNECTOR_ID, SAVE_OBJECT_OPERATION_ID),
|
|
29
|
+
(
|
|
30
|
+
BranchDefinition(SAVED, "Algolia accepted the replacement task.", False),
|
|
31
|
+
BranchDefinition(PROVIDER_REJECTED, "Algolia rejected the record or key.", True),
|
|
32
|
+
BranchDefinition(INVALID_RESPONSE, "Algolia returned malformed or oversized data.", True),
|
|
33
|
+
BranchDefinition(DEFECT, "Local input or credentials are invalid.", True),
|
|
34
|
+
),
|
|
35
|
+
StepDefaults(
|
|
36
|
+
timedelta(seconds=30),
|
|
37
|
+
2,
|
|
38
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
SEARCH_DEFINITION = QueryDefinition(
|
|
43
|
+
OperationRef(CONNECTOR_ID, SEARCH_OPERATION_ID),
|
|
44
|
+
(
|
|
45
|
+
BranchDefinition(SEARCHED, "Algolia returned a hit page, possibly empty.", False),
|
|
46
|
+
BranchDefinition(PROVIDER_REJECTED, "Algolia rejected the search or key.", True),
|
|
47
|
+
BranchDefinition(INVALID_RESPONSE, "Algolia returned malformed or oversized data.", True),
|
|
48
|
+
BranchDefinition(DEFECT, "Local input or credentials are invalid.", True),
|
|
49
|
+
),
|
|
50
|
+
StepDefaults(
|
|
51
|
+
timedelta(seconds=30),
|
|
52
|
+
2,
|
|
53
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
54
|
+
),
|
|
55
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# ruff: noqa
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
|
|
7
|
+
from dex_connectors.sdk import ConnectionRef, Failure, FailureKind
|
|
8
|
+
from dex_connectors.sdk.connectormock import Case, Mutation, Query
|
|
9
|
+
|
|
10
|
+
from ..connection import Connection, new_connection_with_operations
|
|
11
|
+
from ..models import SaveObjectInput, SavedObject, SearchHit, SearchInput, SearchPage
|
|
12
|
+
from .definitions import CONNECTOR_ID, DEFECT, INVALID_RESPONSE, PROVIDER_REJECTED, SAVED, SAVE_OBJECT_DEFINITION, SEARCHED, SEARCH_DEFINITION
|
|
13
|
+
|
|
14
|
+
def save_object_saved(value: SavedObject, failure: Failure | None = None) -> Case[SavedObject]:
|
|
15
|
+
return Case.branch_result(SAVED, value, failure)
|
|
16
|
+
|
|
17
|
+
setattr(save_object_saved, '__module__', "dex_connectors.algolia.algoliamock")
|
|
18
|
+
|
|
19
|
+
def save_object_provider_rejected(value: SavedObject, failure: Failure | None = None) -> Case[SavedObject]:
|
|
20
|
+
return Case.branch_result(PROVIDER_REJECTED, value, failure)
|
|
21
|
+
|
|
22
|
+
setattr(save_object_provider_rejected, '__module__', "dex_connectors.algolia.algoliamock")
|
|
23
|
+
|
|
24
|
+
def save_object_invalid_response(value: SavedObject, failure: Failure | None = None) -> Case[SavedObject]:
|
|
25
|
+
return Case.branch_result(INVALID_RESPONSE, value, failure)
|
|
26
|
+
|
|
27
|
+
setattr(save_object_invalid_response, '__module__', "dex_connectors.algolia.algoliamock")
|
|
28
|
+
|
|
29
|
+
def save_object_defect(value: SavedObject, failure: Failure | None = None) -> Case[SavedObject]:
|
|
30
|
+
return Case.branch_result(DEFECT, value, failure)
|
|
31
|
+
|
|
32
|
+
setattr(save_object_defect, '__module__', "dex_connectors.algolia.algoliamock")
|
|
33
|
+
|
|
34
|
+
def save_object_retry(failure: Failure, after_seconds: float = 0) -> Case[SavedObject]:
|
|
35
|
+
return Case.retry(failure, after_seconds)
|
|
36
|
+
|
|
37
|
+
setattr(save_object_retry, '__module__', "dex_connectors.algolia.algoliamock")
|
|
38
|
+
|
|
39
|
+
def save_object_saved_default_object() -> Case[SavedObject]:
|
|
40
|
+
return Case.branch_result(SAVED, SavedObject(object_id='product-1', task_id=1514562690001))
|
|
41
|
+
|
|
42
|
+
setattr(save_object_saved_default_object, '__module__', "dex_connectors.algolia.algoliamock")
|
|
43
|
+
|
|
44
|
+
def search_searched(value: SearchPage, failure: Failure | None = None) -> Case[SearchPage]:
|
|
45
|
+
return Case.branch_result(SEARCHED, value, failure)
|
|
46
|
+
|
|
47
|
+
setattr(search_searched, '__module__', "dex_connectors.algolia.algoliamock")
|
|
48
|
+
|
|
49
|
+
def search_provider_rejected(value: SearchPage, failure: Failure | None = None) -> Case[SearchPage]:
|
|
50
|
+
return Case.branch_result(PROVIDER_REJECTED, value, failure)
|
|
51
|
+
|
|
52
|
+
setattr(search_provider_rejected, '__module__', "dex_connectors.algolia.algoliamock")
|
|
53
|
+
|
|
54
|
+
def search_invalid_response(value: SearchPage, failure: Failure | None = None) -> Case[SearchPage]:
|
|
55
|
+
return Case.branch_result(INVALID_RESPONSE, value, failure)
|
|
56
|
+
|
|
57
|
+
setattr(search_invalid_response, '__module__', "dex_connectors.algolia.algoliamock")
|
|
58
|
+
|
|
59
|
+
def search_defect(value: SearchPage, failure: Failure | None = None) -> Case[SearchPage]:
|
|
60
|
+
return Case.branch_result(DEFECT, value, failure)
|
|
61
|
+
|
|
62
|
+
setattr(search_defect, '__module__', "dex_connectors.algolia.algoliamock")
|
|
63
|
+
|
|
64
|
+
def search_retry(failure: Failure, after_seconds: float = 0) -> Case[SearchPage]:
|
|
65
|
+
return Case.retry(failure, after_seconds)
|
|
66
|
+
|
|
67
|
+
setattr(search_retry, '__module__', "dex_connectors.algolia.algoliamock")
|
|
68
|
+
|
|
69
|
+
def search_searched_default_index() -> Case[SearchPage]:
|
|
70
|
+
return Case.branch_result(SEARCHED, SearchPage(nb_hits=1, hits=(SearchHit(object_id='product-1', name='Meridian notebook'),)))
|
|
71
|
+
|
|
72
|
+
setattr(search_searched_default_index, '__module__', "dex_connectors.algolia.algoliamock")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Mock:
|
|
76
|
+
|
|
77
|
+
def __init__(self, connection_name: str = "algolia") -> None:
|
|
78
|
+
self.save_object_script: Mutation[SaveObjectInput, SavedObject] = Mutation(SAVE_OBJECT_DEFINITION)
|
|
79
|
+
self.save_object_script.default(save_object_saved_default_object())
|
|
80
|
+
self.search_script: Query[SearchInput, SearchPage] = Query(SEARCH_DEFINITION)
|
|
81
|
+
self.search_script.default(search_searched_default_index())
|
|
82
|
+
self._connection = new_connection_with_operations(
|
|
83
|
+
save_object=self.save_object_script,
|
|
84
|
+
search=self.search_script,
|
|
85
|
+
reference=ConnectionRef(CONNECTOR_ID, connection_name),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
def connection(self) -> Connection:
|
|
89
|
+
return self._connection
|
|
90
|
+
|
|
91
|
+
setattr(Mock, '__module__', "dex_connectors.algolia.algoliamock")
|
|
92
|
+
|
|
93
|
+
__all__ = [
|
|
94
|
+
"Mock",
|
|
95
|
+
"save_object_saved",
|
|
96
|
+
"save_object_provider_rejected",
|
|
97
|
+
"save_object_invalid_response",
|
|
98
|
+
"save_object_defect",
|
|
99
|
+
"save_object_retry",
|
|
100
|
+
"save_object_saved_default_object",
|
|
101
|
+
"search_searched",
|
|
102
|
+
"search_provider_rejected",
|
|
103
|
+
"search_invalid_response",
|
|
104
|
+
"search_defect",
|
|
105
|
+
"search_retry",
|
|
106
|
+
"search_searched_default_index",
|
|
107
|
+
]
|