Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
…ython into users/aarnesen/testmonitor-product-api
  • Loading branch information
adamarnesen committed Oct 24, 2024
2 parents e01bfa8 + 427e978 commit 8d4a8ee
Show file tree
Hide file tree
Showing 35 changed files with 1,305 additions and 276 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/python-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.9', '3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install poetry
run: pipx install poetry
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: "poetry"
Expand All @@ -41,7 +41,7 @@ jobs:
run: pipx install poetry
- uses: actions/setup-python@v4
with:
python-version: 3.8
python-version: 3.9
cache: "poetry"
- run: poetry install
- name: Semantic release
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ version: 2
build:
os: ubuntu-20.04
tools:
python: "3.8"
python: "3.9"

# Build documentation in the docs/ directory with Sphinx
sphinx:
Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,42 @@

<!--next-version-placeholder-->

## v1.6.0 (2024-09-17)

### Feature

* Add Client for File Service ([#65](https://github.com/ni/nisystemlink-clients-python/issues/65)) ([`ebdc4c1`](https://github.com/ni/nisystemlink-clients-python/commit/ebdc4c1adf50f71498d0ce3446402dff55eef6ca))

## v1.5.0 (2024-09-09)

### Feature

* Add client for Artifacts API ([#70](https://github.com/ni/nisystemlink-clients-python/issues/70)) ([`0ada3d7`](https://github.com/ni/nisystemlink-clients-python/commit/0ada3d7c765e1b5d7fabfa4661f15d38c71c7295))

## v1.4.3 (2024-08-08)

### Fix

* Make SpecClient() work with default values for optional args ([#68](https://github.com/ni/nisystemlink-clients-python/issues/68)) ([`fb60801`](https://github.com/ni/nisystemlink-clients-python/commit/fb60801d4093ef66c92e7b2c6e4e1f5fe45bb2f6))

## v1.4.2 (2024-07-15)

### Fix

* Bump setuptools from 67.6.0 to 70.0.0 ([#67](https://github.com/ni/nisystemlink-clients-python/issues/67)) ([`39b13f0`](https://github.com/ni/nisystemlink-clients-python/commit/39b13f0c4b3ed3377e54dd6c4a3237c7e9304e48))

## v1.4.1 (2024-07-08)

### Fix

* Bump certifi from 2023.7.22 to 2024.7.4 ([#66](https://github.com/ni/nisystemlink-clients-python/issues/66)) ([`0fc4ff7`](https://github.com/ni/nisystemlink-clients-python/commit/0fc4ff766895a70be4c0e518ba98db0e69beab43))

## v1.4.0 (2024-06-18)

### Feature

* Make DataFrame and Spec clients compatible with SystemLink Client http configuration ([#61](https://github.com/ni/nisystemlink-clients-python/issues/61)) ([`7954e14`](https://github.com/ni/nisystemlink-clients-python/commit/7954e1479a0e421cfe3e44c741e69186e2f0c45f))

## v1.3.1 (2024-05-21)

### Fix
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ To contribute to this project, it is recommended that you follow these steps:

## Testing

Before running any tests, you must have a supported version of Python (3.8+) and [Poetry](https://python-poetry.org/docs/) installed locally.
Before running any tests, you must have a supported version of Python (3.9+) and [Poetry](https://python-poetry.org/docs/) installed locally.

It is also helpful to install SystemLink Server and configure the NI Web Server
to run on localhost.
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Requirements

* A SystemLink Server installation or a
`SystemLink Cloud <https://www.systemlinkcloud.com/>`_ account to connect to
* CPython 3.8+
* CPython 3.9+

.. _installation_section:

Expand Down
1 change: 1 addition & 0 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ API Reference
api_reference/testmonitor
api_reference/dataframe
api_reference/spec
api_reference/file

Indices and tables
------------------
Expand Down
20 changes: 20 additions & 0 deletions docs/api_reference/file.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.. _api_tag_page:

nisystemlink.clients.file
======================

.. autoclass:: nisystemlink.clients.file.FileClient
:exclude-members: __init__

.. automethod:: __init__
.. automethod:: api_info
.. automethod:: get_files
.. automethod:: delete_file
.. automethod:: delete_files
.. automethod:: upload_file
.. automethod:: download_file
.. automethod:: update_metadata

.. automodule:: nisystemlink.clients.file.models
:members:
:imported-members:
27 changes: 27 additions & 0 deletions docs/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,5 +183,32 @@ Create and Query Specifications
Update and Delete Specifications

.. literalinclude:: ../examples/spec/update_and_delete_specs.py
:language: python
:linenos:


File API
-------

Overview
~~~~~~~~

The :class:`.FileClient` class is the primary entry point of the File API.

When constructing a :class:`.FileClient`, you can pass an
:class:`.HttpConfiguration` (like one retrieved from the
:class:`.HttpConfigurationManager`), or let :class:`.FileClient` use the
default connection. The default connection depends on your environment.

With a :class:`.FileClient` object, you can:

* Get the list of files, download and delete files

Examples
~~~~~~~~

Get the metadata of a File using its Id and download it.

.. literalinclude:: ../examples/file/download_file.py
:language: python
:linenos:
28 changes: 28 additions & 0 deletions examples/artifact/upload_and_download_artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import io

from nisystemlink.clients.artifact import ArtifactClient
from nisystemlink.clients.core import HttpConfiguration


# Setup the server configuration to point to your instance of SystemLink Enterprise
server_configuration = HttpConfiguration(
server_uri="https://yourserver.yourcompany.com",
api_key="YourAPIKeyGeneratedFromSystemLink",
)
client = ArtifactClient(configuration=server_configuration)

# Define the workspace and artifact content
workspace = "your workspace ID"
artifact_stream = io.BytesIO(b"test content")

# Upload the artifact
upload_response = client.upload_artifact(workspace=workspace, artifact=artifact_stream)
if upload_response and upload_response.id:
print(f"Uploaded artifact ID: {upload_response.id}")

# Download the artifact using the ID from the upload response
artifact_id = upload_response.id
download_response = client.download_artifact(artifact_id)
if download_response:
downloaded_content = download_response.read()
print(f"Downloaded artifact content: {downloaded_content.decode('utf-8')}")
30 changes: 30 additions & 0 deletions examples/file/download_file.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Example to download a file from SystemLink."""

from shutil import copyfileobj

from nisystemlink.clients.file import FileClient

client = FileClient()

file_id = "a55adc7f-5068-4202-9d70-70ca6a06bee9"

# Fetch the file metadata to get the name
files = client.get_files(ids=[file_id])

if not files.available_files:
raise Exception(f"File ID {file_id} not found.")


file_name = "Untitled"

file_properties = files.available_files[0].properties

if file_properties:
file_name = file_properties["Name"]

# Download the file using FileId with content inline
content = client.download_file(id=file_id)

# Write the content to a file
with open(file_name, "wb") as f:
copyfileobj(content, f)
4 changes: 1 addition & 3 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mypy_path=nisystemlink/stubs
files=nisystemlink,tests
warn_unused_configs=True
plugins = pydantic.mypy
ignore_missing_imports=True

[mypy-nisystemlink.*]
disallow_untyped_calls=True
Expand All @@ -17,6 +18,3 @@ disallow_untyped_calls=True
disallow_untyped_decorators=True

strict_equality=True

[mypy-uplink.*]
ignore_missing_imports=True
3 changes: 3 additions & 0 deletions nisystemlink/clients/artifact/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._artifact_client import ArtifactClient

# flake8: noqa
83 changes: 83 additions & 0 deletions nisystemlink/clients/artifact/_artifact_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Implementation of ArtifactClient"""

from typing import BinaryIO, Optional

from nisystemlink.clients import core
from nisystemlink.clients.core._uplink._base_client import BaseClient
from nisystemlink.clients.core._uplink._methods import get, post, response_handler
from nisystemlink.clients.core.helpers._iterator_file_like import IteratorFileLike
from requests.models import Response
from uplink import Part, Path

from . import models


class ArtifactClient(BaseClient):
def __init__(self, configuration: Optional[core.HttpConfiguration] = None):
"""Initialize an instance.
Args:
configuration: Defines the web server to connect to and information about
how to connect. If not provided, the
:class:`HttpConfigurationManager <nisystemlink.clients.core.HttpConfigurationManager>`
is used to obtain the configuration.
Raises:
ApiException: if unable to communicate with the Notebook execution Service.
"""
if configuration is None:
configuration = core.HttpConfigurationManager.get_configuration()

super().__init__(configuration, base_path="/ninbartifact/v1/")

@post("artifacts")
def __upload_artifact(
self, workspace: Part, artifact: Part
) -> models.UploadArtifactResponse:
"""Uploads an artifact using multipart/form-data headers to send the file payload in the HTTP body.
Args:
workspace: The workspace containing the artifact.
artifact: The artifact to upload.
Returns:
UploadArtifactResponse: The response containing the artifact ID.
"""

def upload_artifact(
self, workspace: str, artifact: BinaryIO
) -> models.UploadArtifactResponse:
"""Uploads an artifact.
Args:
workspace: The workspace containing the artifact.
artifact: The artifact to upload.
Returns:
UploadArtifactResponse: The response containing the artifact ID.
"""
response = self.__upload_artifact(
workspace=workspace,
artifact=artifact,
)

return response

def _iter_content_filelike_wrapper(response: Response) -> IteratorFileLike:
return IteratorFileLike(response.iter_content(chunk_size=4096))

@response_handler(_iter_content_filelike_wrapper)
@get("artifacts/{id}")
def download_artifact(self, id: Path) -> IteratorFileLike:
"""Downloads an artifact.
Args:
id: The ID of the artifact to download.
Returns:
A file-like object for reading the artifact content.
"""
...
3 changes: 3 additions & 0 deletions nisystemlink/clients/artifact/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from ._upload_artifact_response import UploadArtifactResponse

# flake8: noqa
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from nisystemlink.clients.core._uplink._json_model import JsonModel


class UploadArtifactResponse(JsonModel):
"""Response for an artifact upload request."""

id: str
"""Information about the uploaded artifact."""
31 changes: 31 additions & 0 deletions nisystemlink/clients/core/_http_configuration_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ class HttpConfigurationManager:
HTTP_LOCALHOST_CONFIGURATION_ID = "SYSTEMLINK_LOCALHOST"
"""The default ID of the SystemLink Server's configuration on the SystemLink Server itself."""

_HTTP_JUPYTER_CONFIGURATION_ID = "SYSTEMLINK_VIRTUAL_JUPYTER"
"""Virtual ID of SystemLink Server's configuration for Jupyter Notebook execution on SLE."""

_configs = None
_virtual_configs = None

@classmethod
def get_configuration(
Expand Down Expand Up @@ -78,14 +82,41 @@ def _fallback(cls) -> Optional[core.HttpConfiguration]:
"""
if cls._configs is None:
cls._configs = cls._read_configurations()
if cls._virtual_configs is None:
cls._virtual_configs = cls._read_virtual_configurations()

master_config = cls._configs.get(cls.HTTP_MASTER_CONFIGURATION_ID)
if master_config is not None:
return master_config
localhost_config = cls._configs.get(cls.HTTP_LOCALHOST_CONFIGURATION_ID)
if localhost_config is not None:
return localhost_config

jupyter_config = cls._virtual_configs.get(cls._HTTP_JUPYTER_CONFIGURATION_ID)
if jupyter_config is not None:
return jupyter_config

return None

@classmethod
def _read_virtual_configurations(cls) -> Dict[str, core.HttpConfiguration]:
"""Loads the virtual HTTP configurations.
Returns:
A dictionary mapping each loaded configuration ID to its corresponding
:class:`HttpConfiguration`.
"""
configurations = {} # type: Dict[str, core.HttpConfiguration]
try:
configurations[cls._HTTP_JUPYTER_CONFIGURATION_ID] = (
core.JupyterHttpConfiguration()
)
except KeyError:
# Env variables for Jupyter notebook execution are not available.
pass

return configurations

@classmethod
def _read_configurations(cls) -> Dict[str, core.HttpConfiguration]:
"""Discover and loads the HTTP configuration files.
Expand Down
7 changes: 7 additions & 0 deletions nisystemlink/clients/core/_uplink/_file_like_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from nisystemlink.clients.core.helpers import IteratorFileLike
from requests.models import Response


def file_like_response_handler(response: Response) -> IteratorFileLike:
"""Response handler for File-Like content."""
return IteratorFileLike(response.iter_content(chunk_size=4096))
Loading

0 comments on commit 8d4a8ee

Please sign in to comment.