Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,47 @@ The CDP4Web library is a C# library that provides helpful classes that facilitat

The CDP4ServicesMessaging library is a C# library that provides abstractions over Rabbit MQ to support AMQP messaging

# Using the COMET SDK from Python

The repository contains a small helper that shows how to consume the COMET SDK
assemblies from an existing CPython environment by means of
[`pythonnet`](https://pythonnet.github.io/). The helper can be found in
`python/comet_session_wrapper.py` and demonstrates how to reproduce the C#
session bootstrap code from Python:

```python
from pathlib import Path

from comet_session_wrapper import CometSession, add_comet_references

# Point pythonnet to the directories that contain the built COMET assemblies.
add_comet_references([
Path("/path/to/COMET-SDK-Community-Edition/CDP4Dal/bin/Release/net6.0"),
])

session = CometSession(
url="https://cdp4services-public.cdp4.org",
username="some-user-name",
password="some-password",
)
session.connect()

# ... use session.session (the underlying CDP4Dal.Session instance) ...

session.disconnect()
```

To run the example:

1. Build the SDK (`dotnet build -c Release`) so that the required `.dll` files
are available in the `bin` directories.
2. Install `pythonnet` in your Python environment, for example with
`pip install pythonnet`.
3. Update the `add_comet_references` call so that it points to the build output
directories on your machine.
4. Instantiate :class:`CometSession` with your credentials and call
:py:meth:`CometSession.connect`.

# License

The libraries contained in the COMET-SDK Community Edition are provided to the community under the GNU Lesser General Public License. Because we make the software available with the LGPL, it can be used in both open source and proprietary software without being required to release the source code of your own components.
Expand Down
136 changes: 136 additions & 0 deletions python/comet_session_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
"""High level wrapper for establishing a COMET session from Python via pythonnet.

This module demonstrates how to use the .NET COMET SDK assemblies from an
existing CPython environment. It relies on pythonnet to bridge between the
Python and .NET runtimes and provides a small convenience class that mirrors the
C# sample:

.. code-block:: csharp

var uri = new Uri("https://cdp4services-public.cdp4.org");
var credentials = new Credentials("some-user-name", "some-password", uri);
var dal = new CdpServicesDal();
var session = new Session(dal, credentials);

The :class:`CometSession` class consolidates these steps for Python projects.
"""

from __future__ import annotations

from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Optional

import clr # type: ignore[attr-defined]


# Assemblies that are required to load the COMET SDK types that participate in the
# session establishment flow. The list can be extended if your application uses
# additional features from the SDK.
_DEFAULT_ASSEMBLIES: tuple[str, ...] = (
"CDP4Common.dll",
"CDP4Dal.dll",
"CDP4ServicesDal.dll",
)


def add_comet_references(search_paths: Iterable[Path], assemblies: Optional[Iterable[str]] = None) -> None:
"""Register COMET SDK assemblies with pythonnet.

Parameters
----------
search_paths:
A sequence of directories that contain the built COMET SDK ``.dll``
files. Each path is added to ``sys.path`` visible to pythonnet so that
the assemblies can be resolved.
assemblies:
The assemblies to load. When omitted, a sensible default set that is
sufficient for session creation is used.

Notes
-----
``pythonnet`` resolves assemblies relative to the interpreter process. By
pushing the build output directories first we can keep the calling
environment self-contained.
"""

from sys import path as sys_path

assembly_names = tuple(assemblies) if assemblies is not None else _DEFAULT_ASSEMBLIES

for candidate in search_paths:
resolved = Path(candidate).expanduser().resolve()
if resolved.is_dir():
sys_path.insert(0, str(resolved))

for assembly in assembly_names:
clr.AddReference(assembly)


@dataclass
class CometSession:
"""Helper that encapsulates the creation of a COMET session.

Attributes
----------
url:
The service endpoint (e.g. ``"https://cdp4services-public.cdp4.org"``).
username:
Username for the COMET service.
password:
Password for the COMET service.
dal:
Instance of :class:`CDP4ServicesDal.CdpServicesDal` used to communicate
with the service. The default creates a fresh instance, but a custom one
can be supplied if a pre-configured DAL is required.
session:
The managed :class:`CDP4Dal.Session` created after calling
:meth:`connect`.
"""

url: str
username: str
password: str
dal: Optional["CdpServicesDal"] = None

def __post_init__(self) -> None:
self._session: Optional["Session"] = None

@property
def session(self) -> "Session":
"""The underlying COMET session.

Raises
------
RuntimeError
If :meth:`connect` has not been called yet.
"""

if self._session is None:
raise RuntimeError("Session has not been established. Call connect() first.")
return self._session

def connect(self) -> "Session":
"""Create the COMET session and return the managed .NET object."""

from System import Uri # type: ignore[attr-defined]
from CDP4Dal.DAL import Credentials # type: ignore[attr-defined]
from CDP4ServicesDal import CdpServicesDal # type: ignore[attr-defined]
from CDP4Dal import Session # type: ignore[attr-defined]

uri = Uri(self.url)
credentials = Credentials(self.username, self.password, uri)
dal = self.dal or CdpServicesDal()
self._session = Session(dal, credentials)
return self._session

def disconnect(self) -> None:
"""Dispose of the session if it has been created."""

if self._session is not None:
self._session.Dispose()
self._session = None


__all__ = ["CometSession", "add_comet_references"]

Loading