mercury-ocip-fast
mercury-ocip-fast is the throughput-focused counterpart to mercury-ocip. It leans on session pooling and async concurrency, so it holds up when a back-end has to push a lot of OCI-P traffic at once.
Reach for mercury-ocip when you are scripting or automating. Reach for mercury-ocip-fast when you need to run many requests and want stability under load. Both speak the same command definitions, so you can move command code between them without rewriting it.
Installation
pip install mercury-ocip-fast
Public API
The top-level package exports everything you need:
from mercury_ocip_fast import (
Client,
SessionClient,
SessionPair,
SessionPoolSettings,
SOAPSessionAtom,
SOAPSessionSettings,
TCPSessionAtom,
TCPSessionSettings,
)
Two entry points cover the two ways you are likely to work:
Client logs in once as a single admin identity, keeps a pool of live sessions, and hands you throughput. Use it when one identity drives all the work.SessionClient opens a session per user, lets you send commands over it, and lets you resume it later from a stored token. Use it when a service acts on behalf of many users.
You pick the transport with the atom_type argument: TCPSessionAtom for raw TCP, SOAPSessionAtom for SOAP over HTTP or HTTPS. There is no conn_type string.
Basic usage (TCP)
Client opens a pool of sessions and logs each one in as your user. Drive it inside an async with block:
from mercury_ocip_fast import Client, SessionPoolSettings, TCPSessionAtom, TCPSessionSettings
from mercury_ocip_fast.commands.commands import UserGetRequest23V2
async with Client(
host="your-broadworks.server",
username="admin",
password="your-password",
atom_type=TCPSessionAtom,
session_config=TCPSessionSettings(),
pool_config=SessionPoolSettings(),
) as client:
response = await client.command(
UserGetRequest23V2(user_id="user@domain.com"),
)
print(response.first_name)
A client needs an async setup step before you can use it, and the async with block runs that step for you. If you would rather manage the lifetime yourself, build the client with await Client.create(...) and call await client.close() when you are done.
host, username, password, atom_type, session_config, and pool_config are required. port and tls are optional, and tls defaults to True.
Basic usage (SOAP)
For SOAP, pass atom_type=SOAPSessionAtom and a SOAPSessionSettings, and give the full endpoint URL as the host (no ?wsdl suffix):
from mercury_ocip_fast import Client, SessionPoolSettings, SOAPSessionAtom, SOAPSessionSettings
from mercury_ocip_fast.commands.commands import UserGetRequest23V2
async with Client(
host="https://your-broadworks.server/webservice/services/ProvisioningService",
username="admin",
password="your-password",
atom_type=SOAPSessionAtom,
session_config=SOAPSessionSettings(),
pool_config=SessionPoolSettings(),
) as client:
response = await client.command(
UserGetRequest23V2(user_id="user@domain.com"),
)
print(response.first_name)
The command layer behaves the same over both transports. Single commands, batches, and error handling all work the same way, so your command code carries across TCP and SOAP unchanged.
Response types are inferred
Each request knows its own response class, so command() returns the matching type without any hint from you. Send a UserGetRequest23V2 and you get back a UserGetResponse23V2 (or ErrorResponse), typed and ready:
# Typed as UserGetResponse23V2 | ErrorResponse, inferred from the request:
response = await client.command(UserGetRequest23V2(user_id="user@domain.com"))
if isinstance(response, ErrorResponse):
raise exception
print(response.first_name)
Pass the response_type keyword only when you want to override that default and parse into a different class:
response = await client.command(request, response_type=SomeOtherResponse)
Most code never needs it.
Batch operations
Hand command() a list to send a batch:
from mercury_ocip_fast import Client, SessionPoolSettings, TCPSessionAtom, TCPSessionSettings
from mercury_ocip_fast.commands.commands import UserGetRequest23V2
async with Client(
host="your-broadworks.server",
username="admin",
password="your-password",
atom_type=TCPSessionAtom,
session_config=TCPSessionSettings(),
pool_config=SessionPoolSettings(),
) as client:
users = ["user1@domain.com", "user2@domain.com", "user3@domain.com"]
responses = await client.command(
[UserGetRequest23V2(user_id=user) for user in users],
)
for response in responses:
print(f"{response.user_id}: {response.first_name}")
A batch call takes one session from the pool and sends the commands over it in groups of 15, one group after the next, as the OCI-P spec requires. It never spreads a single call across multiple sessions, and the responses come back in the order you sent them.
To run work in parallel, fire off several command() calls together, for example with asyncio.gather. Each call grabs its own session, so you get concurrency up to the size of the pool.
Pool configuration
SessionPoolSettings controls the pool's size and its wait times:
from mercury_ocip_fast import Client, SessionPoolSettings, TCPSessionAtom, TCPSessionSettings
pool_config = SessionPoolSettings(
max_size=5, # How many sessions the pool holds.
acquire_timeout=10.0, # Seconds to wait to acquire a session.
wait_timeout=10.0, # Seconds to wait for a free session.
)
async with Client(
host="your-broadworks.server",
username="admin",
password="your-password",
atom_type=TCPSessionAtom,
session_config=TCPSessionSettings(),
pool_config=pool_config,
) as client:
...
max_size sets how many sessions the pool holds, which is also the ceiling on how many command() calls run at once, since each call takes one session. Raise it to send more in parallel, up to what your BroadWorks cluster can absorb. The values shown are the defaults.
Session configuration
Session settings hold the transport timeouts for each session. Use TCPSessionSettings for TCP and SOAPSessionSettings for SOAP.
TCPSessionSettings:
from mercury_ocip_fast import TCPSessionSettings
session_config = TCPSessionSettings(
connect_timeout=30, # Seconds to wait for the socket to open.
read_timeout=30, # Seconds to wait for a reply.
read_chunk_size=8192, # Bytes to read from the socket at a time.
max_ttl_seconds=900, # Session lifetime before it goes stale.
)
SOAPSessionSettings:
from mercury_ocip_fast import SOAPSessionSettings
session_config = SOAPSessionSettings(
connect_timeout=30.0, # Seconds to wait for the HTTP connection.
read_timeout=30.0, # Seconds to wait for the HTTP reply.
write_timeout=30.0, # Seconds to wait to send the request.
max_ttl_seconds=900, # Session lifetime before it goes stale.
)
Both take keyword arguments only, and the values above are the defaults.
How SOAP pooling works
BroadWorks ties an OCI-P login to the HTTP session, keyed on its JSESSIONID cookie, rather than to the session id in the request body. Sharing one SOAP client would share one login, so mercury-ocip-fast does not do that. The pool holds several sessions instead, and each one logs in on its own with its own httpx client, its own cookie jar, and its own session id. It is the SOAP form of the TCP session pool: one request at a time per session, many sessions running side by side.
Every session logs in once and then serves request after request, so you end up with a handful of authenticated sessions working in parallel rather than a single login funneling everything.
TLS and non-TLS
tls defaults to True, and the default TCP port is 2209.
# TLS on, TCP, default port 2209:
async with Client(
host="your-broadworks.server",
username="admin",
password="your-password",
atom_type=TCPSessionAtom,
session_config=TCPSessionSettings(),
pool_config=SessionPoolSettings(),
) as client:
...
For a plaintext TCP link, set tls=False. The port stays at 2209 unless you set it, so pass the plaintext port yourself (usually 2208):
# TLS off, TCP, plaintext port 2208:
async with Client(
host="your-broadworks.server",
port=2208,
username="admin",
password="your-password",
atom_type=TCPSessionAtom,
session_config=TCPSessionSettings(),
pool_config=SessionPoolSettings(),
tls=False,
) as client:
...
The tls flag also picks the login flow:
- TLS on: the client sends the plain-text login. The link is encrypted, so the password is safe to send as-is.
- TLS off: the client sends the encrypted login. The password is hashed, so it never crosses the wire in clear text.
For TCP, tls also turns the socket TLS on or off and controls certificate checks. For SOAP, HTTP versus HTTPS comes from the host URL you pass, and tls selects the login flow and controls the httpx certificate check.
Response handling
A command that fails on the server does not raise. The server sends back an ErrorResponse, and command() returns it like any other response. Each request's response type is the union Response | ErrorResponse, so you check which one you got with isinstance:
from mercury_ocip_fast.commands.commands import ErrorResponse
response = await client.command(some_command)
if isinstance(response, ErrorResponse):
print(f"The server returned an error: {response.summary}")
else:
print(response.user_id)
ErrorResponse carries error_code, summary, summary_english, and detail, so you can read the code and message straight off the object.
Batch responses stay in the order you sent them, and a failed command shows up as an ErrorResponse in its own slot. One bad command does not sink the rest of the batch, so check each response on its own:
commands = [cmd1, cmd2, cmd3]
responses = await client.command(commands)
for command, response in zip(commands, responses):
if isinstance(response, ErrorResponse):
print(f"{command} failed: {response.summary}")
else:
... # Handle the successful response.
Login is the exception that does raise. If a session cannot authenticate, the client raises MErrorLogin while it opens the session, before your command runs:
from mercury_ocip_fast.exceptions import MErrorLogin
try:
async with Client(...) as client:
...
except MErrorLogin as error:
print(f"Login failed: {error.message}")
MErrorLogin subclasses MError, the library's base exception, and its message field holds the reason the login was rejected.
SessionClient: sessions per user
SessionClient takes a different shape from Client. It carries no identity of its own and keeps no pool. You open a session for a given user, send commands over it, and close it when you are done. You can also export a session and resume it later, which suits a service that acts for many users.
SessionClient is SOAP only, because only a SOAP session can resume a login. atom_type must be SOAPSessionAtom.
from mercury_ocip_fast import SessionClient, SOAPSessionAtom, SOAPSessionSettings
from mercury_ocip_fast.commands.commands import UserGetRequest23V2
async with SessionClient(
host="https://your-broadworks.server/webservice/services/ProvisioningService",
atom_type=SOAPSessionAtom,
session_config=SOAPSessionSettings(),
) as client:
# Open a session, logged in as the user.
session = await client.open("user_admin", "user_password")
try:
response = await client.command(
session,
UserGetRequest23V2(user_id="user@domain.com"),
)
print(response.first_name)
finally:
# The session is yours. Close it when the work is done.
await client.close(session)
SessionClient takes no username or password, since you supply credentials to open() per user, and no pool_config, since it holds no pool.
client.command(session, request, ...) takes the session as its first argument and otherwise mirrors Client.command: single command or batch, batches split into groups of 15 and sent over the one session in order, and the same optional response_type override.
Export and resume a session
Every session exposes a pair property, a SessionPair value holding the JSESSIONID cookie and the OCI-P session id. Store the pair and you can resume the session later with no fresh login. Treat it as a secret: anyone holding the pair can send commands as that user.
# Export the identity of an open session.
pair = session.pair
# ... store the pair, for example between requests or across a restart ...
# Resume the session later, with no new login.
resumed = await client.resume(pair)
try:
response = await client.command(
resumed,
UserGetRequest23V2(user_id="user@domain.com"),
)
finally:
await client.close(resumed)
Reading session.pair before the session has logged in raises MErrorMissingSessionIdentity.
To keep a SessionClient outside an async with block, build it with await SessionClient.create(...). The client owns no sessions, so it has nothing of its own to close, but you still close every session you open.
Use cases
Pick mercury-ocip for:
- Scripts and automation
- Interactive CLI tools
- General-purpose work
Pick mercury-ocip-fast for:
- Backend APIs and services
- Bulk data migrations
- High-volume reporting
- Production workloads that need stability and throughput
Both share the same OCI-P command definitions, so command code moves between them.
This library can generate a lot of traffic fast, and a BroadWorks cluster that is not sized for it will feel the strain. A few habits keep that in check:
- Start with a small
max_size. - Watch the cluster while a batch runs.
- Add rate limits where you need them.
API Reference
See the Commands Reference for the OCI-P commands.
Clients
Client
An OCI-P Client for a single User.
The client opens sessions, it logs them in as the specified user, and sends commands over them.
The client needs an async setup step before use. Make the client with the create method, or use it in an async with block.
Attributes:
| Name | Type | Description |
|---|
host | str | The host name or address of the BroadWorks server. |
port | int | None | The port of the server. If None, the endpoint or the scheme sets the port. |
username | str | The user name for the login. |
password | str | The password for the login. Treat this value as a secret. |
atom_type | type[SessionAtom[S]] | The class of session to open, for example the SOAP atom or the TCP atom. |
session_config | S | The transport settings for each session, for example the timeouts. |
pool_config | SessionPoolSettings | The settings for the session pool, for example the maximum size. |
user_agent | SessionPoolSettings | The user agent name for the client. |
tls | bool | If true, use a TLS link. A TLS link protects the password, so the client uses the plain-text login. If false, the client uses the encrypted login. |
Source code in src/mercury_ocip_fast/client.py
| @attrs.define(kw_only=True)
class Client[S: (TCPSessionSettings, SOAPSessionSettings)]:
"""An OCI-P Client for a single User.
The client opens sessions, it logs them in as the specified user,
and sends commands over them.
The client needs an async setup step before use. Make the client
with the ``create`` method, or use it in an ``async with`` block.
Attributes:
host: The host name or address of the BroadWorks server.
port: The port of the server. If None, the endpoint or the
scheme sets the port.
username: The user name for the login.
password: The password for the login. Treat this value as a
secret.
atom_type: The class of session to open, for example the SOAP
atom or the TCP atom.
session_config: The transport settings for each session, for
example the timeouts.
pool_config: The settings for the session pool, for example the
maximum size.
user_agent: The user agent name for the client.
tls: If true, use a TLS link. A TLS link protects the password,
so the client uses the plain-text login. If false, the
client uses the encrypted login.
"""
host: str
port: int | None = None
username: str
password: str
atom_type: type[SessionAtom[S]]
session_config: S = attrs.field()
pool_config: SessionPoolSettings = attrs.field()
tls: bool = True
logger: logging.Logger = attrs.field(default=logging.getLogger(__name__))
_requester: Requester = attrs.field(init=False)
_pool: SessionPool[SessionAtom[S]] = attrs.field(init=False)
_authenticator: Authenticator = attrs.field(init=False)
async def _async_setup(self) -> Self:
"""Setup the client and its dependencies.
Returns:
The client
"""
if getattr(self, "_pool", None) is not None:
return self
self.logger.info(
f"Initializing requester for {self.host}:{self.port} (tls={self.tls})"
)
self._requester = Requester()
self._authenticator = Authenticator(
username=self.username, password=self.password, requester=self._requester
)
self._pool = SessionPool(
default_factory=self._default_factory,
pool_settings=self.pool_config,
)
return self
async def _default_factory(self) -> SessionAtom[S]:
"""Create a SessionAtom and log it in based on the client's TLS value."""
atom = await self.atom_type.open(
self.host,
self.port,
settings=self.session_config,
verify_ssl=self.tls,
)
try:
if (
self.tls
): # TLS protects the password, so a TLS link uses the plain-text login.
await self._authenticator.generic_login(atom)
else:
await self._authenticator.encrypted_login(atom)
except BaseException:
await atom.close()
raise
return atom
async def __aenter__(self) -> Self:
"""Do the async setup at the start of an ``async with`` block.
Returns:
The client, now ready for use.
"""
return await self._async_setup()
async def __aexit__(
self, _exc_type: object, _exc_val: object, _exc_tb: object
) -> None:
"""Close the client at the end of an ``async with`` block.
This method closes the client for each exit. It closes the
client after an error, and it closes the client after a normal
exit.
"""
await self.close()
@classmethod
async def create(cls, **kwargs) -> Self:
"""Make a client and do the async setup.
Use this method to get a client without an ``async with`` block.
Keep the client, use it, and then close it with the ``close``
method.
Args:
kwargs: The keyword arguments for the client, for example
the host, the user name, and the password.
Returns:
A client that is ready for use.
"""
self = cls(**kwargs)
return await self._async_setup()
@overload
async def command[R: OCIResponse](self, request: OCIRequest[R]) -> R: ...
@overload
async def command[R: OCIResponse](
self, request: list[OCIRequest[R]]
) -> list[R]: ...
@overload
async def command[R: OCIResponse](
self, request: OCIRequest, *, response_type: type[R]
) -> R: ...
@overload
async def command[R: OCIResponse](
self, request: list[OCIRequest], *, response_type: type[R] | list[R]
) -> list[R]: ...
async def command[R: OCIResponse](
self,
request: OCIRequest[R] | list[OCIRequest[R]],
*,
response_type: type[R] | None = None,
) -> R | list[R]:
"""Send one command, or a batch, and return the parsed response(s).
The result is typed as the request's own response class, resolved
from ``OCIRequest[R]``. Pass ``response_type`` only to override that
with an explicit class.
Args:
request: One OCI request, or a list of requests for a batch.
response_type: The class to parse each response into. If None,
each request's ``_response_cls`` is used.
Returns:
The parsed response, or a list of responses for a batch.
"""
all_results: list[R] = []
async with self._pool.session() as atom:
if isinstance(request, OCIRequest):
return await self._requester.send(
payload=request.to_xml(),
response_type=response_type or request._response_cls,
session=atom,
)
for batch in batched(request, 15):
result = await self._requester.send(
payload=[b.to_xml() for b in batch],
response_type=response_type or [b._response_cls for b in batch],
session=atom,
)
all_results.extend(result)
return all_results
async def close(self) -> None:
"""Close the client and every session in the pool.
This method closes the session pool. The pool closes each
session and lets go of each transport. The method is safe to
call more than once.
"""
pool = getattr(self, "_pool", None) # Survives half constructed client
if pool is not None:
await pool.close()
|
__aenter__() async
Do the async setup at the start of an async with block.
Returns:
| Type | Description |
|---|
Self | The client, now ready for use. |
Source code in src/mercury_ocip_fast/client.py
| async def __aenter__(self) -> Self:
"""Do the async setup at the start of an ``async with`` block.
Returns:
The client, now ready for use.
"""
return await self._async_setup()
|
__aexit__(_exc_type, _exc_val, _exc_tb) async
Close the client at the end of an async with block.
This method closes the client for each exit. It closes the client after an error, and it closes the client after a normal exit.Source code in src/mercury_ocip_fast/client.py
| async def __aexit__(
self, _exc_type: object, _exc_val: object, _exc_tb: object
) -> None:
"""Close the client at the end of an ``async with`` block.
This method closes the client for each exit. It closes the
client after an error, and it closes the client after a normal
exit.
"""
await self.close()
|
close() async
Close the client and every session in the pool.
This method closes the session pool. The pool closes each session and lets go of each transport. The method is safe to call more than once.Source code in src/mercury_ocip_fast/client.py
| async def close(self) -> None:
"""Close the client and every session in the pool.
This method closes the session pool. The pool closes each
session and lets go of each transport. The method is safe to
call more than once.
"""
pool = getattr(self, "_pool", None) # Survives half constructed client
if pool is not None:
await pool.close()
|
command(request, *, response_type=None) async
command(request: OCIRequest[R]) -> R
command(request: list[OCIRequest[R]]) -> list[R]
command(request: OCIRequest, *, response_type: type[R]) -> R
command(request: list[OCIRequest], *, response_type: type[R] | list[R]) -> list[R]
Send one command, or a batch, and return the parsed response(s).
The result is typed as the request's own response class, resolved from OCIRequest[R]. Pass response_type only to override that with an explicit class.
Parameters:
| Name | Type | Description | Default |
|---|
request | OCIRequest[R] | list[OCIRequest[R]] | One OCI request, or a list of requests for a batch. | required |
response_type | type[R] | None | The class to parse each response into. If None, each request's _response_cls is used. | None |
Returns:
| Type | Description |
|---|
R | list[R] | The parsed response, or a list of responses for a batch. |
Source code in src/mercury_ocip_fast/client.py
| async def command[R: OCIResponse](
self,
request: OCIRequest[R] | list[OCIRequest[R]],
*,
response_type: type[R] | None = None,
) -> R | list[R]:
"""Send one command, or a batch, and return the parsed response(s).
The result is typed as the request's own response class, resolved
from ``OCIRequest[R]``. Pass ``response_type`` only to override that
with an explicit class.
Args:
request: One OCI request, or a list of requests for a batch.
response_type: The class to parse each response into. If None,
each request's ``_response_cls`` is used.
Returns:
The parsed response, or a list of responses for a batch.
"""
all_results: list[R] = []
async with self._pool.session() as atom:
if isinstance(request, OCIRequest):
return await self._requester.send(
payload=request.to_xml(),
response_type=response_type or request._response_cls,
session=atom,
)
for batch in batched(request, 15):
result = await self._requester.send(
payload=[b.to_xml() for b in batch],
response_type=response_type or [b._response_cls for b in batch],
session=atom,
)
all_results.extend(result)
return all_results
|
create(**kwargs) async classmethod
Make a client and do the async setup.
Use this method to get a client without an async with block. Keep the client, use it, and then close it with the close method.
Parameters:
| Name | Type | Description | Default |
|---|
kwargs | | The keyword arguments for the client, for example the host, the user name, and the password. | {} |
Returns:
| Type | Description |
|---|
Self | A client that is ready for use. |
Source code in src/mercury_ocip_fast/client.py
| @classmethod
async def create(cls, **kwargs) -> Self:
"""Make a client and do the async setup.
Use this method to get a client without an ``async with`` block.
Keep the client, use it, and then close it with the ``close``
method.
Args:
kwargs: The keyword arguments for the client, for example
the host, the user name, and the password.
Returns:
A client that is ready for use.
"""
self = cls(**kwargs)
return await self._async_setup()
|
SessionClient
OCI-P client.
A multi-tenant client. It holds no identity and keeps no session list. The client shares one requester across all sessions; each opened session is owned by its caller and may be passed around, closed, or exported and later resumed.
Set up the client with create() or an async with block.
Attributes:
| Name | Type | Description |
|---|
host | str | BroadWorks server hostname or address. |
port | int | None | BroadWorks server port. If None, the endpoint or scheme supplies it. |
atom_type | type[SoapAtom] | Resumable session class to open, such as the SOAP atom. |
session_config | SOAPSessionSettings | Transport settings for each session. |
tls | bool | Whether to use TLS. If true, the client uses plain-text login; if false, it uses the encrypted login. |
Source code in src/mercury_ocip_fast/session_client.py
| @attrs.define(kw_only=True)
class SessionClient:
"""OCI-P client.
A multi-tenant client. It holds no identity and keeps no session list.
The client shares one requester across all sessions; each opened session is
owned by its caller and may be passed around, closed, or exported and later
resumed.
Set up the client with ``create()`` or an ``async with`` block.
Attributes:
host: BroadWorks server hostname or address.
port: BroadWorks server port. If ``None``, the endpoint or scheme
supplies it.
atom_type: Resumable session class to open, such as the SOAP atom.
session_config: Transport settings for each session.
tls: Whether to use TLS. If true, the client uses plain-text login; if
false, it uses the encrypted login.
"""
host: str
port: int | None = None
atom_type: type[SoapAtom]
session_config: SOAPSessionSettings = attrs.field()
tls: bool = True
logger: logging.Logger = attrs.field(default=logging.getLogger(__name__))
_requester: Requester = attrs.field(init=False)
async def _async_setup(self) -> Self:
"""Set up the shared requester. Idempotent.
Returns:
The client.
"""
if getattr(self, "_requester", None) is not None:
return self
self.logger.info(
f"Initializing session client for {self.host}:{self.port} (tls={self.tls})"
)
self._requester = Requester()
return self
async def _login_factory(self, username: str, password: str) -> SoapAtom:
"""Open a session and log it in as the given user.
Each caller brings its own credentials, so the authenticator is
built per login rather than held on the client.
"""
atom = await self.atom_type.open(
self.host,
self.port,
settings=self.session_config,
verify_ssl=self.tls,
)
authenticator = Authenticator(
username=username, password=password, requester=self._requester
)
try:
if (
self.tls
): # TLS protects the password, so a TLS link uses the plain-text login.
await authenticator.generic_login(atom)
else:
await authenticator.encrypted_login(atom)
except BaseException:
await atom.close()
raise
return atom
async def _resume_factory(self, pair: SessionPair) -> SoapAtom:
"""Open a session from a stored pair, without a fresh login."""
return await self.atom_type.resume(
self.host,
pair,
settings=self.session_config,
verify_ssl=self.tls,
)
async def __aenter__(self) -> Self:
"""Do the async setup at the start of an ``async with`` block.
Returns:
The client, now ready for use.
"""
return await self._async_setup()
async def __aexit__(
self, _exc_type: object, _exc_val: object, _exc_tb: object
) -> None:
"""Leave the ``async with`` block.
The client owns no sessions, so it has nothing to close here. The
caller must close each session it opened.
"""
@classmethod
async def create(cls, **kwargs) -> Self:
"""Make a client and do the async setup, without an ``async with``.
Args:
kwargs: The keyword arguments for the client, for example the
host and the atom type.
Returns:
A client that is ready for use.
"""
self = cls(**kwargs)
return await self._async_setup()
async def open(self, username: str, password: str) -> SoapAtom:
"""Open a fresh session, logged in as ``username``.
The returned atom is the caller's handle. Pass it to ``command`` and
close it with ``close`` when done.
Args:
username: The user name for the login.
password: The password for the login. Treat this as a secret.
Returns:
A logged-in session, owned by the caller.
Raises:
MErrorLogin: If the server rejects the login.
"""
return await self._login_factory(username, password)
async def resume(self, pair: SessionPair) -> SoapAtom:
"""Open a session from a stored pair, resuming an earlier login.
Use the atom's ``pair`` property to export the identity for later.
Args:
pair: The stored identity from an earlier session.
Returns:
A resumed session, owned by the caller.
"""
return await self._resume_factory(pair)
@overload
async def command[R: OCIResponse](
self, session: SoapAtom, request: OCIRequest[R]
) -> R: ...
@overload
async def command[R: OCIResponse](
self, session: SoapAtom, request: list[OCIRequest[R]]
) -> list[R]: ...
@overload
async def command[R: OCIResponse](
self, session: SoapAtom, request: OCIRequest, *, response_type: type[R]
) -> R: ...
@overload
async def command[R: OCIResponse](
self, session: SoapAtom, request: list[OCIRequest], *, response_type: type[R]
) -> list[R]: ...
async def command[R: OCIResponse](
self,
session: SoapAtom,
request: OCIRequest[R] | list[OCIRequest[R]],
*,
response_type: type[R] | None = None,
) -> R | list[R]:
"""Send one command, or a batch, over the caller's session.
The result is typed as the request's own response class, resolved
from ``OCIRequest[R]``. Pass ``response_type`` only to override that
with an explicit class.
Args:
session: The caller's session, from ``open`` or ``resume``.
request: One OCI request, or a list of requests for a batch.
response_type: The class to parse each response into. If None,
each request's ``_response_cls`` is used.
Returns:
The parsed response, or a list of responses for a batch.
"""
all_results: list[R] = []
if isinstance(request, OCIRequest):
return await self._requester.send(
payload=request.to_xml(),
response_type=response_type or request._response_cls,
session=session,
)
for batch in batched(request, 15):
result = await self._requester.send(
payload=[b.to_xml() for b in batch],
response_type=response_type or [b._response_cls for b in batch],
session=session,
)
all_results.extend(result)
return all_results
async def close(self, session: SoapAtom) -> None:
"""Close one of the caller's sessions and let go of its transport.
Args:
session: The session to close.
"""
await session.close()
|
__aenter__() async
Do the async setup at the start of an async with block.
Returns:
| Type | Description |
|---|
Self | The client, now ready for use. |
Source code in src/mercury_ocip_fast/session_client.py
| async def __aenter__(self) -> Self:
"""Do the async setup at the start of an ``async with`` block.
Returns:
The client, now ready for use.
"""
return await self._async_setup()
|
__aexit__(_exc_type, _exc_val, _exc_tb) async
Leave the async with block.
The client owns no sessions, so it has nothing to close here. The caller must close each session it opened.Source code in src/mercury_ocip_fast/session_client.py
| async def __aexit__(
self, _exc_type: object, _exc_val: object, _exc_tb: object
) -> None:
"""Leave the ``async with`` block.
The client owns no sessions, so it has nothing to close here. The
caller must close each session it opened.
"""
|
close(session) async
Close one of the caller's sessions and let go of its transport.
Parameters:
| Name | Type | Description | Default |
|---|
session | SoapAtom | | required |
Source code in src/mercury_ocip_fast/session_client.py
| async def close(self, session: SoapAtom) -> None:
"""Close one of the caller's sessions and let go of its transport.
Args:
session: The session to close.
"""
await session.close()
|
command(session, request, *, response_type=None) async
command(session: SoapAtom, request: OCIRequest[R]) -> R
command(session: SoapAtom, request: list[OCIRequest[R]]) -> list[R]
command(session: SoapAtom, request: OCIRequest, *, response_type: type[R]) -> R
command(session: SoapAtom, request: list[OCIRequest], *, response_type: type[R]) -> list[R]
Send one command, or a batch, over the caller's session.
The result is typed as the request's own response class, resolved from OCIRequest[R]. Pass response_type only to override that with an explicit class.
Parameters:
| Name | Type | Description | Default |
|---|
session | SoapAtom | The caller's session, from open or resume. | required |
request | OCIRequest[R] | list[OCIRequest[R]] | One OCI request, or a list of requests for a batch. | required |
response_type | type[R] | None | The class to parse each response into. If None, each request's _response_cls is used. | None |
Returns:
| Type | Description |
|---|
R | list[R] | The parsed response, or a list of responses for a batch. |
Source code in src/mercury_ocip_fast/session_client.py
| async def command[R: OCIResponse](
self,
session: SoapAtom,
request: OCIRequest[R] | list[OCIRequest[R]],
*,
response_type: type[R] | None = None,
) -> R | list[R]:
"""Send one command, or a batch, over the caller's session.
The result is typed as the request's own response class, resolved
from ``OCIRequest[R]``. Pass ``response_type`` only to override that
with an explicit class.
Args:
session: The caller's session, from ``open`` or ``resume``.
request: One OCI request, or a list of requests for a batch.
response_type: The class to parse each response into. If None,
each request's ``_response_cls`` is used.
Returns:
The parsed response, or a list of responses for a batch.
"""
all_results: list[R] = []
if isinstance(request, OCIRequest):
return await self._requester.send(
payload=request.to_xml(),
response_type=response_type or request._response_cls,
session=session,
)
for batch in batched(request, 15):
result = await self._requester.send(
payload=[b.to_xml() for b in batch],
response_type=response_type or [b._response_cls for b in batch],
session=session,
)
all_results.extend(result)
return all_results
|
create(**kwargs) async classmethod
Make a client and do the async setup, without an async with.
Parameters:
| Name | Type | Description | Default |
|---|
kwargs | | The keyword arguments for the client, for example the host and the atom type. | {} |
Returns:
| Type | Description |
|---|
Self | A client that is ready for use. |
Source code in src/mercury_ocip_fast/session_client.py
| @classmethod
async def create(cls, **kwargs) -> Self:
"""Make a client and do the async setup, without an ``async with``.
Args:
kwargs: The keyword arguments for the client, for example the
host and the atom type.
Returns:
A client that is ready for use.
"""
self = cls(**kwargs)
return await self._async_setup()
|
open(username, password) async
Open a fresh session, logged in as username.
The returned atom is the caller's handle. Pass it to command and close it with close when done.
Parameters:
| Name | Type | Description | Default |
|---|
username | str | The user name for the login. | required |
password | str | The password for the login. Treat this as a secret. | required |
Returns:
| Type | Description |
|---|
SoapAtom | A logged-in session, owned by the caller. |
Raises:
| Type | Description |
|---|
MErrorLogin | If the server rejects the login. |
Source code in src/mercury_ocip_fast/session_client.py
| async def open(self, username: str, password: str) -> SoapAtom:
"""Open a fresh session, logged in as ``username``.
The returned atom is the caller's handle. Pass it to ``command`` and
close it with ``close`` when done.
Args:
username: The user name for the login.
password: The password for the login. Treat this as a secret.
Returns:
A logged-in session, owned by the caller.
Raises:
MErrorLogin: If the server rejects the login.
"""
return await self._login_factory(username, password)
|
resume(pair) async
Open a session from a stored pair, resuming an earlier login.
Use the atom's pair property to export the identity for later.
Parameters:
| Name | Type | Description | Default |
|---|
pair | SessionPair | The stored identity from an earlier session. | required |
Returns:
| Type | Description |
|---|
SoapAtom | A resumed session, owned by the caller. |
Source code in src/mercury_ocip_fast/session_client.py
| async def resume(self, pair: SessionPair) -> SoapAtom:
"""Open a session from a stored pair, resuming an earlier login.
Use the atom's ``pair`` property to export the identity for later.
Args:
pair: The stored identity from an earlier session.
Returns:
A resumed session, owned by the caller.
"""
return await self._resume_factory(pair)
|
Sessions
SOAPSessionAtom
A SOAP session for BroadWorks.
The transport is an httpx client. The client has its own cookie jar. The cookie jar holds the JSESSIONID cookie after a login.
The session also has an OCI-P session_id. This id goes in the body of each message. The class makes a new UUID for the id by default.
Attributes:
| Name | Type | Description |
|---|
endpoint | str | The URL of the SOAP service. |
http_client | AsyncClient | The httpx client. It holds this session's cookie jar. |
settings | SOAPSessionSettings | The timeouts for the httpx client. |
session_id | str | The OCI-P session id for each message body. |
Source code in src/mercury_ocip_fast/session/soap_session.py
| @attrs.define(kw_only=True, slots=True)
class SOAPSessionAtom:
"""A SOAP session for BroadWorks.
The transport is an httpx client. The client has its own cookie jar.
The cookie jar holds the JSESSIONID cookie after a login.
The session also has an OCI-P ``session_id``. This id goes in the body
of each message. The class makes a new UUID for the id by default.
Attributes:
endpoint: The URL of the SOAP service.
http_client: The httpx client. It holds this session's cookie jar.
settings: The timeouts for the httpx client.
session_id: The OCI-P session id for each message body.
"""
endpoint: str
http_client: httpx.AsyncClient
settings: SOAPSessionSettings = attrs.field(default=SOAPSessionSettings())
session_id: str = attrs.field(factory=lambda: str(uuid.uuid4()))
created_at: float = attrs.field(factory=time.monotonic)
last_used: float = attrs.field(factory=time.monotonic)
@classmethod
async def open(
cls,
endpoint: str,
port: int | None = None,
*,
settings: SOAPSessionSettings,
verify_ssl: bool = True,
) -> SOAPSessionAtom:
"""Make a new SOAP session. The session is not logged in.
The new session has its own httpx client. The client has no
cookies. A login must run before the session can send commands.
Args:
endpoint: The full URL of the SOAP service. The URL can hold the
port, for example ``https://host:8443/webservice``.
port: The port of the service. This argument has priority over a
port in the URL. If None, the URL, or the scheme, sets the
port.
settings: The timeouts for the httpx client.
verify_ssl: If true, verify the TLS certificate of the server.
Returns:
A new SOAP session.
Raises:
MErrorHttpInitialisation: If the URL has no host.
"""
try:
url = override_url_port(endpoint, port)
except ValueError as e:
raise MErrorHttpInitialisation(str(e)) from e
timeout = httpx.Timeout(
connect=settings.connect_timeout,
read=settings.read_timeout,
write=settings.write_timeout,
pool=None,
)
logger.debug(
"Open a SOAP session to %s, connect timeout %ss, read timeout %ss",
url,
settings.connect_timeout,
settings.read_timeout,
)
return cls(
endpoint=url,
http_client=httpx.AsyncClient(
verify=verify_ssl,
timeout=timeout,
),
settings=settings,
)
@classmethod
async def resume(
cls,
endpoint: str,
pair: SessionPair,
*,
settings: SOAPSessionSettings,
verify_ssl: bool = True,
) -> SOAPSessionAtom:
"""Make a SOAP session from a stored session pair.
Use this method to continue a session after a restart. The new
session takes the JSESSIONID cookie and the OCI-P session id from
the pair. The server then accepts the session as logged in.
Args:
endpoint: The URL of the SOAP service.
pair: The stored identity. It holds the cookie and the id.
settings: The timeouts for the httpx client.
verify_ssl: If true, verify the TLS certificate of the server.
Returns:
A SOAP session that uses the given pair.
"""
session = await cls.open(endpoint, settings=settings, verify_ssl=verify_ssl)
session.http_client.cookies.set("JSESSIONID", pair.jsessionid)
session.session_id = pair.session_id
logger.debug("Resume a SOAP session to %s from a stored pair", endpoint)
return session
@property
def jsessionid(self) -> str | None:
"""The JSESSIONID cookie of this session.
The value is None before the login.
Do not write this value to a log. It is a secret. A person who has
this value can send commands as the user.
"""
return self.http_client.cookies.get("JSESSIONID")
@property
def pair(self) -> SessionPair:
"""The session pair of this session. Store it to resume later.
Raises:
MErrorMissingSessionIdentity: If the session has no login yet.
"""
jsessionid = self.jsessionid
if not jsessionid:
raise MErrorMissingSessionIdentity()
return SessionPair(jsessionid=jsessionid, session_id=self.session_id)
async def send(self, payload: str | list[str]) -> str:
"""Send one envelope to the server and return one reply.
This method wraps the payload in the BroadsoftDocument and then in
the SOAP envelope. It posts the envelope with the httpx client. The
JSESSIONID cookie goes with the request. The method then gets the
OCI reply from the response.
Do not write the payload or the reply to a log. They can hold
secrets, for example a password in a login command.
Args:
payload: One OCI command, or a list of OCI commands.
Returns:
The OCI reply as a string.
Raises:
MErrorHttpStatus: If the server returns a non-2xx HTTP status.
MErrorHttpTimeout: If the request does not complete in time.
MErrorHttpInitialisation: If the client cannot connect.
MErrorHttpDropped: If the connection stops during the request.
"""
oci_xml = build_broadsoft_envelope(payload, self.session_id)
soap_envelope = wrap_soap(oci_xml)
logger.debug("Send %d bytes to %s", len(soap_envelope), self.endpoint)
try:
response = await self.http_client.post(
self.endpoint,
content=soap_envelope.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=UTF-8", "SOAPAction": ""},
)
response.raise_for_status()
except httpx.HTTPStatusError as e:
logger.warning(
"The server %s returned HTTP %d", self.endpoint, e.response.status_code
)
raise MErrorHttpStatus(
f"BroadWorks returned HTTP {e.response.status_code}",
status=e.response.status_code,
) from e
except httpx.TimeoutException as e:
logger.warning("The request to %s timed out: %s", self.endpoint, e)
raise MErrorHttpTimeout(str(e)) from e
except httpx.ConnectError as e:
logger.warning("Cannot connect to %s: %s", self.endpoint, e)
raise MErrorHttpInitialisation(str(e)) from e
except httpx.TransportError as e: # a read, write, or protocol error
logger.warning(
"The connection to %s dropped mid-request: %s", self.endpoint, e
)
raise MErrorHttpDropped(str(e)) from e
logger.debug(
"Receive %d bytes from %s, HTTP %d",
len(response.text),
self.endpoint,
response.status_code,
)
return unwrap_soap(response.text)
async def close(self) -> None:
"""Close the httpx client.
This closes the cookie jar and the open sockets. The method is safe
to call more than once.
"""
logger.debug("Close the SOAP session to %s", self.endpoint)
try:
await self.http_client.aclose()
except Exception as e: # noqa: BLE001
logger.warning(
"The connection to %s raised an exception while closing: %s",
self.endpoint,
e,
)
def is_alive(self) -> bool:
"""Tell if the session is still connected."""
return not self.http_client.is_closed
def is_stale(self) -> bool:
"""Tell if the session is past its time to live."""
return (time.monotonic() - self.created_at) > self.settings.max_ttl_seconds
|
jsessionid property
The JSESSIONID cookie of this session.
The value is None before the login.
Do not write this value to a log. It is a secret. A person who has this value can send commands as the user.
pair property
The session pair of this session. Store it to resume later.
Raises:
| Type | Description |
|---|
MErrorMissingSessionIdentity | If the session has no login yet. |
close() async
Close the httpx client.
This closes the cookie jar and the open sockets. The method is safe to call more than once.Source code in src/mercury_ocip_fast/session/soap_session.py
| async def close(self) -> None:
"""Close the httpx client.
This closes the cookie jar and the open sockets. The method is safe
to call more than once.
"""
logger.debug("Close the SOAP session to %s", self.endpoint)
try:
await self.http_client.aclose()
except Exception as e: # noqa: BLE001
logger.warning(
"The connection to %s raised an exception while closing: %s",
self.endpoint,
e,
)
|
is_alive()
Tell if the session is still connected.Source code in src/mercury_ocip_fast/session/soap_session.py
| def is_alive(self) -> bool:
"""Tell if the session is still connected."""
return not self.http_client.is_closed
|
is_stale()
Tell if the session is past its time to live.Source code in src/mercury_ocip_fast/session/soap_session.py
| def is_stale(self) -> bool:
"""Tell if the session is past its time to live."""
return (time.monotonic() - self.created_at) > self.settings.max_ttl_seconds
|
open(endpoint, port=None, *, settings, verify_ssl=True) async classmethod
Make a new SOAP session. The session is not logged in.
The new session has its own httpx client. The client has no cookies. A login must run before the session can send commands.
Parameters:
| Name | Type | Description | Default |
|---|
endpoint | str | The full URL of the SOAP service. The URL can hold the port, for example https://host:8443/webservice. | required |
port | int | None | The port of the service. This argument has priority over a port in the URL. If None, the URL, or the scheme, sets the port. | None |
settings | SOAPSessionSettings | The timeouts for the httpx client. | required |
verify_ssl | bool | If true, verify the TLS certificate of the server. | True |
Returns:
Raises:
| Type | Description |
|---|
MErrorHttpInitialisation | |
Source code in src/mercury_ocip_fast/session/soap_session.py
| @classmethod
async def open(
cls,
endpoint: str,
port: int | None = None,
*,
settings: SOAPSessionSettings,
verify_ssl: bool = True,
) -> SOAPSessionAtom:
"""Make a new SOAP session. The session is not logged in.
The new session has its own httpx client. The client has no
cookies. A login must run before the session can send commands.
Args:
endpoint: The full URL of the SOAP service. The URL can hold the
port, for example ``https://host:8443/webservice``.
port: The port of the service. This argument has priority over a
port in the URL. If None, the URL, or the scheme, sets the
port.
settings: The timeouts for the httpx client.
verify_ssl: If true, verify the TLS certificate of the server.
Returns:
A new SOAP session.
Raises:
MErrorHttpInitialisation: If the URL has no host.
"""
try:
url = override_url_port(endpoint, port)
except ValueError as e:
raise MErrorHttpInitialisation(str(e)) from e
timeout = httpx.Timeout(
connect=settings.connect_timeout,
read=settings.read_timeout,
write=settings.write_timeout,
pool=None,
)
logger.debug(
"Open a SOAP session to %s, connect timeout %ss, read timeout %ss",
url,
settings.connect_timeout,
settings.read_timeout,
)
return cls(
endpoint=url,
http_client=httpx.AsyncClient(
verify=verify_ssl,
timeout=timeout,
),
settings=settings,
)
|
resume(endpoint, pair, *, settings, verify_ssl=True) async classmethod
Make a SOAP session from a stored session pair.
Use this method to continue a session after a restart. The new session takes the JSESSIONID cookie and the OCI-P session id from the pair. The server then accepts the session as logged in.
Parameters:
| Name | Type | Description | Default |
|---|
endpoint | str | The URL of the SOAP service. | required |
pair | SessionPair | The stored identity. It holds the cookie and the id. | required |
settings | SOAPSessionSettings | The timeouts for the httpx client. | required |
verify_ssl | bool | If true, verify the TLS certificate of the server. | True |
Returns:
Source code in src/mercury_ocip_fast/session/soap_session.py
| @classmethod
async def resume(
cls,
endpoint: str,
pair: SessionPair,
*,
settings: SOAPSessionSettings,
verify_ssl: bool = True,
) -> SOAPSessionAtom:
"""Make a SOAP session from a stored session pair.
Use this method to continue a session after a restart. The new
session takes the JSESSIONID cookie and the OCI-P session id from
the pair. The server then accepts the session as logged in.
Args:
endpoint: The URL of the SOAP service.
pair: The stored identity. It holds the cookie and the id.
settings: The timeouts for the httpx client.
verify_ssl: If true, verify the TLS certificate of the server.
Returns:
A SOAP session that uses the given pair.
"""
session = await cls.open(endpoint, settings=settings, verify_ssl=verify_ssl)
session.http_client.cookies.set("JSESSIONID", pair.jsessionid)
session.session_id = pair.session_id
logger.debug("Resume a SOAP session to %s from a stored pair", endpoint)
return session
|
send(payload) async
Send one envelope to the server and return one reply.
This method wraps the payload in the BroadsoftDocument and then in the SOAP envelope. It posts the envelope with the httpx client. The JSESSIONID cookie goes with the request. The method then gets the OCI reply from the response.
Do not write the payload or the reply to a log. They can hold secrets, for example a password in a login command.
Parameters:
| Name | Type | Description | Default |
|---|
payload | str | list[str] | One OCI command, or a list of OCI commands. | required |
Returns:
| Type | Description |
|---|
str | The OCI reply as a string. |
Raises:
| Type | Description |
|---|
MErrorHttpStatus | If the server returns a non-2xx HTTP status. |
MErrorHttpTimeout | If the request does not complete in time. |
MErrorHttpInitialisation | If the client cannot connect. |
MErrorHttpDropped | If the connection stops during the request. |
Source code in src/mercury_ocip_fast/session/soap_session.py
| async def send(self, payload: str | list[str]) -> str:
"""Send one envelope to the server and return one reply.
This method wraps the payload in the BroadsoftDocument and then in
the SOAP envelope. It posts the envelope with the httpx client. The
JSESSIONID cookie goes with the request. The method then gets the
OCI reply from the response.
Do not write the payload or the reply to a log. They can hold
secrets, for example a password in a login command.
Args:
payload: One OCI command, or a list of OCI commands.
Returns:
The OCI reply as a string.
Raises:
MErrorHttpStatus: If the server returns a non-2xx HTTP status.
MErrorHttpTimeout: If the request does not complete in time.
MErrorHttpInitialisation: If the client cannot connect.
MErrorHttpDropped: If the connection stops during the request.
"""
oci_xml = build_broadsoft_envelope(payload, self.session_id)
soap_envelope = wrap_soap(oci_xml)
logger.debug("Send %d bytes to %s", len(soap_envelope), self.endpoint)
try:
response = await self.http_client.post(
self.endpoint,
content=soap_envelope.encode("utf-8"),
headers={"Content-Type": "text/xml; charset=UTF-8", "SOAPAction": ""},
)
response.raise_for_status()
except httpx.HTTPStatusError as e:
logger.warning(
"The server %s returned HTTP %d", self.endpoint, e.response.status_code
)
raise MErrorHttpStatus(
f"BroadWorks returned HTTP {e.response.status_code}",
status=e.response.status_code,
) from e
except httpx.TimeoutException as e:
logger.warning("The request to %s timed out: %s", self.endpoint, e)
raise MErrorHttpTimeout(str(e)) from e
except httpx.ConnectError as e:
logger.warning("Cannot connect to %s: %s", self.endpoint, e)
raise MErrorHttpInitialisation(str(e)) from e
except httpx.TransportError as e: # a read, write, or protocol error
logger.warning(
"The connection to %s dropped mid-request: %s", self.endpoint, e
)
raise MErrorHttpDropped(str(e)) from e
logger.debug(
"Receive %d bytes from %s, HTTP %d",
len(response.text),
self.endpoint,
response.status_code,
)
return unwrap_soap(response.text)
|
TCPSessionAtom
A TCP session for BroadWorks.
The transport is an asyncio stream. The stream has a reader and a writer. The session sends the OCI-P document on the raw socket.
Attributes:
| Name | Type | Description |
|---|
reader | StreamReader | The stream reader for the socket. |
writer | StreamWriter | The stream writer for the socket. |
ssl_context | SSLContext | None | The TLS context for the socket, or None for no TLS. |
settings | TCPSessionSettings | The timeouts and the read size for the socket. |
session_id | str | The OCI-P session id for each message body. |
Source code in src/mercury_ocip_fast/session/tcp_session.py
| @attrs.define(kw_only=True, slots=True)
class TCPSessionAtom:
"""A TCP session for BroadWorks.
The transport is an asyncio stream. The stream has a reader and a writer.
The session sends the OCI-P document on the raw socket.
Attributes:
reader: The stream reader for the socket.
writer: The stream writer for the socket.
ssl_context: The TLS context for the socket, or None for no TLS.
settings: The timeouts and the read size for the socket.
session_id: The OCI-P session id for each message body.
"""
reader: asyncio.StreamReader
writer: asyncio.StreamWriter
ssl_context: SSLContext | None
settings: TCPSessionSettings = attrs.field(default=TCPSessionSettings())
session_id: str = attrs.field(factory=lambda: str(uuid.uuid4()))
created_at: float = attrs.field(factory=time.monotonic)
last_used: float = attrs.field(factory=time.monotonic)
@classmethod
async def open(
cls,
endpoint: str,
port: int | None = None,
*,
settings: TCPSessionSettings,
verify_ssl: bool = True,
) -> TCPSessionAtom:
"""Open a new TCP session. The session is not logged in.
This method opens a socket to the server. It waits for the
connection. If TLS is on, it also does the TLS handshake.
Args:
endpoint: The address of the server. This is a host, a
``host:port`` pair, or a ``scheme://host:port`` URL. An IPv6
host must be in brackets, for example ``[::1]:2209``.
port: The TCP port of the server. This argument has priority over
a port in the endpoint. The default is 2209 (TLS).
settings: The timeouts and the read size for the socket.
verify_ssl: If true, use TLS and verify the server certificate.
Returns:
A new TCP session with an open socket.
Raises:
MErrorSocketTimeout: If the connection does not open in time.
MErrorSocketInitialisation: If the endpoint has no host, or the
socket cannot open.
"""
ssl_context = ssl.create_default_context() if verify_ssl else None
try:
host, tcp_port = split_host_port(endpoint, port, default=2209)
except ValueError as e:
raise MErrorSocketInitialisation(str(e)) from e
logger.debug(
"Open a TCP session to %s:%d, connect timeout %ss",
host,
tcp_port,
settings.connect_timeout,
)
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, tcp_port, ssl=ssl_context),
timeout=settings.connect_timeout,
)
except TimeoutError as e:
logger.warning(
"Cannot open a TCP session to %s:%d: no connection in %ss",
host,
tcp_port,
settings.connect_timeout,
)
raise MErrorSocketTimeout(
f"Connection timeout after {settings.connect_timeout}s"
) from e
except OSError as e:
logger.warning("Cannot open a TCP session to %s:%d: %s", host, tcp_port, e)
raise MErrorSocketInitialisation(f"Connection failed: {e}") from e
return cls(
reader=reader, writer=writer, ssl_context=ssl_context, settings=settings
)
async def send(self, payload: str | list[str]) -> str:
"""Send one envelope to the server and return one reply.
This method builds the BroadsoftDocument from the payload. It
writes the document on the socket. It then reads the reply in
chunks. It stops the read at the end tag of the document.
Do not write the payload or the reply to a log. They can hold
secrets, for example a password in a login command.
Args:
payload: One OCI command, or a list of OCI commands.
Returns:
The OCI reply as a string.
Raises:
MErrorSocketDropped: If the connection stops during the write
or the read.
MErrorSocketTimeout: If a read does not complete in time.
"""
peer = self.writer.get_extra_info("peername")
oci_xml = build_broadsoft_envelope(payload, self.session_id).encode()
logger.debug("Send %d bytes to %s", len(oci_xml), peer)
try:
self.writer.writelines([oci_xml, b"\n"])
await self.writer.drain()
except (ConnectionResetError, BrokenPipeError, RuntimeError) as e:
logger.warning(
"Lost the TCP connection to %s during the write: %s", peer, e
)
raise MErrorSocketDropped(str(e)) from e
content = bytearray()
while True:
try:
chunk: bytes = await asyncio.wait_for(
self.reader.read(self.settings.read_chunk_size),
timeout=self.settings.read_timeout,
)
except TimeoutError as e:
logger.warning(
"No TCP reply from %s in %ss; %d bytes read so far",
peer,
self.settings.read_timeout,
len(content),
)
raise MErrorSocketTimeout(
f"Read timeout after {self.settings.read_timeout}s: {e}"
) from e
except (ConnectionResetError, BrokenPipeError, RuntimeError) as e:
logger.warning(
"Lost the TCP connection to %s during the read after %d bytes: %s",
peer,
len(content),
e,
)
raise MErrorSocketDropped(f"Connection failed: {e}") from e
if not chunk:
break
content.extend(chunk)
if b"</BroadsoftDocument>" in content:
break
response = content.rstrip(b"\n").decode("iso-8859-1")
logger.debug("Receive %d bytes from %s", len(content), peer)
return response
async def close(self) -> None:
"""Close the socket and release the transport.
The method is safe to call more than once.
"""
self.writer.close()
try:
await asyncio.wait_for(self.writer.wait_closed(), timeout=1.0)
except (TimeoutError, Exception) as e: # noqa: BLE001
logger.debug("TLS close did not finish cleanly (harmless): %s", e)
def is_alive(self) -> bool:
"""Tell if the session is still connected.
The session is not healthy if the writer is in a close. The session
is not healthy if the reader is at the end of the stream.
"""
if self.writer.is_closing():
return False
try:
if self.reader.at_eof():
return False
except Exception: # noqa: BLE001
return False
return True
def is_stale(self) -> bool:
"""Tell if the session is past its time to live."""
return (time.monotonic() - self.created_at) > self.settings.max_ttl_seconds
def touch(self) -> None:
"""Mark the session just used."""
self.last_used = time.monotonic()
|
close() async
Close the socket and release the transport.
The method is safe to call more than once.Source code in src/mercury_ocip_fast/session/tcp_session.py
| async def close(self) -> None:
"""Close the socket and release the transport.
The method is safe to call more than once.
"""
self.writer.close()
try:
await asyncio.wait_for(self.writer.wait_closed(), timeout=1.0)
except (TimeoutError, Exception) as e: # noqa: BLE001
logger.debug("TLS close did not finish cleanly (harmless): %s", e)
|
is_alive()
Tell if the session is still connected.
The session is not healthy if the writer is in a close. The session is not healthy if the reader is at the end of the stream.Source code in src/mercury_ocip_fast/session/tcp_session.py
| def is_alive(self) -> bool:
"""Tell if the session is still connected.
The session is not healthy if the writer is in a close. The session
is not healthy if the reader is at the end of the stream.
"""
if self.writer.is_closing():
return False
try:
if self.reader.at_eof():
return False
except Exception: # noqa: BLE001
return False
return True
|
is_stale()
Tell if the session is past its time to live.Source code in src/mercury_ocip_fast/session/tcp_session.py
| def is_stale(self) -> bool:
"""Tell if the session is past its time to live."""
return (time.monotonic() - self.created_at) > self.settings.max_ttl_seconds
|
open(endpoint, port=None, *, settings, verify_ssl=True) async classmethod
Open a new TCP session. The session is not logged in.
This method opens a socket to the server. It waits for the connection. If TLS is on, it also does the TLS handshake.
Parameters:
| Name | Type | Description | Default |
|---|
endpoint | str | The address of the server. This is a host, a host:port pair, or a scheme://host:port URL. An IPv6 host must be in brackets, for example [::1]:2209. | required |
port | int | None | The TCP port of the server. This argument has priority over a port in the endpoint. The default is 2209 (TLS). | None |
settings | TCPSessionSettings | The timeouts and the read size for the socket. | required |
verify_ssl | bool | If true, use TLS and verify the server certificate. | True |
Returns:
Raises:
| Type | Description |
|---|
MErrorSocketTimeout | If the connection does not open in time. |
MErrorSocketInitialisation | If the endpoint has no host, or the socket cannot open. |
Source code in src/mercury_ocip_fast/session/tcp_session.py
| @classmethod
async def open(
cls,
endpoint: str,
port: int | None = None,
*,
settings: TCPSessionSettings,
verify_ssl: bool = True,
) -> TCPSessionAtom:
"""Open a new TCP session. The session is not logged in.
This method opens a socket to the server. It waits for the
connection. If TLS is on, it also does the TLS handshake.
Args:
endpoint: The address of the server. This is a host, a
``host:port`` pair, or a ``scheme://host:port`` URL. An IPv6
host must be in brackets, for example ``[::1]:2209``.
port: The TCP port of the server. This argument has priority over
a port in the endpoint. The default is 2209 (TLS).
settings: The timeouts and the read size for the socket.
verify_ssl: If true, use TLS and verify the server certificate.
Returns:
A new TCP session with an open socket.
Raises:
MErrorSocketTimeout: If the connection does not open in time.
MErrorSocketInitialisation: If the endpoint has no host, or the
socket cannot open.
"""
ssl_context = ssl.create_default_context() if verify_ssl else None
try:
host, tcp_port = split_host_port(endpoint, port, default=2209)
except ValueError as e:
raise MErrorSocketInitialisation(str(e)) from e
logger.debug(
"Open a TCP session to %s:%d, connect timeout %ss",
host,
tcp_port,
settings.connect_timeout,
)
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(host, tcp_port, ssl=ssl_context),
timeout=settings.connect_timeout,
)
except TimeoutError as e:
logger.warning(
"Cannot open a TCP session to %s:%d: no connection in %ss",
host,
tcp_port,
settings.connect_timeout,
)
raise MErrorSocketTimeout(
f"Connection timeout after {settings.connect_timeout}s"
) from e
except OSError as e:
logger.warning("Cannot open a TCP session to %s:%d: %s", host, tcp_port, e)
raise MErrorSocketInitialisation(f"Connection failed: {e}") from e
return cls(
reader=reader, writer=writer, ssl_context=ssl_context, settings=settings
)
|
send(payload) async
Send one envelope to the server and return one reply.
This method builds the BroadsoftDocument from the payload. It writes the document on the socket. It then reads the reply in chunks. It stops the read at the end tag of the document.
Do not write the payload or the reply to a log. They can hold secrets, for example a password in a login command.
Parameters:
| Name | Type | Description | Default |
|---|
payload | str | list[str] | One OCI command, or a list of OCI commands. | required |
Returns:
| Type | Description |
|---|
str | The OCI reply as a string. |
Raises:
| Type | Description |
|---|
MErrorSocketDropped | If the connection stops during the write or the read. |
MErrorSocketTimeout | If a read does not complete in time. |
Source code in src/mercury_ocip_fast/session/tcp_session.py
| async def send(self, payload: str | list[str]) -> str:
"""Send one envelope to the server and return one reply.
This method builds the BroadsoftDocument from the payload. It
writes the document on the socket. It then reads the reply in
chunks. It stops the read at the end tag of the document.
Do not write the payload or the reply to a log. They can hold
secrets, for example a password in a login command.
Args:
payload: One OCI command, or a list of OCI commands.
Returns:
The OCI reply as a string.
Raises:
MErrorSocketDropped: If the connection stops during the write
or the read.
MErrorSocketTimeout: If a read does not complete in time.
"""
peer = self.writer.get_extra_info("peername")
oci_xml = build_broadsoft_envelope(payload, self.session_id).encode()
logger.debug("Send %d bytes to %s", len(oci_xml), peer)
try:
self.writer.writelines([oci_xml, b"\n"])
await self.writer.drain()
except (ConnectionResetError, BrokenPipeError, RuntimeError) as e:
logger.warning(
"Lost the TCP connection to %s during the write: %s", peer, e
)
raise MErrorSocketDropped(str(e)) from e
content = bytearray()
while True:
try:
chunk: bytes = await asyncio.wait_for(
self.reader.read(self.settings.read_chunk_size),
timeout=self.settings.read_timeout,
)
except TimeoutError as e:
logger.warning(
"No TCP reply from %s in %ss; %d bytes read so far",
peer,
self.settings.read_timeout,
len(content),
)
raise MErrorSocketTimeout(
f"Read timeout after {self.settings.read_timeout}s: {e}"
) from e
except (ConnectionResetError, BrokenPipeError, RuntimeError) as e:
logger.warning(
"Lost the TCP connection to %s during the read after %d bytes: %s",
peer,
len(content),
e,
)
raise MErrorSocketDropped(f"Connection failed: {e}") from e
if not chunk:
break
content.extend(chunk)
if b"</BroadsoftDocument>" in content:
break
response = content.rstrip(b"\n").decode("iso-8859-1")
logger.debug("Receive %d bytes from %s", len(content), peer)
return response
|
touch()
Mark the session just used.Source code in src/mercury_ocip_fast/session/tcp_session.py
| def touch(self) -> None:
"""Mark the session just used."""
self.last_used = time.monotonic()
|