Python SDK¶
The Python SDK is how an Open Meridian plugin talks to the deployment. A plugin's whole contract with the platform is its sidecar, listening on loopback beside it, and the SDK is a client for that surface and nothing more. It never learns the bus's address, never holds a broker credential, and never discovers another plugin. Access control, stamping and the bus all live on the sidecar's side. Anything the SDK offers is a convenience, never a decision.
Install¶
Install it in a virtual environment: a bare pip install outside one is refused by some Pythons,
such as Homebrew's on macOS (externally-managed-environment).
| PyPI name | open-meridian |
| Import name | meridian |
| Version | 0.36.0 |
| Python | 3.11 or newer |
| Dependencies | grpcio>=1.68,<2, protobuf>=5.28,<7, jinja2>=3.1,<4 (for pages) |
| Licence | Apache-2.0 |
Not meridian-sdk
The PyPI package meridian-sdk belongs to an unrelated company. Don't install it.
A plugin pins the SDK exactly, open-meridian==0.36.0, in its pyproject.toml. The sidecar it runs beside speaks one version of the interface, and a version range would let a rebuild pick up another. Its Dockerfile builds on the base image for the same version, ghcr.io/open-meridian/plugin-python:0.36.0, so move the two together: meridian plugin migrate moves both, and rewrites the plugin's code where a release changed what it calls (see Moving a plugin to a new release). CLI 0.1.60's meridian plugin new writes a plugin pinned to 0.35.0; meridian plugin migrate moves it to 0.36.0, which runs on chart 0.1.538.
| Optional extra | Installs | For |
|---|---|---|
migrate |
libcst |
Running the SDK's own migrations, python -m meridian.migrations, which meridian plugin migrate does in an image of its own. A plugin never needs it. |
The package includes the wire bindings it speaks to the sidecar with, as meridian.v1 and meridian.plugin.v1.
Quick start¶
Each role a plugin holds is one class extending that role's base, meridian.roles.<role>.Role. Its acts are its methods, what it hears is its on_<event> methods, and meridian.run registers the classes on one connection and runs the plugin until it is stopped.
import meridian
from meridian.roles import custody
# What the version says about itself, named in pyproject.toml's [tool.meridian]
# as declaration = "my_plugin.declaration:DECLARATION"
DECLARATION = meridian.Declaration(
interface=meridian.Interface(port=8000, title="My plugin"),
reads_external_accounts=True,
documentation="docs", # docs/overview.md and docs/setup.md at least
)
class Custody(custody.Role):
async def on_start(self) -> None:
print(self.plugin.identity.instance_id, self.plugin.identity.roles)
await self.report_external_accounts(
accounts=[meridian.ExternalAccount(external_account_id="acct-1", name="Brokerage")]
)
if __name__ == "__main__":
meridian.run(declaration=DECLARATION, roles=[Custody()])
A version registers exactly the declaration it was uploaded with (see The whole declaration). A plugin that serves pages declares them with meridian.Pages, passes it as Interface(pages=...), and gives meridian.run the same pages= and port=. meridian plugin new writes a complete plugin built this way, with one page under Manage and one under Open and View. See Your first plugin.
What the package exports¶
meridian.__all__:
| Name | Kind | Described in |
|---|---|---|
run, run_async |
function, coroutine | meridian.run: the plugin's entry point |
connect |
async function | meridian.connect: the connection beneath run, for a script or a test |
Declaration |
frozen dataclass | The whole declaration: what a version says about itself |
Plugin |
class | The connection: what is the whole plugin's, never a role's |
Identity, Grants, Interface, Page, Setting, Choice, AppliesWhen, Settings, AccountScope, LinkedExternalAccount, Caller |
frozen dataclasses | Types |
Column |
frozen dataclass | Column |
Pages, Request, Response |
class, frozen dataclasses | Pages |
AccessLevel |
generated protobuf enum | AccessLevel |
Money |
frozen dataclass | Typed operations |
StatementFigures, ReportedCollateral, ReportedLot |
frozen dataclasses | Typed operations |
ReportedEncumbrance |
frozen dataclass | Typed operations |
OpeningSource, OpeningPosition, OpeningLot, PendingSettlement, LotTerms, PositionKey, BreakDifference, BreakValue, BreakCause, PendingSettlementRef, AgreementFigures, ReportedPositionValue, PositionEncumbrances, Encumbrance, Adjustment, MovementLine, BasisAdjustment |
frozen dataclasses | Typed operations, the book's |
BreakCategory, BreakCauseCategory, BreakState, LotReliefMethod, LotSource, OpeningSourceKind, PositionBasis, SettlementBucket, BreakHandling, FigureKey, MarginAgreementRef, PendingState, ResolvedByEntries, Reversal, StatementSegmentRef, StreetRecordRef |
generated protobuf enums and messages | Typed operations, the book's |
as_decimal, as_money |
functions | Typed operations |
Identifier, MissReason |
generated protobuf message and enum | Types |
Figure, FigureState |
frozen dataclass, generated protobuf enum | Figures |
AssetClass, CollateralDirection, ExternalAccount, HoldingSide, SyncState |
generated protobuf enums and message | Typed operations |
Heard |
frozen dataclass | Events |
TicketKind, TicketState, TicketResolution, TicketSubject, TicketReference |
generated protobuf enums, StrEnum, frozen dataclass |
file_ticket() |
CustodialActivity |
frozen dataclass | Typed operations |
Storage, RecordKind |
frozen dataclasses | The archive |
StoredSpan, MoveOutcome |
generated protobuf message and enum | The archive |
ActivityKind, ActivityRef |
generated protobuf enum and message | Typed operations |
DatasetDeclaration |
frozen dataclass | A data plugin's catalogue |
Price, Bar, ObservationMeta, SourceTime |
frozen dataclasses | Typed operations |
SubjectRef |
frozen dataclass | A row's subject: its entity_id, and from SDK 0.33.0 its role. Changed in SDK 0.33.0: the typed operations take this only, never the wire's (see 0.32.3 to 0.33.0) |
ObservationMode, PriceKind, PriceBasis, SourceTimeKind, UnansweredReason, VenueKind, Source, SourceChoice, DatasetRef, Unanswered, VenueRecord |
generated protobuf enums and messages | Typed operations, the lake's |
Trade, TradeAttributes, Eligibility, Quote |
frozen dataclasses | Typed operations |
Aggressor, Eligible, TradeCharacteristic, QuoteCharacteristic |
generated protobuf enums | Typed operations |
roles |
module | Role classes, and each role's common part and logic |
helpers |
module | Helpers: logic shared across roles by topic, which grants nothing |
LiveValue, LiveValues, LiveView |
frozen dataclass, classes | Live values and views |
runs_as |
function | A service account runs the plugin's autonomous work |
SubjectKind |
generated protobuf enum | Caller: whom the claims vouch for |
AlgorithmDeclaration |
frozen dataclass | Algorithms: one algorithm a version declares |
OrderLink, OrderChange, PlacementChange, AllocationInstruction, AllocationLine, Leg, Party, ReceiptQuote, StrategyInstruction, Finality, Fee, LocateAnswer, LocateUse, VenueCatalogue, VenueOrderType, VenueStrategy, StrategyParameter, VenueEligibility, Actor |
frozen dataclasses and a generated protobuf message | The order path |
DateBasis, DateCheck, DateRuleRef, SettlementDateInputs, SpotDateInputs, TenorDateInputs, Tenor, RollInputs, BusinessDayInputs |
frozen dataclasses and generated protobuf messages | Settlement calendars |
Outcome, OutcomeDetermination, OutcomeStage |
frozen dataclass, generated protobuf enums | Outcomes. Added in SDK 0.27.0 |
settings, setting, environment, sample_data |
decorator, functions | Typed settings; account_currency added in SDK 0.27.0 |
session_ceilings, ceilings_from, dataset_thresholds, stale_reference_action, stale_reference_after |
functions | Typed settings: latency's and staleness's settings. Added in SDK 0.32.0 |
quality_thresholds |
function | Typed settings: a signal's outlier thresholds. Added in SDK 0.33.0 |
ListedSeries, Statistic, QuoteAtTrade, SeriesWant, LifecycleEvent, LifecycleDelivery, DerivedSeries, SavedScenario, Scenario, ScenarioAxis, ScenarioSet, Shock, ShockBucket, UnitValue |
frozen dataclasses | Helpers and Listed options and futures. Added in SDK 0.33.0 |
Filing, ReportedFact, HoldingsReport, Holding, CorporateActionAnnounced, RatingAction, QualityAssessment |
frozen dataclasses | The filings slice and quality states. Added in SDK 0.33.0 |
RefusedSetting |
frozen dataclass | Settings: a saved value this version cannot read. Added in SDK 0.31.1 |
analytics, Job, WhatIf, WhatIfTrade, ComputedAs, NotEvaluatedReason, ScenarioKey, ScenarioKind, PricedUnit, Sensitivity |
module, generated protobuf messages and enums | The analytics component and a pricing plugin's answers; see The analytics component's operations and a standing job. Added in SDK 0.31.0 |
MeasureRef |
frozen dataclass | A measure a Job, a DerivedSeries or a CorporateActionAnnounced names: its definition, version and, in a standing job, its refresh, cadence_ns and material_move_bp. analytics.measure(...) answers the wire's, which analytics.job takes. Added in SDK 0.36.0 |
EventGroups, EventGroup, EventGroupFlag |
class, generated protobuf message and enum | Event groups; see list_event_groups. Added in SDK 0.31.0 |
reached_environment, is_demo, connection_badge |
functions | The agents' demo-only guard; see Typed settings. Added in SDK 0.31.0 |
CallerMiddleware |
ASGI middleware | CallerMiddleware |
MeridianError, Refused, NoSidecar, NotRegistered, NotGranted, CallFailed, NotLinked, CommandRefused |
exceptions | Exceptions |
DEFAULT_ADDRESS |
str |
"127.0.0.1:9191", where a sidecar listens |
SCHEMA_VERSION |
str |
the interface version sent at registration, "v30"; each SDK release's is in the release notes |
The module meridian.testing holds PageClient and heartbeat, for a plugin's own tests.
meridian.run¶
def run(
declaration: Declaration,
roles: Sequence[RoleBase],
*,
pages: Pages | None = None,
port: int | None = None,
on_settings: Callable[[Any], Awaitable[None] | None] | None = None,
figures: Callable[[], Sequence[Figure]] | None = None,
) -> None
The plugin's process, until it is stopped. run_async takes the same arguments as a coroutine. It:
- handles SIGTERM and SIGINT, so a stop leaves the sidecar cleanly;
- connects with
declarationand registers the classes inroleson one connection: one class per role, two for one role refused naming it, and a class for a role the instance was not launched with not started, which the log says; - delivers the settings to
on_settings, typed where the declaration's settings are a@meridian.settingsclass, the first delivery applied before any class starts, the watch opened again after a backoff when its stream breaks; - runs each class's
on_start, then hears one stream for every class, each delivery handed to every class hearing its row, in order, and starts each class'srun; - serves
pagesonport, reports the plugin healthy, and readsfigureson every heartbeat; - when stopped, cancels the runs, calls each
on_stopand leaves.
A plugin holding no role passes roles=[]. State the classes share is passed to each: roles=[Lake(state), Trading(state)].
meridian.connect¶
async def connect(
address: str | None = None,
*,
heartbeat: bool = True,
wait: float = 60.0,
interface: Interface | None = None,
settings: Sequence[Setting] = (),
reads_external_accounts: bool = False,
declaration: Declaration | None = None,
roles: Sequence[RoleBase],
) -> Plugin
Registers with the sidecar, the role classes in roles registered on it, and returns the admitted connection. A Plugin you hold is always one the sidecar admitted. It is the context manager beneath meridian.run, for a script or a test driving its classes itself: it starts no class's run and serves no page.
| Parameter | Type | Default | Meaning |
|---|---|---|---|
address |
str or None |
None |
The sidecar's address. When None, the value of MERIDIAN_SIDECAR_ADDRESS, then 127.0.0.1:9191. No other configuration is read. |
heartbeat |
bool |
True |
Send a liveness heartbeat to the sidecar every 5 seconds in the background, carrying the health last reported and the plugin's figures. |
wait |
float |
60.0 |
Seconds to wait for a sidecar that is not answering yet. A plugin and its sidecar start together in one pod, in no promised order. |
interface |
Interface or None |
None |
The pages the plugin serves on loopback, if any. Declare it in the Declaration instead: given beside a declaration naming one, it must be the same, pages and tools alike. |
settings |
sequence of Setting |
() |
The settings the plugin needs an admin of it to give it, on the dashboard's Settings form, or a table on its own tab beside it. A plugin sets none of them itself. Declare them in the Declaration instead: given beside a declaration naming some, they must be the same. |
reads_external_accounts |
bool |
False |
True when the plugin reads accounts at an external source and names them by that source's identifiers. An admin of the plugin links those to accounts, and the sidecar translates them on the way in. Declare it in the Declaration instead. |
roles |
sequence of role classes | required | The plugin's role classes, one per role it holds, as run takes them. |
declaration |
Declaration or None |
None |
The version's declaration, the same one meridian plugin upload reads from the image: its settings, its interface, whether it reads external accounts, its documentation, what it does not carry and the storage it asks for (see The whole declaration). See Keep what your custody plugin converts. Where its storage declares kinds of raw record, connect declares each kind's two window settings and the restore route besides; see The archive. For a data plugin, its catalogue; see A data plugin's catalogue. |
What connect is given beside a declaration that the declaration does not name is registered all the same, and warned of (UserWarning): the upload reads the declaration alone, so the sidecar refuses the difference.
The interface version it sends is SCHEMA_VERSION. A sidecar accepts a range of versions, v2 through the one its runtime serves: a plugin built for an older version it still supports registers, and one built for a newer version than the sidecar knows is refused at registration, naming both, rather than running without what it was built for. After an upgrade, relaunch plugins so they get the newer sidecar (meridian upgrade-deployment names the ones that need it).
Raises:
| Exception | When |
|---|---|
NoSidecar |
No sidecar answered within wait seconds. |
Refused |
The sidecar answered and declined to admit the plugin. Its reason says why. Not retried. It refuses a page or setting naming a role the plugin was not launched with, and, on a plugin holding several roles, one naming none, naming it and the plugin's roles. It refuses a registration that differs from the declaration the version was uploaded with, naming the first difference, and a plugin whose version was uploaded with no declaration, naming the missing declaration. |
ValueError (from connect) |
interface or settings given beside a declaration naming different ones: name them once, in the Declaration. |
TypeError |
A Setting's kind is not str, int, bool or list; it has choices and a kind other than str; its default is not of its kind; or its kind is list without columns, or another kind with them. |
ValueError |
A secret Setting declares a default; a default is not one of its choices; a table is secret, or declares a default or choices, names a column twice, or has most_rows outside 0 to 500; a Column's kind is not one of the seven, it is named changed_by or changed_at, or it is a choice without choices; or a Page's path does not begin with /, or it names no level; and, when the declaration is made, a roles= naming something no role could be (a role is lower-case letters, such as custody), or more roles than there are. |
grpc.aio.AioRpcError |
Any other gRPC failure during registration, unchanged. |
When run by the development runner on a development deployment, connect also records the ready event once the plugin is admitted. See plugin dev events.
The whole declaration¶
A version's declaration, meridian.Declaration, is the whole of what it says about itself. meridian plugin upload reads it from the built image, as pyproject.toml's [tool.meridian] names it (declaration = "my_plugin.declaration:DECLARATION"), and the plugin registers with the same one, meridian.run(declaration=DECLARATION, roles=[...]).
import meridian
from .page import TITLE, pages
@meridian.settings
class Config:
api_key: str = meridian.setting("API key", required=True, secret=True)
DECLARATION = meridian.Declaration(
settings=Config,
interface=meridian.Interface(port=8000, title=TITLE, pages=pages), # a fixed port
reads_external_accounts=True,
documentation="docs", # relative to where the plugin runs: /plugin in its image
)
Declaration, a frozen dataclass, keyword only:
| Field | Type | Default | Meaning |
|---|---|---|---|
settings |
sequence of Setting, or a class decorated @meridian.settings |
() |
The settings it declares. See Typed settings. |
interface |
Interface or None |
None |
Its pages, and the tools their typed routes make. Added in SDK 0.29.0. |
reads_external_accounts |
bool |
False |
Whether it reads accounts at an external source. Added in SDK 0.29.0. |
documentation |
str, path or None |
None |
The folder holding its documentation, relative to where the plugin runs: "docs", which is /plugin/docs in its image. Each section, overview.md, setup.md, troubleshooting.md, terms.md and pages.md, is named in the declaration by its sha256: digest and its size, 1 to 65,536 bytes, 256 KiB in all. The SDK reads and digests the files and parses no Markdown. See Write a plugin's documentation. Added in SDK 0.29.0. |
affirmation_deadline |
int |
0 |
An operations plugin's deadline, in minutes after midnight in the deployment's time zone, 1 to 1440, by which the firm allocates, confirms and affirms its trades; a held booking's alert falls due then. 0 declares none; refused on a plugin not holding operations. Added in SDK 0.29.0. |
not_carried |
sequence | () |
What it receives and does not carry. See Keep what your custody plugin converts. |
storage |
Storage or None |
None |
The storage an edge plugin asks for. See The archive. |
catalogue |
sequence of DatasetDeclaration |
() |
A data plugin's datasets. See A data plugin's catalogue. |
algorithms |
sequence of AlgorithmDeclaration |
() |
The algorithms its autonomous work may name. See Algorithms. |
Registration is held to the version. A plugin registers exactly the declaration its version was uploaded with, or its sidecar refuses it, naming the first difference; a version uploaded with no declaration is refused, naming the missing declaration. So the interface's port is fixed in the declaration, never read from the environment, and the module naming the declaration imports every module that declares a page or a route. The two window settings the SDK declares for each kind of raw record (see The window settings) are not among the declaration's own and are not compared. On a development deployment, a live instance's difference is a warning on its Summary instead.
meridian-declaration, installed with the SDK, prints a declaration as JSON for meridian plugin upload: meridian-declaration my_plugin.declaration:DECLARATION. With --documentation it prints the documentation's texts instead, a list of each section's section and markdown, which the upload sends beside the declaration. Both read the same files, so what is uploaded is what the image holds.
Role classes¶
A plugin writes one class per role it holds. Each extends its role's base, meridian.roles.<role>.Role, one module per role: compliance, confirmation, connectivity, custody, data, ems, oms, operations, portfolio, pricing, reference, reporting, servicing, settlement and signal. The bases are generated from the interface's matrix and the role suites.
- Its acts are its methods, one per typed operation its role makes:
await self.record_prices(...). An act two roles hold is the same call from either class, and core checks every act against the plugin's declared roles and grants. - Its events are
on_<event>methods, one per row its role hears, named from the row (on_observations_wanted,on_activity_re_resolved). The ones its role's suite requires are abstract, so a class missing one is refused when it is made, naming the role and the event. An optional one is heard only where the class overrides it, and a row no class hears is never asked of the sidecar. See Events. - Its lifecycle:
on_start(), once the plugin has registered and before the stream opens;run(), its long-running work, started after every class'son_start, and restarted after a backoff when it fails, the failure logged and said on the plugin's health;on_stop(), when the plugin stops. - What it hears:
seed(a class attribute,Trueby default;Falsewhere itson_startreads the store itself),subjects(), the lake's subjects it hears, read when the stream opens, andself.hear_again()when they change, which opens the one stream again and catches up as after a broken stream. - The connection,
self.plugin: what is the whole plugin's, never a role's. See The connection.
import meridian
from meridian.roles import data
class Lake(data.Role):
def __init__(self, state):
self.state = state
self.wants = data.Wants(self, ("daily",))
async def on_start(self) -> None:
await self.wants.read_licences()
async def on_observations_wanted(self, heard: meridian.Heard) -> None:
await self.wants.hear(heard, self.state.answer)
meridian.run(declaration=DECLARATION, roles=[Lake(State())], pages=pages, port=PORT)
A helper making an act is given the role object, never the connection: data.Wants(self, ...), roles.Latency(self), chains.read_chain(self, ...). A page acts through the role object the plugin gives it, a closure or the state it shares, as the reference plugin's pages call CUSTODY.record_holdings_statement(...).
The SDK's common logic is role classes, registered as they are or subclassed: connectivity.Trader, pricing.Pricer, and the data feeds data.pairs.Feed, data.event_contracts.Feed, data.listings.Feed and data.option_chains.Feed. See Each role's common logic.
In a plugin's tests the suites drive a class through their recorder: lake = recorder.bind(Lake(...)) makes its acts the recorder's, recorder.answer(row, ...) says what a row delivers, and await recorder.hear(lake) hands the class each row it hears (recorder.hear(lake, *rows) the rows given alone). recorder.plugin stands in for the connection and makes no act, so a class, helper or page acting through the connection fails in the tests as it does when it runs. A PageClient given a recorder hands its pages the same as request.plugin.
The connection, Plugin¶
Built by connect, and held by every role class as self.plugin and by every page request as request.plugin. It holds what is the whole plugin's: its identity and grants, settings, health and figures, storage, raw records and archive moves, tickets, a person's own settings, views, access, the account scope, and leaving. It makes no act. It is an async context manager: leaving the async with block calls leave(), with the reason "stopping", or the exception's class name if the block raised.
Attributes¶
| Attribute | Type | Meaning |
|---|---|---|
identity |
Identity |
Who the plugin was launched to be: instance, roles, deployment. Read from the registration reply, never sent by the plugin. |
grants |
Grants |
What the deployment allowed, as the topic patterns it allowed them as. |
figures |
sequence of Figure |
The figures the plugin reports on its Summary, as last set. Set it to report a new list. See Figures. |
stored |
sequence of StoredSpan |
What the plugin's storage holds of each kind of raw record it declared, as last set, on every heartbeat from the next on. See What is stored. |
grants is for failing early with a good message, at startup, rather than at the first refused operation. The sidecar refuses independently of what the plugin believes, and the SDK offers no "is this allowed" check.
Methods¶
| Method | Returns | Meaning |
|---|---|---|
settings() |
AsyncIterator[Settings] |
The settings the plugin declared, now and again on every change. |
account_scope() |
AsyncIterator[AccountScope] |
Every account anybody may read or write through this plugin, and the plugin's own links, now and again on every change. |
access() |
Awaitable[PluginAccessReply] |
Who may use this plugin. |
file_ticket(*, title, kind, idempotency_key, for_caller, ...) |
Awaitable[FileTicketReply] |
File a ticket for the person whose request the plugin is serving. See file_ticket(). |
filed_tickets(*, for_caller, ticket_ids=(), idempotency_keys=(), cursor="") |
Awaitable[ReadFiledTicketsReply] |
What became of the tickets the plugin filed. See filed_tickets(). |
archive_unit(record_kind, unit, *, record_count, first_received_ns, last_received_ns) |
Awaitable[None] |
Move a unit past its window to the archive. See The archive. |
restore_unit(record_kind, unit, *, for_caller) |
Awaitable[Path] |
Restore an archived unit for a person, answering where it is readable. See The archive. |
delete_unit(record_kind, unit, *, record_count=0, first_received_ns=0, last_received_ns=0, for_caller=None) |
Awaitable[None] |
Delete a unit, the deletion reported first. See The archive. |
find_record(key) |
RecordMoveRequest or None |
Where a raw record's key stands: the last move of the unit holding it. See The archive. |
report(*, healthy, detail="", figures=None) |
Awaitable[None] |
Report the plugin's health now, outside the heartbeat. It stands until reported again. |
leave(reason="") |
Awaitable[None] |
Say the plugin is stopping, and close the connection. |
Every method raises NotRegistered once the plugin has left.
settings()¶
Yields the plugin's settings as the deployment holds them, typed by what it declared at connect, first as they are now and then on every change. Values for names the plugin did not declare are left out. A setting that declares a default and has no value holds its default, so the plugin uses what the form showed. A value that does not parse as its declared kind raises ValueError rather than being guessed at. A boolean accepts true, yes, 1, on, false, no, 0 and off, in any case.
An admin of the plugin sets its settings on the dashboard's Settings form, and each table on its own tab beside it, the only places a setting is set, and the deployment records each change, naming who made it. The plugin only reads them: the SDK has no call that sets a setting, and the deployment refuses an update a plugin sends. See Set a plugin's settings.
async for current in plugin.settings():
if current.missing_required:
log.warning("waiting for %s", ", ".join(current.missing_required))
continue
token = current.values["api_token"]
While a required setting has no value, the sidecar reports the plugin unhealthy and names the setting.
A table setting arrives as a list of rows, each a dict of its cells as text by column name, with changed_by, the person who added or last changed the row, and changed_at, when, in RFC 3339 UTC. The deployment stamps both when it stores the table; a row left as it was keeps its own. A value that is not a table's rows raises ValueError.
async for current in plugin.settings():
for row in current.values.get("plan_code_links", []):
row["code"], row["instrument"], row["changed_by"], row["changed_at"]
account_scope()¶
Yields the plugin's account scope, now and again on every change. The scope is derived from permissions and from the plugin's links to accounts, and never declared: see Accounts. A plugin reads its whole read scope as itself and serves each person only what their access allows. The sidecar refuses a write outside write, whoever it is for.
Beside the scope come the plugin's own links: which of its external accounts an admin has linked, to which account, and that account's name as the deployment holds it now. They are read as the plugin itself, for nobody. The first delivery comes at once, so a plugin that has just started has every link. Another comes whenever a permission changes, a link is made or removed, or a linked account is renamed or closed. Hold the latest one: a plugin keeps nothing of its own across a restart.
async for scope in plugin.account_scope():
for link in scope.links:
print(link.external_account_id, "->", link.account_id, link.account_name)
if scope.link_of("acct-3") is None:
... # not linked: a row for it raises NotLinked
access()¶
Returns who may use this plugin: each user group naming it, and each person who has signed in to the deployment, with their access. It is for shaping an interface; nothing here is an access decision. The reply is the generated protobuf message:
| Field | Type | Meaning |
|---|---|---|
user_groups |
repeated UserGroupAccess |
user_group_id, name, read_account_ids, write_account_ids and roles. |
people |
repeated PersonAccess |
subject, display_name, user_group_ids, last_signed_in_at_ns, read_account_ids, write_account_ids and roles. Only people who have signed in are listed: the deployment holds no directory. |
The account sets are this plugin's: what the group or person may read through it, and write through it. Every write account is also listed as read.
Each also carries roles, a RoleAccess for each of the plugin's roles the group or person holds account access on: role, level (read or write, never admin, since the table lists account access only), and read_positions and write_positions, the role's accounts as positions in that same message's read_account_ids. The two sets above stay the union over the roles.
file_ticket()¶
async def file_ticket(
self,
*,
title: str,
kind: str | int,
idempotency_key: str,
for_caller: Caller | str,
seen: str = "",
concerns: TicketSubject | str = TicketSubject.PLUGIN,
step: str = "",
operation: str = "",
reason: str = "",
paths: Sequence[str] = (),
references: Sequence[TicketReference] = (),
) -> meridian.v1.sidecar_pb2.FileTicketReply
Files a ticket for the person whose request the plugin is serving: a problem they met that the plugin cannot handle, for someone in the deployment who can act on it. Only ever for a person, at whatever level their session holds, and never as the plugin itself: what a plugin notices on its own is its health, a figure in warn on its Summary, from which a person may choose to file.
reply = await plugin.file_ticket(
title="Break on the growth account still open after its cause was confirmed",
seen=form.get("seen", ""), # the person's words, plain text
kind="defect",
idempotency_key=f"break-still-open-{break_id}",
for_caller=request.caller,
references=[meridian.TicketReference("break", break_id, account_id="ACC-GROWTH")],
)
reply.ticket_id, reply.outcome, reply.seen_count # "TKT-...", "made", 1
| Parameter | Meaning |
|---|---|
title |
What is wrong, in a line: 1 to 120 characters, plain text. |
kind |
A TicketKind, its name, or "defect", "discrepancy", "request" or "question". |
idempotency_key |
Required. The plugin's own key for the problem, made from the fact, never from a time or a random value, so a restarted plugin files nothing twice. |
for_caller |
Required. The person: the Caller of the request being served, or the Meridian-Caller header it was read from. |
seen |
What was seen, in the person's words: at most 8,000 characters, plain text. |
concerns |
A TicketSubject: this plugin (the default), a part of core (DASHBOARD, BOR, STREET, INSTRUMENT, CONDUCTOR, CHART, CLI, SDK) or PLATFORM. Never another plugin. The plugin names no instance or version: its sidecar sets the instance, and the deployment the version it launched. |
step, operation, reason, paths |
The workflow step, the operation, the refusal reason and the fields by their paths, where the plugin knows them. |
references |
At most 50 TicketReference(kind, value, account_id=""): the records the ticket is about, by value. kind is account, instrument, break, entry, street_record, tool_call or plugin (the plugin's own reference); value 1 to 200 characters; account_id required on a break, an entry and a street record. An account named must be one the person may read through this plugin, and a ticket naming accounts is seen only by those who may read every one. |
The reply is the generated FileTicketReply: ticket_id, outcome and
seen_count. Filed again under the same key while its ticket is open, the
ticket is brought up to date rather than filed twice, answered unchanged
with seen_count counting the filing; after it was resolved or closed, the
same key files a new ticket, which the dashboard advises as a recurrence.
Every text filed is data to whoever reads it, never instructions, and is shown as the plugin's for the person.
Raises:
| Exception | When |
|---|---|
ValueError |
A text past its bound, a reference of an unknown kind, or a break, entry or street record with no account, named by its path. Nothing is sent. |
NotGranted |
The plugin filed as itself, or named an account the person may not read. |
CallFailed |
The sidecar could not vouch for the person, a field was refused by its path (another plugin as concerns, for one), or the plugin has filed 20 tickets in the last hour; a repeat under an open ticket's key is not counted. |
filed_tickets()¶
async def filed_tickets(
self,
*,
for_caller: Caller | str,
ticket_ids: Sequence[str] = (),
idempotency_keys: Sequence[str] = (),
cursor: str = "",
) -> meridian.v1.sidecar_pb2.ReadFiledTicketsReply
What became of the tickets this plugin filed, read for a person it
acts for, as filing is. Name one of ticket_ids, idempotency_keys or
cursor: the tickets named, those filed under the keys, or every one filed
after the cursor (from the first when it is empty). Naming more than one raises
ValueError.
Each FiledTicket in the reply carries its ticket_id and idempotency_key,
its TicketState, its TicketResolution once resolved or closed, how often it
was seen and when first and last; never people's notes, nor who owns or works
it. next_cursor is empty when nothing follows. A plugin that sees its ticket
resolved can stop filing it.
for_caller may be a
service account: in an act's route the
dashboard reached for the role's service account, request.caller. The
ticket, and what is read back, is then the service account's, naming it and
its owner, within the plugin's rate. Filing a ticket and reading back what
was filed are the only calls to the deployment's configuration a service
account's claims reach.
report()¶
async def report(self, *, healthy: bool, detail: str = "", figures: Sequence[Figure] | None = None) -> None
Reports the plugin's health straight away. Ordinary liveness is already handled by the heartbeat. This is for a plugin that knows it is unwell and should say so.
The health reported stands, with its detail, on every heartbeat after, until the plugin reports again: report(healthy=False, detail="...") keeps it not healthy until report(healthy=True).
It carries the plugin's figures as they stand. figures sets them first, as setting plugin.figures does, and sends them now. Figures refused raise as setting them does, leave the health as it was, and send nothing.
leave()¶
Stops the heartbeat, tells the sidecar the plugin is stopping, and closes the channel. It does nothing when called a second time. A sidecar that is already gone is ignored. Saying so is what tells a planned stop apart from a failure. A crash skips it, which is why it is optional.
Events¶
A role class hears the rows its role hears through its on_<event> methods, one per row, each handed a Heard. The street store's rows are heard by operations, the custodian's activity, each sync status and an activity re-resolved among them; the book of record's by the roles that read the book; the lake's by the roles each row names:
| Method | Row | heard.message |
Workflow step |
|---|---|---|---|
on_statement_recorded |
StatementRecorded |
A StatementRecordedEvent: a completed statement, with its account, external account and institution, its counts and its figures per margin segment, as the street store announced it. The same message list_statements reads. |
W2.5 |
on_custodial_position_updated |
CustodialPositionUpdated |
A CustodialPositionUpdatedEvent: the position's whole new state in position, removed set when it was removed, the statement that changed it, and its previous quantity. |
W2.6 |
on_position_changed |
PositionChanged |
A PositionChangedEvent: the book's position whole in position, a BookPosition with its lots, pending settlements and encumbrances, removed set when it was moved off a placeholder; its previous_trade_date_quantity; and the entry that changed it. Heard by portfolio, reporting, compliance, oms and operations. |
W9.8 |
on_break_changed |
BreakChanged |
A BreakChangedEvent: the Break whole in break_record, and the entry. Heard by operations, oms, compliance, portfolio and reporting. |
W9.8 |
on_account_figures_recorded |
AccountFiguresRecorded |
An AccountFiguresRecordedEvent: one agreement's AccountFigures in figures, and the entry. Heard by operations, portfolio, compliance and reporting. |
W9.8 |
on_account_attribute_changed |
AccountAttributeChanged |
An AccountAttributeChangedEvent: the account's AccountAttributes in attributes, its standing opening balance among them, and the entry. Heard by portfolio, reporting, compliance, oms and operations, and by an ems and a connectivity plugin for the accounts its ems mapped on it, which may use whether their releases wait for compliance. |
W9.8 |
on_activity_recorded |
ActivityRecorded |
An ActivityRecordedEvent: an activity on the account as the custodian stated it, recorded once. The same message list_activities reads, so a break waiting on its cause can be compared again. preview. |
W2.12 |
on_sync_status_recorded |
SyncStatusRecorded |
A SyncStatusRecordedEvent: a sync status the street kept, so "needs sign-in" is told apart from merely old. The same message list_sync_statuses reads. preview. |
W2.13 |
on_activity_re_resolved |
ActivityReResolved |
An ActivityReResolvedEvent: an activity recorded before its instrument resolved, re-resolved, its re-resolution kept beside it. Caught up from the re_resolutions of list_activities. preview. |
W2.16 |
on_prices_recorded |
PricesRecorded |
A PricesRecordedEvent: one Price the lake recorded, in price, for a subject the class's subjects() names. Heard by reporting, portfolio, compliance and signal, from a dataset the plugin is entitled to. Caught up by list_prices. preview. |
W10.5 |
on_bars_recorded |
BarsRecorded |
A BarsRecordedEvent: one Bar, in bar, likewise. Caught up by list_bars. preview. |
W10.5 |
on_observations_wanted |
ObservationsWanted |
An ObservationsWantedEvent, heard by the data plugin serving its dataset alone: want_id, dataset, data_type, subjects, kinds, interval_ns, the business_date or valid range wanted, and standing. See Wants. preview. |
W10.7 |
on_want_withdrawn |
WantWithdrawn |
A WantWithdrawnEvent, want_id and dataset: a standing want no reader asked for within its dataset's cadence. preview. |
W10.7 |
on_trades_recorded |
TradesRecorded |
A TradesRecordedEvent: one Trade the lake recorded, in trade, for a subject the class's subjects() names. Heard by signal and ems, every one, never conflated. Caught up by list_trades after the watermark last seen. preview. |
W10.5 |
on_quotes_recorded |
QuotesRecorded |
A QuotesRecordedEvent: one Quote, in quote, latest value first per subject, dataset, venue and asset. Heard by signal and ems. Caught up by list_quotes. preview. |
W10.5 |
on_order_updated |
OrderUpdated |
An OrderUpdatedEvent: the order whole, with what changed. Heard by oms, ems, compliance, portfolio, operations and reporting within their read scopes. Caught up by list_orders. preview. |
W11.13 |
on_placement_updated |
PlacementUpdated |
A PlacementUpdatedEvent: the placement whole, with its own change requests (placement.change_requests). Heard by the ems, oms and compliance within scope, and by the connectivity plugin it names alone of its role. preview. |
W11.13 |
on_execution_recorded |
ExecutionRecorded |
An ExecutionRecordedEvent: an execution, never edited; a bust or correction is another, naming the one it supersedes. Heard by the order's hearers, by the connectivity plugin that reported it too (added in SDK 0.27.0), and an unmatched one by the ems linked to it. preview. |
W11.13 |
on_unplaced_fact_recorded |
UnplacedFactRecorded |
An UnplacedFactRecordedEvent: a venue's fact the book could not place, with its reason. Heard by operations. preview. |
W11.13 |
on_trading_instruction_changed |
TradingInstructionChanged |
A TradingInstructionChangedEvent: an instruction set, whatever the plugin's accounts. Heard by oms, ems, connectivity, portfolio and compliance; caught up by list_trading_instructions, a read naming no account. preview. |
W11.16 |
on_locate_updated |
LocateUpdated |
A LocateUpdatedEvent: a locate and its uses. Heard by compliance, the oms and portfolio; caught up by list_locates. preview. |
W11.18 |
on_outcomes_recorded |
OutcomesRecorded |
An OutcomesRecordedEvent: one Outcome, in outcome, every version, never conflated, for a subject the class's subjects() names. Heard by reporting, portfolio, compliance, signal and operations; caught up by list_outcomes after the watermark last seen. Added in SDK 0.27.0; preview. |
W10.5 |
on_report_venue_catalogue |
ReportVenueCatalogue |
A VenueCatalogueEvent: a connectivity plugin's venue catalogue for one venue account code, latest per plugin and code. Heard by the ems and the oms; read again with read_venue_catalogue on start and after a loss. preview. |
W11.20 |
on_rates_recorded |
RatesRecorded |
A RatesRecordedEvent: a rate, conflated, the latest per subject, dataset, kind, tenor, maturity and convention, for a subject the class's subjects() names. Heard by reporting, portfolio, compliance and signal; caught up by list_rates. Added in SDK 0.32.0 |
|
on_liveness_recorded |
LivenessRecorded |
A LivenessRecordedEvent: a dataset's latest sign of life per venue, conflated; roles.Liveness().heard keeps it. Heard by reporting, portfolio, compliance, signal and the ems; caught up by list_datasets. Added in SDK 0.32.0 |
Since SDK 0.35.0 the lake's factors and series are heard too: on_factors_recorded (operations, portfolio, reporting, servicing, signal; every version, never conflated, caught up by list_factors) and on_series_recorded (ems, oms, portfolio, signal).
import meridian
from meridian.roles import operations
class Operations(operations.Role):
async def on_activity_recorded(self, heard: meridian.Heard) -> None: # required of operations
activity = heard.message.activity
...
async def on_statement_recorded(self, heard: meridian.Heard) -> None: # optional: heard because overridden
statement = heard.message # the whole statement, as the store announced it
...
- Seeded. It reads the store first, every row's records across the plugin's read scope, and hands each on, marked
caught_up. With a class'sseed = Falseit reads the store without handing on what it holds to that class, and hands on only what changes after. - Once and in order. Then it hands on each change heard, once, and in order for each account.
- Caught up. Deliveries are at most once: the sidecar queues at most 1024 for the plugin and drops past that, and marks each drop in the stream as a
Lost. Where one was missed (a gap in an account's changes, aLost, a stream that broke, or an account entering the read scope), it reads the changes since from the store and hands them on before anything heard after, each markedcaught_up. A stream that broke is opened again, half a second later and doubling to 15 seconds. An account leaving the read scope is heard of no more.
A plugin catches up from the store, never from the bus, and the SDK does it: a handler never sees the store's numbers, and keeps nothing to catch up with. A change carries its whole new state, so one handed on twice changes nothing a handler keeps; where reading a page at a time cannot say exactly where the store was, a change may be handed on twice rather than never.
The lake's rows. A price or a bar is heard only for the subjects named in subjects=, by their instrument IDs, at most 500, and none without them, from the datasets the plugin is entitled to. Each is delivered latest value first: the sidecar keeps one value waiting per key (a price's dataset, subjects, venue and kind; a bar's dataset, subjects, venue and interval start), a later value replacing one not yet delivered, and the SDK hands on the newest per key, never an older value after a newer one. On start, after a Lost and after a broken stream it reads them again by list_prices or list_bars, latest first and side by side, marked caught_up. Each carries its dataset in Heard.dataset. A price's key holds the asset it is in too, so BTC priced in USD and in USDC on one venue are two values, neither replacing the other.
Trades and quotes. Quotes are heard as prices are: latest value first per subject, dataset, venue and asset, read again at the latest after a loss. Trades are handed on every one, once, in their dataset's order, never dropped for a later one. After a Lost naming them, or a broken stream, the SDK reads the trades recorded after the watermark it last saw (per dataset, the last trade it handed on), side by side for the subjects named, and hands them on marked caught_up, a late print among them, before anything heard after; a trade the stream delivers after the read had it is handed on once. On start it reads none: there is no latest trade to begin from, so a plugin wanting the day's trades reads them by range with list_trades. Each carries its dataset in Heard.dataset.
The order path's rows. A placement is
delivered only to the connectivity plugin instance it names; no other
instance of the role hears it. The trading instructions name no account:
each is heard by every subscriber whatever its read scope, kept under no
account, and caught up after a gap or a loss by a read naming none
(list_trading_instructions, since the watermark last seen). The venue's
catalogue is delivered latest per connectivity plugin and venue account
code, a later one replacing one not yet delivered.
Everything heard is within the plugin's read scope, as the reads are, and a plugin whose read scope is empty hears nothing. It reads and hears for its whole scope, as itself: serve each person from it with caller.read, as always.
Heard¶
@dataclass(frozen=True)
class Heard(Generic[Message]):
row: str
message: Message
caught_up: bool = False
own: bool = False
cause: ChangeCause | None = None
message_id: str = ""
correlation_id: str = ""
causation_id: str = ""
published_at_ns: int = 0
dataset: DatasetRef | None = None
| Field | Meaning |
|---|---|
row |
The row, such as "CustodialPositionUpdated". |
message |
The row's message, from meridian.plugin.v1.operations_pb2, with the store's numbers taken off. |
caught_up |
True when it was read from the store rather than heard: when the plugin started, after a gap or a Lost, after the stream broke, or when an account entered its read scope. |
own |
True when the plugin's own act caused it. |
cause |
Who caused it, where the store recorded it, a ChangeCause: instance_id, the instance that sent the command; acting_for_subject, the person it was sent for, empty when the plugin acted as itself; correlation_id; causation_id, the command's message_id; and committed_at_ns. |
message_id, correlation_id, causation_id, published_at_ns |
From the envelope it was heard in. Empty, and 0, for what was read. |
dataset |
For a price, a bar, a trade or a quote: the dataset it came from, a DatasetRef with its instance, vendor, aggregator and catalogue entry, as list_datasets answers it, which the lake names once rather than on every row. None for any other row, and where the plugin may not list the datasets. |
Raises: a class missing a required event is refused when it is made, naming the role and each event. NotGranted for a row none of the plugin's roles hears. A read to catch up that finds nothing serving it, has no answer in time, or is refused by what serves it is tried again with the stream; any other refusal raises as the read's own would.
Figures¶
Manage opens on the plugin's Summary, which core draws: its status first (its health and why, the version running and the interface version it registered with), then a few figures the plugin reports about its own work, each a tile. A plugin gives them on its heartbeat and builds no summary page of its own. See Manage, Open and View.
from datetime import datetime, timezone
import meridian
plugin.figures = [
meridian.Figure("Connections", 3, state="warn",
why="1 connection needs attention: the brokerage asked to reconnect"),
meridian.Figure("Accounts reached", 7),
meridian.Figure("Last read", datetime.now(timezone.utc)),
]
The list goes on every heartbeat from the next on, in its order, each heartbeat replacing the last; an empty list clears it. await plugin.report(healthy=True, figures=[...]) sends it at once. A figure is about the plugin's own work: it names no account and carries none of an account's data, since Manage shows none. Report figures only where the plugin has work worth counting.
Figure¶
@dataclass(frozen=True)
class Figure:
label: str
value: int | Decimal | str | datetime
as_of: datetime | None = None
state: str | int | None = None
why: str | None = None
| Field | Meaning |
|---|---|
label |
As a person reads it, "Connections". 1 to 40 characters, and given once in the list. |
value |
What kind of figure it is: an int is a count, a Decimal a decimal, a str a text, at most 40 characters, and a timezone-aware datetime a time, such as "Last read". Always given. |
as_of |
When the value was true, where that is not the heartbeat's moment. A timezone-aware datetime. |
state |
The tile's mark: "ok", "warn" or "error", or a meridian.FigureState. None is no state, drawn plain. |
why |
The note beside the tile. At most 200 characters. |
A decimal is carried exactly, as typed operations carry one: at most 18 decimal places and 38 digits, refused rather than rounded. Core writes a time as the dashboard writes every moment.
Bounds¶
At most 8 figures; a label of 1 to 40 characters, given once; a text of at most 40; a why of at most 200; a state of ok, warn or error; a value always given; a decimal within what the wire carries. Anything past a bound is refused, never cut, at the line that sets the list, in the words the sidecar would refuse it with, and nothing is set: the last list stands.
| Raises | When |
|---|---|
ValueError |
A bound is broken, for example 9 figures; a plugin reports at most 8, figures[0].label is 44 characters; a label is at most 40, or figures[0].state is 'amber', which the interface does not define. |
TypeError |
A value is none of a figure's kinds, a datetime has no timezone, or one Figure is given where a list is. |
The sidecar checks them again. A heartbeat it refuses still says the plugin is alive; the plugin is then reported not healthy, the refusal as its why, with no figures. Core draws no figures for a plugin that is not registered or has fallen silent, since they would be stale; its status says why.
In a plugin's tests, meridian.testing.heartbeat is the heartbeat its sidecar receives, or the refusal.
The archive¶
A plugin holding an edge role declares the kinds of raw record it keeps; past each kind's window it archives, keeps or deletes them, as its admin chose, in units it can find again; and it reports every move through its sidecar before it removes anything. What that means for the people using it is in The archive. Each field on the wire is in the data dictionary: RawRecordKind, StoredSpan and RecordMoveRequest.
import meridian
from meridian.declaration import Declaration, RecordKind, Storage
DECLARATION = Declaration(
settings=SETTINGS,
interface=INTERFACE,
storage=Storage(kinds=[
RecordKind("activity", "Reported activity", window_days=2555),
RecordKind("responses", "Raw responses", window_days=30),
RecordKind("session", "Session state", window_days=7, archivable=False),
]),
)
async with await meridian.connect(declaration=DECLARATION, roles=[Custody()]) as plugin:
async for settings in plugin.settings():
window = settings.values["activity_window_days"] # days, the admin's
past = settings.values["activity_past_window"] # archived, kept or deleted
for unit, count, first, last in units_past(window): # the plugin's own units
if past == "archived":
await plugin.archive_unit("activity", unit, record_count=count,
first_received_ns=first, last_received_ns=last)
elif past == "deleted":
await plugin.delete_unit("activity", unit, record_count=count,
first_received_ns=first, last_received_ns=last)
plugin.stored = [meridian.StoredSpan(record_kind="activity", record_count=...,
first_received_ns=..., last_received_ns=...)]
Storage and RecordKind¶
@dataclass(frozen=True)
class Storage:
retention_days: int = 0
kinds: Sequence[RecordKind] = ()
@dataclass(frozen=True)
class RecordKind:
name: str
label: str
window_days: int
archivable: bool = True
Storage is the storage a version asks for, on its Declaration; only a plugin holding an edge role may ask for it. It takes kinds.
| Field | Meaning |
|---|---|
Storage.kinds |
The kinds of raw record the version keeps, at most 16, each named once. |
Storage.retention_days |
The reach of a backfill, 1 to 36,500 days. Left 0 with kinds, it is the longest of their windows. A version declaring no kinds keeps its records under it. |
RecordKind.name |
The name its code and settings use: 1 to 40 lowercase letters, digits and underscores, beginning with a letter, such as activity. |
RecordKind.label |
The label a person reads, 1 to 40 characters, such as Reported activity. |
RecordKind.window_days |
Its default window, 1 to 36,500 days from when a record was received. |
RecordKind.archivable |
Whether a unit of it can be moved to an archive. False for state read and updated in place. |
Raises ValueError when the declaration is built: a bound broken, a kind declared twice, or a setting of the plugin's own taking one of the window settings' names below.
The window settings¶
For each kind, connect declares two settings beside the plugin's own, the same for every edge plugin, which an admin of the plugin sets on its Settings form. On a plugin naming roles, they serve its edge roles.
| Setting | Label | Kind | Default |
|---|---|---|---|
<kind>_window_days |
Label: window | int, days |
the kind's window_days |
<kind>_past_window |
Label: past the window | a choice of archived, kept or deleted |
archived where the instance was given an archive and the kind is archivable; kept otherwise |
The names are the SDK's: a plugin declaring a setting of either name for a kind it declares is refused by the SDK, by the sidecar at registration, and by meridian plugin check (window-settings). Read them from settings() as delivered. The deployment refuses a save setting a window below the hold over the instance, naming the setting, and archived for a kind not archivable or an instance allowed no archive.
edge.archive_dir()¶
In meridian.edge. The archive a deployment admin allowed this instance, where it is mounted (MERIDIAN_ARCHIVE_DIR), or None: in a test, for an instance allowed none, or where the archive is a bucket instead (MERIDIAN_ARCHIVE_BUCKET, an s3:// URL read through boto3, which a plugin deployed with one installs). The helpers below reach either through the same interface; a plugin need not read the archive itself. Its storage is edge.storage_dir(), as before.
A unit¶
A unit is a file or a directory in the plugin's storage, named by its path there, such as activity/ACC-1/2019-03. The records it holds are the paths within it, which rows name as their raw record's key (plugin.raw_record("activity/ACC-1/2019-03/act-77.json")). The helpers keep an index of what moved inside the storage, under .meridian/, which is never a unit.
archive_unit()¶
async def archive_unit(
self, record_kind: str, unit: str, *,
record_count: int, first_received_ns: int, last_received_ns: int,
) -> None
Moves one unit past its window to the archive: writes it there, checks that each file landed (its size and SHA-256), reports the move through the sidecar with its rule, the kind's window setting and its value (activity_window_days 2555), and only then removes it from storage. record_count and the span are the records the unit holds, 1 or more, the span from the first received to the last.
| Raises | When |
|---|---|
ValueError |
The kind is not declared, or declared not archivable; the unit is archived already; the count or span is out of bounds; or the kind's <kind>_past_window is not archived. |
RuntimeError |
The instance is allowed no archive; the settings have not arrived yet; or the unit would take the archive past its bound (MERIDIAN_ARCHIVE_MOST_BYTES), saying what the archive holds, its bound and the unit's size. Nothing is written or reported, and the unit stays in storage. |
FileNotFoundError |
The unit is not in the plugin's storage. |
CallFailed, NotGranted |
The sidecar refused the move: nothing is recorded, and the unit is kept. |
restore_unit()¶
Restores an archived unit for the person who asked: copies it back from the archive to the restore area in storage, each file checked against what was archived, reports the restore for for_caller (their Caller, or the header it was read from), and answers where the unit is readable. It stays readable for seven days, the restore period; then it is removed and its return reported, under the rule restore period 7 days. Asked again meanwhile, it answers the same path and reports nothing.
| Raises | When |
|---|---|
ValueError |
The unit is not archived. |
RuntimeError |
The instance has no archive now: what it holds is kept, and restorable once a deployment admin allows one again. |
NotGranted |
The person does not hold write on one of the plugin's edge roles. The copy is removed. |
OSError |
A file did not come back as it was archived. |
The restore route¶
An edge plugin declaring kinds and serving pages through meridian.Pages offers POST /archive/restore on its host, which connect declares at write on the plugin's edge roles and the deployment derives as the restore_unit tool on its MCP surface. It takes record_kind (at most 40 characters) and unit (at most 512), as a form or as the tool's input, restores the unit for the person the claims name, and answers a form with a redirect to the page it came from and the tool with {"record_kind": ..., "unit": ..., "outcome": "restored"}. A refusal names the field (record_kind or unit), or says not_granted or not_restored. A plugin's page offers it as a form under Open, posting the two fields:
{% if level == "write" %}
<form method="post" action="/archive/restore">{{ csrf_input }}
<input type="hidden" name="record_kind" value="responses">
<input type="hidden" name="unit" value="{{ unit }}">
<button>Restore</button>
</form>
{% endif %}
delete_unit()¶
async def delete_unit(
self, record_kind: str, unit: str, *,
record_count: int = 0, first_received_ns: int = 0, last_received_ns: int = 0,
for_caller: Caller | str | None = None,
) -> None
Deletes one unit, the deletion reported before anything is removed, so a refusal keeps it. Either a unit in storage past its window, as the plugin itself, where the kind's <kind>_past_window is deleted, its count and span given; or an archived unit, which is an admin's act, for_caller the person holding admin on one of the plugin's edge roles, its count and span the index's.
| Raises | When |
|---|---|
CommandRefused |
The unit's last record was received inside the deployment's hold over the instance: reason_name is REFUSAL_REASON_WITHIN_HOLD. Nothing is recorded, and nothing is deleted. The plugin is never told the hold; try again on a later pass. |
ValueError |
The kind is not declared; the unit is deleted already; the unit is archived and no for_caller is given; or, as the plugin itself, the kind's <kind>_past_window is not deleted. |
RuntimeError |
The settings have not arrived yet. |
FileNotFoundError |
The unit is not in the plugin's storage. |
NotGranted |
The person does not hold admin on one of the plugin's edge roles. |
CallFailed |
The sidecar or the deployment refused it otherwise. |
find_record()¶
Where a raw record's key stands, by the index: the last move of the unit holding it, the unit itself or a path in it, whose outcome is MOVE_OUTCOME_ARCHIVED (archived, restorable), MOVE_OUTCOME_RESTORED (readable in the restore area) or MOVE_OUTCOME_DELETED; or None for a record never moved, in storage where the plugin put it. A row's raw-record key resolves through it on the plugin's own page, never to nothing.
What is stored¶
plugin.stored is what the plugin's storage holds of each kind, as only the plugin knows it: one StoredSpan per kind, set whenever it changes and sent on every heartbeat from the next on, which the plugin's Summary draws beside what the archive holds.
StoredSpan field |
Meaning |
|---|---|
record_kind |
A declared kind, once. |
record_count |
How many records of it the storage holds. |
first_received_ns, last_received_ns |
When the first and the last of them were received; both 0 with no record. |
bytes |
The bytes the kind uses of the archive. The SDK fills it in from its index on every heartbeat, in place of any the plugin set, and adds a kind the plugin left out that the archive holds some of. |
Raises ValueError for a kind not declared, a kind twice, more than 16, or a span that is not from the first record to the last, and TypeError for anything but a StoredSpan; nothing is set then.
The moves¶
What a move carries, as the sidecar records it (RecordMoveRequest): record_kind, unit, record_count, first_received_ns, last_received_ns, outcome (MoveOutcome: MOVE_OUTCOME_ARCHIVED, MOVE_OUTCOME_RESTORED, MOVE_OUTCOME_RETURNED or MOVE_OUTCOME_DELETED) and rule, a window's or the restore period's. A move made for a person names no rule: the person is read from the assertion, never a field. Nothing of a record's content is in a move.
The sidecar checks each before it leaves, refusing CallFailed with kind="invalid" naming the field by its path: a kind the version did not declare, archived of a kind not archivable, a key, count, span or rule out of bounds. The deployment refuses archived for an instance allowed no archive. A restore, its return and an archiving for a person need write on one of the plugin's edge roles, and a deletion for a person admin (NotGranted).
The lake¶
preview. The lake keeps what sources say about prices, append-only and point in time. A data plugin puts prices and bars in; reporting, portfolio, compliance and signal read and hear them. It puts trades and quotes in too, which signal and ems read and hear: see Trades and quotes. The SDK carries the typed operations and nothing of any vendor's: no HTTP or WebSocket client, no vendor-file parser, no reconnecting stream. Each operation is in Typed operations; each field in the lake's data dictionary.
A data plugin's catalogue¶
A data plugin declares the datasets it serves in its declaration, from code, beside its settings and storage: Declaration(catalogue=[DatasetDeclaration(...)]). A dataset's ID in a deployment is the instance, a colon and its key, f"{plugin.identity.instance_id}:daily", which each row names.
from meridian import DatasetDeclaration, Declaration, RecordKind, Storage
DECLARATION = Declaration(
settings=SETTINGS,
storage=Storage(kinds=[RecordKind("responses", "Raw responses", window_days=30)]),
catalogue=[
DatasetDeclaration(
key="daily",
vendor="Coinbase",
data_types=["meridian.v1.Price", "meridian.v1.Bar", "meridian.v1.Bar.trade_count"],
modes=["pull", "push"],
cadence=86_400,
history=3650,
terms_and_pricing="Public market data, free of charge; one person's own use.",
day_time_zone="Etc/UTC",
day_end_minute=0,
gap_recovery="backfill",
),
],
)
DatasetDeclaration, keyword only:
| Field | Type | Meaning |
|---|---|---|
key |
str |
Required. 1 to 40 lowercase letters, digits and underscores, a letter first; once in the catalogue. |
vendor |
str |
Required. Who originated the data, 1 to 64 characters, as the plugin names it. |
data_types |
sequence of str |
Required, 1 to 32: the lake's types it serves by their message (meridian.v1.Price, meridian.v1.Bar), and the optional fields it fills by their dictionary entry (meridian.v1.Bar.vwap), each once. |
modes |
sequence of str or ObservationMode |
Required, 1 to 3, each once: pull, push, stream. |
aggregator |
str |
Who carries it, where an aggregator does; empty when reached directly. |
cadence |
int |
Seconds between the source's updates, which say when it is silent; 0 when it updates only when asked. |
history |
int |
Days of history the source serves; 0 for none stated. |
terms_and_pricing |
str |
The vendor's terms and pricing in plain words, at most 2,000 characters: the sources behind it, what it charges and on what meters, what it forbids. Shown on Data sources and in the plugin's docs, read by the deployment admin's agent as data; never a licence, which is the deployment admin's (see Licences and entitlements). Added in SDK 0.36.0, in place of licence_default. |
day_time_zone |
str |
The IANA zone its business dates are in; empty for a dataset that is not daily. |
day_end_minute |
int |
Minutes after local midnight the day ends, 0 to 1,439. See Dates and time. |
venue_id |
str |
The venue the dataset is, a venue master ID (VEN-); empty for a consolidated dataset. |
calendar_id |
str |
The settlement calendar its business dates follow, its CAL- ID; empty for none. The lake flags a daily value dated on a day the calendar says is closed, and refuses nothing by it. Added in SDK 0.28.0. |
sessions_calendar_id |
str |
The trading-sessions calendar its live rows follow, a CAL- ID beside calendar_id: an FX dataset names the FX week. helpers.sessions.read_sessions(self, dataset) reads its sessions. Added in SDK 0.36.0. |
declared_delay_ns, price_source_class |
int, str |
How far behind real time the vendor serves it (0 for real time), and the class of source its prices come from (traded, firm_quote, composite or evaluated; empty for not stated). See Data age and liveness. |
gap_recovery |
str |
How a gap in it is recovered: backfill (the vendor serves history, so the lake asks again through the want), stream_only or none; empty reads as none. See Gaps. Added in SDK 0.36.0. |
The catalogue declares no licence: DatasetLicence and DataEligibility left the SDK in 0.36.0, and meridian plugin migrate writes a licence_default as terms_and_pricing (see 0.35.5 to 0.36.0). A dataset nobody has licensed is kept and read, each use right read restrictively, until a deployment admin licenses it.
Shards. Declaration(most_shards=...), 0 to 64, is the most pods of one instance its vendor allows; 0 or 1 says it is not shardable. A deployment admin may launch the instance with up to that many shards; the deployment keeps them on the launch, and the plugin runs one pod today (see Its limits, shards and standby pod). Added in SDK 0.36.0.
Raises ValueError when the declaration is built, naming the dataset: a key out of form, a vendor or aggregator too long, a data type or field no dictionary entry names, one named twice, no mode or a mode twice, a negative cadence or history, a zone that is no IANA zone, a day end out of range, a venue that is no VEN- ID, a calendar that is no CAL- ID, terms and pricing past 2,000 characters, a gap_recovery that is none of its three words, most_shards outside 0 to 64, and a catalogue on a version not holding data. The upload's JSON carries the catalogue only where one is declared, its modes as words. The sidecar holds a catalogue to the same rules at registration, and refuses the plugin naming the field.
Recording¶
A data plugin records with record_prices and record_bars, 1 to 500 rows a call, refused outside the bound before anything is sent, recorded whole or refused naming the row and field. Resolve each subject first with resolve_identifier, and each venue with resolve_venue; report what does not resolve. A want for an instrument another source resolved, such as a custodian's, names the deployment's ID: read its record with resolve_instrument and name it to the vendor by an identifier the vendor takes. The answer counts the rows recorded, restated and unchanged.
Wants¶
A data plugin hears what the lake wants of it with its data class's on_observations_wanted (required of data) and on_want_withdrawn, records against a want with want_id=, and declines per subject with decline_want. data.Wants(self, ...) does this for a plugin's own conversion (see Each role's common logic); by hand:
class Lake(data.Role):
async def on_observations_wanted(self, heard: meridian.Heard) -> None:
want = heard.message # dataset, data_type, subjects, kinds, business_date or a range
await self.record_prices(prices=fetched(want), want_id=want.want_id)
await self.decline_want(want_id=want.want_id, subjects=uncovered, reason="not_covered")
async def on_want_withdrawn(self, heard: meridian.Heard) -> None:
stop_keeping_current(heard.message.want_id)
A standing want asks that its subjects be kept current until it is withdrawn.
Reading¶
A reader reads with list_prices and list_bars, and learns the datasets it may read with list_datasets. The readers' choices are the reads' parameters, not a library:
from datetime import date
closes = await role.list_prices(subjects=held, kinds=["close"], business_date=date(2026, 10, 8))
then = await role.list_prices(subjects=held, business_date=date(2026, 10, 8), as_of_ns=cut)
both = await role.list_prices(subjects=held, sources=meridian.SourceChoice(side_by_side=True))
named = await role.list_prices(subjects=held, sources=meridian.SourceChoice(named=["alpaca-1:daily"]))
and hears what is recorded after with on_prices_recorded and on_bars_recorded, for the subjects its class's subjects() names; see Events.
The source priorities. A role that reads the lake (reporting, portfolio, compliance, signal, ems, oms, operations, servicing) reads the order a default read takes its sources in, list_source_priorities: each priority with only the datasets the plugin is entitled to read, and a priority naming none of them not at all, so a page can say which source its values come from next. Setting one stays a deployment admin's. Added in SDK 0.36.0.
read = await role.list_source_priorities()
for priority in read.priorities:
print(priority.data_type, priority.kind, list(priority.datasets))
Rates¶
Added in SDK 0.32.0; preview. meridian.roles.data.rates
is the shared rates and curve conversion: each publisher's client stays in
its own plugin. See Rates and
Record rates in a data plugin.
| Name | What |
|---|---|
Feed(plugin, source_name, *, wants=None, day_zone="UTC") |
A data plugin's rates: subject(*identifiers, description="", instrument_type="") resolves a referential record once per process, reporting one not held as a miss once a day and, changed in SDK 0.32.3, asking it again the same day after 5, then 15, then every 60 minutes; rate(subject, key, kind, value, *, business_date, published_ns=0, convention=None, tenor=None, maturity_date="", preliminary=False, volume=None, transaction_count=None, percentiles=None, series="", raw_key="") builds one Rate, and, changed in SDK 0.32.3, refuses one with no convention, an index level taking the convention of the rate it compounds; curve(subject, key, kind, points, **given) one row per tenor published, none for one left empty; record(key, rows, want_id="") sends them 500 a batch, through wants where given |
read_curve(plugin, subject, business_date, *, kinds=(), tenors=(), sources=None) |
A curve's points for a date, every page of list_rates, in tenor order, a point with an exact maturity after the tenors |
tenor(label), tenor_label(tenor), tenor_days(tenor) |
A publisher's label (1 Mo, 13 WEEKS, 10 Yr, 3M, ON, TN, SN) as the interface's Tenor, refusing a fraction; its one label (3M); its length in days, to order a curve |
percent(value), index_level(value, base="") |
A UnitValue in per cent, or an index's level with its base period |
MONEY_MARKET, BOND_EQUIVALENT, DISCOUNT, CONTINUOUS |
The conventions the US and euro publishers state: actual/360 simple; semi-annual compounding; a bill's actual/360 discount; continuous on actual/365 fixed |
In meridian.roles.data.testing: rates_producers(cases, *, source), the
data suite's rates cases for a plugin built on rates.Feed; RATES_CASES,
their names; and, added in SDK 0.32.1, RATES_NOT_PRESENTED, every case a
plugin recording only rates never presents, each with why ("it records
rates, never prices" among them). Changed in SDK 0.32.2: it no longer
names records-a-datasets-liveness, which a plugin recording each poll as
its dataset's liveness presents, and one recording none adds itself, with
why. The testing Recorder answers RecordRates with its count, as it
does prices, from SDK 0.32.2. PAIRS_NOT_PRESENTED and
EVENT_CONTRACTS_NOT_PRESENTED do the same for a pairs venue and an
event-contract venue, which publish no rates.
A reader reads rates with list_rates,
by publication time with published_as_of_ns=, and hears them with
on_rates_recorded.
Latency and liveness¶
Added in SDK 0.32.0; preview. In meridian.roles, the
recorders the data and connectivity helpers and an ems use. See
Data age and liveness.
| Name | What |
|---|---|
Latency(plugin, *, ceilings=None) |
A plugin's recorders: rtt, order_ack and fill_delay per session, age(dataset, hop, value_ns) and aged(...) per hop, count, backlog, alive and clock_skew; one log-linear histogram per session and metric, every value recorded, never sampled. session_latency() answers each open session's SessionLatency, for a connection page's Sessions section; report() takes and resets the snapshots every 10 seconds, and run() does it in a loop |
SessionLatency, Percentiles, FeedDelay, SessionCeilings, Histogram |
A session's latency: p50, p95 and p99 over the last minute, each dataset's feed delay, congestion over the last hour, the backlog, the clock skew, its last sign of life and the ceilings in force |
Liveness() |
The latest liveness heard per dataset and venue: heard, given each on_liveness_recorded; listed(datasets) from a listing; at(dataset, venue_id="") |
confirmed_ns(row, liveness=None), source_ns(row) |
A value's confirmed time, the later of its source time and its dataset's liveness after it; a row's source time |
in_session(sessions, at_ns, *, extended=False) |
Whether a moment falls in a venue's trading sessions, a dated day's in place of its weekday's |
Changed in SDK 0.33.0: Latency.report() sends the snapshots it takes as
ReportLatency, every 10 seconds
under run(), and core's latency monitor judges them (see
Data age and liveness). In
SDK 0.32 it sent nothing. Nothing in a plugin's code changes.
A data plugin's Wants takes latency= and records each row's age at the
edge hop as it records it, and Wants.liveness(key, alive_at_ns=0,
venue_id="") records a dataset's sign of life
(record_liveness). A
connectivity.Trader takes latency= and session=. A pairs venue's feed
records its stream's heartbeats (alive(), alive_ns, liveness_ns), and,
added in SDK 0.32.3, so does a listings feed (listings.Feed): heard
notes the stream's last message and flush records its live dataset's
liveness, with no venue, once a pass when a message was heard since the
last. A data plugin's Connection draws Sessions only once a session is
measured, from SDK 0.32.2, so a polled plugin's page has none.
The data suite¶
meridian.suites carries each role's conformance suite, as for custody: a plugin maps each case to its own recorded or synthetic exchange with its vendor, runs its own conversion against a Recorder (its typed operations, recorded rather than sent, answered as a sidecar answers them), and run compares what it sent with the case.
from meridian.suites import run
report = run("data", PRODUCERS, instance_id="my-prices-1")
assert report.passed, report.failures
A case naming a row the plugin hears, such as a want, gives it what recorder.answer(row, reply) returns, heard by the plugin's own class bound to the recorder:
async def a_want_recorded_against(recorder):
recorder.answer("ObservationsWanted", lambda _: synthetic_want())
await recorder.hear(recorder.bind(Lake(State())))
The cases are listed in Write a data plugin against its suite. A plugin holding data is verified for it only by passing every case it presents, which meridian plugin check --verified --run-tests holds it to. The reading roles, reporting, portfolio, compliance, signal and ems, have suites of their own, run the same way, as do connectivity, oms, confirmation, servicing and settlement; meridian plugin check --verified holds only the custody, data and connectivity suites, so a reading role's suite is run by the plugin's own tests alone.
A case not presented¶
A case asserting one value of a closed list (Case.closed_list) that the plugin's source never presents may be named in not_presented, with why. So may a case about one kind of data (Case.about, in the interface's words: closes, bars, trades, quotes, prices on a venue, a currency's rate against another currency, a crypto asset's price, a security's price) that the plugin declares it never publishes. Every other case is required, and a case not presented without a reason fails:
report = run("data", PRODUCERS, not_presented={
"a-trade-recorded": "daily closes and bars only: this source states no trades",
})
So may a case about one field (Case.about_field, a dictionary entry such as meridian.v1.Trade.aggressor) that the plugin's source never states, though it publishes the kind: see A field your source never states.
Case.about names a kind of work as well as a kind of data, in any role's suite: every role's reads-a-calendar is about calendars, asks-for-a-settlement-date (every role's but connectivity's and operations') about settlement dates, connectivity's records-a-locate and resolves-a-venue-symbol-by-the-facts-date about locates, and its reads-whether-releases-wait-for-compliance and hears-an-accounts-attributes-change about compliance waits. A plugin whose work never uses one names the case in not_presented, with why, and is verified on the rest:
report = run("custody", PRODUCERS, not_presented={
"reads-a-calendar": "this plugin reports what its custodian states; it reads no calendar",
})
connectivity's resolves-an-executions-venue and reports-a-venue-not-held carry Case.about_field, meridian.v1.ReportExecutionRequest.venue_id: a plugin whose venue never states a fill's place of execution names both. The runner refuses, as a failure, a name in not_presented that is neither a closed-list case nor one with an about or an about_field.
More cases carry about (added in SDK 0.27.0), each a thing the plugin's source never states or its work never does:
about |
Cases |
|---|---|
plan fund codes |
custody's activity-plan-code-not-linked, activity-plan-code-linked-by-a-person, activity-re-resolved-when-a-plan-code-is-linked-later |
sweep funds |
custody's cash-net-of-a-fund-counted-in-cash, fund-larger-than-cash |
splits |
custody's activity-moves-no-cash |
currencies not stated |
custody's currency-not-stated, for a custodian that always states the currency |
charges without a fill |
connectivity's reports-a-charge |
busts |
connectivity's reports-a-bust |
venue-originated fills |
connectivity's reports-a-fill-on-no-placement, reports-a-later-fill-on-the-accepted-placement, re-reports-a-charge-on-the-accepted-placement |
value-date rolls |
connectivity's reports-a-value-date-roll, for a venue that settles its trades |
outcomes |
the data suite's records-an-outcome and records-a-partial-payout; hears-and-reads-an-outcome in reporting's, portfolio's, compliance's, signal's and operations' suites |
disputed outcomes |
the data suite's restates-a-disputed-outcome, for a source that states no dispute |
rates |
Added in SDK 0.32.0. The data suite's seven rates cases (a-rate-fixing, a-same-day-revision, a-curve-for-a-date, a-tenor-not-published, two-conventions-one-tenor, an-index-level, a-curve-point-from-another-source) |
streams |
Added in SDK 0.32.0. The data suite's records-a-datasets-liveness, for a source that is polled and streams nothing |
rfqs |
Added in SDK 0.32.0. Connectivity's records-a-dealers-quote-on-an-rfq-placement, for a venue that takes no RFQ |
option chains |
Added in SDK 0.33.0. The data suite's records-a-chains-series, records-open-interest and a-chain-row-is-never-resolved |
filings |
Added in SDK 0.33.0. The data suite's a-filing-recorded, a-fact-restated, a-fact-confirmed and resolves-a-filer |
holdings |
Added in SDK 0.33.0. The data suite's a-complete-holdings-report, a-cusip-used-as-key and a-stake-crossed |
corporate actions |
Added in SDK 0.33.0. The data suite's a-dividend-declared |
ratings |
Added in SDK 0.33.0. The data suite's a-rating-action |
quotes |
Added in SDK 0.33.0. The data suite's a-crossed-quote-is-recorded-invalid, a-duplicate-is-recorded-suspicious and rows-after-a-gap-are-suspicious, which expect quote rows |
options |
Added in SDK 0.33.0. Custody's reports-an-assignment, connectivity's reports-an-expiry and servicing's proposes-an-exercise-by-exception |
packages |
Added in SDK 0.33.0. Connectivity's reports-a-package-fill |
derived series, scenarios, quality states |
Added in SDK 0.33.0. Signal's records-a-derived-series, saves-a-scenario-set and escalates-an-outlier |
Changed in SDK 0.33.0: the data suite's reports-its-feed-delay,
connectivity's reports-a-sessions-round-trip and the ems's
reports-its-pre-trade-checks-data-age are no longer about: latency, and
are required. The reading roles' v28 cases (a chain, open interest, a
statistic heard, an account's lifecycle events, a derived series, a saved
scenario set, declared dividends, a company's filings, reported facts, who
holds a security and a rating history, each read or heard) are required.
meridian.roles.data.testing.feed_delay maps the data suite's feed-delay
case for a plugin built on the SDK's kinds.
| trades | Changed in SDK 0.32.0: the data suite's queries-the-lake-within-bounds, which a data plugin recording no trades marks "it records no trades" |
| closes | Changed in SDK 0.32.1: the data suite's an-exact-price and a-want-recorded-against, which a plugin recording only rates marks "it records rates, never prices" |
| latency | Removed in SDK 0.33.0. In SDK 0.32 the data suite's reports-its-feed-delay, connectivity's reports-a-sessions-round-trip and the ems's reports-its-pre-trade-checks-data-age carried it, every plugin holding the role naming them not presented; they are required in full |
report = run("connectivity", PRODUCERS, not_presented={
"reports-a-value-date-roll": "an equity broker: its trades settle, and nothing rolls",
"reports-a-charge": "commission-free: the broker states no fee without a fill",
})
any_of. Added in SDK 0.27.0. An expectation may name its fields any_of, in place of fields (Expectation.any_of), where the interface answers a case in more than one way: a message on the row passes when it matches one of the ways in full. Connectivity's reports-a-fill-with-its-settlement-date asks the date read with a security's settlement inputs or a pair's spot inputs, from its trade date or the fill's moment; custody's currency-not-stated takes a currency derived by a named rule or supplied by a person (PROVENANCE_KIND_SUPPLIED); and the data suite's records-an-outcome an outcome at OUTCOME_STAGE_DETERMINED or OUTCOME_STAGE_FINAL. A plugin sends whichever its market needs, named in full:
async def fills_with_its_settlement_date(recorder):
await recorder.compute_date(spot=SpotDateInputs(pair=[eur, usd], at_ns=fill_ns))
...
A restart's gap¶
A data plugin that records trades reads, on start, the last trade its own instance recorded of each subject it streams, list_trades(subjects=..., latest_recorded=True), and records what the venue's history states since. meridian.roles.data.backfill_restart(plugin, subjects, history) does it, in batches of 500, history answering the trades after a given one (the lake's row, its subject, dataset, venue and time in its meta), and answers how many it sent. A trade recorded again is answered unchanged by its row key. The data suite's a-restarts-gap-backfilled holds it.
A data plugin also reads list_datasets, answered only its own declared datasets with their licences, so its Datasets page says whether each is licensed (Served.licensed in meridian.roles.data).
Trades and quotes¶
preview. Still typed operations only: no WebSocket client, no reconnecting stream, no bar builder.
A data plugin records them from a dataset declaring data_types=["meridian.v1.Trade", "meridian.v1.Quote"] (or one of them) and modes=["stream"], with record_trades and record_quotes, 1 to 500 a call. Each is keyed by UTC instants and names no business date. A trade carries its TradeAttributes, converted at the edge from its source's condition codes, a code with no conversion kept in meta.unconverted; and its aggressor where its source says which side took liquidity (a source naming the resting order's side records the other). A withdrawn trade is the next version under its row key, cancelled=True. A quote's empty side is left unset. Keep a standing want of a streamed dataset current from the stream.
signal and ems read them, with list_trades (over a range within a day, or after_watermark=) and list_quotes (at the latest, per asset, or over a range), and hear them with on_trades_recorded and on_quotes_recorded, for the subjects their class's subjects() names; see Events. reporting reads neither. See Read trades and quotes in a signal or ems plugin.
Outcomes¶
Added in SDK 0.27.0; preview. A data plugin
records an event contract's outcome
from a dataset declaring data_types=["meridian.v1.Outcome"], with
record_outcomes, 1 to 500 a call,
one row key per contract, each change of stage the next version under it:
await role.record_outcomes(outcomes=[meridian.Outcome(
meta=meridian.ObservationMeta(row_key=f"{ticker}:outcome", subjects=[subject], ...),
determination="pays_part", stage="final",
settlement_value=meridian.Money(Decimal("0.4500"), "USD"),
settled_at_ns=settled,
determination_as_reported=edge.as_reported("kalshi:result", "scalar"),
)])
reporting, portfolio, compliance, signal and operations read them
with list_outcomes by subject, the
version in force or as_of_ns= a recorded time, or after_watermark= the
last seen, and hear every version with on_outcomes_recorded, for the subjects
their class's subjects() names, caught up from the watermark after a loss as trades are.
The data suite's records-an-outcome, restates-a-disputed-outcome and
records-a-partial-payout hold it.
A daily value on a closed day. Added in SDK 0.27.0. A price or bar read back
carries ObservationMeta.closed_day, a DateCheck, where the lake found
it dated on a closed day of its dataset's calendar, and a listed dataset
its closed_day_count and outside_coverage_count. The lake sets them;
a data plugin sends neither.
Money and dates¶
A Money names its cash instrument: Money(Decimal("12.50"), "USD") keeps its shape, and core resolves the code; a token is named by instrument_id alone. Every field the dictionary types date takes a datetime.date or its ISO text, and refuses text that is no date and a datetime. See Money and instruments and Dates and time.
The order path¶
An order reaches a venue
through the book. Each role does its part through
typed operations, one per row it may
send, and hears the book's records as they change (see Events);
the book holds every order, placement, execution and charge, and alone
decides what an order's state allows. See
The order lifecycle. Every operation
here is preview.
# portfolio, or the oms: propose. A unit quantity or a cash quantity, never both.
await role.propose_order(
account_id="ACC-1", instrument_id=aapl, side="buy", quantity=Decimal("10"),
order_type="limit", limit_price=Money(Decimal("227.50"), "USD"),
time_in_force="day", idempotency_key="propose:ACC-1:pm-ticket-4471",
acting_for=request.caller.header,
)
# the oms: release it to be worked.
await role.release_order(order_id=order_id, idempotency_key=f"release:{order_id}",
acting_for=request.caller.header)
# the ems: claim it, and place at a connectivity plugin linked to it, with the
# venue account code its map holds for the account.
await role.set_working_condition(order_id=order_id, condition="claimed", set=True,
acting_for=request.caller.header)
await role.create_placement(
order_id=order_id, connectivity_instance_id="venue-1", venue_account_code="X12345",
quantity=Decimal("10"), idempotency_key=f"place:{order_id}:1",
acting_for=request.caller.header,
)
# a connectivity plugin: say what the venue said of the ems's placement, each
# with its raw record.
await role.report_placement(
placement_id=placement_id, event="acknowledged", venue_order_id="V-881",
raw_record=role.plugin.raw_record(key),
)
- Who calls what.
compliancechecks and decides (record_check,decide_approval); anemsworks a strategy in house as child orders of a released order (propose_order(link=OrderLink(kind="split", parent_order_id=...))); theomsrecords a telephoned or outside order for a person (record_order,origin="telephoned"); a connectivity plugin reports fills, busts and charges on theems's placements (report_execution,report_charge), records a locate (record_locate), and publishes its venue's catalogue (report_venue_catalogue), which anomsand anemsread (read_venue_catalogue). Anems's admin page maps an account to a venue account code withmap_venue_account, acting for the admin viewing it. - Orders are
portfolio's and theoms's, placements theems's, fills the connectivity plugin's. A connectivity plugin records no order and creates no placement: itsrecord_orderorcreate_placementisNotGranted, refused by its sidecar. It reports a fill the venue originated on no placement, which theemsanswers: see Venue-originated fills. - Every command but a fact report acts for someone: a person, through
acting_for=request.caller.header, or the role's service account. A fact a connectivity plugin reports names nobody.record_orderis a command: theoms's, for a person or its service account. - A placement reaches only the connectivity plugin it names. An
emspage offers the instances inrequest.caller.connectivity_instance_ids; the sidecar checks the placement against them whatever the page draws, and the book refuses and records one at any other (REFUSAL_REASON_CONNECTIVITY_NOT_ALLOWED). - A placement carries its own change requests. When the
emsor theomsasks to cancel or replace a placement (request_placement_change), the placement its connectivity plugin hears holds each request inchange_requests, with itsPlacementChangeand its outcome. Tell a cancel from a replace by the change'skind, and read a replace's new quantity, limit and time in force from it, never from a condition's reason (see A placement's change requests). - A connectivity plugin states an execution's settlement date: the
venue's, or one asked of core with
compute_date, carried with its basis (computed_settlement_date,settlement_date_basis). A fill with neither is refused, namingsettlement_date. - The trading instructions reach every subscriber whatever its
accounts. Read those in force on start with
list_trading_instructionsand hear each change withon_trading_instruction_changed; honour a pause and a stop as your plugin's role requires (see Trading instructions). - Refusals. One by the order's state, a claim or a link is a
CommandRefusedwith its code:REFUSAL_REASON_ORDER_STATE,_CLAIMED,_CHECK_BLOCKS,_APPROVAL_REQUIRED,_ALLOCATION_UNBALANCED,_IDENTIFIER_IN_USE,_CONNECTIVITY_NOT_ALLOWED,_CONNECTIVITY_LINKED,_QUANTITY_EXCEEDS_ORDER,_EXECUTION_STATEand_EXECUTIONS_DIFFER(the last three added in SDK 0.27.0). A command naming an order or a placement by its ID whose account is outside the plugin's write scope or the person's write access isNotGranted, with no code. An order stating both or neither ofquantityandcash_quantityisCallFailed, kind"invalid", naming both in.fields. - The suites.
connectivityandomseach have a suite, andportfolio's,ems's,compliance's,operations' andreporting's hold the order path's cases. Everyconnectivitycase is required but seven:records-a-locateandresolves-a-venue-symbol-by-the-facts-date(aboutlocates; the second resolves the symbol on a prime's answer to a locate),reads-whether-releases-wait-for-complianceandhears-an-accounts-attributes-change(compliance waits) andreads-a-calendar(calendars), which a plugin whose work never uses them names innot_presented, with why; andresolves-an-executions-venueandreports-a-venue-not-held(about_field), which one whose venue never states a fill's place of execution names. Six more may be (added in SDK 0.27.0):reports-a-bust(busts),reports-a-charge(charges without a fill), the three venue-originated fill cases andreports-a-value-date-roll; and theemssuite holds the accept's and the DK's four cases, required. See A case not presented.
The transition table. The order's states, conditions and transitions
are one table, which the SDK carries as published,
meridian/transitions.json:
from importlib.resources import files
import json
table = json.loads(files("meridian").joinpath("transitions.json").read_text())
- An FX order names its pair's quote currency (added in SDK 0.27.0),
propose_order(quote_currency=usd), the second currency as the pair is quoted, by its cash instrument's ID; a quote currency naming the order's own instrument, or a limit or stop priced in another currency, isCallFailed, kind"invalid", naming the field. A fee'skindmay befinancingorcommission(added in SDK 0.27.0).
Venue-originated fills¶
Added in SDK 0.27.0. A connectivity plugin
reports a fill the venue originated on no placement with
report_execution, naming its instrument_id, side, venue_order_id
and, where sent, venue_client_order_id; the book holds it unmatched
(Execution.state, EXECUTION_STATE_UNMATCHED). The ems lists them,
list_orders(execution_state="unmatched"), and answers each, for a person
or as its service account:
from meridian.plugin.v1 import operations_pb2 as ops
await role.accept_execution(execution_ids=[e1, e2], order_id=order_id,
note="Placed in the broker's app.", acting_for=request.caller.header)
await role.accept_execution(execution_ids=[e3], new_order=ops.NewOrderFromExecutions(sleeve="core"),
note="The broker's liquidation.", acting_for=request.caller.header)
await role.dont_know_execution(execution_ids=[e4], reason="no_matching_order",
acting_for=request.caller.header)
accept_execution takes one of order_id and new_order (the oneof
matched_order; both is refused naming it), answers the executions, the
order, the placement created at filled and the journal reference;
dont_know_execution takes a DkReason and text, required with
other, and answers the executions. The connectivity plugin hears each
answer (on_execution_recorded, the executions it reported) and
tells its venue as its catalogue says
(VenueCatalogue(execution_acknowledgement="bn" | "dont_know_trade" |
"none")). See
Answer a venue-originated fill
and The order lifecycle.
A value-date roll¶
Added in SDK 0.27.0. A connectivity plugin whose venue rolls spot FX positions reports each roll, the new value date asked of core for the roll's moment, after the financing charges it made:
answer = await role.compute_date(spot=meridian.SpotDateInputs(pair=[eur, usd], at_ns=rolled_ns))
rule = answer.basis.rule
basis = meridian.DateBasis(
rule=meridian.DateRuleRef(kind="spot", subject=rule.subject, version=rule.version),
calendars=list(answer.basis.calendars),
)
await role.report_value_date_roll(
venue_account_code="X12345", pair=[eur, usd],
from_value_date="2026-10-14", to_value_date=answer.date, value_date_basis=basis,
rolled_at_ns=rolled_ns, charge_ids=[financing_charge_id],
raw_record=role.plugin.raw_record(key), idempotency_key=f"roll:X12345:{rolled_ns}",
)
Each financing charge is reported first with report_charge, its fee's
kind financing. A fill booked after its trade was rolled gets a catch-up
roll from the value date it was recorded with. See
A venue's value-date roll.
A placement's change requests¶
A connectivity plugin hearing its placement reads each cancel or replace asked of it from the placement itself, each tracked to its outcome (pending, applied or rejected); the order's own change requests stay on the order:
from meridian.plugin.v1 import operations_pb2 as ops
async def on_placement(heard: meridian.Heard[ops.PlacementUpdatedEvent]) -> None:
placement = heard.message.placement
for request in placement.change_requests:
if request.outcome != ops.CHANGE_REQUEST_OUTCOME_PENDING:
continue
change = request.placement_change
if change.kind == ops.PLACEMENT_CHANGE_KIND_CANCEL:
... # cancel placement.venue_order_id at the venue
elif change.kind == ops.PLACEMENT_CHANGE_KIND_REPLACE:
limit = meridian.as_money(change.limit_price) # and the rest
Algorithms¶
A version declares the algorithms its autonomous work may name, each
identifier stable across versions and reinstalls, with a description a
person reads on the plugin's page (1 to 1,024 characters), and the work
names the one it runs; the book records it on each entry's service account
actor, where a reporting plugin reads which algorithm decided or executed:
from meridian import AlgorithmDeclaration
from meridian.declaration import Declaration
DECLARATION = Declaration(algorithms=[
AlgorithmDeclaration("twap-v1", "Time-weighted slices over the order's window."),
])
async with meridian.runs_as("ems", algorithm_id="twap-v1") as act:
await act("work_order", {"order_id": order_id}, reason="TWAP slice 3 of 12.")
Each identifier is declared once, with no spaces around it. One the version
does not declare is refused by runs_as before anything is sent, as the
sidecar refuses it (CallFailed, kind "invalid", naming algorithm_id).
An empty algorithm_id names none; a person's act names none. The declaration's algorithms are uploaded with it and shown
on the plugin's Summary.
Settlement calendars¶
A plugin never works out a
settlement or value date itself: it reads a calendar's days
(read_calendar) or asks core for the date (compute_date), answered with
the rule and calendar versions it was worked out on, the same answer for
every plugin. Any role calls both, whatever its accounts. The SDK carries
the two operations and no calendar library. See
Settlement calendars.
from meridian.plugin.v1 import operations_pb2 as ops
answer = await role.compute_date(
settlement=meridian.SettlementDateInputs(instrument_id=aapl, trade_date="2026-10-09")
)
if answer.outcome == ops.DATE_OUTCOME_COMPUTED:
settles = answer.date # "2026-10-13": Columbus Day skipped
basis = answer.basis # the rule and each calendar, with versions
await role.compute_date(spot=meridian.SpotDateInputs(pair=[eur, usd], trade_date="2027-03-24"))
await role.compute_date(
tenor=meridian.TenorDateInputs(pair=[usd, jpy], tenor=meridian.Tenor(count=1, unit="month"),
trade_date=date(2026, 10, 30))
)
days = await role.read_calendar(calendar_id=cal, from_date="2026-10-01", to_date="2026-10-31")
compute_date takes exactly one question -- settlement, spot, tenor,
roll (a date rolled by a convention on a set of calendars) or
business_day (whether a date is a business day on a set of calendars) --
and as_of_ns, a recorded time, for the versions in force then; 0 for the
latest. Its answer's outcome is one of:
outcome |
Means |
|---|---|
DATE_OUTCOME_COMPUTED |
date, with its basis |
DATE_OUTCOME_CALENDAR_NOT_HELD |
A calendar is not held yet: core asks the platform for it; ask again later |
DATE_OUTCOME_OUTSIDE_COVERAGE |
A day the question needs is outside a calendar's coverage; nothing is computed |
DATE_OUTCOME_RULE_NOT_HELD |
No rule is held for the subject yet, asked of the platform as a calendar is |
A malformed question is refused, CallFailed of kind "invalid", naming
the field. read_calendar answers a calendar's weekends, listed days (each
closed weekday or weekend business day, with its name and source),
coverage, sources and version over the range, as of a recorded time or its
latest version; one not held is answered not found and asked of the
platform.
A value a plugin records from the date read carries the provenance
computed, with the date's basis (Provenance.date_basis), since a date
core worked out is not the plugin's own rule. Every role's suite holds
reads-a-calendar, about calendars, and every role's but
connectivity's and operations' asks-for-a-settlement-date, about
settlement dates: a plugin whose work never uses them names them in
not_presented, with why (see A case not presented).
connectivity's reports-a-fill-with-its-settlement-date and
operations' re-checks-a-pending-value-date ask compute_date too, and
carry no about: they stay required. The first passes on whichever inputs the fill's market needs, a security's settlement inputs or
a pair's spot inputs from its trade date or the fill's moment (any_of).
A pair named by a deployment's provisional LCL- records finds no
platform spot rule: name a pair by the platform's INS- IDs of its
currencies' cash instruments. A spot-date rule's counting mode is the
platform's, applied in core; a plugin asks the same way (see Settlement calendars).
Types¶
All the dataclasses are frozen.
Identity¶
| Field | Type | Default | Meaning |
|---|---|---|---|
instance_id |
str |
The instance's name. | |
roles |
tuple[str, ...] |
() |
The roles the instance was launched with. They decide its topic access. None is a plugin admitted with no topics. |
deployment_id |
str |
"" |
The deployment it runs in. |
Grants¶
| Field | Type | Default | Meaning |
|---|---|---|---|
publish |
tuple[str, ...] |
() |
Topic patterns the plugin may publish to. |
subscribe |
tuple[str, ...] |
() |
Topic patterns it may subscribe to. |
Interface¶
The pages the plugin serves to people, on loopback. Only the plugin's sidecar reaches them, forwarding requests the dashboard vouched for.
| Field | Type | Default | Meaning |
|---|---|---|---|
port |
int |
The loopback port the pages listen on. | |
title |
str |
The plugin's title. | |
pages |
Pages or sequence of Page |
() |
The plugin's pages, one list in the order shown, each with the levels it serves. A Pages registry declares each where its view is and refuses a session at a level it does not serve. Keyword only. |
admin_pages |
sequence of Page |
() |
Retired: declare pages at admin in pages. meridian plugin migrate rewrites it into pages. |
The dashboard shows a plugin's pages in its area, under the home's button for each level a page serves: Manage for admin, Open for write, View for read. A plugin that declares no page at write or read has one there, its /. See Manage, Open and View.
Page¶
One of the plugin's pages, at a path on its own host, and the levels it serves. Pages makes one for each @pages.page; build one yourself only for a page served some other way.
| Member | Type | Meaning |
|---|---|---|
path |
str |
The path, beginning with /. Anything else raises ValueError at connect. |
title |
str |
The tab's title. |
levels |
sequence of str or AccessLevel |
The levels it serves: one or several of "admin", "write" and "read", an AccessLevel, or its name ("ACCESS_LEVEL_ADMIN"). A string alone is one level. Anything else raises ValueError, and a page with none raises it at connect. |
roles |
sequence of str |
The roles it serves, from those the plugin was launched with. Default (): a plugin holding one role, or none, names none. See Roles. |
serves(caller: Caller) -> bool |
method | Whether the caller's session is at one of its levels; for a page naming roles whether the caller's level on one of them is. A session at no level is served nothing. |
statements = meridian.Page("/statements", "Statements", levels=["write", "read"])
if not statements.serves(caller):
... # answer 403
One path may serve several levels, and adapts by caller.level: the dashboard frames the same path under Open and under View.
AccessLevel¶
The generated protobuf enum meridian.v1.sidecar_pb2.AccessLevel: a person's level on a plugin, the same three for every plugin.
| Value | Number | Button | Spelling |
|---|---|---|---|
ACCESS_LEVEL_UNSPECIFIED |
0 | A session at no level, which holds nothing. | |
ACCESS_LEVEL_READ |
1 | View | "read" |
ACCESS_LEVEL_WRITE |
2 | Open | "write" |
ACCESS_LEVEL_ADMIN |
3 | Manage | "admin" |
Setting¶
One setting the plugin needs, declared at connect.
| Field | Type | Default | Meaning |
|---|---|---|---|
name |
str |
The setting's name. | |
kind |
type |
str |
str, int or bool, or list for a table. Anything else raises TypeError at connect. |
required |
bool |
False |
Whether the plugin needs a value to be healthy. |
secret |
bool |
False |
A secret is set through the dashboard and never read back, displayed, logged, reported or bundled. The plugin receives it in Settings and nowhere else. |
description |
str |
"" |
What the setting is, sent with its declaration. |
label |
str |
"" |
The field's name on the dashboard's form; for a table, the title of its tab. |
default |
str, int, bool or None |
None |
Shown greyed in the empty field, and held in Settings.values while the setting is unset. Of the setting's kind, one of its choices if it has any, and never on a secret. |
unit |
str |
"" |
Shown beside a number. |
choices |
tuple[Choice, ...] |
() |
Makes the setting a choice, one of these, shown as radio buttons. Its kind must be str. |
applies_when |
AppliesWhen or None |
None |
The setting applies only while another holds one of some values. |
developer |
bool |
False |
Shown only on a development deployment. |
columns |
tuple[Column, ...] |
() |
A table's columns, in the order its tab shows them. Only with kind=list, which needs at least one. |
most_rows |
int |
0 |
The most rows a table holds, at most 500; 0 is 500. |
roles |
sequence of str |
() |
The roles it serves, from those the plugin was launched with: it is shown to an admin of any of them and set only by one holding admin on every one. Not who may read the value: the plugin reads every setting it declared. None on a plugin holding one role or none. See Roles. |
per_person |
bool |
False |
Each person's own, saved for them and read from their requests, never on the Settings form and never in Settings.values. Never secret, never required. See A person's own settings. |
DECLARATION = meridian.Declaration(
settings=[
meridian.Setting("api_token", secret=True, required=True, description="The rail's API token"),
meridian.Setting("poll_seconds", kind=int, description="How often to poll"),
],
reads_external_accounts=True,
)
plugin = await meridian.connect(declaration=DECLARATION, roles=[Custody()]) # its custody class
A table is rows of typed columns an admin enters in the dashboard on a tab of its own, after Settings and titled with its label: an entry grid checked cell by cell, saved through the same checks as the form. It has no default and is never secret:
PLAN_CODES = meridian.Setting(
"plan_code_links",
list,
label="Plan-code links",
columns=(
meridian.Column("account", "external_account", label="Account", required=True),
meridian.Column("code", label="Plan code", required=True),
meridian.Column("instrument", "instrument", label="Instrument", required=True),
),
most_rows=200,
)
Column¶
One column of a table setting.
| Field | Type | Default | Meaning |
|---|---|---|---|
name |
str |
The key each row's cell is held under. Never changed_by or changed_at, which the deployment stamps on each row. |
|
kind |
str |
"text" |
How the form takes a cell and the deployment checks it: "text", "integer", "decimal", "date", "choice", "external_account" or "instrument". |
label |
str |
"" |
The column's heading on the table's tab. |
required |
bool |
False |
Every row must fill it; a row with it empty is refused, naming the cell. |
description |
str |
"" |
Words shown beside its heading. |
choices |
tuple[Choice, ...] |
() |
A choice column's options, which it needs; a cell holds an option's value. |
Every cell arrives as text. What each kind accepts:
| Kind | A cell is |
|---|---|
"text" |
One line of plain text, at most 500 characters: no newline, tab, control character, or character that hides or reorders text (bidirectional, zero-width, tag or private-use). |
"integer" |
A whole number, such as "42". |
"decimal" |
An exact decimal, at most 18 places, such as "12.5". Read it with decimal.Decimal, never float. |
"date" |
A date, "YYYY-MM-DD". |
"choice" |
One of the column's choices, by its value. |
"external_account" |
The identifier of an external account this plugin reported. |
"instrument" |
A deployment instrument record's ID, picked by search and checked to exist, never a symbol. |
See Set a plugin's settings for what a table's tab checks, and the data dictionary's SettingColumn.
Choice¶
One option of a setting that is a choice.
| Field | Type | Default | Meaning |
|---|---|---|---|
value |
str |
The value the plugin receives. | |
label |
str |
"" |
What the form shows for it. |
description |
str |
"" |
What it means: a line of the setting's hint on the form. |
AppliesWhen¶
| Field | Type | Meaning |
|---|---|---|
setting |
str |
Another setting, declared before this one. |
one_of |
tuple[str, ...] |
The values of it for which this setting applies. Only then does the form show it, and ask for it when it is required. |
plugin = await meridian.connect(
roles=[Lake()], # its data class
settings=[
meridian.Setting(
"environment",
label="Environment",
default="sandbox",
choices=(meridian.Choice("sandbox", "Sandbox"), meridian.Choice("live", "Live")),
),
meridian.Setting(
"live_account", label="Live account", required=True,
applies_when=meridian.AppliesWhen("environment", ("live",)),
),
],
)
Settings¶
| Field | Type | Meaning |
|---|---|---|
values |
dict of str to str, int, bool or list |
Each declared setting the deployment holds a value for, typed by its declaration, and the default of each that has one and no value. A table's is a list of rows, each a dict of text by column name with changed_by and changed_at. |
missing_required |
tuple[str, ...] |
Required settings with no value yet, a refused one among them. |
refused |
tuple[RefusedSetting, ...] |
The saved values this version cannot read, each delivered unset. Added in SDK 0.31.1. |
A saved value this version cannot read is never raised and never mapped to another value. A value no longer among the setting's choices, as one a plugin's new release renamed, or one not of its kind, is delivered unset: a required setting holds nothing, not even its default, and is in missing_required, so the plugin's own not-ready path names it; an optional one holds its default. Each is in refused, a meridian.RefusedSetting: its name, its label, the value as saved (empty for a secret) and its reason, NOT_A_CHOICE or NOT_ITS_KIND. Its message is what a page shows: "Environment was saved as 'practice', which this version no longer offers; save it again". A typed settings class says the same in .refused, and a person's own values refused are in request.own_settings_refused. An environment so refused is empty: not live, so the plugin still reaches the demo host, and not demo, so the agents' guard counts it live until a person saves it again.
AccountScope¶
| Field | Type | Default | Meaning |
|---|---|---|---|
read |
frozenset[str] |
empty | Every account anybody may read through this plugin, and every account one of its external accounts is linked to. |
write |
frozenset[str] |
empty | Every open account anybody may write through this plugin, and every open account one of its external accounts is linked to. |
links |
tuple[LinkedExternalAccount, ...] |
() |
This plugin's links, and nobody else's. An external account the plugin reported that none of them names is unlinked. |
| Method | Returns | Meaning |
|---|---|---|
link_of(external_account_id: str) |
LinkedExternalAccount or None |
The link naming this external account, or None while it is unlinked. |
A link puts its account in both read and write while it stands. A closed account stays in read alone, and its link is still listed.
LinkedExternalAccount¶
One of the plugin's external accounts, linked to one of the deployment's accounts by an admin of the plugin.
| Field | Type | Default | Meaning |
|---|---|---|---|
external_account_id |
str |
The external account, as the plugin reported it with report_external_accounts. |
|
account_id |
str |
The deployment's account it is linked to. | |
account_name |
str |
"" |
That account's name as the deployment holds it now, so a page can say which account a link points at without acting for anybody. A rename arrives as a new delivery. |
Caller¶
Who a request for the plugin's page came from, as the dashboard vouched and the sidecar checked before forwarding it. It is read, not checked, by the SDK: only the sidecar can reach the page, and the sidecar removes every other claim the request arrived with.
A person opens a plugin at one level they hold, by a button on the dashboard's home: Manage at admin, Open at write, View at read. The session carries only that level, and the accounts are cut to it: under Open, read and write; under View, read alone; under Manage, neither, since admin configures a plugin and sees no account's data.
A person holds a level on each role of a plugin, and the session carries one entry for each role they hold something on within its button: under Open a role held at write is at write and one held at read at read, under View each at read, under Manage each role they administer at admin, with no account. roles, level_for, read_for and write_for read it. level, read and write stay the session's button and the union over its roles.
| Member | Type | Meaning |
|---|---|---|
subject |
str |
The person, as the deployment's directory names them. Deployment-local, never an address. A service account's SA- ID where subject_kind says so. |
display_name |
str |
Their name, for display. |
level |
int, an AccessLevel |
The level the session was opened at. ACCESS_LEVEL_UNSPECIFIED is a session that holds nothing. |
admin |
bool property |
Whether the session was opened by Manage: level is ACCESS_LEVEL_ADMIN. |
read |
frozenset[str] |
The accounts this plugin may show them in this session. Empty under Manage. |
write |
frozenset[str] |
The accounts this plugin may act on for them in this session. Every one is also in read. Empty under View and Manage. |
header |
str |
The Meridian-Caller header as received. Pass it as acting_for on a typed operation to send it for this person. |
deployment_admin |
bool |
Whether the person is a deployment admin. It opens no page and reaches no account. It says only that, linking an external account under Manage, they may name a new account rather than an existing one. |
delegation_id |
str |
The delegation the person acted through, when they came through a client, such as the CLI, their own agent or an MCP client, rather than a browser. Empty for a browser. |
client_name |
str |
That client's registered name. Empty for a browser. |
through_a_client |
bool property |
Whether the person came through a client on a delegation: delegation_id is not empty. |
roles |
Mapping[str, int] |
Each role's level within the session's button, an AccessLevel by role. Empty where the claims carry no per-role entry. |
level_for(role: str) -> int |
method | Their level on role within the session's button; ACCESS_LEVEL_UNSPECIFIED for a role they hold nothing on here. Claims carrying no per-role entry read every role at level. |
read_for(role: str) -> frozenset[str] |
method | The accounts the plugin may show them for role: within read, empty under Manage and for a role they hold nothing on. Claims carrying no per-role entry read every role as read. |
write_for(role: str) -> frozenset[str] |
method | The accounts the plugin may act on for them in role, those the sidecar admits a command of that role for: within read_for(role), empty under View and Manage and for a role held at read. Claims carrying no per-role entry read every role as write. |
Caller.from_header(header: str) -> Caller |
classmethod | Decode a Meridian-Caller header: base64url, unpadded. |
may_read(account_id: str) -> bool |
method | Whether they may read the account through this plugin in this session: account_id in read. |
may_write(account_id: str) -> bool |
method | Whether they may write the account through this plugin in this session: account_id in write. |
prepared_call_id |
str |
The prepared change this request was opened from or makes, PC-…; empty for none. Read it with await request.prepared(). |
own_settings |
Mapping[str, str] |
The person's own values of settings declared per_person, as the dashboard keeps them, under Open and View; empty under Manage. request.own_settings reads them typed. |
second_pair_of_eyes |
frozenset[str] |
The plugin's acts whose second pair of eyes is on for this person now, by tool name: hide Save for them and offer Prepare. For showing only: the dashboard refuses a direct make whatever the page draws. |
subject_kind |
int, a SubjectKind |
Whom the claims vouch for: SUBJECT_KIND_UNSPECIFIED, a person; SUBJECT_KIND_SERVICE_ACCOUNT, a service account, set by the dashboard only for an act's route reached for a call it admitted for the role's service account (see A service account runs the plugin's autonomous work). |
owner_subject |
str |
A service account's owner when the call was admitted, the person answerable for it; empty for a person. |
algorithm_id |
str |
The algorithm a service account's call named, one of the version's declared algorithms; empty for a person and where the call named none. |
connectivity_instance_ids |
frozenset[str] |
For an ems, the connectivity plugin instances this user may route through for it, worked out by the dashboard from the links the ems's admin made: for showing which may be chosen. It names the instances, never the person's user groups; the sidecar checks a placement against it whatever the page draws. Empty for every other plugin. |
The levels are the same for every plugin, granted in the deployment's access groups. A plugin names no parts of itself, so there is nothing finer to ask than the person's level on each of its roles, which are the deployment's, not the plugin's. The sidecar checks every command sent for a person again, whatever the plugin believes: it admits one only in a session at write, and only by their write on a role whose grants include the command, the account among that role's write accounts, refusing otherwise and naming the role. See Access.
The per-role accounts reach the plugin as positions in the claims' read accounts, which Caller resolves; a position past them is a header that does not read. Claims carrying no per-role entry, from a plugin holding no role, read every role as the session's level and accounts.
A person who came through a client on a delegation stays the actor: delegation_id and client_name say only through what, for the plugin to show and record as it chooses. The sidecar stamps the delegation beside the person on every command sent for them. See The delegation.
deployment_admin opens no page
A deployment admin holds on a plugin what their grants give, and a plugin's admin need not be one. Serve a page at admin by the session's level: declare it with levels=["admin"], or ask caller.admin.
Caller.access and TagAccess are gone
Access is not given tag by tag: Caller.access raises an AttributeError, and from meridian import TagAccess an ImportError, each saying to read Caller.read and Caller.write, or ask may_read and may_write.
Identifier¶
The generated protobuf message meridian.plugin.v1.operations_pb2.Identifier: one typed identifier for an instrument or a venue.
| Field | Type | Meaning |
|---|---|---|
scheme |
str |
A global scheme ("figi", "isin", "cusip", "sedol"; a currency's "iso4217", a token's "caip19"; a venue's MIC, "iso10383") or a source-scoped one ("symbol"). |
value |
str |
The identifier. |
source |
str |
The namespace a source-scoped identifier belongs to, such as "snaptrade". Empty for a global scheme. |
valid_from_ns, valid_until_ns |
int |
Its window on the record, as the instrument store answers it; valid_until_ns 0 while it is on the record. Filled by the store, never by a plugin: leave both 0 in what you send. |
isin = meridian.Identifier(scheme="isin", value="US0378331005")
symbol = meridian.Identifier(scheme="symbol", value="AAPL", source="snaptrade")
mic = meridian.Identifier(scheme="iso10383", value="XNYS") # a venue, for resolve_venue
letter = meridian.Identifier(scheme="symbol", value="V", source="alpaca") # a vendor's code for one
MissReason¶
The generated protobuf enum meridian.plugin.v1.operations_pb2.MissReason: why a resolution did not produce exactly one instrument.
| Value | Number | Meaning |
|---|---|---|
MISS_REASON_UNSPECIFIED |
0 | Not set. |
MISS_REASON_NOT_FOUND |
1 | No instrument matched. |
MISS_REASON_AMBIGUOUS |
2 | More than one matched. It is reported as a miss rather than resolved by picking one. |
Pages¶
A plugin's pages are declared and enforced in one place: each page is a view function and a template, declared where the view is with the levels it serves. meridian.Pages sends the list at registration and answers 403 to a session at any other level before the view runs, so the dashboard's tab row and the plugin agree by the same declaration.
from pathlib import Path
import meridian
pages = meridian.Pages("Statements", templates=Path(__file__).parent / "templates")
@pages.page("/connections", "Connections", levels="admin")
async def connections(request: meridian.Request) -> str:
return pages.render("connections.html", linked=...) # no account's data
@pages.page("/", "Statements", levels=["write", "read"])
async def statements(request: meridian.Request) -> str:
rows = [...] # cut to request.caller.read
return pages.render("statements.html", rows=rows)
@pages.route("/sync", levels="write", methods=["POST"]) # an endpoint, not a tab
async def sync(request: meridian.Request) -> str: ...
DECLARATION = meridian.Declaration(
interface=meridian.Interface(port=8000, title="Statements", pages=pages),
)
meridian.run(declaration=DECLARATION, roles=[Custody()], pages=pages, port=8000)
Pages¶
class Pages(title: str = "", *, templates: str | os.PathLike | None = None, kit: str = KIT, max_body: int = MAX_BODY)
| Parameter | Meaning |
|---|---|
title |
The heading the base template draws, and the end of each page's <title>. |
templates |
The directory render reads templates from. A directory that does not exist raises FileNotFoundError. |
kit |
The kit version the base template links, at /.meridian/ui/<kit>/: pages.KIT, 0.16.0 in SDK 0.36.0, whose <om-live cells> draws live values. The dashboard answers any 0.x with the newest 0.x it carries. |
max_body |
The largest request body, in bytes, the pages take: meridian.pages.MAX_BODY, 1 MiB, unless said. A larger one is answered 413 before any view runs, so a view need not measure what it is sent. A negative number, or anything but an int, raises ValueError. |
| Member | Meaning |
|---|---|
page(path, title, *, levels, roles=(), methods=("GET",)) |
Decorator. A tab, shown under the home's button for each of levels, in the order declared, and served only in a session at one of them; naming roles in a session where the person's level on one of them is. |
route(path, *, levels, roles=(), methods=("GET",)) |
Decorator. An endpoint that is not a tab, such as a form's action or a page's data, served only in a session at one of levels; naming roles, as a page's. Its tool serves the same roles. |
tool(*, replaces, ..., roles=None) |
Decorator. A tool replacing the one derived from the route at replaces; it serves the route's roles, or those roles= names. See Offer your plugin's pages to agents. |
render(template, /, **context) -> str |
template, from templates, rendered with context. Called from a view, while it serves a request; anywhere else raises RuntimeError. |
csrf_token(caller) -> str |
The token a request from this person, in a session at this level, carries back when it changes something. |
declared |
The tabs, in the order declared: what registration sends. |
dispatch(request) -> Response |
The view at the request's path and method: 404 for no such path, 405 for a method the path does not take, 403 for a level it does not serve, or for a session holding none of its levels on any of its roles. A HEAD runs the GET view, where no view is declared for HEAD, and answers its headers with no body. |
app(plugin=None) |
The pages as an ASGI application, with the caller read as CallerMiddleware reads it. A body over max_body is answered 413 without being read; a view that raises is logged and answered 500. |
serve(plugin, port, *, loop=None, max_body=None) |
Runs app(plugin) on 127.0.0.1:<port> with the standard library's threaded server, in a thread of its own; each view runs on loop, the running one by default, where the plugin's operations are. A request whose Content-Length is over max_body, the pages' own unless said, is answered 413 without its body being read. Returns the server, whose shutdown() stops it. A view taking more than 60 seconds is answered 500. |
levels takes the same values as Page. A path or a route with no level raises ValueError when it is declared, and so does the same path and method declared twice. A view is sync or async, takes a Request, and answers a str, the page's HTML, or a Response.
HEAD is answered for every path that takes GET: the GET view runs, with request.method "HEAD", and its answer's headers are sent, with the length of the body, and no body. A path that takes GET lists HEAD in a 405's Allow too.
Request field |
Meaning |
|---|---|
method, path |
As asked. |
caller |
The Caller, with the session's level. |
plugin |
The connection the pages serve: what is the whole plugin's. A view acts through the role object the plugin gives it, a closure or the state it shares, with acting_for=request.caller.header to act for the person. |
query, form |
The query string's and a urlencoded form's fields, the last of each name. |
headers |
By lower-case name. |
body |
The body as sent. |
csrf_token |
The token a request that changes something carries back, set before the view runs. |
Response field |
Default | Meaning |
|---|---|---|
body |
"" |
str or bytes. text reads it as text. |
status |
200 |
|
content_type |
"text/html; charset=utf-8" |
|
headers |
() |
More headers, as (name, value) pairs. |
Roles¶
A person's level is granted on each role of a plugin, so a plugin holding several names the roles each page, route, tool and setting serves with roles=, from those it was launched with. A plugin holding one role, or none, names none, and nothing changes: the sidecar serves its declarations that role.
@pages.page("/statements", "Statements", roles=["custody"], levels=["write", "read"])
async def statements(request: meridian.Request) -> str:
caller = request.caller
rows = [...] # cut to caller.read_for("custody")
return pages.render("statements.html", rows=rows,
may_record=bool(caller.write_for("custody")))
@pages.page("/blotter", "Blotter", roles=["custody", "operations"], levels=["write", "read"])
async def blotter(request: meridian.Request) -> str: ... # each role's rows by read_for(role)
settings = [meridian.Setting("api_key", secret=True, roles=["custody", "operations"])]
- A page or route is served, and a page's tab shown, when the person's level on one of its roles within the session's button is one of its levels. Otherwise
Pagesanswers 403 before the view, naming the roles and what the session holds on each, such as/statements is for custody under Open and View; under Open this session holds nothing on custody. - A tool derived from a route takes its route's roles;
@pages.tool(..., roles=[...])names others. It is listed at the deployment's/mcpto a person holding one of its levels on one of its roles. - A setting is shown to an admin of any of its roles and set only by one holding admin on every one.
- On a plugin holding several roles, the sidecar refuses the registration of a page or setting naming no role, or a role the plugin was not launched with, naming it; a tool so declared is refused by name, without refusing the plugin.
meridian plugin checksays where first (roles-declared). - What is shown, never what is admitted. A declaration's roles decide what a person is shown. The sidecar decides every command sent for them, by their
writeon the role whose grants include it.
A page serving several roles adapts per role with caller.level_for(role), read_for(role) and write_for(role): offer a role's actions only where write_for(role) holds accounts.
Templates¶
render renders a Jinja2 template, autoescaped, so every value is escaped unless it is meridian.pages.Markup, text that is HTML already. Beside what the view passes, the context always holds:
| Name | What it is |
|---|---|
caller |
The Caller asking. |
level |
The session's level, as "admin", "write" or "read". A template adapts by it: {% if level == "write" %}. |
csrf_input |
The hidden field carrying the CSRF token, for inside a form. |
csrf_token |
The token itself, for a script's X-CSRF-Token header. |
A page's template extends the kit's base template, meridian/base.html, which Pages carries. It links the kit's stylesheet and script, draws the page's heading and, where more than one page is at the session's level, its tab row, marking the tab the request is for; the kit drops both when the dashboard frames the page, and draws them when the page is opened on its own. A form's action that answers by rendering a page, at a path that is no tab, marks the tab the form was posted from, by the request's Referer, and takes that tab's title. It has four blocks:
| Block | Holds |
|---|---|
title |
The document's <title>: by default the page's title and the plugin's, as Statements · Holdings. |
status |
An om-status marked data-om-header, which the dashboard draws beside the plugin's name. |
head_actions |
Buttons marked data-om-action, which the dashboard draws in its header, left of the level switch; one also marked data-om-icon="refresh" is drawn as a circular arrow. |
content |
The page. |
{% extends "meridian/base.html" %}
{% block status %}<om-status data-om-header state="ok" label="Synced"></om-status>{% endblock %}
{% block head_actions %}{% if level == "write" %}
<form class="inline" method="post" action="/sync">{{ csrf_input }}<button data-om-action="sync">Sync</button></form>
{% endif %}{% endblock %}
{% block content %}
<om-grid row-key="id"><script type="application/json">{{ grid | tojson }}</script></om-grid>
{% endblock %}
tojson writes a kit component's data safely inside its <script>. {% include %} and macros compose a page from pieces. See Build a plugin's page.
CSRF¶
A request that changes something carries this plugin's CSRF token. The plugin's host keeps the person's session in a cookie, and every plugin's host is the same site as the dashboard, so the cookie's SameSite does not stop another plugin's page from posting here as the person, and the plugin never sees the host it is reached at to check an Origin against. So Pages refuses every request but GET and HEAD with 403, before the view runs, unless it carries the token back: {{ csrf_input }} inside a form, which sends a field named csrf, or the X-CSRF-Token header from a script.
The token is an HMAC of the person and the session's level, under a secret the process makes when it starts. A restart, which each save on a development deployment is, makes a page open before it stale until it is reloaded.
Views and live values¶
A view is a targeted read kept open: await plugin.subscribe(row, *, subjects=(), series_keys=(), all_series=False, expiration_from="", expiration_to="", fields=(), linger=30.0) answers a LiveView of what row delivers for the selector, kept current by the SDK from deliveries. A page reads it with no bus call, and the plugin keeps no copy of its own. Identical selectors are one subscription, shared; close() it when done, and it is dropped linger seconds after its last reader. A loss or a broken stream reads its snapshot again.
A stream held to its far end. A view's stream, and the delivery stream, that names the lake's rows or carries a view and hears nothing for receive.QUIET_SECONDS (60 seconds, doubling to 15 minutes while nothing comes) is opened again and caught up as after a break, so a far end restarted without breaking the stream never leaves a view frozen. Each read it catches up by is given receive.READ_SECONDS (60) to answer. A class that seeds may be handed the latest of what it hears again, as often as every quarter hour while a market is closed: keep each on_<event> safe to be handed a row twice. Added in SDK 0.36.0.
Views are keyed by entity. A book record in a view is held by its entity, meridian.views.ENTITY_KEYS: an order by order_id, a placement by placement_id, an execution by execution_id, a position by its account and instrument, and so on. So an order whose venue's ID is set later is held once, its key order_id=<id> alone. A record of a kind not listed is keyed by its own first identifier.
A page's live channel, @pages.live(path, *, levels, roles=()), serves what a request shows: a view (plugin.subscribe(...)), or the live values worked out from views. GET path answers its snapshot and GET path/events its event stream, each event what changed over 250 ms, the latest per key. The kit's <om-live> draws a view into an <om-grid>:
<om-live src="rates/live/events" snapshot="rates/live" for="rates"></om-live>
<om-grid id="rates"></om-grid>
Live values. Where a page shows something worked out from the rows, a mid, a cross or a move, rather than the rows as recorded, its pages.live function answers await pages.live_values(key, compute, *, views=None, wakes=None, clock=time.time_ns, linger=30.0): the values compute works out from the views views opens the first time, worked out again on each of their changes, and once a second, at most one event every 250 ms. One LiveValues is shared per key. Each value is a LiveValue:
import meridian
from meridian import LiveValue
from meridian.helpers import sessions
pages = meridian.Pages("Rates", templates=TEMPLATES)
@pages.live("/rates/live", levels="read")
async def rates_live(request: meridian.Request):
return await pages.live_values(
("rates", tuple(PAIRS)), # what they are of
compute, # views -> LiveValue rows
views=lambda: opened(request.plugin), # the views they come from
)
async def opened(plugin):
return [await plugin.subscribe("QuotesRecorded", subjects=PAIRS)]
def compute(views):
return [
LiveValue.of(f"pair:{name}", {"mid": text(mid(quote))}, [quote],
datasets=LISTED, liveness=LIVENESS, sessions=sessions.FX_WEEK)
for name, quote in by_pair(views[0].rows().values()) # the plugin's own grouping
]
LiveValue.of(key, shown, rows, *, datasets=(), liveness=None, sessions=None, stale_after_ns=0, tone=None, title="", why="") works out, from the rows it is computed from:
- its source, the datasets its inputs are from (
datasets, the lake's listing); - its age, from the oldest input's confirmed time: its source time, a quote stating none timed by its
valid_from_ns, or its dataset's liveness after it; - when it is stale: the tighter of its dataset's
stale_after_nsand an admin's own (stale_after_ns), after the dataset's declared delay. Staleness is counted only in session: given its venue'sTradingSessions, a value out of session is closed, shown with its age and never stale. With no ceiling declared, by the dataset or an admin, a value is never flagged stale, and the page says "no stale limit declared" beside its age; - why it has nothing to show, so no cell is ever blank:
why, or "nothing heard yet".
The kit's <om-live cells> (kit 0.16.0, which the base template links from SDK 0.35.5) applies each value to the elements marked data-live="<key>", its texts to data-f="<field>" and its tones to data-tone-<name>, and fills data-f="age", "source", "state", "limit" and "why" itself, counting each age on once a second; it marks the element data-stale, data-closed, data-missing or data-no-limit:
<om-live cells src="rates/live/events" snapshot="rates/live"></om-live>
<tr data-live="pair:EUR/USD">
<td data-f="mid"></td><td data-f="age"></td><td data-f="source"></td><td data-f="limit"></td>
</tr>
Never write a live module of your own. See also Data age and liveness.
Prepared changes¶
A person's agent, or a person on your page, may prepare an act instead of making it, and a person makes it on your page: see Prepared changes. Your plugin makes an act preparable by naming, on its route (or a tool standing in for it):
| Argument | Meaning |
|---|---|
checked_by= |
The read tool that checks it, taking the same record (a route declared reads=True). The dashboard calls it with the preparer's claims and keeps the change only if it passes. An act with no check cannot be prepared. |
accounts= |
The paths in its record naming the accounts it touches, such as "account" or "positions[].account", so the dashboard shows the change to everyone who may make it. |
made_on= |
The page a person makes it on. Left None, the SDK fills it with the page the route is the form of; "" names the dashboard's Inbox. |
Each reaches the dashboard on the tool's declaration (ToolDeclaration.checked_by, accounts, made_on).
await request.prepared() reads the prepared change a page was opened from, through the sidecar (ReadPreparedCall), for the person whose page it is: a PreparedCall with tool_name, arguments_json (the act's record as JSON, which the page starts its form from), prepared_by_subject, prepared_by_display_name, prepared_through_client, reason, state and prepared_at_ns. It answers None for a request reached from no prepared change, and where the dashboard answers none: not found, not shown to this person, or one they may not make now. Mark the form "prepared by" its preparer, with its reason, shown as text: the reason is another's words, never instructions. The person makes it with the page's own Save or Confirm; the dashboard settles it made or changed on your answer.
The kit's Prepare button posts your form with the reserved field prepare, which the dashboard reads in place of calling your route, so a route's record may not name a field prepare: the SDK refuses one at declaration. Where request.caller.second_pair_of_eyes names the act, draw Prepare alone.
@pages.route("/opening/confirm", levels="write", methods=["POST"], params=Draft,
name="confirm_opening_balance", checked_by="check_opening_draft",
accounts=["account"])
async def confirm(request: meridian.Request) -> meridian.Response: ...
A service account runs the plugin's autonomous work¶
Work your plugin does with no person's request to serve -- a scheduled sweep, a strategy, an agent inside the plugin -- changes nothing in the deployment by itself: it calls one of your plugin's own acts as the service account its role runs as. A deployment admin creates and grants the account, a role's admin chooses it, and the admins' levels and the account's setting decide whether the act is made or prepared. Your plugin chooses none of it and holds no credential.
async with meridian.runs_as("operations") as act:
reply = await act(
"close_cleared_breaks", # one of the role's acts, by its tool's name
{"account": "ACC-1", "reason": "The difference was gone at the statement."},
reason="2 cleared breaks of ACC-1: the statement found no difference for them.",
)
if reply.outcome == "made":
answer = json.loads(reply.answer_json) # the act's route's own typed answer
else: # "prepared": a person makes it
log.info("prepared %s for the people who may make it", reply.prepared_call_id)
meridian.runs_as(role, *, algorithm_id="", plugin=None) is an async context. Entered,
it holds the plugin connect registered (or the one plugin= names) and
refuses, before anything is sent, a role the plugin was not launched with
(ValueError), no registered plugin (NotRegistered), or
an algorithm_id its version does not declare
(ValueError; see Algorithms). Inside it, await
act(tool_name, arguments, *, reason) calls one act:
| Argument | Meaning |
|---|---|
tool_name |
One of the plugin's declared acts on role, by its tool's name: never a read, another role's act or a core dashboard__ tool. |
arguments |
The act's typed record of inputs: the record itself, a mapping, or its JSON object, sent as arguments_json (a Decimal as its string, a date in ISO 8601). Anything but a JSON object raises before anything is sent. |
reason |
Why, 1 to 4,000 characters, plain text: kept as a prepared change's reason, and untrusted as one is. |
Refused here, before anything is sent, with ValueError (or TypeError
for the wrong type): an empty tool_name or reason, either past its bound,
a core dashboard__ tool, and arguments that are not a JSON object or past
the field's bound. Otherwise it answers a CallAsServiceAccountReply:
outcome |
Means | Carries |
|---|---|---|
"made" |
The dashboard called the act's route as the service account | answer_json, the route's own answer. A route that refuses is still made: read its outcome in answer_json |
"prepared" |
Kept as a prepared change for everyone who may make it and the account's owner; nothing changed | prepared_call_id, its PC- ID |
A refusal is raised naming the field, as the dashboard named it, in
.fields:
| Raised | .fields |
When |
|---|---|---|
NotGranted |
role |
The account is disabled, removed or not held; it holds less than write on the role; or the role runs it at read |
NotGranted |
tool_name |
The act is at May not |
CallFailed, kind "refused" |
role |
The role runs as no service account: its admin has not chosen one |
CallFailed, kind "refused" |
tool_name |
The act names no check tool, and the account is set to prepare only |
CallFailed, kind "refused" |
the check's own | The act's check tool refused it: nothing kept |
CallFailed, kind "invalid" |
role, tool_name, reason |
A role the instance was not launched with; a tool that is not one of the role's acts; a reason that is not plain text (a control, zero-width or bidirectional override character) |
CallFailed, kind "unimplemented" |
The plugin's sidecar is too old to call acts as a service account |
Nothing is declared for it. The work calls acts the plugin already
declares, each naming its check tool (checked_by=, see
Prepared changes) so it can be prepared: an act with none is
refused to an account set to prepare only, and
meridian plugin check fails a plugin that calls one through runs_as
(runs-as-checked).
The act's route acts as for a person. Made, the dashboard sends the
arguments to the act's route with claims minted for that call alone, good
for at most 60 seconds: request.caller.subject_kind is
SubjectKind.SUBJECT_KIND_SERVICE_ACCOUNT, subject the account's SA-
ID and owner_subject the person answerable for it, at write with the
account's accounts on the role, with no delegation and no client. Do what
you do for a person, handing request.caller.header back as acting_for:
the sidecar admits the command by the account's grants and stamps the
account, its kind and its owner, so the book records the service account.
Such claims open no page, and reach no read or command of the deployment's
configuration but filing a ticket and reading back what was
filed: request.prepared() answers none, and saving a person's own
settings and linking are refused.
The call is sent as the plugin itself, never for a person, and recorded by the dashboard: the account's row shows its last use and last refusal, and its page each call by its act and outcome, never its arguments.
A person's own settings¶
A setting declared per_person=True is each person's own: a watchlist, a chart's choices. It is not on the Settings form and has no value for the whole deployment.
- Save a person's values from a page under Open, acting for them:
await plugin.set_own_settings({"watchlist": rows}, for_caller=request.caller). It names 1 to 64 settings, each typed as declared (a table as its rows); an empty text clears one; those not named keep theirs. It answers the person's values as they now stand. The sidecar (SetOwnSettings) admits it only for a person holdingwriteon a role each setting serves, never as the plugin itself; a setting not declared per person is refused before anything is sent. - Read them from each of the person's requests, typed:
request.own_settings, under Open and View, at most 32 KiB, never under Manage. A stateless plugin serves each person theirs this way, keeping nothing. A value this version cannot read is delivered unset and listed inrequest.own_settings_refused(seeSettings).
Each change is the dashboard's own record, shown to that person alone. No other person reads them, a deployment admin included, and no tool reads them; an agent saves a person's own through your plugin's tool for the page, as the person.
Each role's common part¶
Every plugin of a role has the same first-use path, pages, sections, labels, verbs and order, and the same Summary figures (see Using a data plugin). meridian.roles holds each role's part, in data, custody, operations, reporting, connectivity and, from SDK 0.31.0, pricing:
- A record per common page, named
<Page>Read, holding the common fields, the page's tool's answer (answers=), which a plugin extends:data.ConnectionRead,data.DatasetsRead(withServed,Want,Credential);custody.ConnectionsRead,AccountLinksRead,StatementsRead,HistoryRead;operations.BreaksRead,ReconciliationsRead,OpeningBalancesRead;reporting.DatasetsRead. figures(...), the role's figures in its order, each a plain value or ameridian.Figure, for the plugin to follow with its own within the eight:data.figures(connection=..., entitled_at_vendor=..., stream=..., last_recorded=..., rate_use=...),custody.figures(...),operations.figures(...),reporting.figures(last_valued=..., not_priced=...).- The role's common settings: custody's
poll_seconds()andstale_after_hours(), operations'reconcile(),due_business_days()andstreet_fresh_within_hours(), reporting'sreporting_currency(). - Templates
meridian/roles/<role>/<page>.html, each extendingmeridian/base.html, drawing the common sections from the record, with the blocksvendor_notices,vendor_rows(one per section, its title insection),vendor_columns(one per grid row, the row inrow) andvendor_sectionsfor the vendor's nuance. data.backfill_restart: see A restart's gap.
from meridian.roles import data
@pages.page("/", "Connection", levels="admin", answers=data.ConnectionRead, name="read_connection")
async def connection(request: meridian.Request) -> meridian.Response:
return pages.answer("connection.html", data.ConnectionRead(mode="Live", ...))
Never rename, reorder or drop a common page, section, row, column, label, setting or figure: meridian plugin check --verified fails it (role-pages). Custody's, operations' and reporting's items are offered as candidates until a second plugin of the role needs them.
Each role's common logic¶
What every plugin of a role must do the same way to fulfil its typed operations is the SDK's, tested once there and generic over the venue. How a plugin talks to its venue, its API client, a WebSocket or FIX, stays its own, so it can change its transport without changing what it does.
- The data role (
meridian.roles.data).Wants(role, ...)hears a want and answers it with the plugin's own conversion: it declines, by reason, what the plugin does not serve (an answer that raises declines every subjectsource_silent), keeps a standing want until it is withdrawn, records rows in the lake's batches and counts them overall and per dataset, reads its datasets' licences, and reads a restart's gap back once per subject per process.Venueresolves the venue master's ID once and reports a venue not held once;unconvertedcarries a value that did not read.servedandwant_rowsfill Datasets, andVenueConnectionis Connection for a source of one venue.number,json_decimalandUnreadread a venue's figures from its own text, never a float;Session,Health,LimitsandEndpointsay how the venue answers. - A venue's whole work, by what it lists.
meridian.roles.data.pairsis for a venue listing pairs, a base asset priced in a quote asset: daily closes and bars, live prices, trades and quotes, the stream kept current and its gaps read back.meridian.roles.data.event_contractsis for a venue of binary event contracts: live prices and quotes, and each contract's outcome. The plugin hands itsFeedaMarket, its client's answers read into the module's values, and for a stream its own session (Feed(..., stream_factory=Stream)). Each draws its pages from the kind's templates (meridian/roles/data/pairs/connection.htmlanddatasets.html), filled byconnection_read,datasets_readandfigures_read. -
The checks a plugin runs unchanged (
meridian.roles.data.testing): the lake as a test stands it in (Lake), the data suite's producers for each kind, andPairsChecksandEventContractsChecks, run as one test class against the plugin's own conversion: -
Raw records as files (
meridian.edge.RawStore): a vendor's answers kept before anything is read from them, a unit a UTC day, a live answer a line added only when it changed, and the units past the kind's window archived, kept or deleted (store.tend(plugin, window_days, past, today)). -
The connectivity role (
meridian.roles.connectivity).Traderdoes the role's whole work over a venue's adapter: it hears each placement naming its instance and places it in the account itsemsmapped, noted in storage before it is sent and never sent twice (Sent,place_once); reports what the venue did, a send never answered resolved from the venue's orders (resolve_unanswered) and a status not mapped said as reported (report_status_not_mapped); reports each fill into the book as the venue stated it or inferred from two averages (fill_price,execution_id,filled_in_book), and a bust (bust_id); works cancels and replaces; keeps the firm's pauses and stops; publishes the venue's catalogue; and reports fills of orders placed outside the platform (Watched). The plugin hands it aVenue, its client's answers read intoVenueOrderand a send raisingVenueRefusedorVenueUnanswered, and the listing market's time zone (market_zone=). Its Trading page istrading_read, drawn bymeridian/roles/connectivity/trading.html, withTradingAskedandTradingEnabledfor Enable trading.meridian.roles.connectivity.testing.Rigruns a plugin's own adapter over its fake broker against the suites' recorder: fakes only, never a real broker or a token.from meridian.roles import connectivity trader = connectivity.Trader( lambda: venue, now=lambda: datetime.now(UTC), sent=connectivity.Sent(sent), enabled=connectivity.Sent(enabled), watched=connectivity.Watched(originated), market_zone=ZoneInfo("America/New_York"), poll_seconds=lambda: 5.0, ) meridian.run(declaration=DECLARATION, roles=[trader], pages=pages, port=PORT) -
Custody's pages (
meridian.roles.custody.account_pages, added in SDK 0.31.0): Account links, Statements and History, each the same answer as its tool, declared for a plugin that hands it itsCustodySource, the vendor's own reads.meridian plugin checkcounts the pages the helper declares as the plugin's own. - A broker's US equity listings (
meridian.roles.data.listings, added in SDK 0.31.0): the lake layer for a broker's listings, a plugin subclassinglistings.Feedand converting only its broker's trade conditions. - The Trader, from SDK 0.31.0, also reports the charges a venue states apart from its orders (
VenueCharge) and a fill's settlement date where the venue states it, awaits an async adapter, and dates a fill in its order's own zone. Its adapter says which environment its session reached (environment, read withmeridian.reached_environment), which the Trader publishes on its catalogue. - The pricing role (
meridian.roles.pricing.Pricer, added in SDK 0.31.0): each batch the analytics component routes to the plugin answered whole, from exactly the market objects each unit carries, and the batches still waiting read after a restart from the last sequence answered, kept in memory, sincepricingholds no storage. - Connectivity's pages (added in SDK 0.32.0), each declared by the SDK with its acts and checks, from the plugin's state as it stands (
TradingState: itstrader, its venue adaptervenue, its accountscope, and for raw records itsstoreand keptvalues) and its words for the venue:connectivity.trading_pages(pages, state, *, venue, credentials, environment, environment_setting, ...)declares Trading at/admin/connections(TRADING), with Enable trading (ENABLE_TRADING) and its check, and a Sessions section fromlatency=;connectivity.raw_records_page(pages, state, *, kinds, why)declares Raw records (RAW) for a plugin holding custody or connectivity; and, for a plugin holdingdataas well,connectivity.connection_pages(pages, state, connection, *, venue, ...)declares one Connection at/(CONNECTION) for both roles, the data role's record carrying connectivity's astrading(ConnectionRead.trading), in place of a Trading page of its own.TradingReadcarriessession_latency,reached_environment,badgeandenvironment_to_save.meridian plugin checkcounts the pages each declares as the plugin's own. - The merged Connection's tabs (changed in SDK 0.32.3):
connection_pagesdraws one group at a time under a tab row of its sections' own titles, Key, Limits and use, Stream, Trading, and Sessions once measured, picked by?tab=; Enable trading answers on Trading, andread_connectionstill answers every section. A page given no tabs draws every section at once. An account's Enable trading column shows only where its button does, and Raw records' record wraps within its box. - The Trader's placement types and RFQs (added in SDK 0.32.0): an adapter may answer
placement_types(code), the types its venue takes beside DMA, and where it takes RFQsrequest_quotes(code, asked),quotes(code, request_id)(each aVenueQuote) andaccept_quote(code, request_id, quote, asked); theTraderlists the types on the catalogue (Trader.placement_types()), rejects one an account does not take with why, records each dealer's quote on the placement and accepts the pick.VenueOrdercarriesidentifiersandcurrency;connectivity.testing.placement(placement_type=...)makes a test's placement, andRig.latencyits recorders. See Placement types and RFQ quotes. - An
ems's checks (meridian.roles.ems, added in SDK 0.32.0):ReferenceCheck(action, after_ns, latency, liveness)judges the reference price's age before a placement (stale(price, dataset=, asset_class=, sessions=)), holding the order with its cause (hold(plugin, order_id, stale)) underREFUSEor marking the placement (placement(stale)) underWARN;after_ns(rows)reads the admin'sstale_reference_after;open_quotes(placement)orders an RFQ placement's open quotes best first. Added in SDK 0.33.0:permitted(plugin)reads the stale-reference actions the deployment permits, at the plugin's own role, kept aswarn_permitted;refusesis true forWARNtoo where warn is not permitted. See Set staleness thresholds and the stale-reference action. -
An event-contract venue's events (added in SDK 0.32.0):
event_contracts.Feed.keep_events(sleep=asyncio.sleep, *, clock=..., once=False)reports the events of the contracts served, each read once its contract is first served and again everyEVENTS_EVERY_SECONDS(600), a pass everyEVENTS_FIRST_SECONDS(15), reported where new or stated otherwise than last reported, in place of a loop of the plugin's own. A range member's bounds go with its group (member_bounds). Added in SDK 0.32.3: aResolutioncarries the venue's optional deadline to challenge a proposed determination,challenge_ends_at_ns, 0 unset by default, andFeed.outcome()carries it to theOutcomerecorded. -
A data plugin's common parts (SDK 0.35.0):
data.SentRows(capacity), a row sent again only when what it says changed, by a digest of its values;data.Conflator(every_ns), the latest per key once a period, never a trade, a fill or an outcome;data.Timetable, a polled source's standing wants read at each dataset's cadence, gated by its sessions and the market's days, within the vendor's allowance as the plugin keeps it; and the two common settings,data.timeliness(label, choices, default)(real_time,delayed,end_of_day) anddata.api_key(label, description). - A broker's listings (
data.listings.Feed(..., timeliness=, datasets_by_timeliness=, mic_of=)): the dataset of the plan's timeliness in force, a participant's stated time kept beside the consolidated one, a venue resolved by its MIC, each row judged at the edge; its pageslistings.connection_read,datasets_readandfigures_read. - An option chain's source (
data.option_chains.Feedover the plugin'sChainSource): a source's chains recorded under each underlying, its series, quotes, open interest and volume, only what changed, standing wants kept current inrun(). - A pairs venue's FX feed (
data.pairs.Feed(conflate_first=True, day_clock=...)): prices held the latest per key and kept as raw records once a pass, a day by the declared zone.
Where a feed's method had an act's name it is renamed: a pairs feed's record_trades_of and record_quotes_of, an event-contract feed's record_outcomes_of and record_bars_of, a listings feed's seed_symbols. What faces a vendor, its transport, its pacing, its Retry-After and its allowance, stays the plugin's: the data template's stream.py and pacing.py show one way.
The SDK's templates/ holds a role template for connectivity, custody, data, operations, pricing and reporting. CLI 0.1.60 writes each, on SDK 0.35.0: meridian plugin new <name> --role <role>, then meridian plugin migrate to move it to 0.36.0.
Helpers¶
meridian.roles holds only roles' modules: a role gates permissions and access, and a module there reads as one. Logic shared across roles by topic, which grants nothing, is meridian.helpers. A helper the SDK carries is logic core or the lake depends on every plugin doing the same way, held to behaviour cases every SDK passes. It holds no protocol: a vendor's transport stays the plugin's.
| Module | What |
|---|---|
meridian.helpers.sessions |
Supervisor, one session with a source kept open over the plugin's own Link (open, subscribe, receive, close): reopened after a drop or a silence with Backoff's jittered waits, a refused key waited out longer, at least a failure's retry_after, subscribed to exactly what is wanted, its state for Connection (SessionState, label), and a catch-up after a reopen. reopen_on_want=True opens a new session for what is wanted. Every message received is handed to heard, a heartbeat as []. FX_WEEK, the FX week's sessions (below). await read_sessions(self, dataset), the trading sessions a dataset's live rows follow, read from its sessions_calendar_id, or None where it names none (added in SDK 0.36.0) |
meridian.helpers.calendars |
DayClock.of(declaration), a dataset's business day by its declared zone and end minute (business_date_of, close_of, opened_at); MarketDays(role, calendar_id), the deployment's calendar read through a role object (is_open, previous_open, next_open, business_date_of) |
meridian.helpers.chains |
A chain's series, keys and reads: see below |
meridian.helpers.packages |
A package's legs, proposal and fill: see below |
meridian.helpers.lifecycle |
The book's lifecycle events reported, proposed, released and read, listed options' and bonds': see below and Bonds and fixed income |
meridian.helpers.debt |
Fixed income's money rules, accrual and factors: see Bonds and fixed income |
The FX week. meridian.helpers.sessions.FX_WEEK is the spot FX market's trading sessions, Sunday 17:00 to Friday 17:00 New York, its edges the 17:00 roll in America/New_York's own time, so they stay at 17:00 there on both sides of daylight saving. Give it wherever sessions are taken and an FX dataset names no calendar of its own: LiveValue.of(..., sessions=sessions.FX_WEEK), ems.ReferenceCheck.stale(..., sessions=sessions.FX_WEEK), roles.in_session. It is one shared message: read it, never change it.
from meridian.helpers import sessions
LiveValue.of("pair:EUR/USD", {"mid": mid}, [quote], datasets=listed, sessions=sessions.FX_WEEK)
Listed options and futures¶
See Listed options and futures and Record and read an option chain.
| Module | Names | What |
|---|---|---|
meridian.helpers.chains |
osi_symbol, osi_terms, osi_key |
The 21-character OSI symbol from a root, an expiration, call or put and a strike; its parts read back; a series' key under occ_osi |
option_series, statistic, series_meta |
A data plugin's chain: a series as the listed_option template's fields, a series' open interest or volume for a trading date, and a chain row's envelope, the underlying as its subject and the series' key, never its own record |
|
read_chain, read_open_interest, want_series |
A reader's: an underlying's series in an expiration window, every page; a chain's open interest for a trading date; a series' INS- record asked for, with its cause and a note |
|
meridian.helpers.lifecycle |
report |
An expiry, assignment, exercise or day of variation sent once, as the broker states it (expiry, assignment, exercise and variation build each) |
exercise_by_exception, propose, EXERCISE_THRESHOLD |
servicing's: the exercise a long option's holder can expect where it is in the money by the threshold, a cent by default, against the underlying's close; the proposals sent |
|
due_for_release, release |
operations': whether a proposal no report answered may be released, the morning after its date with its position gone from the statement; its release, with a note |
|
read_events |
An account's lifecycle events in a range of dates, every page; naming no account, every account in scope | |
settle_claim |
A person marks a factor revision's open claim settled. See Bonds and fixed income | |
meridian.helpers.packages |
legs, MOST_LEGS |
A package's legs, each an instrument, a side, a ratio and open or close, in lowest terms, 2 to 16 |
propose |
A package proposed: its legs, its units and one net limit per unit, positive a debit | |
report_fill, LegFill |
A venue's fill of a package, sent once for the package and once per leg, each leg linked to the package's |
A signal's derived series and scenarios¶
meridian.roles.signal. See The lake: derived series
and Scenarios.
| Names | What |
|---|---|
derived, read_derived |
One value of a derived series, with its rule and each dataset it read; a subject's derived series read by name over a range of dates |
spot_steps, date_steps |
A grid's usual axes: every spot moved by percentages; today, forward by days, and to settlement |
saved, read_scenarios |
A scenario set's axes, or one scenario, as a row of the plugin's own dataset under its row key; saved scenarios read by row key, the latest or a version |
escalate, Baseline, OutlierThresholds |
A row judged against its subject's robust baseline, and each found suspicious or invalid recorded as an assessment, states only escalating; the admin's thresholds from meridian.quality_thresholds() |
The filings slice and quality states¶
Core serves both: see The lake: filings, quality states and data use. Declared cash dividends, recorded with this module, are served.
| Module | Names | What |
|---|---|---|
meridian.roles.data.filings |
Companies |
A filer, an issuer, a holder or a rater resolved once by its CIK or LEI to its ENT- record (resolve_company), and a builder for each of the slice's rows: a filing, a reported fact, a holdings report and its holdings, a declared dividend and a rating action. Wants.record takes them |
meridian.roles.data.quality |
EdgeChecks |
Every row a data plugin records unasked or for a standing want, judged at the edge: a quote crossed past crossed_tolerance invalid; a duplicate under a new row key, the rows after a sequence gap until resynced, a row out of order or stale suspicious; a source time ahead of the clock invalid. A state the plugin set is only ever escalated. Wants.quality holds the checks, and Wants.edge() |
thresholds_from |
The checks' thresholds, from meridian.dataset_thresholds()'s columns crossed_tolerance, out_of_order_tolerance and future_skew_max |
|
Baseline, OutlierThresholds, assessment |
A signal's robust rolling baseline, its thresholds, and one row's assessment |
A pairs venue numbers its quotes (Top.sequence) for the gap check.
ems.ReferenceCheck.permitted
and warn_permitted are in Each role's common logic.
Typed settings¶
A plugin's settings may be one class, its fields the settings:
@meridian.settings
class Settings:
environment: str = meridian.environment()
api_token: str = meridian.setting("API access token", secret=True, required=True,
applies_when=("environment", "live"))
poll_seconds: int = meridian.setting("Read every", default=60, at_least=10)
sample_data: bool = meridian.sample_data()
DECLARATION = meridian.Declaration(settings=Settings)
async def configure(current: Settings) -> None:
... # the first delivery before any class starts, then each change
meridian.run(declaration=DECLARATION, roles=[Lake()], on_settings=configure)
meridian.setting(label, ...)takesSetting's fields by keyword, andat_leastandat_mostto bound an integer.meridian.environment(): the settingenvironment, a required choice labelled "Environment", Live (live) or Sandbox (sandbox, the default), declared first, for a vendor with more than one environment; a credential differing by it is gated withapplies_when=("environment", "sandbox"). A plugin is Live only where a person chose Live, or where its own code saysenvironment(default="live"); a plugin reading the value treats anything butliveas the sandbox.environment(demo="paper")offers Paper (paper) in place of Sandbox, for a vendor that calls its demo that, anddemo_label=names it as the vendor does. Added in SDK 0.31.0. For the agents' demo-only guard, a connection is demo only where both keys say so:meridian.reached_environment(address, live=..., paper=..., sandbox=...)names the environment a session reached from the host it called,meridian.is_demo(setting, reported)is true only where the setting and the session both saysandboxorpaper, andmeridian.connection_badge(setting, reported)draws Demo, Live or Live (environment not confirmed). No report, or a disagreement, counts live.meridian.sample_data(): the settingsample_data, "Sample data", off, a developer setting, last; the one convention for a plugin answering from made-up data on a development deployment, in place of each plugin's ownsynthetic.meridian.account_currency(), added in SDK 0.27.0: the settingaccount_currency, a table labelled "Account currency", each row a linked account, or none for every linked account without a row of its own, and its currency, an ISO 4217 code. A plugin whose source states no currency for an account's securities declares it, andmeridian.edge.supplied_currency(rows, external_account_id, field)answers the account's code with its provenance, supplied by the person who set the row (PROVENANCE_KIND_SUPPLIED), orNonewhile no row covers it; until then the plugin reports nothing that needs the currency, never a guess.- Latency's and staleness's settings, added in SDK 0.32.0, each a plugin admin's (see Data age and liveness):
meridian.session_ceilings(), the table "Session ceilings", each row a connection, a session and its ceilings in milliseconds or counts, whichmeridian.ceilings_from(rows)turns intoroles.Latency'sceilings;meridian.dataset_thresholds(), the table "Dataset thresholds", each row a dataset, its asset class and Stale after in seconds, which the lake reads by that name; and anems'smeridian.stale_reference_action(), "Stale reference action", Refuse (the default) or Warn, andmeridian.stale_reference_after(), the table "Stale reference after", each row an asset class and After in seconds. - The quality settings, added in SDK 0.33.0:
meridian.dataset_thresholds()gains the edge's tolerances beside Stale after,crossed_tolerance(how far a bid may sit above its ask before a quote is invalid, in its price),out_of_order_toleranceandfuture_skew_max(in seconds), read byroles.data.quality.thresholds_from; and a signal'smeridian.quality_thresholds(), the table "Quality thresholds", each row a dataset, itsoutlier_sigma,sigma_floor,warm_up,invalid_sigmaandcross_source_tolerance. Core does not yet act on quality states (see The lake). - The class is a frozen dataclass of the values, with
declared(),label(name),.missingand.refused;plugin.settings(Settings)delivers an instance at each change,plugin.first_settings(Settings)the first.
Every page's template loader also holds the kit's macros, meridian/kit.html (facts among them).
meridian.testing¶
PageClient asks a plugin's pages in its own tests, in process, as its sidecar would forward them: each request carries a Meridian-Caller header for a session at one level, its accounts cut to that level as the dashboard cuts them.
from meridian.testing import PageClient
from my_plugin.page import pages
client = PageClient(pages, plugin=stand_in, read={"ACC-1", "ACC-2"}, write={"ACC-2"})
assert client.get("/", "write").status == 200
assert client.get("/", "admin").status == 403
client.assert_no_account_data("AAPL", "125", "12,500.00") # what the stand-in holds for ACC-1
| Member | Meaning |
|---|---|
PageClient(pages, plugin=None, *, read=(), write=(), subject=..., display_name=..., deployment_admin=False, roles=()) |
plugin is what a view reaches as request.plugin: a stand-in whose operations answer in the test. read and write are the accounts the person may read and write, cut to each session's level. The person is Ada Park, a local account, unless subject and display_name say otherwise, and a deployment admin when deployment_admin is True, which every request the client sends says. roles are the roles the plugin was launched with, each of which the person holds at every level; none, for a plugin holding one role or none, carries no per-role entry, and pages serve as before. |
get(path, level, **query) |
A GET in a session at level: "admin", "write" or "read". |
post(path, level, form=None, *, headers=None, roles=None) |
A form posted from the plugin's page, carrying its CSRF token back unless form gives one of its own. headers sends more, such as the Referer of the page it was posted from. roles narrows the session as caller does. |
request(method, path, level, *, form=None, query=None, headers=None, roles=None) |
The request as given, with no token added: for testing that one without a token, or with somebody else's, is refused. roles as post's. |
caller(level, *, roles=None, deployment_admin=None) |
The client's person, as a Caller in a session at level: a deployment admin as the client was made, unless deployment_admin says. The session carries an entry for each of the client's roles at level, or for those roles names; a mapping gives each its own level within the button, {"operations": "write", "custody": "read"} under Open. A role the client was not made with raises ValueError, and so does a level the button cannot carry: under Open a role is at write or read, under View at read, under Manage at admin. |
call_tool(name, arguments=None, *, level=None, ..., roles=None, subject=None, subject_kind=..., owner_subject="") |
Calls a tool as the deployment's /mcp would; see Offer your plugin's pages to agents. roles as caller's. subject_kind=meridian.SubjectKind.SUBJECT_KIND_SERVICE_ACCOUNT calls it as the dashboard calls an act it admitted for a role's service account: subject its SA- ID (the client's person's otherwise), owner_subject the person answerable for it, and no delegation and no client; algorithm_id the algorithm its work named. |
every_page() |
Each declared page, in order, under Manage, Open and View: a list of Rendered, each with page, level and response, 200 where the page serves the level and 403 where it does not. On a client made with roles under each level for each role alone and for all of them together, each Rendered naming them in roles. |
assert_no_account_data(*held) |
Fails, naming the page and what it showed, when under Manage a page at admin does not answer 200, any other page does not answer 403, or a page at admin shows any of held, as given or as a template escapes it. held is the account data the test put where the plugin reads it: holdings, quantities, values, balances, a statement's rows. Called with nothing to look for, it raises ValueError. |
meridian.testing.heartbeat(*, healthy=True, detail="", figures=()) is the heartbeat a plugin's sidecar receives from a plugin reporting these: its figures as the wire carries them, in the plugin's order. It raises as setting plugin.figures does, so a test of the figures a plugin computes asserts on it, or on the refusal. See Figures.
meridian.testing.caller_header(level, *, read=(), write=(), subject=..., display_name=..., deployment_admin=False, delegation_id="", client_name="", tool_name="", roles=()) makes the header alone, for a test that serves the pages another way. delegation_id and client_name are a person's who came through a client rather than a browser. roles are the roles the session carries an entry for, as caller's, each role's accounts as positions in the claims' read accounts, as the dashboard mints them. subject_kind and owner_subject make it a service account's, subject its SA- ID. algorithm_id and connectivity_instance_ids are the algorithm a service account's call named and the connectivity plugins the user may route through, for an ems's tests. It is unsigned: only a plugin's tests read it, never a sidecar.
A plugin holding several roles holds each page to them in its tests:
client = PageClient(pages, plugin=stand_in, read={"ACC-1", "ACC-2"}, write={"ACC-2"},
roles=["custody", "operations"])
session = {"operations": "write", "custody": "read"}
assert client.get("/blotter", "write").status == 200
# a route at write naming custody, asked where custody is at read: refused before the view
assert client.post("/statements/record", "write", {...}, roles=session).status == 403
for rendered in client.every_page(): # each level, each role alone, and both
...
The client is synchronous, for a plain pytest test, and not for use inside a running event loop. A view that raises fails the test with its traceback.
A Manage session holds no account's data, and the sidecar refuses reads for the person in one. What the plugin holds for an account, such as a synced statement's holdings, quantities, values and balances, nothing technical keeps off a page at admin, so the plugin's tests do, with assert_no_account_data.
An account's identity is not its data: its ID, name, custodian, type, owner and note. A Manage page may list every account of the deployment, as the accounts read answers them, as a link target, so assert_no_account_data looks only for the strings the test names, not for every account the person may read or write. A string named is looked for whatever it is, an identity too.
CallerMiddleware¶
A pure ASGI middleware, so it works with any ASGI framework and needs none. On each HTTP request it reads the one Meridian-Caller header the sidecar forwarded and puts a Caller at scope["state"]["caller"]. In Starlette or FastAPI that is request.state.caller.
| Request | Answer |
|---|---|
Exactly one Meridian-Caller header that decodes |
Passed to the app with scope["state"]["caller"] set. |
No Meridian-Caller header, or more than one |
401, open this page from the dashboard. Such a request did not come through a sidecar. |
| A header that does not decode | 401, the caller header does not read. |
A scope that is not http |
Passed to the app unchanged, with no caller. |
It checks nothing about the caller. That is the sidecar's job, and only the sidecar can reach the page.
Exceptions¶
Every exception carries the sidecar's own words, so a log line says whether the fix is the plugin author's, the operator's, or nobody's. The words are for a person, and may be reworded at any release. Where a plugin must act on which refusal it met, the SDK raises a class of its own for it, chosen by the code the sidecar sends beside the refusal and never by the words: NotLinked and CommandRefused.
| Exception | Attributes | Raised when |
|---|---|---|
MeridianError |
Base class for everything below. | |
Refused |
reason: str |
connect: the sidecar declined registration, for example because access control has not loaded, the role carries no grants, or the schema does not match. A refusal is a statement about configuration. It is not retried, and asking again changes nothing. |
NoSidecar |
address: str, waited_seconds: float |
connect: no sidecar answered at address within wait seconds. It may still be starting, or not be there at all. |
NotRegistered |
Any method, after leave(). |
|
NotGranted |
topic: str, reason: str, fields: tuple[str, ...] |
A typed operation was refused permission. topic holds the operation's name (for example "RecordHolding"), and reason names what was missing. fields names the fields the refusal is about where the sidecar named any, such as role or tool_name from runs_as; empty otherwise. Also the book's refusal of a command naming an order, a placement, a change request or a locate by its ID whose account is outside the plugin's write scope or the person's write access: no code, nothing changed. |
CallFailed |
topic: str, kind: str, detail: str, fields: tuple[str, ...] |
A typed operation did not produce an answer. topic holds the operation's name. kind says which failure it was; see Typed operations. fields names the fields a refusal is about where the sidecar named any, and kind is "unimplemented" from runs_as beside a sidecar too old for it. The order path's and the calendars' refusals of kind "invalid" name their fields: both quantity and cash_quantity for an order stating both or neither, target.user_group_id, algorithm_id for an algorithm the version does not declare, settlement_date for a fill stating none. |
NotLinked |
as CallFailed, with kind="refused" |
A typed operation named an external account nobody has linked to an account, so nothing was recorded for it. A subclass of CallFailed, so code that caught CallFailed still catches it. Raised only when the refusal carries the code REFUSAL_REASON_EXTERNAL_ACCOUNT_NOT_LINKED. Not worth retrying: the next statement after an admin links the account records it. See Typed operations. |
CommandRefused |
as CallFailed, with kind="refused"; reason: int, reason_name: str, fields: tuple[str, ...] |
The book of record refused a command with a code of its own, such as REFUSAL_REASON_OPENING_BALANCE_RECORDED; reason is the code's number and reason_name its name. fields names each field an incomplete entry left out, by its path in the call (positions[0].settled_quantity, positions[1].lots[0].terms.cost), for REFUSAL_REASON_INCOMPLETE, and is empty for every other refusal. A subclass of CallFailed. Not retried: the same command meets the same refusal. See Typed operations. Also the sidecar's refusal of a deletion inside the deployment's hold, REFUSAL_REASON_WITHIN_HOLD, from delete_unit(): the unit is kept, and a later pass may try again once the hold has passed; and the order path's codes beside ABORTED: REFUSAL_REASON_ALLOCATION_UNBALANCED, _APPROVAL_REQUIRED, _CHECK_BLOCKS, _CLAIMED, _ORDER_STATE, _IDENTIFIER_IN_USE, _CONNECTIVITY_NOT_ALLOWED and _CONNECTIVITY_LINKED; and an ems's mapping naming an account not granted to it, REFUSAL_REASON_ACCOUNT_NOT_LINKABLE, beside FAILED_PRECONDITION. See The order lifecycle. |
Refused for load, asked again. An act or a read refused RESOURCE_EXHAUSTED with REFUSAL_REASON_OVERLOADED (a core component's budget is full) or REFUSAL_REASON_RATE_LIMITED (the plugin passed its limits at its sidecar) is asked again by the SDK after at least the refusal's retry_after_seconds, up to a fifth more (client.LOAD_WAIT_JITTER), within the call's own deadline, or client.LOAD_WAIT_MOST_SECONDS (300) in all where it has none. A bare RESOURCE_EXHAUSTED, a count cap that waiting does not clear, is raised at once. client.load_wait does this. Added in SDK 0.36.0.
The sidecar's status is mapped onto these for typed operations:
| gRPC status from the sidecar | Raised as |
|---|---|
PERMISSION_DENIED |
NotGranted |
FAILED_PRECONDITION carrying REFUSAL_REASON_EXTERNAL_ACCOUNT_NOT_LINKED |
NotLinked, kind="refused" |
FAILED_PRECONDITION carrying any other code |
CommandRefused, kind="refused": REFUSAL_REASON_WITHIN_HOLD |
FAILED_PRECONDITION, with no code |
CallFailed, kind="refused" |
UNAVAILABLE |
CallFailed, kind="no handler" |
DEADLINE_EXCEEDED |
CallFailed, kind="timeout" |
ABORTED carrying a code |
CommandRefused, kind="refused" |
ABORTED, with no code |
CallFailed, kind="handler error" |
INVALID_ARGUMENT |
CallFailed, kind="invalid" |
UNAUTHENTICATED |
CallFailed, kind="not vouched for" |
| any other | grpc.aio.AioRpcError, unchanged |
Note
settings(), account_scope(), access() and report() do not map gRPC errors. A failure there reaches the caller as grpc.aio.AioRpcError.
Environment variables¶
| Variable | Read by | Meaning |
|---|---|---|
MERIDIAN_SIDECAR_ADDRESS |
connect |
The sidecar's address, when address is not given. |
MERIDIAN_STORAGE_DIR |
edge.storage_dir() |
Where the deployment mounted the instance's own storage, for a plugin holding an edge role. Unset where none is granted. |
MERIDIAN_ARCHIVE_DIR |
edge.archive_dir(), the archive's helpers |
Where the instance's archive is mounted, beside its storage, once a deployment admin allows it one. Unset where none is allowed. See The archive. |
MERIDIAN_ARCHIVE_BUCKET |
the archive's helpers | In a cloud, the instance's own prefix of the deployment's bucket, in place of MERIDIAN_ARCHIVE_DIR, an s3:// URL, read through boto3. |
MERIDIAN_ARCHIVE_MOST_BYTES |
archive_unit() |
The archive's bound in bytes, as a deployment admin set it. Unset, or 0, for none. A value that is not a whole number of bytes is refused. |
MERIDIAN_LIVE_DIR |
meridian-dev run |
The live folder. Default /plugin/live. |
MERIDIAN_LIVE_SEED |
meridian-dev run |
What a new live folder is filled from. Default /plugin. |
MERIDIAN_DEV_EVENTS, MERIDIAN_DEV_REVISION |
connect |
Set by meridian-dev run for the process it starts. When present, connect records ready for that revision. |
meridian-dev¶
The package installs one command, meridian-dev run. On a development deployment it is the plugin container's command. It runs the plugin's own entry point, the first one in [project.scripts], from the live folder, and restarts it on each change the sidecar writes. The pod, the sidecar and its credential stay as they are. It uses the standard library alone. A plugin author doesn't run it directly. See Development deployments and plugin dev events.
Release notes¶
Each release of the SDK, the interface version it declares, the runtime it needs, and the CLI whose plugin new writes a plugin on it. What a plugin's author does to move to it is in Moving a plugin to a new release.
0.36.0: breaking
open-meridian 0.36.0 declares a new interface version (SCHEMA_VERSION
"v30") and needs chart 0.1.538. Adds
standing jobs
(analytics.job(..., standing=True), each measure's refresh,
analytics.Jobs().standing answering a LiveView); a stream held to
its far end (receive.QUIET_SECONDS); an act or read refused for load
asked again after its retry-after (client.load_wait); a dataset's
terms_and_pricing, sessions_calendar_id and gap_recovery, and
Declaration(most_shards=); an event contract's terms and category
stated at its resolve; a bond's terms offered as a value of its record
and a quoted yield whose day count is unstated; the benchmark_yield
rate kind; and the source priorities read. DatasetLicence,
DataEligibility and licence_default are gone. Base image
ghcr.io/open-meridian/plugin-python:0.36.0; CLI 0.1.60's plugin new
writes 0.35.0, which meridian plugin migrate moves to it. See
0.35.5 to 0.36.0.
0.35.5: additive
open-meridian 0.35.5 holds the FX week once:
helpers.sessions.FX_WEEK, Sunday 17:00 to Friday 17:00
New York, for wherever sessions are taken and an FX dataset names no
calendar of its own. The base template links kit 0.16.0 (pages.KIT),
whose <om-live cells> draws a page's live values.
Base image ghcr.io/open-meridian/plugin-python:0.35.5; chart 0.1.408.
0.35.4: a fix and an addition
A book record's view is keyed by its entity (views.ENTITY_KEYS), so an
order whose venue's ID is set later is held once. A quote stating no
source time is timed by its valid_from_ns, so ems.ReferenceCheck no
longer judges it stale. Adds live values,
pages.live_values and LiveValue.of: a page's values worked out from
its views, with source, age and staleness counted only in session.
0.35.3: a fix
A supervised session hands heard every message, [] for a heartbeat,
waits at least a failure's retry_after, and with
reopen_on_want=True opens a new session for what is wanted. A rate or a
statistic delivered latest value first no longer fails the hearing
loop, and a chain's rows are told apart by their series key.
RawStore.tend reads from the settings only what it is not given. A
pricing plugin declares its models,
Declaration(pricing_models=[PricingModelDeclaration(model_id, model_version)]).
0.35.2: a fix
A typed delivery of the settings holds every setting delivered in
config.values, the storage kinds' window settings among them, and
RawStore.tend(plugin, settings=config, today=...) reads a kind's
window from it.
0.35.1: a fix
An act is made through the role object, never the connection: the pairs
and event-contract feeds resolve their venue through the feed. The
suites' Recorder.plugin stands in for the connection and makes no act.
meridian.run applies the first settings before any class's on_start
and keeps the settings watch.
0.35.0: breaking
Each role is one class, meridian.roles.<role>.Role,
its acts methods and its events on_<event> methods; seven role modules
added (compliance, confirmation, oms, portfolio, reference,
servicing, settlement). meridian.run is the entry point,
connect(roles=) requires the classes, the connection makes no act, and
receive is gone. connectivity.Trader, pricing.Pricer and the data
feeds are role classes. Adds meridian.helpers.sessions and
.calendars, data.SentRows, Conflator, Timetable, timeliness,
api_key and data.option_chains. No contract change. See
0.34.0 to 0.35.0.
0.34.0: breaking
Adds bonds and fixed income:
the money rules, accrual and factors (helpers.debt), the bond records
and servicing's expectations (helpers.lifecycle),
analytics.DEBT_MEASURES, a quality assessment's data type and a derived
value in money. meridian.roles holds only roles: chains,
packages, lifecycle and debt moved to meridian.helpers
with no shim. See 0.33.5 to 0.34.0.
0.33.2 to 0.33.5: additive
Reads ask for what a page shows (paths, fields, an expiration
window); chains.read_expirations from the typed list_expirations;
RawStore.keep_line keeps a stream's memory flat; and
views, plugin.subscribe and pages.live, with each
dataset's class of source.
0.33.1: additive
open-meridian 0.33.1 adds an event-contract venue's bars. A Market
answering bars(contract, interval, first_ns, until_ns) -- each
Candle of an hour (HOUR) or the venue's session day (DAY) --
publishes a third dataset keyed bars, of the lake's bar kind, and
Feed.keep_bars(declared) keeps each served contract's closed hours and
days; a want of bars is answered from the venue's history. A Market
without the hook is unchanged, and EventContractsChecks refuse any
third dataset but bars. The data suite's feed-delay producer stands
its want on the quote where the venue's last trade states no time or
its live dataset declares no prices, and a row's interval and business
date are part of what makes it a duplicate. Base image
ghcr.io/open-meridian/plugin-python:0.33.1; runtime chart 0.1.313 or
later.
0.33.0
Breaking: the typed
operations take meridian.SubjectRef only, which carries its role
(role=), and refuse the wire's; every role's suite holds v28's cases,
each case about what a plugin's source or work may never present marked
by its about; and the three latency cases are required again.
Latency.report sends ReportLatency, and an ems's
ReferenceCheck.permitted reads the stale-reference actions the
deployment permits. Listed options and futures: chains, lifecycle
events and package orders (meridian.roles.chains, .lifecycle,
.packages); a signal's derived series, saved scenarios and a row's
quality escalated (meridian.roles.signal); the filings slice
(meridian.roles.data.filings); quality states at the edge
(meridian.roles.data.quality); a licence's eligibility
(DataEligibility). meridian plugin migrate rewrites a wire
SubjectRef handed to the SDK (see 0.32.3 to 0.33.0).
It needs a deployment (chart 0.1.313 or later). CLI 0.1.53's
plugin new writes a plugin on it. Its base image is
ghcr.io/open-meridian/plugin-python:0.33.0.
0.32.3: a rate's convention, listings' liveness, the not-affiliated notice
open-meridian 0.32.3, released on PyPI on 2026-10-24, needs chart 0.1.309 or later, as 0.32.0 does. rates.Feed.rate refuses a rate with no
convention, an index level taking the convention of the rate it
compounds, and Feed.subject asks a subject missed again the same day,
after 5, then 15, then every 60 minutes. A listings feed records its
live dataset's liveness, as a pairs feed does. An event contract's
Resolution carries the venue's deadline to challenge a determination
(challenge_ends_at_ns) to the Outcome. DatasetLicence writes
not_affiliated_notice. The merged Connection (connection_pages)
draws one group at a time under a tab row of its sections' titles, and
Raw records' record wraps within its box. Set the licence's attribution
terms only on chart 0.1.311 or later. Only the pins move, but a rate
built with no convention now raises (see
0.32.2 to 0.32.3). CLI 0.1.52's plugin new writes a
plugin on 0.32.1, and plugin migrate moves it here. Its base image is
ghcr.io/open-meridian/plugin-python:0.32.3
(sha256:596236b9c7e2a2e324f7704df68551a311d1a1ce9387ec6d5bd4d7ba661674f3).
0.32.2: the licence's attribution terms, Sessions only once measured
open-meridian 0.32.2, released on PyPI on 2026-10-23, needs chart 0.1.309 or later, as 0.32.0 does. DatasetLicence writes the v27 wire's
redistribution, attribution_required, attribution_text,
copyright_class and no_more_restrictive_terms, an answer given by
its word, with or without its prefix. A data plugin's Connection draws
Sessions only once a session is measured. The testing Recorder
answers RecordRates with its count. RATES_NOT_PRESENTED no longer
names records-a-datasets-liveness: a polled rates plugin recording
each poll as its dataset's liveness presents it. Set the licence's new
terms only on chart 0.1.311 or later: an earlier v27 deployment refuses
the registration. Only the pins move (see
0.32.1 to 0.32.2). CLI 0.1.52's plugin new writes a
plugin on 0.32.1, and plugin migrate moves it here. Its base image is
ghcr.io/open-meridian/plugin-python:0.32.2
(sha256:2235d96dcca58e315dbb9d38fe9dda753d1a4bfbcac67b09f614b07d54020b80).
0.32.1: two data cases about closes
open-meridian 0.32.1, released on PyPI on 2026-10-23, needs chart 0.1.309 or later, as 0.32.0 does. The data suite's an-exact-price and
a-want-recorded-against are about closes, so a data plugin
recording rates and no prices names them not presented, with why: "it
records rates, never prices". meridian.roles.data.testing.RATES_NOT_PRESENTED
names every case such a plugin never presents. A plugin recording
prices maps them as before. Only the pins move (see
0.32.0 to 0.32.1). CLI 0.1.52's plugin new writes a
plugin on it. Its base image is
ghcr.io/open-meridian/plugin-python:0.32.1
(sha256:92f58ed38f475a50a6ca4f28c90e70b30d5f9c4528541d1720045ca09a70db9f).
0.32.0
open-meridian 0.32.0, released on PyPI on 2026-10-22, needs chart 0.1.309 or later: the analytics component's pnl version 2, realized P&L
beside unrealized (analytics.measure("pnl", 2)), and
execution_cost; a job's business date and reporting currency
(analytics.job(..., business_date=, reporting_currency=)), and a job
with no account refused as it is built; the reference computations'
18-place rounding of a quoted price per unit, inverted and crossed fiat
legs, and each position's input price and each conversion leg in the
provenance. The lake's rates (meridian.roles.data.rates); latency
recorded (roles.Latency) and not yet reported, since core receives
ReportLatency; a dataset's liveness (Wants.liveness,
roles.Liveness) and declared delay; the settings for session
ceilings, dataset thresholds and the stale-reference action; an ems's
reference check (roles.ems.ReferenceCheck) and an RFQ placement's
quotes (roles.ems.open_quotes); a connectivity Trader's placement
types and RFQs (VenueQuote); connectivity's pages declared by the SDK
(connectivity.trading_pages, connection_pages, raw_records_page);
and an event-contract venue's events kept (Feed.keep_events) with a
range member's bounds. A DatasetLicence writes both derived_use and
derived_use_terms. Its typed operations are in
RFQ quotes.
Breaking: every role's suite holds new cases a plugin's suite test must
map (see 0.31.1 to 0.32.0). It needs a runtime on chart 0.1.309 or later. CLI 0.1.51's plugin new writes
a plugin on it. Its base image is
ghcr.io/open-meridian/plugin-python:0.32.0
(sha256:8ea1713eb354d7cadcf2f83ee0903d9ed2d157c3f49c1036a65ce7dccd53b5cd).
0.31.1: refused settings, the pricing template without storage
open-meridian 0.31.1, released on PyPI on 2026-10-19, needs chart 0.1.308 or later, as 0.31.0 does. A saved setting value that is no longer one of its
choices, or not of its kind, is delivered unset and listed in
Settings.refused (meridian.RefusedSetting), never raised and never
mapped to another value (see Settings). An
event-contract venue's contract map keeps a range market's bounds, and
event_contracts.Feed.stated_identifiers, by which
EventContractsChecks holds a record complete, is public. The pricing
template declares no storage, since pricing is no edge role; every
template's AGENTS.md says how a version is published to a registry,
and its pre-push hook says a tag push runs nothing. Only the pins move
(see 0.31.0 to 0.31.1). CLI 0.1.50's plugin new
writes a plugin on it. Its base image is
ghcr.io/open-meridian/plugin-python:0.31.1
(sha256:67781f140800652d7b8052f685355738901cd7f8dab20c13473c2142a3535b39).
0.31.0
open-meridian 0.31.0, released on PyPI on 2026-10-19, needs chart 0.1.308 or later: the analytics component, asked through
meridian.analytics (a job's typed builders, its answer and every
figure with how it was computed, and a job answered later caught up by
sequence), with the reference computations its engine equals exactly;
the pricing role, meridian.roles.pricing.Pricer, and its template;
event groups (meridian.EventGroups, event_contracts.Feed.report_event);
the agents' demo-only guard (environment(demo="paper"),
reached_environment, connection_badge, Caller.agent_refused);
plugin.query_lake, one bounded query of the lake, and
plugin.count_model_tokens. It also carries more of each role's common
logic: custody's pages (custody.account_pages), a broker's listings
(roles.data.listings), and the Trader's stated charges and settlement
dates. Its typed operations are in
The analytics component's operations.
Breaking: every role's suite holds new cases a plugin's suite test must
map (see 0.30.0 to 0.31.0). It needs a runtime on chart 0.1.308 or later. CLI 0.1.49's plugin new writes
a plugin on it, --role pricing among its templates. Its base image is
ghcr.io/open-meridian/plugin-python:0.31.0
(sha256:e041f83c89cfcd020f1406bf918aba88c1ff68978874a5de52399d46824cf5d2).
0.30.0: each role's common logic
open-meridian 0.30.0, released on PyPI on 2026-10-18, still needs chart 0.1.306 or later. It carries each role's common logic, generic over the
venue (see Each role's common logic):
meridian.roles.data (Wants, Venue, the venue's reading,
served, want_rows, VenueConnection), with
meridian.roles.data.pairs and .event_contracts doing a venue's
whole work by what it lists, and meridian.roles.data.testing's
checks; meridian.edge.RawStore, raw records as files; and
meridian.roles.connectivity (Trader over a venue's adapter, its
Trading page and testing.Rig), with a connectivity role
template. The data role's Datasets template pages its wants with the
kit's om-pager. A plugin that copied this middle layer from a
sibling plugin moves onto the SDK's helpers with
meridian plugin migrate (see
0.29.0 to 0.30.0). It needs a runtime on chart 0.1.307 or later. CLI 0.1.47's plugin new
writes a plugin on it, --role connectivity among its templates. Its base image is
ghcr.io/open-meridian/plugin-python:0.30.0
(sha256:a238c6d9292779b4f1df0a06cd30836367c01412a703b559a7414137f3473ed6).
0.29.0
open-meridian 0.29.0, released on PyPI on 2026-10-17, needs chart 0.1.306 or later. A version's declaration is the whole of what it says
about itself (Declaration(interface=, reads_external_accounts=,
documentation=)): its settings, its interface and the tools its
routes make, and the vendor's documentation, the plugin's docs/
sections named by digest (see
Write a plugin's documentation).
meridian-declaration --documentation prints the texts the upload
sends, and a plugin declaring v25 registers exactly the declaration
its version was uploaded with, or is refused naming the first
difference. An operations plugin declares its affirmation deadline
(Declaration(affirmation_deadline=)); a break names an unrecognized
security's identifiers (record_break(unresolved_identifiers=)); a
venue is resolved by its ID (resolve_venue(venue_id=)); a
connectivity plugin no longer reads whether orders may name a local
record; and the give-up's suite case is about give-ups. With it, chart 0.1.306 brings the platform's registries
(Phase 0: Open Meridian's own plugins in Community), CLI 0.1.46's
plugin publish, plugin key, plugin download, upload --bundle,
status and logs, the book's snapshots (bor.snapshotInterval), a
version retired from the catalogue, the licensed schemes declared,
a listing identifier's trading currency, a give-up naming nothing held
kept unplaced, and an FX trade after Friday's cut dated Monday.
meridian plugin migrate rewrites what it
can (see 0.28.0 to 0.29.0). It needs a runtime on chart 0.1.306 or later, and CLI 0.1.46's
plugin new writes a plugin on it. Its base image is
ghcr.io/open-meridian/plugin-python:0.29.0
(sha256:f3b62d27899acb0c2c9a29f32fdb8d505b1628fbcaadd8a1052ac6af389279f0).
0.28.0
open-meridian 0.28.0, released on PyPI on 2026-10-15, needs chart 0.1.305 or later.
The oms books an order's executions to its
accounts (book_order, hand_off_booking, correct_booking), with
the block order, the error account, sleeve crosses
(record_sleeve_cross) and give-ups (report_give_up); portfolio and
operations set an allocation instruction
(set_allocation_instruction); compliance raises and acknowledges
alerts (raise_alert, acknowledge_alert); and readers list bookings
and alerts (list_bookings, list_alerts). Every instrument is named
by the platform's INS- ID, a deployment's local LCL- record
provisional until the platform answers
(ResolveIdentifierResult.provisional; see
One ID per instrument); every role hears a
replacement and catches up with list_replacements, the SDK keeping a
current-ID map from both, and reads the templates (list_templates,
read_template); reference offers a template's fields
(offer_template_fields); and the order path's roles read whether an
order may name a local record (read_local_record_orders). An
account's base currency is a balance and every other currency opens
lots. A data plugin's dataset names the settlement calendar its
business dates follow (DatasetDeclaration.calendar_id).
meridian.environment() defaults to Sandbox, Live only where a
person chose it or the plugin's code says environment(default="live").
The suites gain v24's cases, the SDK answering every suite's two new
ones for a plugin that maps neither. Every operation it adds is
preview. meridian plugin migrate rewrites
what changed (see 0.27.0 to 0.28.0). It needs a
runtime on chart 0.1.305 or later, and CLI 0.1.45's
plugin new writes a plugin on it. Its base image is
ghcr.io/open-meridian/plugin-python:0.28.0
(sha256:ea7720e34c11d40fded19254448317685351b3668eab3bb318e505770f997f5b).
0.27.0
open-meridian 0.27.0 needs chart 0.1.303 or later.
A connectivity plugin reports a
venue-originated fill on no placement
(report_execution's instrument_id, side, venue_order_id and
venue_client_order_id), which waits unmatched (Execution.state);
the ems accepts it (accept_execution, to an order_id or a
new_order) or answers it DK (dont_know_execution, with a reason);
a connectivity plugin hears the executions it reported, and its
catalogue says which acknowledgement its venue takes
(VenueCatalogue.execution_acknowledgement); list_orders takes
execution_state. A connectivity plugin reports a venue's
value-date roll (report_value_date_roll), a fee
may be financing or commission, and an FX order names its
quote_currency. A data plugin records an event contract's
outcomes (record_outcomes, Outcome), which prices'
readers and operations hear and read (list_outcomes); a daily value
on a closed day carries ObservationMeta.closed_day. A plugin whose
source states no currency declares
meridian.account_currency(). The suites gain v23's
cases, their about markers and any_of.
Every operation it adds is preview. Nothing a plugin calls changed
shape: meridian plugin migrate moves the pins (see
0.26.0 to 0.27.0). It needs a runtime on chart 0.1.303 or later, and CLI 0.1.43's plugin new
writes a plugin on it. Released 2026-10-14.
Notice: SDK 0.28.0 changes three stable reads
The one-ID revision changes
ResolveIdentifier,
ResolveInstrument and
ReportMissingInstrument. Every
instrument comes to be keyed by its platform INS- ID: a deployment's
provisional records stay LCL-, resolved to their INS- record, and a
resolved LCL- ID is accepted forever. The open_meridian identifier
stops being added to records. Plugins treat IDs as opaque and need no
change; a plugin that reads the open_meridian identifier should stop
relying on it. A stable row changes only after a version whose notice
says it will, and SDK 0.27.0 is that version.
0.26.0
open-meridian 0.26.0 needs chart 0.1.301 or later:
the order path and
settlement calendars. Each role on the order
path does its part through typed operations (propose_order,
release_order, create_placement, report_execution and the rest)
and hears the book's records (receive(order_updated=...,
placement_updated=..., execution_recorded=...,
trading_instruction_changed=...)); a refusal by the order's state, a
claim or a link is a CommandRefused with its code, and the order's
transition table ships as meridian/transitions.json. Every role works
out a date in core (read_calendar, compute_date). A version declares
the algorithms its autonomous work may name
(Declaration(algorithms=[AlgorithmDeclaration(...)])), and
runs_as(role, algorithm_id=...) names one; a
Caller carries algorithm_id and, for an ems, the
connectivity plugins the user may route through
(connectivity_instance_ids). A placement carries its own change
requests (change_requests). A connectivity plugin reports fills on
the ems's placements and records no order and creates no placement. The order path's and the
calendars' operations are preview. At the upgrade to chart 0.1.301 the
book records that every account it holds waits for compliance, each
its own entry, and no connectivity plugin is available or linked, each
recorded so. Nothing a plugin calls changed
shape: meridian plugin migrate moves the pins (see
0.25.0 to 0.26.0). It needs a runtime on chart 0.1.301 or later, and CLI 0.1.42's plugin new writes
a plugin on it. Released 2026-10-13.
0.25.0
A plugin's autonomous work -- a scheduled sweep, a strategy, an agent
inside the plugin -- calls its own acts as the
service account its role runs as,
meridian.runs_as(role), answered made or prepared; a
refusal names its field (NotGranted.fields, CallFailed.fields), and
a sidecar older than v21 is said to be (CallFailed, kind
"unimplemented"). An act's route reached for such a call reads a
Caller whose subject_kind is a service account, naming
its owner_subject (meridian.SubjectKind), and acts as for a person;
meridian.testing calls a tool so (subject_kind=, owner_subject=).
Nothing a plugin calls changed shape: meridian plugin migrate moves
the pins (see 0.24.0 to 0.25.0). It needs a runtime
runtime on chart 0.1.299 or later, and CLI 0.1.41's
plugin new writes a plugin on it. Released 2026-10-12.
0.24.0
An act can be prepared rather than made (checked_by=,
accounts=, made_on=, request.prepared()); a setting may be
each person's own (per_person,
plugin.set_own_settings, request.own_settings); a plugin links only
to the accounts a deployment admin granted it; a data plugin reads its
own datasets' licences and backfills a restart's gap
(meridian.roles.data.backfill_restart); the roles dgm, ccm and
match are named data, connectivity and confirmation; and each
role's common part is meridian.roles, with
typed settings (@meridian.settings, meridian.environment(),
meridian.sample_data()) and the kit's macros (meridian/kit.html).
From 0.23.0 meridian plugin migrate rewrites the old role names; see
0.23.0 to 0.24.0. It needs a runtime on chart 0.1.298 or later, and CLI 0.1.39's plugin new writes a
plugin on it. Released 2026-10-11.
At the upgrade to chart 0.1.298 the instrument store opens a pulled
identifier's window from the start, each record so corrected a version
of its own naming each window and the date it replaced; a duplicate
local record minted before it, beside the platform's, by an
earlier-dated report or a data plugin naming a currency the deployment
did not hold, is left for a person to merge on the Instruments page.
Each account a plugin had linked is granted to it at the upgrade,
recorded as made then, so nothing that worked stops.
A deployment's grants and access entries naming an old role name are
rewritten once by core at the upgrade.
At the upgrade every act is "may make", with the second pair of eyes
off, which is what every delegation could do before, each recorded as
set then.
A secret of 16 characters or more shows its last four characters in
its placeholder once it is set again after the upgrade.
0.23.0
open-meridian 0.23.0 adds the lake's trades
and quotes: a data plugin records them in batches, signal and ems read and
hear them, every trade in full and caught up after a watermark;
reporting resolves an identifier; the suites' 1b cases and a case
about a kind of data a plugin never publishes marked not presented. It
needs a runtime on chart 0.1.294 or later, which
refuses nothing a plugin on 0.22.0 sends. Released 2026-10-10. See
Trades and quotes.
0.22.0
open-meridian 0.22.0 adds the lake: a
data's catalogue, prices and bars recorded in batches, wants, the
readers, a Money naming its instrument and real dates. It needs a
runtime on chart 0.1.292 or later, which refuses
nothing a plugin on 0.21.0 sends. See Plugin manifest.
A plugin built before it keeps sending the currency code it always sent,
and core resolves it to the currency's cash instrument. At the upgrade
to chart 0.1.292 the street and the book resolve every currency code they
hold to its cash instrument, once, each resolution its own record saying
it was filled in then, not when the amount was first recorded; no entry
or record is rewritten, and a code the instrument store does not answer
for yet is resolved by a later sweep.
0.21.0
open-meridian 0.21.0 adds the archive:
a plugin at the edge declares the kinds of raw record it keeps, and past
each kind's window archives, keeps or deletes them as its admin chose,
each move reported. It needs a runtime on chart
0.1.277 or later, and CLI 0.1.36's plugin new writes a plugin on it;
CLI 0.1.36's meridian up asks where older records go (--archive,
--no-archive). A plugin declaring no kinds keeps one retention, as
before. SnapTrade 0.13.0, the first plugin on it, drops 0.12.0's
Keep raw responses for (raw_retention_days) and Keep activity
records for (activity_retention_days) and carries neither value
over: its windows, Raw responses: window (responses_window_days,
30 days by default) and Reported activity: window
(activity_window_days, 2,555 days), take their place, set once at
upgrade. Upgrade the deployment first, note what the two old settings
hold, launch 0.13.0 in place, then set the two windows and what is done
past each. What 0.12.0 deleted is not brought back; on its first pass
0.13.0 moves 0.12.0's records into units, keeping their times.
0.20.0
open-meridian 0.20.0 adds a person's level is granted
per role of a plugin (see Access per role),
and a page, route, tool and setting names the roles it serves; a custody
plugin re-resolves an activity once its instrument resolves. It needs a
runtime on chart 0.1.268 or later, and CLI 0.1.35's
plugin new writes a plugin on it. A runtime serving v14 or earlier
grants each plugin as a whole. The upgrade to chart 0.1.268 rewrites
every grant once, changing what nobody can do: every access entry on a
plugin holding exactly one role is rewritten to name it, at the level it
had, in the group it was in (a plugin's roles being what its sidecar
last reported, or its latest launch); an entry on a plugin holding no
role is left naming none; user groups, account groups, permissions and
links are untouched, and All plugins (admin) grants admin on every
role. A delegation covering everything is untouched; each row of a
narrowed one is rewritten to name the plugin's one role, and a row the
rewrite cannot name is kept, covers nothing, and is flagged on Connected
clients. Each group the upgrade rewrote is recorded, made by no person.
A runtime at chart 0.1.268 keeps no session for a terminal and serves CLI
0.1.25 or later, which holds a delegation
in place of a session.
0.19.0
open-meridian 0.19.0 needs a newer runtime:
the custodian's activity,
reported by a custody plugin, and read, heard and linked by
operations, with each sync status the street keeps; and a setting may
be a table of typed columns. It needs a runtime on chart 0.1.262 or later, and CLI 0.1.34's plugin new writes a plugin
on it. Released 2026-10-05. Every settings change is
its own record. At the upgrade the deployment redacts the records of
every setting already secret; a change made before it, recorded with
who and when and without the value, is filled in from the value the
setting still holds where its latest change set it, and says so; and
each setting changed before the upgrade gets one more record saying
the earlier values are not known before then.
The plugin UI kit, by version
The kit pages link, at /.meridian/ui/<version>/, served by the
deployment. A 0.x release only adds, and the dashboard serves the newest
0.x it carries to any 0.x request (see
Build a plugin's page).
| Version | What it added |
|---|---|
| 0.1.0 | The brand's tokens and colour schemes, the platform's CSS classes, and om-grid (with its high-rate mode), om-chart, om-asof, om-instrument-picker, om-live and om-panels |
| 0.2.0 | The seamless frame: a page the dashboard frames drops its own heading and tab row, and the frame is sized to the page. See Inside the dashboard's frame. |
| 0.3.0 | om-account-map; om-moment; the grid's declared JSON, rich cells and narrow layouts; list rows that wrap; option and field-row styles, and a select as tall as an input |
| 0.4.0 | Header actions: a framed page's head buttons, drawn by the dashboard in its own header. See Header actions. |
| 0.5.0 | om-account-map at scale: a dense table with search, filters, grouping, pages, a chooser found by typing, suggestions, and several links in one form (link-several). See At thousands of accounts. |
| 0.6.0 | om-status, a status dot with its note on hover, focus or a tap; and in om-account-map, each account's optional status and values, a Status column and a filter by state |
| 0.7.0 | Header status: a framed page's head om-status marked data-om-header, drawn by the dashboard beside the plugin's name. See Header status. |
| 0.8.0 | Icon actions: a button marked data-om-icon="refresh" is drawn as a circular arrow, in the page and as a header action in the dashboard's header. See Icon actions. |
| 0.9.0 | om-entry-grid: a table of typed inputs a person enters rows in, posted with the page's own form, the server's messages placed on their cells, a spreadsheet's paste and a CSV in a dialog |
| 0.10.0 | Every page fits one screen: a height budget the dashboard holds to the screen, compact chrome, one-line rows (table.one-line, om-grid's one-line) with the whole row a click away, om-pager, as many rows a page as fit, and the overflow check |
| 0.11.0 | A search beside the pager, search on om-grid, om-pager and om-entry-grid, search-param on om-pager, and the om-search event; and fields gated on a choice, data-om-applies-when and data-om-one-of. See To search a list, and show a field only when it applies. Served from chart 0.1.293. |
| 0.12.0 | An instrument column on om-entry-grid with its src, found as you type; the patterns Facts, Next step and Prepare; and meridian:open with data-om-opens, a link from a framed page to one of the dashboard's own tabs. See Role pages, instruments and Prepare. Served from chart 0.1.298. |
| 0.13.0 | om-sidebar, a grouped left sidebar of plain links, its groups given as data, collapsing to icons on a desktop and a drawer on a phone, with .om-shell and .om-shell-main and the kit's stroked icons (lib/icons.js). The dashboard's settings are drawn with it. See A site's sections: om-sidebar. Served from chart 0.1.309. |
Moving a plugin to a new release¶
Move a plugin one release at a time or several at once: meridian plugin migrate runs every step between the plugin's pin and the target, in order, and its table of what each step does lists every step from 0.5.0. The recent ones:
| From | To | Contract | Runtime | plugin new writes it from |
plugin migrate rewrites |
Then by hand |
|---|---|---|---|---|---|---|
| 0.35.5 | 0.36.0 | 30 | chart 0.1.538 | none: CLI 0.1.60 writes 0.35.0 | a dataset's licence_default=DatasetLicence(...) into terms_and_pricing="..."; a base pinned by digest, moved with its digest dropped |
see 0.35.5 to 0.36.0 |
| 0.35.4 | 0.35.5 | 29 | chart 0.1.408 | none: CLI 0.1.59 writes 0.35.0 | nothing: only the pins move | an FX week of the plugin's own replaced by helpers.sessions.FX_WEEK |
| 0.35.3 | 0.35.4 | 29 | chart 0.1.408 | none: CLI 0.1.59 writes 0.35.0 | nothing: only the pins move | a live module of the plugin's own moved onto pages.live_values and LiveValue.of, its page script onto <om-live cells>; a book view's row is keyed order_id=<id> alone |
| 0.35.2 | 0.35.3 | 29 | chart 0.1.408 | none: CLI 0.1.59 writes 0.35.0 | nothing: only the pins move | what meridian plugin migrate --to 0.35.3 names in a plugin already on 0.35 |
| 0.35.1 | 0.35.2 | 29 | chart 0.1.408 | none: CLI 0.1.59 writes 0.35.0 | nothing: only the pins move | a Declaration(settings=Config.declared()) names the class again, settings=Config |
| 0.35.0 | 0.35.1 | 29 | chart 0.1.408 | none: CLI 0.1.59 writes 0.35.0 | nothing: only the pins move | an act on the connection made on the role object holding it |
| 0.34.0 | 0.35.0 | 29 | chart 0.1.408 | CLI 0.1.55 to 0.1.59 | receive calls into role classes' on_<event> methods, the SDK's renamed methods, self.plugin.<act> onto self |
see 0.34.0 to 0.35.0 |
| 0.33.5 | 0.34.0 | 29 | chart 0.1.408 | none | every import of meridian.roles.chains, .packages, .lifecycle or .debt onto meridian.helpers |
see 0.33.5 to 0.34.0 |
| 0.33.1 | 0.33.5 | 28 | chart 0.1.334 or later for subscriptions | CLI 0.1.54 writes 0.33.3 | nothing: only the pins move | nothing |
| 0.33.0 | 0.33.1 | 28 | chart 0.1.313 or later | none: CLI 0.1.53 writes 0.33.0 | nothing: only the pins move | a venue keeping bars declares bars, names BARS_DECLARED in its venue_cases and drops a-daily-bar from NOT_PRESENTED; a plugin's own feed-delay producer standing on the quote can be removed |
| 0.32.3 | 0.33.0 | 28 | chart 0.1.313 or later | CLI 0.1.53 | a wire SubjectRef handed to the SDK, into meridian.SubjectRef; a base pinned by digest, moved with its digest dropped where it cannot be read |
see 0.32.3 to 0.33.0 |
| 0.32.2 | 0.32.3 | 27 | chart 0.1.309 or later; 0.1.311 for the licence's attribution terms | none: CLI 0.1.52 writes 0.32.1 | nothing: only the pins move | give every rate a convention: see 0.32.2 to 0.32.3 |
| 0.32.1 | 0.32.2 | 27 | chart 0.1.309 or later; 0.1.311 for the licence's attribution terms | none: CLI 0.1.52 writes 0.32.1 | nothing: only the pins move | a rates plugin recording no liveness names one more case not presented: see 0.32.1 to 0.32.2 |
| 0.32.0 | 0.32.1 | 27 | chart 0.1.309 or later | CLI 0.1.52 | nothing: only the pins move | a rates-only data plugin names two more cases not presented: see 0.32.0 to 0.32.1 |
| 0.31.1 | 0.32.0 | 27 | chart 0.1.309 or later | CLI 0.1.51 | a base pinned by digest, moved with its digest dropped where it cannot be read | see 0.31.1 to 0.32.0 |
| 0.31.0 | 0.31.1 | 26 | chart 0.1.308 or later | CLI 0.1.50 | nothing: only the pins move | nothing |
| 0.30.0 | 0.31.0 | 26 | chart 0.1.308 or later | CLI 0.1.49 | a base pinned by digest, moved with its digest dropped where it cannot be read | see 0.30.0 to 0.31.0 |
| 0.29.0 | 0.30.0 | 25 | chart 0.1.307 or later | CLI 0.1.47 | a module copied from a sibling plugin, and a trading layer in SnapTrade 0.16.0's shape, into re-exports of the SDK's helpers | see 0.29.0 to 0.30.0 |
| 0.28.0 | 0.29.0 | 25 | chart 0.1.306 or later | CLI 0.1.46 | a connectivity plugin's read of the escape hatch, and its test's mapping of the dropped case | see 0.28.0 to 0.29.0 |
| 0.27.0 | 0.28.0 | 24 | chart 0.1.305 or later | CLI 0.1.45 | a test of the LCL- prefix; a read of the open_meridian identifier; the suites' LCL-suite-n fixtures |
see 0.27.0 to 0.28.0 |
| 0.26.0 | 0.27.0 | 23 | chart 0.1.303 or later | CLI 0.1.43 | nothing: only the pins move | see 0.26.0 to 0.27.0 |
| 0.25.0 | 0.26.0 | 22 | chart 0.1.301 or later | CLI 0.1.42 | nothing: only the pins move | see 0.25.0 to 0.26.0 |
| 0.24.0 | 0.25.0 | 21 | chart 0.1.299 or later | CLI 0.1.41 | nothing: only the pins move | see 0.24.0 to 0.25.0 |
| 0.23.0 | 0.24.0 | 20 | chart 0.1.298 or later | CLI 0.1.39 | the renamed roles: dgm to data, ccm to connectivity, match to confirmation |
see 0.23.0 to 0.24.0 |
| 0.22.0 | 0.23.0 | 19 | chart 0.1.294 or later | CLI 0.1.38 | nothing: only the pins move | a data plugin's suite test meets the data suite's trades and quotes cases |
| 0.21.0 | 0.22.0 | 18 | chart 0.1.292 or later | CLI 0.1.37 | nothing: only the pins move | see 0.21.0 to 0.22.0 |
| 0.20.0 | 0.21.0 | 16 | CLI 0.1.36 | nothing: only the pins move | see 0.20.0 to 0.21.0 | |
| 0.19.0 | 0.20.0 | 15 | CLI 0.1.35 | nothing: only the pins move | see 0.19.0 to 0.20.0 | |
| 0.18.0 | 0.19.0 | 14 | CLI 0.1.34 | nothing: only the pins move | see 0.18.0 to 0.19.0 |
Before 0.19.0, plugin new pinned 0.18.0 from CLI 0.1.33, 0.17.0 from CLI 0.1.29 to 0.1.32, 0.16.0 from CLI 0.1.28, 0.15.0 from CLI 0.1.27, 0.14.0 from CLI 0.1.26, 0.12.0 from CLI 0.1.24 and 0.1.25, 0.11.0 from CLI 0.1.23, 0.10.1 from CLI 0.1.22, 0.10.0 from CLI 0.1.21, 0.9.0 from CLI 0.1.20, 0.7.1 from CLI 0.1.18 and 0.1.19, and 0.6.1 from CLI 0.1.16 and 0.1.17.
0.35.5 to 0.36.0¶
The to_0_36_0 step. Breaking: a new interface version, and a dataset's
catalogue entry declares no licence. meridian plugin migrate --to 0.36.0
moves the pins and rewrites:
-
A dataset's licence into words (
licence-default-described): eachlicence_default=DatasetLicence(...)whose arguments are literals becomesterms_and_pricing="...", the terms it declared in words, part by part. A licence shared through a module-level name keeps its name, which then holds the words.DatasetLicenceandDataEligibilityare dropped from afrom meridian import ...where nothing else names them. -
The base image (
base-digest-dropped): a base pinned by digest at an older release is moved to 0.36.0 with its digest dropped, since the migration runs with no network.
Then, by hand where they apply:
- a licence the rule could not read (an argument that is not a literal, a
dataset built from a mapping, terms past 2,000 characters in words), or
DatasetLicence,DataEligibilityor.licence_defaultstill named, such as in a test (licence-default-left): write the vendor's terms interms_and_pricing, read them back as.terms_and_pricing; - a
DerivedSeriesorCorporateActionAnnouncedof the SDK's given the wire'sMeasureRefas itsrule(measure-ref-sdk-form): build it asmeridian.MeasureRef(definition=..., version=...); - a suite test of a role that gained cases (
suite-cases-v30):holds-a-standing-figurein thereporting,portfolio,complianceandsignalsuites, andreads-the-source-prioritiesin those of the eight roles that read the lake. Map each, or name it not presented, with why; - the plugin's e2e images, pinned to a core of this release by digest
(
core-images), and the base's digest pinned again (base-digest).
Then, where it helps: give each dataset its gap_recovery, an FX or other
session-bound dataset its sessions_calendar_id, and a vendor allowing
more than one connection per key its most_shards.
0.34.0 to 0.35.0¶
The to_0_35_0 step. Breaking, with no contract change: each role is one
class. meridian plugin migrate --to 0.35.0 moves the pins
and rewrites:
- A
receivecall whose handlers are one class's methods on one declared role's events: that class'son_<event>methods, the class given the role's base,seed=Falseitsseed = False, the connection it took dropped; the SDK's own handlers dropped from areceive. - The SDK's role logic: a Trader's, a feed's and a Pricer's renamed
methods and arguments, the connection given them dropped (a recorder's
call
recorder.bind(...)). - An act on the connection in a role class,
self.plugin.<act>, ontoself; a helper givenself.plugingivenself; an unchanged template entry point replaced bymeridian.run. - A listings feed's own edge glue: its
at_the_edgeand the_recordoverride calling it deleted, and its change copies madedata.SentRows.
Then, by hand where they apply:
- an entry point connecting by hand, moved to
meridian.run; - an act on the connection, made on the role object holding it;
- a
receiveno rewrite reached; - a role class lacking a required event;
- a suite test hearing through
recorder.receive, or giving a class a stand-in connection:recorder.hear(recorder.bind(...)); - the SDK's helpers adopted where the plugin keeps its own: a stream
supervisor onto
helpers.sessions.Supervisor, a chains machine ontodata.option_chains.Feed, a calendar reader ontohelpers.calendars, a polling loop ontodata.Timetable, a raw store ontomeridian.edge.RawStore.
0.33.5 to 0.34.0¶
The to_0_34_0 step. Breaking: a new interface version; and four
modules moved. meridian plugin migrate --to 0.34.0 moves the pins and
rewrites every import and name of meridian.roles.chains, .packages,
.lifecycle or .debt onto meridian.helpers: import
meridian.roles.chains, from meridian.roles import chains, from
meridian.roles.chains import X, from meridian.roles import Latency,
chains split in two, meridian.roles.debt.factor(...), and a patch
target's string. A base pinned by digest is moved with its digest dropped.
Then, by hand where they apply:
- map each role suite's v29 cases, the data suite's
records-a-pools-factorand every reader'sreads-a-pools-factorsandhears-a-factor-recorded, or name those about what the plugin never presents innot_presented, with why; - name a
QualityAssessment'sdata_type; - an old path still named in a comment, a dynamic import or read through
from meridian.roles import *; - pin the base's digest again.
0.32.3 to 0.33.0¶
The to_0_33_0 step. Breaking: a new interface version; the typed
operations take meridian.SubjectRef only, and every role's suite holds
new cases, so a plugin running its role's suite is not verified until its
test maps them. meridian plugin migrate --to 0.33.0 moves the pins and
rewrites:
- A wire
SubjectRefhanded to the SDK (subject-ref-from-the-sdk):ops.SubjectRef(...),operations_pb2.SubjectRef(...), or one imported frommeridian.plugin.v1.operations_pb2, intomeridian.SubjectRef(...). One built inside another wire message, or added to a wire message's field, stays the wire's. - A base pinned by digest (
base-digest-dropped): moved to 0.33.0 with its digest dropped, since the migration runs with no network.
Then, by hand where they apply:
- Subjects from the wire (
subjects-from-the-wire): a typed operation, or one of the SDK's rows, given a heard message's subjects (subjects=want.subjects). Convert each:[meridian.SubjectRef(entity_id=s.entity_id) for s in want.subjects], keepingrole=s.rolewhere the row states one. - The suites' v28 cases (
suite-cases-v28): map each to the plugin's own producer through the SDK's v28 helpers (see Helpers), or, for a case about what its source or work never presents, name it innot_presentedwith why (see A case not presented). - The latency cases (
latency-cases-required): the three a v27 plugin named not presented, "core receives ReportLatency", are required: map each to the plugin'sroles.Latency, which now reports.meridian.roles.data.testing.feed_delay, and the connectivity andemsproducers in the SDK's testing modules, do it for the SDK's kinds. - The base's digest (
base-digest): pin it again,ghcr.io/open-meridian/plugin-python:0.33.0@sha256:<digest>.
0.32.2 to 0.32.3¶
Released 2026-10-24 (the to_0_32_3 step). meridian plugin migrate --to
0.32.3 moves the pins and changes no code. One change can raise: a rate
built with rates.Feed.rate and no convention is refused at the plugin,
with a clear error, as the lake refuses it; give it the publisher's
convention, and an index level the convention of the rate it compounds
(the SOFR Index rates.MONEY_MARKET). Nothing else a plugin calls changed
shape: a missed rates subject is asked again the same day, a listings feed
records its live dataset's liveness, a Resolution may carry
challenge_ends_at_ns, DatasetLicence takes not_affiliated_notice, and
the merged Connection is tabbed. Set the licence's attribution terms only
for a deployment on chart 0.1.311 or later.
0.32.1 to 0.32.2¶
Released 2026-10-23 (the to_0_32_2 step). Not breaking: meridian plugin
migrate --to 0.32.2 moves the pins and changes no code. DatasetLicence
takes redistribution, attribution_required, attribution_text,
copyright_class and no_more_restrictive_terms; a deployment before chart
0.1.311 refuses the registration of a declaration setting any of them, so
set them only for one on 0.1.311 or later. RATES_NOT_PRESENTED no longer
names records-a-datasets-liveness: a rates-only plugin recording no
liveness adds it to not_presented itself, with why. A data plugin's
Connection drops its empty Sessions until a session is measured, so a
page check expecting it on a polled plugin no longer finds it.
0.32.0 to 0.32.1¶
Released 2026-10-23 (the to_0_32_1 step). Not breaking: nothing a plugin
calls changed shape, and meridian plugin migrate --to 0.32.1 moves the
pins and changes no code. The data suite's an-exact-price and
a-want-recorded-against are now about closes: a data plugin recording
rates and no prices names them in not_presented, "it records rates, never
prices", most simply by passing
meridian.roles.data.testing.RATES_NOT_PRESENTED. A plugin recording
prices maps them as before.
0.31.1 to 0.32.0¶
Released 2026-10-22 (the to_0_32_0 step). Breaking: the SDK needs chart 0.1.309 or later, and every role's suite holds new cases, so a plugin running
its role's suite is not verified until its test maps them. Nothing a plugin
called is removed. meridian plugin migrate --to 0.32.0 moves the pins; a
base pinned by digest moves with its tag, the new digest read from its
registry, or with its digest dropped where it cannot be read
(base-digest-dropped).
Then, by hand where they apply:
- The base's digest (
base-digest): pin it again,ghcr.io/open-meridian/plugin-python:0.32.0@sha256:<digest>. - The suites' v27 cases (
suite-cases-v27): map each to the plugin's own producer. The reading roles (reporting,portfolio,compliance,signal) read a curve for a date (rates.read_curve) and hear a rate and a dataset's liveness (receive(rates_recorded=..., liveness_recorded=roles.Liveness().heard)); they, theems, theomsandoperationsread an account's execution costs (list_execution_costs); theemspicks a quote on its RFQ placement; theomssets a block member's allocation instruction;operationsreads a trade date's FX close. A case about something the plugin's source never presents (about:rates,streams,rfqs,trades) is named innot_presentedwith why;meridian.roles.data.testing'sPAIRS_NOT_PRESENTEDandEVENT_CONTRACTS_NOT_PRESENTEDname a kind of venue's. - The latency cases:
reports-its-feed-delay(data),reports-a-sessions-round-trip(connectivity) andreports-its-pre-trade-checks-data-age(ems) areabout: latency. Every plugin holding the role names them innot_presentedwith the reason "core receives ReportLatency, ruling 515": the SDK records latency and sends nothing until chart 0.1.313, when they are required in full. - A data plugin querying the lake without trades
(
lake-query-without-trades): the data suite'squeries-the-lake-within-boundsis about trades. A plugin whose catalogue declares no trades names it not presented, "it records no trades", rather than fake it. - A connectivity plugin's own Trading page (
trading-page-onto-sdk): callconnectivity.trading_pages(...)andraw_records_page(...)in its place, or, holding the data role too,connection_pages(...)(see Each role's common logic). - An event-contract plugin's own events loop (
events-onto-sdk): runfeed.keep_events()in its place, theMarketansweringevent(ref). - An e2e workflow's logs (
e2e-hidden-files): a workflow uploading the harness's logs from.e2e/withactions/upload-artifactaddsinclude-hidden-files: true, or a failure keeps no logs.
0.31.0 to 0.31.1¶
Released 2026-10-19. Not breaking: nothing a plugin calls changed shape,
and meridian plugin migrate --to 0.31.1 moves the pins and changes no
code. What changes is how a saved setting value this version cannot read
arrives: unset, never raised, and listed in Settings.refused, a typed
class's .refused and request.own_settings_refused (see
Settings). A plugin that renamed a choice can show each
refused value's message on its page, asking the person to save the
setting again.
0.30.0 to 0.31.0¶
Released 2026-10-19. Breaking: a new interface version; and every
role's suite holds new cases, so a plugin running its role's suite is not
verified until its test maps them. Nothing a plugin called is removed.
meridian plugin migrate --to 0.31.0 moves the pins; a base pinned by
digest moves with its tag, the new digest read from its registry, or with
its digest dropped where it cannot be read (base-digest-dropped).
Then, by hand where they apply:
- The base's digest (
base-digest): pin it again,ghcr.io/open-meridian/plugin-python:0.31.0@sha256:<digest>. - The suites' v26 cases (
suite-cases-v26): map each to the plugin's own producer, or name one about something its source never presents innot_presented, with why. A reading role (reporting,portfolio,compliance,signal) runs a job and reads its answer, catches up finished jobs and queries the lake within bounds;oms,portfolio,complianceandemsread the agents' guard, and theemsmeets the guard's five cases;data,emsandoperationsquery the lake within bounds; and the data suite reports an event group and a flag the venue does not state. Every role'shears-and-reads-an-event-groupis answered by the SDK unless the plugin maps it. - A connectivity adapter's environment
(
venue-reports-no-environment): give the adapter anenvironment, read from the host its session called withmeridian.reached_environment(...). The catalogue carries it, and a connection with no report counts live, so an agent's placement on it is refused while the agents' guard is on. - Custody pages, a listings layer or a trader of the plugin's own
(
custody-pages-onto-sdk,lake-onto-sdk,trading-onto-sdk): keep only what is the vendor's, and callcustody.account_pages, subclasslistings.Feed, or constructconnectivity.Traderover the adapter (see Each role's common logic).
0.29.0 to 0.30.0¶
Released 2026-10-18. Not breaking: the interface is unchanged, and a plugin
left on 0.29.0 keeps working. meridian plugin migrate --to 0.30.0 moves
the pins and moves a plugin's copied middle layer onto the SDK's helpers
(see Each role's common logic):
- A module copied from a sibling plugin (
market.py,products.py,contracts.py,raw.pyordgm.py, saying "The same module in ... and ...") becomes a re-export of the SDK's names that replace it, so every import of it still resolves (copied-module-onto-sdk). - A trading layer in SnapTrade 0.16.0's shape (
trading.pydefiningTrader,Noted,Venue,Watchedandtrading_files) becomes a re-export ofmeridian.roles.connectivity, itsTraderthe SDK's in the module's market zone andNotedthe SDK'sSent(copied-trader-onto-sdk).
Then, by hand where they apply:
- The re-exports (
re-export-left,trader-re-export-left): import the names from the SDK where the plugin uses them and delete the module; constructconnectivity.Traderwithmarket_zone=, and useSentforNoted. - Copied pages (
copied-page,trading-page-onto-sdk): build the data role's Connection and Datasets with the kind's builders and templates, and the Trading page withtrading_readand the SDK's template, extending a template only for the venue's own nuance. - Copied tests (
copied-tests,trading-rig-onto-sdk): one class of the SDK's checks (PairsChecksorEventContractsChecks), and a subclass ofmeridian.roles.connectivity.testing.Rigfor the trading rig. - A copied
stream.py(stream-is-the-plugins): keep it as the plugin's own transport and hand it to the feed as its stream factory; a stream given another connection by assigning itsconnect(stream-connect-assigned):retargetit instead. - The Makefile's
suite-same(suite-same-retired): remove it, since nothing is copied between plugins any more. - A trading layer the migration could not rewrite (
trader-by-hand): move it ontomeridian.roles.connectivityby hand, keeping only the venue's adapter and its client in the plugin.
0.28.0 to 0.29.0¶
Released 2026-10-17. Breaking: a plugin registers exactly the declaration its version was uploaded with, so a
plugin whose declaration does not carry its interface is refused at
registration until it does. meridian plugin migrate --to 0.29.0 moves the
pins, and the CI's MERIDIAN_VERSION in .github/workflows/check.yaml, and rewrites, in a plugin holding connectivity and no role that keeps
the read (compliance, ems, oms, portfolio):
- A read of the escape hatch,
await plugin.read_local_record_orders()as a statement of its own, is removed (escape-hatch-read-dropped): connectivity's grant ofReadLocalRecordOrdersis dropped, since a connectivity plugin proposes and accepts nothing on a record. - A test mapping the dropped suite case
reads-whether-orders-may-name-local-recordsis removed (escape-hatch-case-dropped): the connectivity suite no longer has it.
Then, by hand where they apply:
connectgiven an interface, settings orreads_external_accounts(connect-names-the-declaration): name them in the version's declaration,Declaration(settings=Config, interface=meridian.Interface(port=8000, title=..., pages=pages), reads_external_accounts=True, documentation="docs"), with the interface's port fixed rather than read from the environment, in a module importing every module that declares a page or a route, and passconnect(declaration=DECLARATION). See The whole declaration.- A
[tool.meridian]naming no declaration (declaration-not-named): adddeclaration = "<module>:DECLARATION", and writedocs/overview.mdanddocs/setup.md(troubleshooting.mdandterms.mdprompted,pages.mdoptional);meridian plugin checkchecks them. See Write a plugin's documentation. - A Dockerfile whose last stage copies no documentation into the image,
such as one installing only the package from a mount or a stage of its
own (
docs-not-in-image): addCOPY docs/ docs/underWORKDIR /pluginto its last stage, so the image holds/plugin/docs, whichDeclaration(documentation="docs")reads where the plugin runs; without itmeridian-declaration, and the upload with it, fail. The templates' Dockerfile copies the whole build context (COPY . .),docs/with it. - A connectivity plugin's read of the escape hatch the migration cannot
remove alone, its answer used or the read inside another expression
(
escape-hatch-read-kept): remove the read and what depends on its answer; an order on a provisional record is theoms's and theems's to weigh. - An operations plugin may declare its affirmation deadline,
Declaration(affirmation_deadline=21 * 60), and record an unrecognized security's identifiers on its break (unresolved_identifiers), which the book requires on that category.
A plugin left on 0.28.0's pins keeps working against a runtime on chart 0.1.306 or later: it is admitted as before and not held to a declaration it never made, and its Documentation tab shows the Reference from its registration.
0.27.0 to 0.28.0¶
Released 2026-10-15. meridian plugin migrate --to 0.28.0 moves the
pins and rewrites what the one-ID revision changed (see
One ID per instrument):
- A test of the
LCL-prefix on a record's instrument ID, such asrecord.instrument_id.startswith("LCL-"), is rewritten to read the record'sprovisional(lcl-prefix-provisional). The prefix only says the deployment minted the ID: a resolvedLCL-ID staysLCL-. - A read of the
open_meridianidentifier for the platform's ID is rewritten to the record'sinstrument_id, which is the platform's ID , or the read's default where the record is provisional (open-meridian-instrument-id). Theopen_meridianidentifier is no longer added to records. - The suites'
LCL-suite-nfixtures becomeINS-suite-n(suite-fixture-ins); a provisional record's staysLCL-suite-n, answered by aRecorder(provisional=True).
Then, by hand where they apply:
- An ID tested for the prefix
LCL-with no record at hand (lcl-prefix-tested), or theopen_meridianidentifier read or written another way (open-meridian-read): read the record'sprovisionaland itsinstrument_idinstead. - A plugin that keeps instrument IDs in its own storage or state
follows each replacement to the record it became:
meridian.CurrentIdMapdoes it fromlist_replacementsand the deliveries. One that keeps none needs nothing.meridian plugin checkwarns (replaced-ids-followed), failing nothing, on anINS-orLCL-ID kept as a string in the plugin's code outside its tests. - A suite test meets v24's cases. Every suite's
hears-and-reads-a-replacementandreads-the-template-cataloguethe SDK answers for a plugin that maps neither. The rest are booking's and the one-ID model's, mapped by a plugin holding the role:oms's eleven booking cases andreads-whether-orders-may-name-local-records;operations'corrects-a-handed-off-booking,names-an-unsettled-allocation-as-a-breaks-cause,sets-a-clients-allocation-instruction,hears-a-booking-handed-offandhears-and-reads-a-held-bookings-alert;compliance's alert, booking and local-record cases;portfolio'samends-an-allocation-instruction,hears-and-reads-bookingsandreads-whether-orders-may-name-local-records;connectivity's andconfirmation'sreports-a-give-up-rejected;ems's andconnectivity'sreads-whether-orders-may-name-local-records;custody'srecords-a-thinly-described-holding-unresolved; andreporting'shears-and-reads-bookingsandhears-and-reads-alerts. None carries anaboutmarker, so none may be named innot_presented. - A plugin declaring
meridian.environment()that was Live by default saysenvironment(default="live")only where Live is meant; otherwise its admin chooses.
A plugin that does none of these keeps working on 0.27.0's pins against a
runtime on chart 0.1.305 or later: IDs are opaque, and a resolved LCL- ID is
accepted on every write, forever.
0.26.0 to 0.27.0¶
Released 2026-10-14. meridian plugin migrate --to 0.27.0 moves the pins
and rewrites nothing: chart 0.1.303 adds, and changes the shape of nothing a
plugin calls. Then, by hand where they apply:
- A suite test meets v23's cases.
connectivity's four (reports-a-fill-on-no-placement,reports-a-later-fill-on-the-accepted-placement,re-reports-a-charge-on-the-accepted-placement, aboutvenue-originated fills, andreports-a-value-date-roll, aboutvalue-date rolls), the data suite's three outcome cases (aboutoutcomesanddisputed outcomes), and each reader'shears-and-reads-an-outcome(aboutoutcomes) may be named innot_presented, with why, where the plugin's source or work never has them;ems's four andoperations' andcompliance's new cases are required. Cases that were required may now be marked too: custody's plan-code, sweep-fund, split and currency-not-stated cases, and connectivity'sreports-a-bustandreports-a-charge. See A case not presented. - A test that skips or expects a suite test to fail fails CLI 0.1.43's
role-suite: run the suite, mark what is not presented, andassert report.passed, report.failuresin the next statement (seemeridian plugin check). - A client of
dashboard__list_datasetsordashboard__list_source_prioritiesacceptsoutcome: "unchanged", which they answer, as well asread. - A custody plugin whose source states no currency declares
meridian.account_currency()and gives the currency it supplies with its provenance (see Typed settings).
0.25.0 to 0.26.0¶
Released 2026-10-13. meridian plugin migrate --to 0.26.0 moves the pins
and rewrites nothing: chart 0.1.301 adds, and changes the shape of nothing a
plugin calls. Then, by hand where they apply:
- A suite test meets the settlement calendars' cases: every role's
suite now holds
reads-a-calendar, and every role's butconnectivity's andoperations'asks-for-a-settlement-date. Map each to a call ofread_calendarandcompute_date(the role templates'test_suite.pyshow both), or, where your plugin's work never uses calendars or settlement dates, name it innot_presented, with why: the two are aboutcalendarsandsettlement dates(see A case not presented). A plugin holdingconnectivityoromsmeets its role's suite, and one holdingportfolio,ems,compliance,operationsorreportingthat role's order path cases. - A settlement or value date your plugin works out itself is asked of
core instead, with
compute_date, and recorded with its basis (see Settlement calendars). - Work the plugin does on its own that runs an algorithm declares it,
Declaration(algorithms=[AlgorithmDeclaration("twap-v1", "...")]), and names it,meridian.runs_as(role, algorithm_id="twap-v1"), keeping each identifier stable across versions. - A plugin that reads positions, lots or movement lines finds a
sleeveon each, empty for an account's unsleeved part.
0.24.0 to 0.25.0¶
meridian plugin migrate --to 0.25.0 moves the pins
and rewrites nothing: chart 0.1.299 adds, and changes the shape of nothing a
plugin calls. Then, by hand where they apply:
- Work the plugin does on its own that changes something calls the
plugin's own act as its role's service account,
meridian.runs_as(role), and reads whether it was made or prepared. Each act called so names its check tool (checked_by=): from CLI 0.1.41meridian plugin checkfails one that names none (runs-as-checked). - An act's route may be reached for a service account: it acts as for
a person,
acting_for=request.caller.header. Showrequest.caller.subject_kindandowner_subjectwhere the page says who acted. - A test that compares a refusal's attributes finds
fieldsonNotGrantedandCallFailedtoo, empty where the sidecar named none.
0.23.0 to 0.24.0¶
meridian plugin migrate --to 0.24.0 moves the pins and
rewrites one thing, the renamed roles, wherever a plugin names a role: its
pyproject.toml's [tool.meridian] roles, a roles= argument, the role
meridian.suites.run, run_async, suite or names is given, and the role
Caller.level_for, read_for or write_for asks; dgm becomes data, ccm
connectivity and match confirmation. Nothing else a plugin calls changed
shape. The release is marked breaking for the rename alone. Then, by hand
where they apply:
- An act you want preparable names its check tool, accounts and page
(
checked_by=,accounts=;made_on=is filled for you): see Prepared changes. A verified plugin's act without a check failsmeridian plugin check --verifiedfrom CLI 0.1.39. - A form field named
prepareis refused: rename it. - A data plugin meets the data suite's three new cases: map
a-restarts-gap-backfilled(or, where it records no trades, it is about trades and may be marked not presented) andreads-its-datasets-licences; mapthe-takers-side-statedor, where the source never states the aggressor, mark it with why. See Write a data plugin. - A plugin linking external accounts is answered only the accounts a
deployment admin granted it, nothing at first: say so on its page, and
expect
REFUSAL_REASON_ACCOUNT_NOT_LINKABLEfor any other. - A plugin of a role with a common part (
data,custody,operations,reporting) may move its pages ontomeridian.roles, and is held to the role's pages byrole-pagesunder--verifiedfrom CLI 0.1.39. synthetic, each plugin's own until now, may becomemeridian.sample_data()(sample_data), the ruled convention: a plugin chooses when, at its next versioned push.
0.21.0 to 0.22.0¶
meridian plugin migrate moves only the pins: open-meridian==0.22.0 and plugin-python:0.22.0. A Money naming its currency by its ISO 4217 code keeps its shape, and core resolves the code to the currency's cash instrument; a date sent as valid ISO text is sent as before. Left for you: a test comparing a Money read back with one it made, which now carries instrument_id (compare amount and currency_code), and a date sent as text that is no date, now refused. A plugin built on 0.22.0 needs a runtime of its release or later, and an older runtime refuses it at registration, naming both versions. See Money and instruments.
0.20.0 to 0.21.0¶
meridian plugin migrate moves only the pins: open-meridian==0.21.0 and plugin-python:0.21.0. Nothing a plugin calls changed, and a plugin keeping one retention, Storage(retention_days=...), keeps its records under it as before. A plugin adopting record kinds drops any setting of its own that held a window, and its release notes name that setting and the window setting it maps to, for the admin to set once at upgrade. A plugin built on 0.21.0 needs a runtime of its release or later, and an older runtime refuses it at registration, naming both versions.
0.19.0 to 0.20.0¶
meridian plugin migrate moves only the pins: open-meridian==0.20.0 and plugin-python:0.20.0. Nothing a plugin calls changed. A custody plugin that resolves activities by anything a person can change later, such as a plan code's link, adds re_resolve_activity; an operations plugin that reads activities reads re_resolutions beside them. One that does neither needs nothing more (see Report the custodian's activity). A plugin built on 0.20.0 needs a runtime of its release or later, and an older runtime refuses it at registration, naming both versions. 0.20.0 also brings access per role, which changes nothing for a plugin holding one role.
0.18.0 to 0.19.0¶
meridian plugin migrate moves only the pins: open-meridian==0.19.0 in pyproject.toml, and plugin-python:0.19.0 in the Dockerfile. Nothing a plugin calls changed. What 0.19.0 adds, a plugin adds by hand:
- A custody plugin sends
history_fromon its sync status and reports each activity withrecord_activity, its raw record kept, and maps the custody suite's new cases. - An operations plugin may read and hear activity and sync statuses, and link a cause to an activity. One that does none of this needs nothing more.
A plugin built on 0.19.0 needs a runtime of its release or later, and an older runtime refuses it at registration, naming both versions: upgrade the deployment first (Upgrade a deployment), then relaunch the plugin on the new version. A test that stands in for the SDK's private Operations._receive is now handed only the rows given a handler, not every row with None for the rest.