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
15 changes: 13 additions & 2 deletions docs/development/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The folllowing commands demonstrate how to do this using ``conda`` (assuming you

conda create -n ffpy -c conda-forge python jupyter astropy # jupyter and astropy are needed for running examples
conda activate ffpy
pip install -e ".[docs]" # editable installation with docs dependencies
pip install -e ".[tests,docs]" # editable installation with tests and docs dependencies


Now you can run the examples notebooks/scripts in the ``examples/`` directory.
Expand Down Expand Up @@ -53,7 +53,18 @@ above command and reload the above html file in browser.
The Sphinx docs include rendered Jupyter notebooks (via ``nbsphinx``), which can require **pandoc**.
If you see an error like ``PandocMissing``, install pandoc first (e.g., ``brew install pandoc`` on macOS).

Unit Tests
----------

Unit tests live in the ``tests/`` directory and use ``pytest``.
Make sure you have the virtual environment activated with test dependencies installed (``[tests]``), then run:

.. code-block:: shell

pytest tests/

Development Tests/Examples
--------------------------

Refer to the `examples/development_tests directory <https://github.com/Caltech-IPAC/firefly_client/tree/master/examples/development_tests>`_ of firefly-client GitHub repository.
The ``examples/development_tests/`` directory contains notebooks for manually testing behaviour that requires a live Firefly server.
Refer to the `examples/development_tests directory <https://github.com/Caltech-IPAC/firefly_client/tree/master/examples/development_tests>`_ of the firefly_client GitHub repository.
4 changes: 3 additions & 1 deletion docs/development/new-release-procedure.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

## Procedure
1. To push a new release you must be a maintainer in pypi ([see pypi below](#pypi))
2. Bump version in pyproject.toml (this step might be done in the PR)
2. Bump versions (this step might be done in the PR):
- Upgrade project.version in pyproject.toml
- If this release depends on the updates made in the Firefly server, **wait** until a firefly release is made. Then update the minimum compatible server version in `firefly_client/_server_compat.py` (the `MIN_SERVER_VERSION` variable) to the version of the new firefly release.
3. Clean out old distribution
- `rm dist/*`
4. Create the distribution
Expand Down
353 changes: 353 additions & 0 deletions examples/development_tests/test-version.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "b89ca528-303e-45a8-abe9-7eb93b0b6825",
"metadata": {},
"source": [
"# Test version compatibility\n",
"\n",
"Covers all logical branches of `_confirm_version` and its helper functions, across different server configurations."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "befc8619",
"metadata": {},
"outputs": [],
"source": [
"from firefly_client import FireflyClient\n",
"\n",
"# only needed for the mock scenario\n",
"from firefly_client._server_compat import is_server_compatible\n",
"from unittest.mock import patch, MagicMock"
]
},
{
"cell_type": "code",
"execution_count": 2,
"id": "cf4a4e98",
"metadata": {},
"outputs": [],
"source": [
"# Uncomment for debugging outputs\n",
"FireflyClient._debug = True"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "3567a13e",
"metadata": {},
"outputs": [],
"source": [
"def pprint_confirm_version(result):\n",
" \"\"\"For pretty-printing the result of _confirm_version() for debugging.\"\"\"\n",
" print(f\"reachable: {result['reachable']}\")\n",
" print(f\"compatible: {result['compatible']}\")\n",
" print(f\"server_version: {result['server_version']!r}\")\n",
" try:\n",
" raw = result['response'].json()\n",
" except Exception:\n",
" raw = result['response'].text[:200]\n",
" print(f\"raw response: {raw}\")"
]
},
{
"cell_type": "markdown",
"id": "dcc284a4",
"metadata": {},
"source": [
"## Compatible server\n",
"Creating a FireflyClient instance should succeed without warnings or errors."
]
},
{
"cell_type": "markdown",
"id": "a3000001",
"metadata": {},
"source": [
"### Base Firefly app"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "a3000002",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DEBUG: new instance: http://localhost:8080/firefly\n"
]
}
],
"source": [
"fc_base = FireflyClient.make_client(url='http://localhost:8080/firefly', launch_browser=False)"
]
},
{
"cell_type": "markdown",
"id": "e73b6aaf",
"metadata": {},
"source": [
"`_confirm_version()` should return `reachable=True` and `compatible=True` and note that the server version is under the `Version` key."
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "6c97d585",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"reachable: True\n",
"compatible: True\n",
"server_version: '2026.1-DEV:FIREFLY-1331-version-validation_c5e3'\n",
"raw response: {'Version': '2026.1-DEV:FIREFLY-1331-version-validation_c5e3', 'Built On': 'Tue Apr 07 14:25:15 PDT 2026', 'Git commit': 'c5e3756bf'}\n"
]
}
],
"source": [
"pprint_confirm_version(fc_base._confirm_version())"
]
},
{
"cell_type": "markdown",
"id": "afaef722",
"metadata": {},
"source": [
"### App using Firefly"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "60fed01a",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"DEBUG: new instance: http://localhost:8080/irsaviewer/\n"
]
}
],
"source": [
"fc_app = FireflyClient.make_client(url='http://localhost:8080/irsaviewer/', launch_browser=False)"
]
},
{
"cell_type": "markdown",
"id": "b5d0485d",
"metadata": {},
"source": [
"`_confirm_version()` should return `reachable=True` and `compatible=True` and note that the version appears under the `Firefly Library Version` key instead."
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "8440d219",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"reachable: True\n",
"compatible: True\n",
"server_version: '2026.1-DEV:FIREFLY-1331-version-validation_c5e3'\n",
"raw response: {'Firefly Git Commit': 'c5e3756bf', 'Version': 'v1.0', 'Built On': 'Tue Apr 07 14:25:55 PDT 2026', 'Git commit': '1331d2dd', 'Firefly Library Version': '2026.1-DEV:FIREFLY-1331-version-validation_c5e3'}\n"
]
}
],
"source": [
"pprint_confirm_version(fc_app._confirm_version())"
]
},
{
"cell_type": "markdown",
"id": "a4000001",
"metadata": {},
"source": [
"## Incompatible server version\n",
"\n",
"Intercept the version endpoint response of the local irsaviewer and substitute an old version string, then verify that creating a FireflyClient instance raises `ValueError` with the message how to rectify the issue.\n",
"\n",
"We don't have such a server available yet so we'll just mock the response of `_confirm_version` to simulate this scenario."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "c709b81c",
"metadata": {},
"outputs": [],
"source": [
"INCOMPATIBLE_VERSION = '2025.5.5' # below the MIN_SERVER_VERSION (2025.6-DEV)\n",
"\n",
"mock_resp_incompat = MagicMock()\n",
"mock_resp_incompat.status_code = 200\n",
"mock_resp_incompat.json.return_value = {'Version': INCOMPATIBLE_VERSION}\n",
"\n",
"ver_incompat = {\n",
" 'reachable': True,\n",
" 'compatible': is_server_compatible(INCOMPATIBLE_VERSION),\n",
" 'server_version': INCOMPATIBLE_VERSION,\n",
" 'response': mock_resp_incompat,\n",
"}"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "a4000003",
"metadata": {},
"outputs": [
{
"ename": "ValueError",
"evalue": "Version of the provided Firefly server http://localhost:8080/firefly/ is not compatible with this version of firefly_client.\n Server version: 2025.5.5\n Required: >=2026.1-DEV\n Please use the URL of a compatible Firefly server\n",
"output_type": "error",
"traceback": [
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
"\u001b[31mValueError\u001b[39m Traceback (most recent call last)",
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[9]\u001b[39m\u001b[32m, line 3\u001b[39m\n\u001b[32m 1\u001b[39m \u001b[38;5;66;03m# Patch _confirm_version at class level so the mock takes effect inside make_client()\u001b[39;00m\n\u001b[32m 2\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m patch.object(FireflyClient, \u001b[33m'\u001b[39m\u001b[33m_confirm_version\u001b[39m\u001b[33m'\u001b[39m, return_value=ver_incompat):\n\u001b[32m----> \u001b[39m\u001b[32m3\u001b[39m \u001b[43mFireflyClient\u001b[49m\u001b[43m.\u001b[49m\u001b[43mmake_client\u001b[49m\u001b[43m(\u001b[49m\u001b[43murl\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m'\u001b[39;49m\u001b[33;43mhttp://localhost:8080/firefly/\u001b[39;49m\u001b[33;43m'\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlaunch_browser\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43;01mFalse\u001b[39;49;00m\u001b[43m)\u001b[49m\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/dev/cm/firefly_client/firefly_client/firefly_client.py:223\u001b[39m, in \u001b[36mFireflyClient.make_client\u001b[39m\u001b[34m(cls, url, html_file, launch_browser, channel_override, verbose, token, viewer_override)\u001b[39m\n\u001b[32m 175\u001b[39m \u001b[38;5;129m@classmethod\u001b[39m\n\u001b[32m 176\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mmake_client\u001b[39m(\u001b[38;5;28mcls\u001b[39m, url=_default_url, html_file=_def_html_file, launch_browser=\u001b[38;5;28;01mTrue\u001b[39;00m,\n\u001b[32m 177\u001b[39m channel_override=\u001b[38;5;28;01mNone\u001b[39;00m, verbose=\u001b[38;5;28;01mFalse\u001b[39;00m, token=\u001b[38;5;28;01mNone\u001b[39;00m, viewer_override=\u001b[38;5;28;01mNone\u001b[39;00m):\n\u001b[32m 178\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m 179\u001b[39m \u001b[33;03m Factory method to create a Firefly client in a plain Python, IPython, or\u001b[39;00m\n\u001b[32m 180\u001b[39m \u001b[33;03m notebook session, and attempt to open a display. If a display cannot be\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 221\u001b[39m \u001b[33;03m A FireflyClient that works in the lab environment\u001b[39;00m\n\u001b[32m 222\u001b[39m \u001b[33;03m \"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m223\u001b[39m fc = \u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43murl\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mEnv\u001b[49m\u001b[43m.\u001b[49m\u001b[43mresolve_client_channel\u001b[49m\u001b[43m(\u001b[49m\u001b[43mchannel_override\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mhtml_file\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtoken\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mviewer_override\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 224\u001b[39m verbose \u001b[38;5;129;01mand\u001b[39;00m Env.show_start_browser_tab_msg(fc.get_firefly_url())\n\u001b[32m 225\u001b[39m launch_browser \u001b[38;5;129;01mand\u001b[39;00m fc.launch_browser()\n",
"\u001b[36mFile \u001b[39m\u001b[32m~/dev/cm/firefly_client/firefly_client/firefly_client.py:275\u001b[39m, in \u001b[36mFireflyClient.__init__\u001b[39m\u001b[34m(self, url, channel, html_file, token, viewer_override)\u001b[39m\n\u001b[32m 273\u001b[39m debug(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33mFirefly server\u001b[39m\u001b[38;5;130;01m\\'\u001b[39;00m\u001b[33ms version endpoint response: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mver[\u001b[33m\"\u001b[39m\u001b[33mresponse\u001b[39m\u001b[33m\"\u001b[39m].json()\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m)\n\u001b[32m 274\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m ver[\u001b[33m'\u001b[39m\u001b[33mcompatible\u001b[39m\u001b[33m'\u001b[39m]:\n\u001b[32m--> \u001b[39m\u001b[32m275\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\n\u001b[32m 276\u001b[39m \u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33mVersion of the provided Firefly server \u001b[39m\u001b[38;5;132;01m{\u001b[39;00murl\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m is not compatible with this version of firefly_client.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m'\u001b[39m\n\u001b[32m 277\u001b[39m \u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33m Server version: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mver[\u001b[33m\"\u001b[39m\u001b[33mserver_version\u001b[39m\u001b[33m\"\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m'\u001b[39m\n\u001b[32m 278\u001b[39m \u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33m Required: >=\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mMIN_SERVER_VERSION\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m'\u001b[39m\n\u001b[32m 279\u001b[39m \u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33m Please use the URL of a compatible Firefly server\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m'\u001b[39m\n\u001b[32m 280\u001b[39m )\n\u001b[32m 282\u001b[39m debug(\u001b[33mf\u001b[39m\u001b[33m'\u001b[39m\u001b[33mnew instance: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00murl\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m'\u001b[39m)\n",
"\u001b[31mValueError\u001b[39m: Version of the provided Firefly server http://localhost:8080/firefly/ is not compatible with this version of firefly_client.\n Server version: 2025.5.5\n Required: >=2026.1-DEV\n Please use the URL of a compatible Firefly server\n"
]
}
],
"source": [
"# Patch _confirm_version at class level so the mock takes effect inside make_client()\n",
"with patch.object(FireflyClient, '_confirm_version', return_value=ver_incompat):\n",
" FireflyClient.make_client(url='http://localhost:8080/firefly/', launch_browser=False)"
]
},
{
"cell_type": "markdown",
"id": "7eaed749",
"metadata": {},
"source": [
"`_confirm_version()` should return `reachable=True` and `compatible=False`."
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "a4000002",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"reachable: True\n",
"compatible: False\n",
"server_version: '2025.5.5'\n",
"raw response: {'Version': '2025.5.5'}\n"
]
}
],
"source": [
"pprint_confirm_version(ver_incompat)"
]
},
{
"cell_type": "markdown",
"id": "a5000001",
"metadata": {},
"source": [
"## Server's Version endpoint not reachable\n",
"\n",
"When the version endpoint returns a non-200 response, creating a FireflyClient instance should emit a warning and proceed rather than raise an error."
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "a5000004",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"WARNING: Could not retrieve version of the Firefly server https://irsa.ipac.caltech.edu/irsaviewer/. Proceeding without compatibility check.\n",
"DEBUG: Firefly server's version endpoint response: {'success': False, 'error': {}}\n",
"DEBUG: new instance: https://irsa.ipac.caltech.edu/irsaviewer/\n"
]
}
],
"source": [
"fc_no_version = FireflyClient.make_client(url='https://irsa.ipac.caltech.edu/irsaviewer/', launch_browser=False)"
]
},
{
"cell_type": "markdown",
"id": "9a3c4582",
"metadata": {},
"source": [
"`_confirm_version()` should return `reachable=False` and hence `compatible=False` and `server_version=None`."
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "8f14c22b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"reachable: False\n",
"compatible: False\n",
"server_version: None\n",
"raw response: {'success': False, 'error': {}}\n"
]
}
],
"source": [
"pprint_confirm_version(fc_no_version._confirm_version())"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b30f97cd",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "ffpy",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading