Skip to content
Merged
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
13 changes: 13 additions & 0 deletions CHANGE.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
+++++++++++
LabKey Python Client API News
+++++++++++
What's New in the LabKey 2.3.0 package
==============================

*Release date: 06/30/2022*
- Add "hostname" property to ServerContext
- Add "base_url" property to ServerContext
- Add "webdav_client" method to ServerContext
- This method returns a webdavclient3 Client instance
- Add "webdav_path" method to ServerContext
- Add docs for WebDav support
- Add unit tests for ServerContext

What's New in the LabKey 2.2.0 package
==============================

*Release date: 08/11/2021*
- Add `domain.get_domain_details` API to domain module.
- Support saving domain options via `domain.save`.
- Fix `ConditionalFormat.to_json()` to match server response.
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ Security API - [sample code](samples/security_example.py)

- Available for administrating and configuring user accounts and permissions.

WebDav

- Documentation and example code can be found [here](docs/webdav.md).

## Installation
To install, simply use `pip`:

Expand Down
52 changes: 52 additions & 0 deletions docs/webdav.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# WebDav Support

Our Python API includes some convenience methods for creating "webdavclient3" clients, and building webdav file paths.

### Creating a WebDav client
First, make sure you have the [webdavclient3](https://github.com/ezhov-evgeny/webdav-client-python-3) library installed:

```bash
$ pip install webdavclient3
```

Then you can use your `APIWrapper` to create a client:

```python
from labkey.api_wrapper import APIWrapper

domain = "localhost:8080"
container = "MyContainer"
api = APIWrapper(domain, container)
webdav_client = api.server_context.webdav_client()
```

The `webdav_client` method has a single optional argument, `webdav_options`, a dict that you can use to pass any options
that you would pass to the [webdavclient3](https://github.com/ezhov-evgeny/webdav-client-python-3#webdav-api) library.
If you are using API Key authentication with your APIWrapper we will automatically configure the WebDav Client to use
API Key authentication with your API Key. If you are using a `.netrc` file for authentication it should automatically
detect your `.netrc` file and authenticate using those credentials.


### The webdav_path utility method
If you are using the `webdavclient3` library you'll still need to know the appropriate WebDav path in order to access
your files. We provide a utility method, `webdav_path` to make it easier to construct LabKey WebDav paths. The method
takes two keyword arguments, `container_path`, and `file_name`.

```python
from labkey.api_wrapper import APIWrapper

domain = "localhost:8080"
container = "MyContainer"
api = APIWrapper(domain, container)
webdav_client = api.server_context.webdav_client()

# Constructs a webdav path to "MyContainer"
path = api.server_context.webdav_path()
print(webdav_client.info(path))
# Constructs a webdav path to the "data.txt" file in "MyContainer"
path = api.server_context.webdav_path(file_name='data.txt')
print(webdav_client.info(path))
# Constructs a webdav path to the "data.txt" file in "other_container"
path = api.server_context.webdav_path(container_path="other_container", file_name="data.txt")
print(webdav_client.info(path))
```
2 changes: 1 addition & 1 deletion labkey/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@
from labkey import domain, query, experiment, security, utils

__title__ = "labkey"
__version__ = "2.2.0"
__version__ = "2.3.0"
__author__ = "LabKey"
__license__ = "Apache License 2.0"
72 changes: 65 additions & 7 deletions labkey/server_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,81 @@ def __init__(
def __repr__(self):
return f"<ServerContext [ {self._domain} | {self._context_path} | {self._container_path} ]>"

def build_url(self, controller: str, action: str, container_path: str = None) -> str:
sep = "/"
@property
def hostname(self) -> str:
return self._scheme + self._domain

url = self._scheme + self._domain
@property
def base_url(self) -> str:
base_url = self.hostname

if self._context_path is not None:
url += sep + self._context_path
base_url += "/" + self._context_path

return base_url

def build_url(self, controller: str, action: str, container_path: str = None) -> str:
url = self.base_url

if container_path is not None:
url += sep + container_path
url += "/" + container_path
elif self._container_path is not None:
url += sep + self._container_path
url += "/" + self._container_path

url += sep + controller + "-" + action
url += "/" + controller + "-" + action

return url

def webdav_path(self, container_path: str = None, file_name: str = None):
path = "/_webdav"
container_path = container_path or self._container_path

if container_path is not None:
if container_path.endswith("/"):
# trim the slash
container_path = container_path[0:-1]

if not container_path.startswith("/"):
path += "/"

path += container_path

path += "/@files"

if file_name is not None:
if not file_name.startswith("/"):
path += "/"

path += file_name

return path

def webdav_client(self, webdav_options: dict = None):
# We localize the import of webdav3 here so it is an optional dependency. Only users who want to use webdav will
# need to pip install webdavclient3
from webdav3.client import Client
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is rad.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's rare that his feature is useful, but it's powerful when used!


options = {
"webdav_hostname": self.base_url,
}

if self._api_key is not None:
options["webdav_login"] = "apikey"
options["webdav_password"] = f"apikey|{self._api_key}"

if webdav_options is not None:
options = {
**options,
**webdav_options,
}

client = Client(options)

if self._verify_ssl is False:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

client.verify = False # Set verify to false if using localhost without HTTPS

return client

def handle_request_exception(self, exception):
if type(exception) in [RequestAuthorizationError, QueryNotFoundError, ServerNotFoundError]:
raise exception
Expand Down
46 changes: 46 additions & 0 deletions test/unit/test_server_context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from labkey.server_context import ServerContext
import pytest


@pytest.fixture(scope="session")
def server_context():
return ServerContext("example.com", "test_container", "test_context_path")


@pytest.fixture(scope="session")
def server_context_no_context_path():
return ServerContext("example.com", "test_container")


@pytest.fixture(scope="session")
def server_context_no_ssl():
return ServerContext("example.com", "test_container", "test_context_path", use_ssl=False)


def test_base_url(server_context, server_context_no_context_path, server_context_no_ssl):
assert server_context.base_url == "https://example.com/test_context_path"
assert server_context_no_context_path.base_url == "https://example.com"
assert server_context_no_ssl.base_url == "http://example.com/test_context_path"


def test_build_url(server_context):
assert (
server_context.build_url("query", "getQuery.api")
== "https://example.com/test_context_path/test_container/query-getQuery.api"
)
assert (
server_context.build_url("query", "getQuery.api", "different_container")
== "https://example.com/test_context_path/different_container/query-getQuery.api"
)


def test_webdav_path(server_context, server_context_no_context_path, server_context_no_ssl):
assert server_context.webdav_path() == "/_webdav/test_container/@files"
assert (
server_context.webdav_path(file_name="test.jpg")
== "/_webdav/test_container/@files/test.jpg"
)
assert (
server_context.webdav_path("my_container/with_subfolder", "data.txt")
== "/_webdav/my_container/with_subfolder/@files/data.txt"
)