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
70 changes: 62 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,81 @@
# Launch a server
# Copick Server

A server for [copick](https://github.com/kephale/copick) projects.

## Installation

```bash
pip install .
```
uv run copick_server/demo_server.py

## Usage

### Launch a server

#### Using a config file

```bash
python -m copick_server.server serve --config path/to/copick_config.json --cors "*" --port 8000
```

# Launch a client
#### Using dataset IDs from CZ cryoET Data Portal

```bash
python -m copick_server.server serve --dataset-ids 10440 --cors "*" --port 8017
```
uv run copick_server/client.py

You can use multiple dataset IDs by repeating the option:

```bash
python -m copick_server.server serve --dataset-ids 10440 --dataset-ids 10441 --cors "*" --port 8017
```

# Running Tests
#### Using uv run

With config file:
```bash
uv run copick_server/server.py serve --config ~/Data/copick/full_ml_challenge_czcdp.json --cors "*" --port 8017
```

With dataset IDs:
```bash
uv run copick_server/server.py serve --dataset-ids 10440 --cors "*" --port 8017
```

### Launch a client

```bash
uv run copick_server/client.py
```

## Running Tests

```bash
pip install ".[test]"
pytest
```

For coverage report:
```
```bash
pytest --cov=copick_server
```

# References
## CLI Options

```
python -m copick_server.server serve --help
```

| Option | Description | Default |
|-------------------|--------------------------------------------------------|---------------|
| -c, --config | Path to the configuration file | None |
| -ds, --dataset-ids| Dataset IDs to include in the project | None |
| --overlay-root | Root URL for the overlay storage | /tmp/overlay_root |
| --cors | Origin to allow CORS. Use wildcard '*' to allow all | None |
| --host | Bind socket to this host | 127.0.0.1 |
| --port | Bind socket to this port | 8000 |
| --reload | Enable auto-reload | False |

## References

The idea is influenced by https://github.com/manzt/simple-zarr-server, but aimed at supporting https://github.com/copick/copick.
The idea is influenced by https://github.com/manzt/simple-zarr-server, but aimed at supporting https://github.com/kephale/copick.
6 changes: 3 additions & 3 deletions copick_server/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
from fsspec import get_mapper

# First read the tomogram to get the shape
store = get_mapper("http://localhost:8000/14268/Tomograms/VoxelSpacing7.84/wbp.zarr")
store = get_mapper("http://localhost:8017/16463/Tomograms/VoxelSpacing10.012/wbp.zarr")
tomo = zarr.open(store, mode='r')
full_shape = tomo["0"].shape
print(f"Tomogram shape: {full_shape}")

# Example parameters
voxel_size = 7.84 # must match your tomogram's voxel spacing
voxel_size = 10.012
user_id = "kisharrington"
session_id = "copickServer"
name = "membrane" # This must match a pickable object in your config
Expand All @@ -33,7 +33,7 @@
data_bytes = shape_header + data.tobytes()

# Send to server as a single request
url = f"http://localhost:8000/14268/Segmentations/{seg_filename}"
url = f"http://localhost:8017/16463/Segmentations/{seg_filename}"
response = requests.put(url, data=data_bytes)

if response.status_code == 200:
Expand Down
7 changes: 0 additions & 7 deletions copick_server/demo_server.py

This file was deleted.

115 changes: 99 additions & 16 deletions copick_server/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,30 +227,54 @@ def create_copick_app(root: copick.models.CopickRoot, cors_origins: Optional[Lis

return app

def serve_copick(config_path: str, allowed_origins: Optional[List[str]] = None, **kwargs):
def serve_copick(config_path: Optional[str] = None, dataset_ids: Optional[List[int]] = None, overlay_root: str = "/tmp/overlay_root", allowed_origins: Optional[List[str]] = None, **kwargs):
"""Start an HTTP server serving a Copick project.

Parameters
----------
config_path : str
config_path : str, optional
Path to Copick config file
dataset_ids : list of int, optional
Dataset IDs to include in the project
overlay_root : str, optional
Root URL for the overlay storage, default is "/tmp/overlay_root"
allowed_origins : list of str, optional
List of allowed CORS origins. Use ["*"] to allow all.
**kwargs
Additional arguments passed to uvicorn.run()

Notes
-----
Either config_path or dataset_ids must be provided, but not both.
"""
root = copick.from_file(config_path)
if config_path and dataset_ids:
raise ValueError("Either config_path or dataset_ids must be provided, but not both.")
elif config_path:
root = copick.from_file(config_path)
elif dataset_ids:
root = copick.from_czcdp_datasets(
dataset_ids=dataset_ids,
overlay_root=overlay_root,
overlay_fs_args={"auto_mkdir": True},
)
else:
raise ValueError("Either config_path or dataset_ids must be provided.")

app = create_copick_app(root, allowed_origins)
uvicorn.run(app, **kwargs)
return app

def serve_copick_threaded(config_path: str, allowed_origins: Optional[List[str]] = None, **kwargs):
def serve_copick_threaded(config_path: Optional[str] = None, dataset_ids: Optional[List[int]] = None, overlay_root: str = "/tmp/overlay_root", allowed_origins: Optional[List[str]] = None, **kwargs):
"""Start an HTTP server in a background thread and return the app.

Parameters
----------
config_path : str
config_path : str, optional
Path to Copick config file
dataset_ids : list of int, optional
Dataset IDs to include in the project
overlay_root : str, optional
Root URL for the overlay storage, default is "/tmp/overlay_root"
allowed_origins : list of str, optional
List of allowed CORS origins. Use ["*"] to allow all.
**kwargs
Expand All @@ -260,8 +284,24 @@ def serve_copick_threaded(config_path: str, allowed_origins: Optional[List[str]]
-------
app : FastAPI
FastAPI application

Notes
-----
Either config_path or dataset_ids must be provided, but not both.
"""
root = copick.from_file(config_path)
if config_path and dataset_ids:
raise ValueError("Either config_path or dataset_ids must be provided, but not both.")
elif config_path:
root = copick.from_file(config_path)
elif dataset_ids:
root = copick.from_czcdp_datasets(
dataset_ids=dataset_ids,
overlay_root=overlay_root,
overlay_fs_args={"auto_mkdir": True},
)
else:
raise ValueError("Either config_path or dataset_ids must be provided.")

app = create_copick_app(root, allowed_origins)

# Start the server in a background thread
Expand All @@ -275,8 +315,36 @@ def serve_copick_threaded(config_path: str, allowed_origins: Optional[List[str]]

return app

@click.command()
@click.argument("config", type=click.Path(exists=True))
@click.group()
@click.pass_context
def cli(ctx):
pass


@cli.command()
@click.option(
"-c",
"--config",
type=click.Path(exists=True),
help="Path to the configuration file.",
required=False,
metavar="PATH",
)
@click.option(
"-ds",
"--dataset-ids",
type=int,
multiple=True,
help="Dataset IDs to include in the project (multiple inputs possible).",
metavar="ID",
)
@click.option(
"--overlay-root",
type=str,
default="/tmp/overlay_root",
help="Root URL for the overlay storage.",
show_default=True,
)
@click.option(
"--cors",
type=str,
Expand All @@ -298,15 +366,30 @@ def serve_copick_threaded(config_path: str, allowed_origins: Optional[List[str]]
show_default=True,
)
@click.option("--reload", is_flag=True, default=False, help="Enable auto-reload.")
def main(config: str, cors: Optional[str], host: str, port: int, reload: bool):
@click.pass_context
def serve(ctx, config: Optional[str] = None, dataset_ids: Optional[tuple] = None, overlay_root: str = "/tmp/overlay_root", cors: Optional[str] = None, host: str = "127.0.0.1", port: int = 8000, reload: bool = False):
"""Serve a Copick project over HTTP."""
serve_copick(
config,
allowed_origins=[cors] if cors else None,
host=host,
port=port,
reload=reload
)
if config and dataset_ids:
ctx.fail("Either --config or --dataset-ids must be provided, not both.")
elif not config and not dataset_ids:
ctx.fail("Either --config or --dataset-ids must be provided.")

try:
serve_copick(
config_path=config,
dataset_ids=dataset_ids if dataset_ids else None,
overlay_root=overlay_root,
allowed_origins=[cors] if cors else None,
host=host,
port=port,
reload=reload
)
except Exception as e:
ctx.fail(f"Error serving Copick project: {str(e)}")


def main():
cli()

if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"click",
"copick",
"copick>=1.0.1",
"numpy",
"uvicorn",
"zarr",
Expand Down
Loading