dex-connector-alphavantage 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_alphavantage-0.1.0/.gitignore +6 -0
- dex_connector_alphavantage-0.1.0/PKG-INFO +51 -0
- dex_connector_alphavantage-0.1.0/README.md +40 -0
- dex_connector_alphavantage-0.1.0/examples/demo/__init__.py +3 -0
- dex_connector_alphavantage-0.1.0/examples/demo/flow.py +79 -0
- dex_connector_alphavantage-0.1.0/examples/demo/worker.py +51 -0
- dex_connector_alphavantage-0.1.0/pyproject.toml +37 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/__init__.py +48 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/_generated/__init__.py +2 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/_generated/config.py +25 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/_generated/connection.py +131 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/_generated/definitions.py +57 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/_generated/mock.py +119 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/_generated/models.py +74 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/_generated/steps.py +134 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/alphavantagemock/__init__.py +55 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/client.py +362 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/connection.py +45 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/dex-connector.json +87 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/models.py +20 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/py.typed +0 -0
- dex_connector_alphavantage-0.1.0/src/dex_connectors/alphavantage/steps.py +36 -0
- dex_connector_alphavantage-0.1.0/tests/test_manifest_contract.py +15 -0
- dex_connector_alphavantage-0.1.0/tests/test_operations.py +147 -0
- dex_connector_alphavantage-0.1.0/uv.lock +932 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dex-connector-alphavantage
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Alpha Vantage 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
|
+
# Alpha Vantage connector for Python
|
|
13
|
+
|
|
14
|
+
Install with `pip install dex-connector-alphavantage==0.1.0`. The package
|
|
15
|
+
requires `dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and
|
|
16
|
+
implements the manifest Queries `getGlobalQuote` and `getTimeSeriesDaily` with
|
|
17
|
+
the same branches, defaults, and failure classification as the Go module.
|
|
18
|
+
|
|
19
|
+
## Operations
|
|
20
|
+
|
|
21
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
22
|
+
| --- | --- | --- | --- |
|
|
23
|
+
| `getGlobalQuote` | `new_get_global_quote_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
24
|
+
| `getTimeSeriesDaily` | `new_get_time_series_daily_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
25
|
+
|
|
26
|
+
Both take a ticker symbol of 1 to 25 letters, digits, `.`, `_`, or `-`.
|
|
27
|
+
Alpha Vantage reports problems in HTTP 200 bodies: a `Note`, or an
|
|
28
|
+
`Information` message about rate limits, call frequency, or requests per day,
|
|
29
|
+
retries as a rate limit; another `Information` or an `Error Message` selects
|
|
30
|
+
`providerRejected`; an empty quote or series selects `notFound`. HTTP 404 also
|
|
31
|
+
selects `notFound`; 408, 429, 5xx, and transport failures retry. The daily
|
|
32
|
+
series is returned newest day first.
|
|
33
|
+
|
|
34
|
+
## Authorization
|
|
35
|
+
|
|
36
|
+
Create a Dex Web connection named `alphavantage` as described in the
|
|
37
|
+
[connector README](../README.md#authorization). Alpha Vantage accepts the API
|
|
38
|
+
key only as the `apikey` query parameter, so the key is part of each request
|
|
39
|
+
URL; it is resolved for every attempt.
|
|
40
|
+
|
|
41
|
+
## Example
|
|
42
|
+
|
|
43
|
+
[`examples/demo`](examples/demo/flow.py) reads a quote and records it. Run the
|
|
44
|
+
Worker with `uv run python -m examples.demo.worker` after `dexcli dev` has
|
|
45
|
+
loaded a project containing the `alphavantage` connection.
|
|
46
|
+
|
|
47
|
+
## Tests
|
|
48
|
+
|
|
49
|
+
`alphavantagemock.Mock` scripts both Queries and defaults to the manifest
|
|
50
|
+
answers. Run `uv run pytest tests` for provider parity and manifest contract
|
|
51
|
+
checks.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Alpha Vantage connector for Python
|
|
2
|
+
|
|
3
|
+
Install with `pip install dex-connector-alphavantage==0.1.0`. The package
|
|
4
|
+
requires `dex-connectors-sdk==0.1.0` and `dex-python-sdk>=1.28.0`, and
|
|
5
|
+
implements the manifest Queries `getGlobalQuote` and `getTimeSeriesDaily` with
|
|
6
|
+
the same branches, defaults, and failure classification as the Go module.
|
|
7
|
+
|
|
8
|
+
## Operations
|
|
9
|
+
|
|
10
|
+
| Operation | Factory | Happy branch | Optional branches |
|
|
11
|
+
| --- | --- | --- | --- |
|
|
12
|
+
| `getGlobalQuote` | `new_get_global_quote_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
13
|
+
| `getTimeSeriesDaily` | `new_get_time_series_daily_step` | `found` | `notFound`, `providerRejected`, `invalidResponse`, `defect` |
|
|
14
|
+
|
|
15
|
+
Both take a ticker symbol of 1 to 25 letters, digits, `.`, `_`, or `-`.
|
|
16
|
+
Alpha Vantage reports problems in HTTP 200 bodies: a `Note`, or an
|
|
17
|
+
`Information` message about rate limits, call frequency, or requests per day,
|
|
18
|
+
retries as a rate limit; another `Information` or an `Error Message` selects
|
|
19
|
+
`providerRejected`; an empty quote or series selects `notFound`. HTTP 404 also
|
|
20
|
+
selects `notFound`; 408, 429, 5xx, and transport failures retry. The daily
|
|
21
|
+
series is returned newest day first.
|
|
22
|
+
|
|
23
|
+
## Authorization
|
|
24
|
+
|
|
25
|
+
Create a Dex Web connection named `alphavantage` as described in the
|
|
26
|
+
[connector README](../README.md#authorization). Alpha Vantage accepts the API
|
|
27
|
+
key only as the `apikey` query parameter, so the key is part of each request
|
|
28
|
+
URL; it is resolved for every attempt.
|
|
29
|
+
|
|
30
|
+
## Example
|
|
31
|
+
|
|
32
|
+
[`examples/demo`](examples/demo/flow.py) reads a quote and records it. Run the
|
|
33
|
+
Worker with `uv run python -m examples.demo.worker` after `dexcli dev` has
|
|
34
|
+
loaded a project containing the `alphavantage` connection.
|
|
35
|
+
|
|
36
|
+
## Tests
|
|
37
|
+
|
|
38
|
+
`alphavantagemock.Mock` scripts both Queries and defaults to the manifest
|
|
39
|
+
answers. Run `uv run pytest tests` for provider parity and manifest contract
|
|
40
|
+
checks.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Runnable Alpha Vantage demo Flow using the getGlobalQuote Query."""
|
|
4
|
+
|
|
5
|
+
import dex
|
|
6
|
+
from dex_connectors.sdk import QueryResult
|
|
7
|
+
|
|
8
|
+
from dex_connectors.alphavantage import (
|
|
9
|
+
Connection,
|
|
10
|
+
GlobalQuoteOutput,
|
|
11
|
+
StockInput,
|
|
12
|
+
new_get_global_quote_step,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
FLOW_TYPE = "AlphaVantageDemo"
|
|
16
|
+
CONNECTION_NAME = "alphavantage"
|
|
17
|
+
_OPERATION_STEP_TYPE = "CallGetGlobalQuote"
|
|
18
|
+
_OUTCOME = dex.Attribute("alphavantage-demo-outcome", GlobalQuoteOutput)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def map_to_operation_input(input: StockInput) -> StockInput:
|
|
22
|
+
"""Read the requested symbol unchanged."""
|
|
23
|
+
return input
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# dex:group group-id:demo group-label:"Demo"
|
|
27
|
+
# dex:explanation text:"Start the quote lookup with the requested symbol."
|
|
28
|
+
class _StartRequest(dex.Step[StockInput]):
|
|
29
|
+
def get_step_type(self) -> str:
|
|
30
|
+
return "StartGetGlobalQuote"
|
|
31
|
+
|
|
32
|
+
def execute(self, context: dex.Context, input: StockInput) -> dex.StepDecision:
|
|
33
|
+
return dex.go_to(dex.StepRef(_OPERATION_STEP_TYPE), input)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# dex:group group-id:demo group-label:"Demo"
|
|
37
|
+
# dex:explanation text:"Record the quote and complete the Flow."
|
|
38
|
+
class _CompleteResult(dex.Step[QueryResult[GlobalQuoteOutput]]):
|
|
39
|
+
def get_step_type(self) -> str:
|
|
40
|
+
return "CompleteGetGlobalQuote"
|
|
41
|
+
|
|
42
|
+
def execute(self, context: dex.Context, input: QueryResult[GlobalQuoteOutput]) -> dex.StepDecision:
|
|
43
|
+
_OUTCOME.set(context, input.value)
|
|
44
|
+
return dex.graceful_complete(input.value)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class DemoFlow(dex.Flow[StockInput]):
|
|
48
|
+
"""Read a stock quote through the static alphavantage connection."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, connection: Connection) -> None:
|
|
51
|
+
if connection.reference.name != CONNECTION_NAME:
|
|
52
|
+
raise ValueError("demo requires the static alphavantage connection")
|
|
53
|
+
self._start = _StartRequest()
|
|
54
|
+
self._complete = _CompleteResult()
|
|
55
|
+
self._read = new_get_global_quote_step(
|
|
56
|
+
step_type=_OPERATION_STEP_TYPE,
|
|
57
|
+
step_input_type=StockInput,
|
|
58
|
+
annotations={
|
|
59
|
+
"groupId": "provider",
|
|
60
|
+
"groupLabel": "Alpha Vantage",
|
|
61
|
+
"explanation": "Call the provider's getGlobalQuote operation.",
|
|
62
|
+
},
|
|
63
|
+
connection=connection,
|
|
64
|
+
connection_name=CONNECTION_NAME,
|
|
65
|
+
map_to_operation_input=map_to_operation_input,
|
|
66
|
+
found=self._complete,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def get_flow_type(self) -> str:
|
|
70
|
+
"""Return the stable Flow identity used in Dex Web."""
|
|
71
|
+
return FLOW_TYPE
|
|
72
|
+
|
|
73
|
+
def get_steps(self) -> dex.StepList[StockInput]:
|
|
74
|
+
"""Register the start, provider read, and completion Steps."""
|
|
75
|
+
return dex.StepList.start_step(self._start).other_steps(self._read, self._complete)
|
|
76
|
+
|
|
77
|
+
def get_persistence_schema(self) -> dex.PersistenceSchema:
|
|
78
|
+
"""Persist the quote outcome."""
|
|
79
|
+
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 Alpha Vantage demo 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.alphavantage import new_project_connection
|
|
14
|
+
|
|
15
|
+
from .flow import CONNECTION_NAME, DemoFlow
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def run_worker() -> None:
|
|
19
|
+
"""Load the named Dex Web connection and serve quote lookups."""
|
|
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([DemoFlow(connection)], allow_async_handlers=True)
|
|
26
|
+
cache_directory = os.environ.get(
|
|
27
|
+
"DEX_BLOB_CACHE_DIR", str(Path(tempfile.gettempdir()) / "dex-alphavantage-demo-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-alphavantage"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Alpha Vantage 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,48 @@
|
|
|
1
|
+
# Copyright (c) 2026 Super Durable
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
"""Alpha Vantage stock market Queries for Dex connector Steps."""
|
|
4
|
+
|
|
5
|
+
from .client import (
|
|
6
|
+
CONNECTOR_ID,
|
|
7
|
+
DEFECT,
|
|
8
|
+
FOUND,
|
|
9
|
+
GET_GLOBAL_QUOTE_DEFINITION,
|
|
10
|
+
GET_TIME_SERIES_DAILY_DEFINITION,
|
|
11
|
+
INVALID_RESPONSE,
|
|
12
|
+
NOT_FOUND,
|
|
13
|
+
PROVIDER_REJECTED,
|
|
14
|
+
Client,
|
|
15
|
+
Config,
|
|
16
|
+
Credentials,
|
|
17
|
+
GetGlobalQuoteOperation,
|
|
18
|
+
GetTimeSeriesDailyOperation,
|
|
19
|
+
)
|
|
20
|
+
from .connection import Connection, new_connection, new_connection_with_operations, new_project_connection
|
|
21
|
+
from .models import DailyPrice, DailySeriesOutput, GlobalQuoteOutput, StockInput
|
|
22
|
+
from .steps import new_get_global_quote_step, new_get_time_series_daily_step
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"Client",
|
|
26
|
+
"Config",
|
|
27
|
+
"Connection",
|
|
28
|
+
"Credentials",
|
|
29
|
+
"DailyPrice",
|
|
30
|
+
"DailySeriesOutput",
|
|
31
|
+
"GetGlobalQuoteOperation",
|
|
32
|
+
"GetTimeSeriesDailyOperation",
|
|
33
|
+
"GlobalQuoteOutput",
|
|
34
|
+
"StockInput",
|
|
35
|
+
"new_connection",
|
|
36
|
+
"new_connection_with_operations",
|
|
37
|
+
"new_get_global_quote_step",
|
|
38
|
+
"new_get_time_series_daily_step",
|
|
39
|
+
"new_project_connection",
|
|
40
|
+
"CONNECTOR_ID",
|
|
41
|
+
"DEFECT",
|
|
42
|
+
"FOUND",
|
|
43
|
+
"GET_GLOBAL_QUOTE_DEFINITION",
|
|
44
|
+
"GET_TIME_SERIES_DAILY_DEFINITION",
|
|
45
|
+
"INVALID_RESPONSE",
|
|
46
|
+
"NOT_FOUND",
|
|
47
|
+
"PROVIDER_REJECTED",
|
|
48
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
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://www.alphavantage.co"
|
|
14
|
+
timeout_seconds: float = 15
|
|
15
|
+
max_response_bytes: int = 1048576
|
|
16
|
+
|
|
17
|
+
setattr(Config, '__module__', 'dex_connectors.alphavantage.client')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True, slots=True)
|
|
21
|
+
class Credentials:
|
|
22
|
+
|
|
23
|
+
api_key: SecretString
|
|
24
|
+
|
|
25
|
+
setattr(Credentials, '__module__', 'dex_connectors.alphavantage.client')
|
|
@@ -0,0 +1,131 @@
|
|
|
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, 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, GET_GLOBAL_QUOTE_DEFINITION, GET_TIME_SERIES_DAILY_DEFINITION
|
|
13
|
+
from ..models import DailySeriesOutput, GlobalQuoteOutput, StockInput
|
|
14
|
+
|
|
15
|
+
class Connection:
|
|
16
|
+
|
|
17
|
+
__slots__ = ('_get_global_quote', '_get_time_series_daily', '_client', 'reference')
|
|
18
|
+
|
|
19
|
+
def __init__(self,
|
|
20
|
+
reference: ConnectionRef,
|
|
21
|
+
get_global_quote: Query[StockInput, GlobalQuoteOutput],
|
|
22
|
+
get_time_series_daily: Query[StockInput, DailySeriesOutput],
|
|
23
|
+
client: Client | None = None,
|
|
24
|
+
) -> None:
|
|
25
|
+
reference.validate()
|
|
26
|
+
if reference.provider != CONNECTOR_ID or get_global_quote is None or get_time_series_daily is None:
|
|
27
|
+
raise ValueError("Alpha Vantage connection provider and operations are required")
|
|
28
|
+
if get_global_quote.definition().operation != GET_GLOBAL_QUOTE_DEFINITION.operation:
|
|
29
|
+
raise ValueError("Alpha Vantage connection operation identity differs")
|
|
30
|
+
if get_time_series_daily.definition().operation != GET_TIME_SERIES_DAILY_DEFINITION.operation:
|
|
31
|
+
raise ValueError("Alpha Vantage connection operation identity differs")
|
|
32
|
+
self.reference = reference
|
|
33
|
+
self._get_global_quote = get_global_quote
|
|
34
|
+
self._get_time_series_daily = get_time_series_daily
|
|
35
|
+
self._client = client
|
|
36
|
+
|
|
37
|
+
def get_global_quote(self) -> Query[StockInput, GlobalQuoteOutput]:
|
|
38
|
+
return self._get_global_quote
|
|
39
|
+
|
|
40
|
+
def get_time_series_daily(self) -> Query[StockInput, DailySeriesOutput]:
|
|
41
|
+
return self._get_time_series_daily
|
|
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 "alphavantage.Connection([REDACTED])"
|
|
49
|
+
|
|
50
|
+
def __reduce__(self) -> tuple[object, ...]:
|
|
51
|
+
raise TypeError("Alpha Vantage connections cannot be serialized")
|
|
52
|
+
|
|
53
|
+
setattr(Connection, '__module__', 'dex_connectors.alphavantage.connection')
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def new_connection(client: Client, reference: ConnectionRef) -> Connection:
|
|
57
|
+
if client is None:
|
|
58
|
+
raise ValueError("Alpha Vantage client is required")
|
|
59
|
+
return Connection(reference, client.get_global_quote(), client.get_time_series_daily(), client)
|
|
60
|
+
|
|
61
|
+
setattr(new_connection, '__module__', 'dex_connectors.alphavantage.connection')
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def new_connection_with_operations(
|
|
65
|
+
get_global_quote: Query[StockInput, GlobalQuoteOutput],
|
|
66
|
+
get_time_series_daily: Query[StockInput, DailySeriesOutput],
|
|
67
|
+
reference: ConnectionRef,
|
|
68
|
+
) -> Connection:
|
|
69
|
+
return Connection(reference, get_global_quote, get_time_series_daily)
|
|
70
|
+
|
|
71
|
+
setattr(new_connection_with_operations, '__module__', 'dex_connectors.alphavantage.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
|
+
api_key = material.get("api_key")
|
|
83
|
+
if not isinstance(api_key, str) or not api_key:
|
|
84
|
+
raise ValueError("Alpha Vantage credential api_key is required")
|
|
85
|
+
return Credentials(SecretString(api_key))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def new_project_connection(
|
|
89
|
+
project: LoadedProject, connection_name: str, http_client: httpx.AsyncClient | None = None
|
|
90
|
+
) -> Connection:
|
|
91
|
+
if project is None:
|
|
92
|
+
raise ValueError("Alpha Vantage connection requires loaded project configuration")
|
|
93
|
+
reference = ConnectionRef(CONNECTOR_ID, connection_name)
|
|
94
|
+
reference.validate()
|
|
95
|
+
key = ConnectionKey(CONNECTOR_ID, connection_name)
|
|
96
|
+
matching = [
|
|
97
|
+
entry
|
|
98
|
+
for entry in project.configuration.connections
|
|
99
|
+
if entry.connector_id == CONNECTOR_ID and entry.connection_name == connection_name
|
|
100
|
+
]
|
|
101
|
+
if len(matching) != 1:
|
|
102
|
+
raise ValueError("Alpha Vantage project connection is missing or duplicated")
|
|
103
|
+
if matching[0].provider != CONNECTOR_ID:
|
|
104
|
+
raise ValueError("Alpha Vantage project connection provider differs")
|
|
105
|
+
config = _decode_config(matching[0].configuration)
|
|
106
|
+
client = Client(config, _ProjectCredentialSource(project, key), http_client)
|
|
107
|
+
return new_connection(client, reference)
|
|
108
|
+
|
|
109
|
+
setattr(new_project_connection, '__module__', 'dex_connectors.alphavantage.connection')
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _decode_config(value: dict[str, Any]) -> Config:
|
|
113
|
+
if set(value) - {"baseUrl", "timeout", "maxResponseBytes"}:
|
|
114
|
+
raise ValueError("Alpha Vantage configuration has unknown fields")
|
|
115
|
+
config_arguments: dict[str, Any] = {}
|
|
116
|
+
if "baseUrl" in value:
|
|
117
|
+
base_url = value["baseUrl"]
|
|
118
|
+
if not (isinstance(base_url, str)):
|
|
119
|
+
raise ValueError("Alpha Vantage configuration baseUrl has an invalid type")
|
|
120
|
+
config_arguments['base_url'] = base_url
|
|
121
|
+
if "timeout" in value:
|
|
122
|
+
timeout = value["timeout"]
|
|
123
|
+
if not (type(timeout) is int):
|
|
124
|
+
raise ValueError("Alpha Vantage configuration timeout has an invalid type")
|
|
125
|
+
config_arguments['timeout_seconds'] = timeout / 1_000_000_000
|
|
126
|
+
if "maxResponseBytes" in value:
|
|
127
|
+
max_response_bytes = value["maxResponseBytes"]
|
|
128
|
+
if not (type(max_response_bytes) is int):
|
|
129
|
+
raise ValueError("Alpha Vantage configuration maxResponseBytes has an invalid type")
|
|
130
|
+
config_arguments['max_response_bytes'] = max_response_bytes
|
|
131
|
+
return Config(**config_arguments)
|
|
@@ -0,0 +1,57 @@
|
|
|
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 = "alphavantage"
|
|
18
|
+
PROVIDER = "alphavantage"
|
|
19
|
+
GET_GLOBAL_QUOTE_OPERATION_ID = "getGlobalQuote"
|
|
20
|
+
GET_TIME_SERIES_DAILY_OPERATION_ID = "getTimeSeriesDaily"
|
|
21
|
+
DEFECT = "defect"
|
|
22
|
+
FOUND = "found"
|
|
23
|
+
INVALID_RESPONSE = "invalidResponse"
|
|
24
|
+
NOT_FOUND = "notFound"
|
|
25
|
+
PROVIDER_REJECTED = "providerRejected"
|
|
26
|
+
|
|
27
|
+
GET_GLOBAL_QUOTE_DEFINITION = QueryDefinition(
|
|
28
|
+
OperationRef(CONNECTOR_ID, GET_GLOBAL_QUOTE_OPERATION_ID),
|
|
29
|
+
(
|
|
30
|
+
BranchDefinition(FOUND, "Alpha Vantage returned the requested result.", False),
|
|
31
|
+
BranchDefinition(NOT_FOUND, "No market data exists for the requested symbol.", True),
|
|
32
|
+
BranchDefinition(PROVIDER_REJECTED, "The provider conclusively rejected the request.", True),
|
|
33
|
+
BranchDefinition(INVALID_RESPONSE, "The provider returned malformed or oversized data.", True),
|
|
34
|
+
BranchDefinition(DEFECT, "Local input or credentials are invalid.", True),
|
|
35
|
+
),
|
|
36
|
+
StepDefaults(
|
|
37
|
+
timedelta(seconds=30),
|
|
38
|
+
2,
|
|
39
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
GET_TIME_SERIES_DAILY_DEFINITION = QueryDefinition(
|
|
44
|
+
OperationRef(CONNECTOR_ID, GET_TIME_SERIES_DAILY_OPERATION_ID),
|
|
45
|
+
(
|
|
46
|
+
BranchDefinition(FOUND, "Alpha Vantage returned the requested result.", False),
|
|
47
|
+
BranchDefinition(NOT_FOUND, "No market data exists for the requested symbol.", True),
|
|
48
|
+
BranchDefinition(PROVIDER_REJECTED, "The provider conclusively rejected the request.", True),
|
|
49
|
+
BranchDefinition(INVALID_RESPONSE, "The provider returned malformed or oversized data.", True),
|
|
50
|
+
BranchDefinition(DEFECT, "Local input or credentials are invalid.", True),
|
|
51
|
+
),
|
|
52
|
+
StepDefaults(
|
|
53
|
+
timedelta(seconds=30),
|
|
54
|
+
2,
|
|
55
|
+
execute_retry=default_connector_step_execute_retry_policy(),
|
|
56
|
+
),
|
|
57
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
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, Query
|
|
9
|
+
|
|
10
|
+
from ..connection import Connection, new_connection_with_operations
|
|
11
|
+
from ..models import DailyPrice, DailySeriesOutput, GlobalQuoteOutput, StockInput
|
|
12
|
+
from .definitions import CONNECTOR_ID, DEFECT, FOUND, GET_GLOBAL_QUOTE_DEFINITION, GET_TIME_SERIES_DAILY_DEFINITION, INVALID_RESPONSE, NOT_FOUND, PROVIDER_REJECTED
|
|
13
|
+
|
|
14
|
+
def get_global_quote_found(value: GlobalQuoteOutput, failure: Failure | None = None) -> Case[GlobalQuoteOutput]:
|
|
15
|
+
return Case.branch_result(FOUND, value, failure)
|
|
16
|
+
|
|
17
|
+
setattr(get_global_quote_found, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
18
|
+
|
|
19
|
+
def get_global_quote_not_found(value: GlobalQuoteOutput, failure: Failure | None = None) -> Case[GlobalQuoteOutput]:
|
|
20
|
+
return Case.branch_result(NOT_FOUND, value, failure)
|
|
21
|
+
|
|
22
|
+
setattr(get_global_quote_not_found, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
23
|
+
|
|
24
|
+
def get_global_quote_provider_rejected(value: GlobalQuoteOutput, failure: Failure | None = None) -> Case[GlobalQuoteOutput]:
|
|
25
|
+
return Case.branch_result(PROVIDER_REJECTED, value, failure)
|
|
26
|
+
|
|
27
|
+
setattr(get_global_quote_provider_rejected, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
28
|
+
|
|
29
|
+
def get_global_quote_invalid_response(value: GlobalQuoteOutput, failure: Failure | None = None) -> Case[GlobalQuoteOutput]:
|
|
30
|
+
return Case.branch_result(INVALID_RESPONSE, value, failure)
|
|
31
|
+
|
|
32
|
+
setattr(get_global_quote_invalid_response, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
33
|
+
|
|
34
|
+
def get_global_quote_defect(value: GlobalQuoteOutput, failure: Failure | None = None) -> Case[GlobalQuoteOutput]:
|
|
35
|
+
return Case.branch_result(DEFECT, value, failure)
|
|
36
|
+
|
|
37
|
+
setattr(get_global_quote_defect, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
38
|
+
|
|
39
|
+
def get_global_quote_retry(failure: Failure, after_seconds: float = 0) -> Case[GlobalQuoteOutput]:
|
|
40
|
+
return Case.retry(failure, after_seconds)
|
|
41
|
+
|
|
42
|
+
setattr(get_global_quote_retry, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
43
|
+
|
|
44
|
+
def get_global_quote_found_default() -> Case[GlobalQuoteOutput]:
|
|
45
|
+
return Case.branch_result(FOUND, GlobalQuoteOutput(symbol='IBM', price='255.25', latest_trading_day='2026-10-28'))
|
|
46
|
+
|
|
47
|
+
setattr(get_global_quote_found_default, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
48
|
+
|
|
49
|
+
def get_time_series_daily_found(value: DailySeriesOutput, failure: Failure | None = None) -> Case[DailySeriesOutput]:
|
|
50
|
+
return Case.branch_result(FOUND, value, failure)
|
|
51
|
+
|
|
52
|
+
setattr(get_time_series_daily_found, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
53
|
+
|
|
54
|
+
def get_time_series_daily_not_found(value: DailySeriesOutput, failure: Failure | None = None) -> Case[DailySeriesOutput]:
|
|
55
|
+
return Case.branch_result(NOT_FOUND, value, failure)
|
|
56
|
+
|
|
57
|
+
setattr(get_time_series_daily_not_found, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
58
|
+
|
|
59
|
+
def get_time_series_daily_provider_rejected(value: DailySeriesOutput, failure: Failure | None = None) -> Case[DailySeriesOutput]:
|
|
60
|
+
return Case.branch_result(PROVIDER_REJECTED, value, failure)
|
|
61
|
+
|
|
62
|
+
setattr(get_time_series_daily_provider_rejected, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
63
|
+
|
|
64
|
+
def get_time_series_daily_invalid_response(value: DailySeriesOutput, failure: Failure | None = None) -> Case[DailySeriesOutput]:
|
|
65
|
+
return Case.branch_result(INVALID_RESPONSE, value, failure)
|
|
66
|
+
|
|
67
|
+
setattr(get_time_series_daily_invalid_response, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
68
|
+
|
|
69
|
+
def get_time_series_daily_defect(value: DailySeriesOutput, failure: Failure | None = None) -> Case[DailySeriesOutput]:
|
|
70
|
+
return Case.branch_result(DEFECT, value, failure)
|
|
71
|
+
|
|
72
|
+
setattr(get_time_series_daily_defect, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
73
|
+
|
|
74
|
+
def get_time_series_daily_retry(failure: Failure, after_seconds: float = 0) -> Case[DailySeriesOutput]:
|
|
75
|
+
return Case.retry(failure, after_seconds)
|
|
76
|
+
|
|
77
|
+
setattr(get_time_series_daily_retry, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
78
|
+
|
|
79
|
+
def get_time_series_daily_found_default() -> Case[DailySeriesOutput]:
|
|
80
|
+
return Case.branch_result(FOUND, DailySeriesOutput(symbol='IBM', days=(DailyPrice(date='2026-10-28', open='254.00', high='256.00', low='253.00', close='255.25', volume='1200000'),)))
|
|
81
|
+
|
|
82
|
+
setattr(get_time_series_daily_found_default, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class Mock:
|
|
86
|
+
|
|
87
|
+
def __init__(self, connection_name: str = "alphavantage") -> None:
|
|
88
|
+
self.get_global_quote_script: Query[StockInput, GlobalQuoteOutput] = Query(GET_GLOBAL_QUOTE_DEFINITION)
|
|
89
|
+
self.get_global_quote_script.default(get_global_quote_found_default())
|
|
90
|
+
self.get_time_series_daily_script: Query[StockInput, DailySeriesOutput] = Query(GET_TIME_SERIES_DAILY_DEFINITION)
|
|
91
|
+
self.get_time_series_daily_script.default(get_time_series_daily_found_default())
|
|
92
|
+
self._connection = new_connection_with_operations(
|
|
93
|
+
get_global_quote=self.get_global_quote_script,
|
|
94
|
+
get_time_series_daily=self.get_time_series_daily_script,
|
|
95
|
+
reference=ConnectionRef(CONNECTOR_ID, connection_name),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def connection(self) -> Connection:
|
|
99
|
+
return self._connection
|
|
100
|
+
|
|
101
|
+
setattr(Mock, '__module__', "dex_connectors.alphavantage.alphavantagemock")
|
|
102
|
+
|
|
103
|
+
__all__ = [
|
|
104
|
+
"Mock",
|
|
105
|
+
"get_global_quote_found",
|
|
106
|
+
"get_global_quote_not_found",
|
|
107
|
+
"get_global_quote_provider_rejected",
|
|
108
|
+
"get_global_quote_invalid_response",
|
|
109
|
+
"get_global_quote_defect",
|
|
110
|
+
"get_global_quote_retry",
|
|
111
|
+
"get_global_quote_found_default",
|
|
112
|
+
"get_time_series_daily_found",
|
|
113
|
+
"get_time_series_daily_not_found",
|
|
114
|
+
"get_time_series_daily_provider_rejected",
|
|
115
|
+
"get_time_series_daily_invalid_response",
|
|
116
|
+
"get_time_series_daily_defect",
|
|
117
|
+
"get_time_series_daily_retry",
|
|
118
|
+
"get_time_series_daily_found_default",
|
|
119
|
+
]
|