From f9c84729ab8f1f101079a76356cd7027dc9167f1 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 06:52:19 +0200 Subject: [PATCH 01/15] adding tests --- .github/workflows/publish.yml | 39 ++++++++--- pyproject.toml | 4 +- src/a10y/a10y.tcss | 119 ++++++++++++++++++++++++++++++++++ src/a10y/app.py | 2 +- src/a10y/main.py | 2 +- tests/__init__.py | 0 tests/test_app.py | 41 ++++++++++++ 7 files changed, 195 insertions(+), 12 deletions(-) create mode 100644 src/a10y/a10y.tcss create mode 100644 tests/__init__.py create mode 100644 tests/test_app.py diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f96e891..3474c94 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,19 +2,40 @@ name: Publish Python Package on: push: - branches: - - main - tags: - - "[0-9]+.[0-9]+.[0-9]+" - - "[0-9]+.[0-9]+.[0-9]+a[0-9]+" - - "[0-9]+.[0-9]+.[0-9]+b[0-9]+" - - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" + pull_request: - branches: - - main + jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: "3.11" # Change as needed + + - name: Install Dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev] # Ensure pytest is in dev dependencies + + - name: Run Tests + run: | + pytest --junitxml=pytest-report.xml --cov=my_package # Change `my_package` + + - name: Upload Pytest Report + uses: actions/upload-artifact@v4 + with: + name: pytest-report + path: pytest-report.xml release-build: runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') steps: - name: Checkout Repository diff --git a/pyproject.toml b/pyproject.toml index 1b6f9c6..84daf27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,8 @@ dependencies = [ "msgpack>=1.1.0", "multidict>=6.0.4", "pygments>=2.17.2", + "pytest>=8.3.4", + "pytest-asyncio>=0.25.3", "requests>=2.31.0", "rich>=13.7.0", "textual==0.43.2", @@ -46,4 +48,4 @@ packages = ["src/a10y"] [build-system] requires = ["hatchling"] -build-backend = "hatchling.build" \ No newline at end of file +build-backend = "hatchling.build" diff --git a/src/a10y/a10y.tcss b/src/a10y/a10y.tcss new file mode 100644 index 0000000..434a086 --- /dev/null +++ b/src/a10y/a10y.tcss @@ -0,0 +1,119 @@ +Screen { + layout: vertical; +} + +.box { + border: solid green; + min-width: 150; +} + +.hide { + display: none; +} + +#explanations-keys { + margin-left: 2; +} + +Requests { + layout: grid; + grid-size: 5 5; + grid-columns: 0.7fr 1fr 1fr 1fr 1fr; + grid-rows: 1 3 3 3 3; + max-height: 15; +} + +#request-title { + column-span: 5; +} + +#nodes-container { + row-span: 4; + max-width: 24; +} + +#nodes { + max-height: 9; +} + +#nslc, #timeframe, #options, #send-request { + column-span: 4; +} + +.request-label { + margin-top: 1; +} + +.short-input { + width: 16; +} + +AutoComplete { + max-height: 4; + max-width: 16; + margin-right: 3; + align: left top; +} + +.date-input { + width: 27; + margin-right: 1; +} + +#times { + width: 30; +} + +#mergegaps { + max-width: 12; +} + +#request-button { + margin: 0 5 0 5; +} + +#post-file { + width: 50; +} + +#status-container { + max-height: 5; +} + +#status-collapse { + max-height: 10; +} + +#error-results { + margin-left: 2; +} + +ContentSwitcher { + margin-left: 2; +} + +#info-bar { + background: $primary; +} + +#lines { + height: auto; +} + +#results-container { + max-height: 29; + height: auto; +} + +#plain-container { + max-height: 30; + height: auto; +} + +.result-item { + height: auto; +} + +CursoredText { + margin-left: 2; +} \ No newline at end of file diff --git a/src/a10y/app.py b/src/a10y/app.py index e7394a4..109f9f8 100644 --- a/src/a10y/app.py +++ b/src/a10y/app.py @@ -1,7 +1,7 @@ from textual.app import App from textual.widgets import Header, Footer, Checkbox, Select, Input, Button, Collapsible, ContentSwitcher,Static,Label from textual.containers import ScrollableContainer , Container, Horizontal -from widgets import Explanations, Requests, Results, Status, CursoredText # Import modular widgets +from a10y.widgets import Explanations, Requests, Results, Status, CursoredText # Import modular widgets import requests from datetime import datetime, timedelta from textual.binding import Binding diff --git a/src/a10y/main.py b/src/a10y/main.py index cb93488..72f1c13 100644 --- a/src/a10y/main.py +++ b/src/a10y/main.py @@ -4,7 +4,7 @@ import logging import tomli from datetime import datetime, timedelta -from app import AvailabilityUI # Import from same directory # Import the main UI application +from a10y.app import AvailabilityUI # Import from same directory # Import the main UI application def parse_arguments(): diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_app.py b/tests/test_app.py new file mode 100644 index 0000000..cec6df6 --- /dev/null +++ b/tests/test_app.py @@ -0,0 +1,41 @@ +from a10y.app import AvailabilityUI +from textual.app import App +import pytest + +@pytest.mark.asyncio +async def test_send_button(): + """Test clicking the send button.""" + + + config = { + "default_starttime": "2024-01-01T00:00:00", + "default_endtime": "2024-01-02T00:00:00", + "default_mergegaps": "0.0", + "default_merge_samplerate": False, + "default_merge_quality": False, + "default_merge_overlap": False, + "default_quality_D": False, + "default_quality_R": False, + "default_quality_Q": False, + "default_quality_M": False, + "default_includerestricted": False, + "default_file": "", + } + + app = AvailabilityUI(nodes_urls=[], routing = "https://www.orfeus-eu.org/eidaws/routing/1/query?", **config) + + async with app.run_test() as pilot: + + button = app.query_one("#request-button") + assert button is not None, "Button not found!" + + # Click the send button + await pilot.click("#request-button") + + + assert button.disabled is True, "Button should be disabled after click" + + await pilot.pause(2) # Waits for 500ms before checking the button + + assert button.disabled is False, "Button should be re-enabled after request found" + From 12e8dbfa8333416570162661891b7cf7618b53ea Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 06:53:28 +0200 Subject: [PATCH 02/15] Update publish.yml --- .github/workflows/publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3474c94..71f0bad 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,7 +25,7 @@ jobs: - name: Run Tests run: | - pytest --junitxml=pytest-report.xml --cov=my_package # Change `my_package` + pytest --junitxml=pytest-report.xml --cov=eida-a10y # - name: Upload Pytest Report uses: actions/upload-artifact@v4 From 199dd373161c12428ebce2124824eb64d7a8850a Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 06:58:19 +0200 Subject: [PATCH 03/15] adding workflows --- .github/workflows/publish.yml | 39 ++++++++--------------------------- .github/workflows/test.yml | 35 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 71f0bad..f96e891 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -2,40 +2,19 @@ name: Publish Python Package on: push: - + branches: + - main + tags: + - "[0-9]+.[0-9]+.[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+a[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+b[0-9]+" + - "[0-9]+.[0-9]+.[0-9]+rc[0-9]+" pull_request: - + branches: + - main jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.11" # Change as needed - - - name: Install Dependencies - run: | - python -m pip install --upgrade pip - pip install -e .[dev] # Ensure pytest is in dev dependencies - - - name: Run Tests - run: | - pytest --junitxml=pytest-report.xml --cov=eida-a10y # - - - name: Upload Pytest Report - uses: actions/upload-artifact@v4 - with: - name: pytest-report - path: pytest-report.xml release-build: runs-on: ubuntu-latest - needs: test - if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') steps: - name: Checkout Repository diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..11823a4 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: Run Tests + +on: + push: + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install UV + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available + + - name: Install Dependencies using UV + run: | + uv venv .venv # Create virtual environment + source .venv/bin/activate # Activate virtual environment + uv pip install -e .[dev] pytest pytest-cov # Install dependencies + + - name: Run Pytest with Coverage + run: | + source .venv/bin/activate + pytest --junitxml=pytest-report.xml --cov=src # Ensure `src` matches your package + + - name: Upload Pytest Report + uses: actions/upload-artifact@v4 + with: + name: pytest-report + path: pytest-report.xml From ebe6b6b1e9bad5e7e4b960c8cee94a5e847cc754 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 07:03:17 +0200 Subject: [PATCH 04/15] Update test.yml --- .github/workflows/test.yml | 42 ++++++++++++-------------------------- 1 file changed, 13 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 11823a4..4e29a10 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,35 +1,19 @@ -name: Run Tests +name: Python Tests -on: - push: - pull_request: +on: [push] jobs: - test: + lock_file: runs-on: ubuntu-latest - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - - name: Install UV - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available - - - name: Install Dependencies using UV - run: | - uv venv .venv # Create virtual environment - source .venv/bin/activate # Activate virtual environment - uv pip install -e .[dev] pytest pytest-cov # Install dependencies + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: uv lock --locked - - name: Run Pytest with Coverage - run: | - source .venv/bin/activate - pytest --junitxml=pytest-report.xml --cov=src # Ensure `src` matches your package - - - name: Upload Pytest Report - uses: actions/upload-artifact@v4 - with: - name: pytest-report - path: pytest-report.xml + tests: + runs-on: ubuntu-latest + needs: [lock_file] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - run: uv run pytest -v --durations=0 --cov From 0ecae99d73b1ad2cb7d6563a547faeb9bce81890 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 07:05:06 +0200 Subject: [PATCH 05/15] Update test.yml --- .github/workflows/test.yml | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e29a10..d8f193e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,16 +3,11 @@ name: Python Tests on: [push] jobs: - lock_file: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - - run: uv lock --locked + tests: runs-on: ubuntu-latest - needs: [lock_file] + steps: - uses: actions/checkout@v4 - uses: ./.github/actions/setup From 5d921ba0868a3f39dc38059aa3c7468cccb9ac7a Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 07:06:13 +0200 Subject: [PATCH 06/15] Update test.yml --- .github/workflows/test.yml | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d8f193e..07cc290 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,12 +3,35 @@ name: Python Tests on: [push] jobs: - + lock_file: + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install UV + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available + + - name: Lock Dependencies + run: uv lock --locked tests: runs-on: ubuntu-latest - + needs: [lock_file] steps: - - uses: actions/checkout@v4 - - uses: ./.github/actions/setup - - run: uv run pytest -v --durations=0 --cov + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Install UV + run: | + curl -LsSf https://astral.sh/uv/install.sh | sh + echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available + + - name: Install Dependencies + run: | + uv pip install -e .[dev] pytest pytest-cov # Install dependencies + + - name: Run Pytest using UV + run: uv run pytest -v --durations=0 --cov From cdc45045b2c66e2d8e5fed0d460815a556c6d775 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 07:07:44 +0200 Subject: [PATCH 07/15] Update test.yml --- .github/workflows/test.yml | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 07cc290..da546ad 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,13 @@ -name: Python Tests +name: Run Tests -on: [push] +on: + push: + pull_request: jobs: - lock_file: + test: runs-on: ubuntu-latest + steps: - name: Checkout Repository uses: actions/checkout@v4 @@ -14,24 +17,9 @@ jobs: curl -LsSf https://astral.sh/uv/install.sh | sh echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available - - name: Lock Dependencies - run: uv lock --locked - - tests: - runs-on: ubuntu-latest - needs: [lock_file] - steps: - - name: Checkout Repository - uses: actions/checkout@v4 - - name: Install UV + - name: Run Pytest with Coverage run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available + uv run pytest --junitxml=pytest-report.xml --cov=src - - name: Install Dependencies - run: | - uv pip install -e .[dev] pytest pytest-cov # Install dependencies - - name: Run Pytest using UV - run: uv run pytest -v --durations=0 --cov From 1f0132426f7139ba7b28e4b62acf1dae9877f94a Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 07:09:21 +0200 Subject: [PATCH 08/15] Update test.yml --- .github/workflows/test.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index da546ad..b9b2557 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,9 +17,15 @@ jobs: curl -LsSf https://astral.sh/uv/install.sh | sh echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available + - name: Install Dependencies using UV + run: | + uv venv .venv # Create virtual environment + source .venv/bin/activate # Activate virtual environment + uv pip install -e .[dev] pytest pytest-cov # Install dependencies - name: Run Pytest with Coverage run: | - uv run pytest --junitxml=pytest-report.xml --cov=src + + uv run pytest --junitxml=pytest-report.xml --cov=src # Ensure `src` matches your package From f8a159bc28a0ef38af63897e621de82fac2bad60 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 07:46:00 +0200 Subject: [PATCH 09/15] Update test.yml --- .github/workflows/test.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b9b2557..2daada5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,15 +17,8 @@ jobs: curl -LsSf https://astral.sh/uv/install.sh | sh echo "$HOME/.local/bin" >> $GITHUB_PATH # Ensure UV is available - - name: Install Dependencies using UV - run: | - uv venv .venv # Create virtual environment - source .venv/bin/activate # Activate virtual environment - uv pip install -e .[dev] pytest pytest-cov # Install dependencies - - name: Run Pytest with Coverage run: | - uv run pytest --junitxml=pytest-report.xml --cov=src # Ensure `src` matches your package From ef44981fa7b1c0ac5a4591e3b40194db52cac912 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Tue, 18 Feb 2025 07:49:06 +0200 Subject: [PATCH 10/15] test --- .coverage | Bin 0 -> 53248 bytes pyproject.toml | 5 +++++ 2 files changed, 5 insertions(+) create mode 100644 .coverage diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..42cee207ce43b93ce41f93738a7333834fb50d35 GIT binary patch literal 53248 zcmeI)%WoS+90%~(UB`~|=%!Sa8$~5k1f*#brzsK$IM6_Ih+0t!6;f3RcWqCSrS`77 zyUyc4le9=h2>t`a4Q`x}5Ele@E>!#hMO>)FLqaXBpjFH7w~yFP6DJ3x%H_L?*ZY{6 zo%zhmYde1H^hrBVa@O^#Rv@1f147fpF)4)*S$g!-BUv)EkxF*xS!>$fYBMYHm(KQ^ zZ;QV4$HIK4|CBl2_euY=z1RAx*$;a^%~;tIoxlPC2tWV=|8Ie{(|wuT;GlN-y}&9~ zRNz@9K!+a)WoojDoI zt0Z*YS7n~)=on3P955fo=dFTWrdWZRqao^v>a zJQznGm7bbao^ndck8*L)E|2z%URmkMqPfkshgW+|bP>yG*bFyN*;#s#FhDdD8-pF!tS_kHmS=-d0VP8A?F&6V$~h{VqL9Q{+MjU91Dx#xMPwFK`r2!-gNH8J({3a zsh0eU6?(l@urRQgiqB;zxeLm(=2SjD8x0GOx*<{1narkh)5BXeo%JSAm|WlNSsP4e zazjJfN+x|IK10PB&!lrxLt7Om z4p*3rH@ep{sZ4I~UTrPv5j-yOqpPJ5e5BC@HcSLN&wkAc=HF zoU>4vjFv3(kw%tJsO9kmt3snm&9Xh}vNQ%qGl1lPWs3q^ zMYkS=?U#4M=E-wUBfn7sjs3))LXC#Ose8ZS`o2xmDo}j3i%_l=(-hU zInmreF=l+artQlx4-d~uuQbx|mpP5Og^@5%$>hSr{2A(WN&ljaYSnVe@qES8aFi;I z{oFbEI?*6P5#WG?$K!810$?*H&SY*L;`U%`fHFhP6!Y@L_GO9(TrQoGJJ; zsaLHe+wrd6(nvO(Y_ykCG~m*!o#uAJc1;|-8(o11x2Oh-kwOwGey!6p$~X9$c~{UM z76?E90uX=z1Rwwb2tWV=5P$##9zOv?)3prW|Lf*Y!u*3)SReoa2tWV=5P$##AOHaf zKmY;|=tzM+-Pms?-|#pxpczAh@pk}T$UmQ-JlaF0(#;#fykY*;kp)DiApijgKmY;| zfB*y_009U<00IzL7w9wgYsuRHdXF)bjo$^}&;QfAZVK}U^D}e7JYf#g4i*SN00Izz z00bZa0SG_<0uX>e=L;N48{*tV)d?ob6R+DBTz_JE=J?cvHF;!t!uLwiM%A(%`Z<9^ zT{{TNF)8e5$`EJUq+2N1jvW*V6m;A$gw-Z!n3qMnJg4YaRVZb>eac~Qt5&0+dCd@| z?NV;a34i{d-gQfuC(Uc7G(X>U%UtaIDn79g)@Bh0F8E4z2+jQ5R${SX@pd0T0jcrnHy8qYnMk%{p;hJ)S-~U68fB*y_ z009U<00Izz00bZa0SG+S0tS6mpr1eg*UWo@{;)s*0uX=z1Rwwb2tWV=5P$##An+Ip z7)F1VKmRw(6=D8ueo0SQAOHafKmY;|fB*y_009U<00I#B?*)z;DJ?r|+`s<)otxj> z`A56^;8NxGSK>QejD56Q=#2~NVnAO0pj*h9Cywh~T6V2lzrXVB>VqFw4jki9`$Rek zyibVLCx1Qg^%vL8``SHCJe{22XG5&64y5!0vP(;4v%8++&;JeccVXT&Z_yJL2tWV= z5P$##AOHafKmY;|fB*zKT_8=6.0.0", +] From 5e4eb276315ea9f2be74b148ff4c0097bd3f7a61 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Fri, 21 Feb 2025 11:32:40 +0200 Subject: [PATCH 11/15] cache nodes --- pyproject.toml | 1 + src/a10y/a10y.tcss | 27 +++++-- src/a10y/app.py | 68 +++++++++++++---- src/a10y/main.py | 179 ++++++++++++++++++++++++++------------------ src/a10y/widgets.py | 3 + tests/test_app.py | 1 - 6 files changed, 184 insertions(+), 95 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b62748b..3141039 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,6 +6,7 @@ requires-python = "<4.0,>=3.11" dependencies = [ "aiohttp>=3.11.12", "aiosignal>=1.3.1", + "appdirs>=1.4.4", "async-timeout>=4.0.3", "attrs>=23.1.0", "certifi>=2023.11.17", diff --git a/src/a10y/a10y.tcss b/src/a10y/a10y.tcss index 434a086..22de9d3 100644 --- a/src/a10y/a10y.tcss +++ b/src/a10y/a10y.tcss @@ -17,28 +17,39 @@ Screen { Requests { layout: grid; - grid-size: 5 5; - grid-columns: 0.7fr 1fr 1fr 1fr 1fr; + grid-size: 6 5; + grid-columns: 0.7fr 0.5fr 0.1fr 1fr 1fr 1fr; grid-rows: 1 3 3 3 3; - max-height: 15; + max-height: 20; + grid-gutter: 1; } #request-title { - column-span: 5; + column-span: 6; } #nodes-container { row-span: 4; max-width: 24; } - +#reload-nodes{ + row-span:2; + height:100%; + align: center middle; + text-align: center; + margin: 1 0; +} #nodes { - max-height: 9; + height: 100%; } -#nslc, #timeframe, #options, #send-request { +#timeframe,#nslc{ column-span: 4; } +#options, #send-request { + column-span: 5; + width:100% +} .request-label { margin-top: 1; @@ -48,6 +59,8 @@ Requests { width: 16; } + + AutoComplete { max-height: 4; max-width: 16; diff --git a/src/a10y/app.py b/src/a10y/app.py index 109f9f8..17040f9 100644 --- a/src/a10y/app.py +++ b/src/a10y/app.py @@ -5,13 +5,21 @@ import requests from datetime import datetime, timedelta from textual.binding import Binding -from textual_autocomplete import AutoComplete, Dropdown, DropdownItem -from textual.app import App, ComposeResult +from textual_autocomplete import DropdownItem +from textual.app import ComposeResult from textual import work from textual.worker import get_current_worker import math import os import sys +import json +import threading +from pathlib import Path +from appdirs import user_cache_dir + +CACHE_DIR = Path(user_cache_dir("a10y")) +CACHE_FILE = CACHE_DIR / "nodes_cache.json" +QUERY_URL = "https://www.orfeus-eu.org/eidaws/routing/1/globalconfig?format=fdsn" class AvailabilityUI(App): def __init__(self, nodes_urls, routing, **kwargs): @@ -28,7 +36,7 @@ def action_quit(self) -> None: else: os.system("reset") # Linux/macOS: Reset terminal - CSS_PATH = "a10y.css" + CSS_PATH = "a10y.tcss" BINDINGS = [ Binding("ctrl+c", "quit", "Quit"), Binding("tab/shift+tab", "navigate", "Navigate"), @@ -54,19 +62,46 @@ def compose(self) -> ComposeResult: id="application-container" ) yield Footer() + def fetch_nodes_from_api(self): + """Fetch fresh nodes from API and update cache.""" + nodes_urls = [] + try: + response = requests.get(QUERY_URL, timeout=60) + response.raise_for_status() + data = response.json() + + for node in data.get("datacenters", []): + node_name = node["name"] + fdsnws_url = None + + for repo in node.get("repositories", []): + for service in repo.get("services", []): + if service["name"] == "fdsnws-station-1": + fdsnws_url = service["url"] + break + if fdsnws_url: + break + + if fdsnws_url: + fdsnws_url = fdsnws_url.rstrip("/") + "/" + nodes_urls.append((node_name, fdsnws_url, True)) + + if nodes_urls: + self.save_nodes_to_cache(nodes_urls) + except requests.RequestException: + pass + finally: + self.exit() + def save_nodes_to_cache(self, nodes): + """Save nodes to cache file permanently (no expiration).""" + CACHE_DIR.mkdir(parents=True, exist_ok=True) + with open(CACHE_FILE, "w", encoding="utf-8") as f: + json.dump({"nodes": nodes}, f) - - def on_checkbox_changed(self, event: Checkbox.Changed) -> None: - """A function to select/deselect all nodes when corresponding checkbox is clicked""" - if event.checkbox == self.query_one("#all-nodes"): - if self.query_one("#all-nodes").value: - self.query_one("#nodes").select_all() - else: - self.query_one("#nodes").deselect_all() - - + + def on_select_changed(self, event: Select.Changed) -> None: """A function to issue appropriate request and update status when a Node or when a common time frame is selected""" if event.select == self.query_one("#times"): @@ -232,8 +267,11 @@ def change_button_disabled(self, disabled: bool) -> None: @work(exclusive=True, thread=True) async def on_button_pressed(self, event: Button.Pressed) -> None: # Disable the button to prevent multiple clicks - self.call_from_thread(lambda: self.change_button_disabled(True)) - + if event.button.id == "reload-nodes": + button = self.query_one("#reload-nodes") + button.label = "Reloading..." + button.disabled = True + self.fetch_nodes_from_api() try: start = self.query_one("#start").value end = self.query_one("#end").value diff --git a/src/a10y/main.py b/src/a10y/main.py index 72f1c13..366c0aa 100644 --- a/src/a10y/main.py +++ b/src/a10y/main.py @@ -4,8 +4,31 @@ import logging import tomli from datetime import datetime, timedelta -from a10y.app import AvailabilityUI # Import from same directory # Import the main UI application - +from a10y.app import AvailabilityUI +from pathlib import Path +from appdirs import user_cache_dir +import json + +# Common constants +DEFAULT_NODES = [ + ("GFZ", "https://geofon.gfz.de/fdsnws/station/1/", True), + ("ODC", "https://orfeus-eu.org/fdsnws/station/1/", True), + ("ETHZ", "https://eida.ethz.ch/fdsnws/station/1/", True), + ("RESIF", "https://ws.resif.fr/fdsnws/station/1/", True), + ("INGV", "https://webservices.ingv.it/fdsnws/station/1/", True), + ("LMU", "https://erde.geophysik.uni-muenchen.de/fdsnws/station/1/", True), + ("ICGC", "https://ws.icgc.cat/fdsnws/station/1/", True), + ("NOA", "https://eida.gein.noa.gr/fdsnws/station/1/", True), + ("BGR", "https://eida.bgr.de/fdsnws/station/1/", True), + ("BGS", "https://eida.bgs.ac.uk/fdsnws/station/1/", True), + ("NIEP", "https://eida-sc3.infp.ro/fdsnws/station/1/", True), + ("KOERI", "https://eida.koeri.boun.edu.tr/fdsnws/station/1/", True), + ("UIB-NORSAR", "https://eida.geo.uib.no/fdsnws/station/1/", True), +] + +CACHE_DIR = Path(user_cache_dir("a10y")) +CACHE_FILE = CACHE_DIR / "nodes_cache.json" +QUERY_URL = "https://www.orfeus-eu.org/eidaws/routing/1/globalconfig?format=fdsn" def parse_arguments(): """Parse command-line arguments.""" @@ -14,36 +37,79 @@ def parse_arguments(): parser.add_argument("-c", "--config", default=None, help="Configuration file path") return parser.parse_args() +def ensure_cache_dir(): + """Ensure the cache directory exists.""" + CACHE_DIR.mkdir(parents=True, exist_ok=True) -def load_nodes(): - """Fetch node URLs dynamically or use fallback values if request fails.""" +def fetch_nodes_from_api(): + """Fetch fresh nodes from API and save to cache.""" nodes_urls = [] + try: - response = requests.get("https://orfeus-eu.org/epb/nodes", timeout=5) - if response.status_code == 200: - nodes_urls = [(n["node_code"], f"https://{n['node_url_base']}/fdsnws/", True) for n in response.json()] - except requests.RequestException: - pass # Fall back to default nodes if request fails - - # Fallback nodes - if not nodes_urls: - nodes_urls = [ - ("GFZ", "https://geofon.gfz-potsdam.de/fdsnws/", True), - ("ODC", "https://orfeus-eu.org/fdsnws/", True), - ("ETHZ", "https://eida.ethz.ch/fdsnws/", True), - ("RESIF", "https://ws.resif.fr/fdsnws/", True), - ("INGV", "https://webservices.ingv.it/fdsnws/", True), - ("LMU", "https://erde.geophysik.uni-muenchen.de/fdsnws/", True), - ("ICGC", "https://ws.icgc.cat/fdsnws/", True), - ("NOA", "https://eida.gein.noa.gr/fdsnws/", True), - ("BGR", "https://eida.bgr.de/fdsnws/", True), - ("BGS", "https://eida.bgs.ac.uk/fdsnws/", True), - ("NIEP", "https://eida-sc3.infp.ro/fdsnws/", True), - ("KOERI", "https://eida.koeri.boun.edu.tr/fdsnws/", True), - ("UIB-NORSAR", "https://eida.geo.uib.no/fdsnws/", True), - ] - return nodes_urls + response = requests.get(QUERY_URL, timeout=60) + response.raise_for_status() + data = response.json() + + for node in data.get("datacenters", []): + node_name = node["name"] + fdsnws_url = None + + for repo in node.get("repositories", []): + for service in repo.get("services", []): + if service["name"] == "fdsnws-station-1": + fdsnws_url = service["url"] + break + if fdsnws_url: + break + + if fdsnws_url: + fdsnws_url = fdsnws_url.rstrip("/") + "/" + nodes_urls.append((node_name, fdsnws_url, True)) + + if nodes_urls: + save_nodes_to_cache(nodes_urls) + return nodes_urls + + except requests.RequestException as e: + logging.warning(f"Failed to fetch nodes from API: {e}") + + return None + +def save_nodes_to_cache(nodes): + """Save nodes to cache file.""" + ensure_cache_dir() + with open(CACHE_FILE, "w", encoding="utf-8") as f: + json.dump({"nodes": nodes}, f) + +def load_cached_nodes(): + """Load cached nodes, fetch from API if missing or invalid.""" + ensure_cache_dir() + + if CACHE_FILE.exists(): + try: + with open(CACHE_FILE, "r", encoding="utf-8") as f: + cache_data = json.load(f) + nodes = cache_data.get("nodes", []) + + if not all(isinstance(n, list) and len(n) == 3 for n in nodes): + raise ValueError("Invalid cache format") + + return [(str(name), str(url), True) for name, url, _ in nodes] + except (json.JSONDecodeError, ValueError) as e: + logging.warning(f"Cache file is corrupted: {e}. Deleting it.") + CACHE_FILE.unlink() + + nodes_from_api = fetch_nodes_from_api() + if nodes_from_api: + return nodes_from_api + + return DEFAULT_NODES + +def load_nodes(): + """Always load from cache if available, otherwise use fallback values.""" + cached_nodes = load_cached_nodes() + return cached_nodes if cached_nodes else DEFAULT_NODES def load_defaults(): """Return default configuration values.""" @@ -62,21 +128,15 @@ def load_defaults(): "default_includerestricted": True, } - - def load_config(config_path, defaults): """Load configuration from a TOML file and update defaults.""" - if not config_path: - # No config file provided, try the default location config_dir = os.getenv("XDG_CONFIG_DIR", "") config_path = os.path.join(config_dir, "a10y", "config.toml") if config_dir else "./config.toml" if not os.path.isfile(config_path): - # Config file is missing, return defaults without modification return defaults - # Try to load the config file try: with open(config_path, "rb") as f: config = tomli.load(f) @@ -84,7 +144,7 @@ def load_config(config_path, defaults): logging.error(f"Invalid format in config file {config_path}") raise ValueError(f"Invalid TOML format in config file: {config_path}") - # Handle starttime + # Process starttime if "starttime" in config: try: parts = config["starttime"].split() @@ -92,39 +152,32 @@ def load_config(config_path, defaults): num = int(parts[0]) defaults["default_starttime"] = (datetime.now() - timedelta(days=num)).strftime("%Y-%m-%dT%H:%M:%S") else: - datetime.strptime(config["starttime"], "%Y-%m-%dT%H:%M:%S") # Validate format + datetime.strptime(config["starttime"], "%Y-%m-%dT%H:%M:%S") defaults["default_starttime"] = config["starttime"] except (ValueError, IndexError): raise ValueError(f"Invalid starttime format in {config_path}") - # Handle endtime + # Process other config options if "endtime" in config: - if config["endtime"].lower() == "now": - pass # Keep default - else: + if config["endtime"].lower() != "now": try: - datetime.strptime(config["endtime"], "%Y-%m-%dT%H:%M:%S") # Validate format + datetime.strptime(config["endtime"], "%Y-%m-%dT%H:%M:%S") defaults["default_endtime"] = config["endtime"] except ValueError: raise ValueError(f"Invalid endtime format in {config_path}") - # Handle mergegaps if "mergegaps" in config: try: - defaults["default_mergegaps"] = str(float(config["mergegaps"])) # Ensure it's a valid number + defaults["default_mergegaps"] = str(float(config["mergegaps"])) except ValueError: raise ValueError(f"Invalid mergegaps format in {config_path}") - # Handle quality settings if "quality" in config: if not isinstance(config["quality"], list) or any(q not in ["D", "R", "Q", "M"] for q in config["quality"]): raise ValueError(f"Invalid quality codes in {config_path}") - defaults["default_quality_D"] = "D" in config["quality"] - defaults["default_quality_R"] = "R" in config["quality"] - defaults["default_quality_Q"] = "Q" in config["quality"] - defaults["default_quality_M"] = "M" in config["quality"] + for code in ["D", "R", "Q", "M"]: + defaults[f"default_quality_{code}"] = code in config["quality"] - # Handle merge options if "merge" in config: if not isinstance(config["merge"], list) or any(m not in ["samplerate", "quality", "overlap"] for m in config["merge"]): raise ValueError(f"Invalid merge options in {config_path}") @@ -132,42 +185,24 @@ def load_config(config_path, defaults): defaults["default_merge_quality"] = "quality" in config["merge"] defaults["default_merge_overlap"] = "overlap" in config["merge"] - # Handle restricted data setting if "includerestricted" in config: defaults["default_includerestricted"] = bool(config["includerestricted"]) - return defaults # Return updated defaults - - - + return defaults def main(): - - - # Parse command-line arguments args = parse_arguments() - - # Load network nodes nodes_urls = load_nodes() - - # Load default settings defaults = load_defaults() - - # Load configuration from file (if provided) - defaults["default_file"] = args.post # Overwrite default POST file if provided + defaults["default_file"] = args.post defaults = load_config(args.config, defaults) - routing = "https://www.orfeus-eu.org/eidaws/routing/1/query?" - - # Run the application with loaded settings app = AvailabilityUI( nodes_urls=nodes_urls, - routing=routing, # Pass routing URL - **defaults # Pass unpacked defaults + routing="https://www.orfeus-eu.org/eidaws/routing/1/query?", + **defaults ) app.run() - -# Ensure the script can still be executed manually if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/src/a10y/widgets.py b/src/a10y/widgets.py index 7b84663..d1e40b2 100644 --- a/src/a10y/widgets.py +++ b/src/a10y/widgets.py @@ -31,6 +31,7 @@ def compose(self) -> ComposeResult: Checkbox("Select all Nodes", True, id="all-nodes"), SelectionList(*self.nodes_urls, id="nodes"), id="nodes-container" + ) yield Horizontal( @@ -54,8 +55,10 @@ def compose(self) -> ComposeResult: Input(classes="short-input", id="channel"), Dropdown(items=[], id="channels") ), + id="nslc" ) + yield Button("Reload Nodes\n(Restart the app)", variant="primary", id="reload-nodes", disabled=False) yield Horizontal( Label("Start Time:", classes="request-label"), Input(classes="date-input", id="start", value=self.config["default_starttime"]), diff --git a/tests/test_app.py b/tests/test_app.py index cec6df6..db54225 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -1,5 +1,4 @@ from a10y.app import AvailabilityUI -from textual.app import App import pytest @pytest.mark.asyncio From 4d22b3a1ba3205647fde499df4104d97f59c57b6 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Fri, 21 Feb 2025 11:52:53 +0200 Subject: [PATCH 12/15] url parse --- src/a10y/app.py | 6 ++++-- src/a10y/main.py | 33 +++++++++++++++++---------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/a10y/app.py b/src/a10y/app.py index 17040f9..42733c4 100644 --- a/src/a10y/app.py +++ b/src/a10y/app.py @@ -16,6 +16,7 @@ import threading from pathlib import Path from appdirs import user_cache_dir +from urllib.parse import urlparse CACHE_DIR = Path(user_cache_dir("a10y")) CACHE_FILE = CACHE_DIR / "nodes_cache.json" @@ -83,8 +84,9 @@ def fetch_nodes_from_api(self): break if fdsnws_url: - fdsnws_url = fdsnws_url.rstrip("/") + "/" - nodes_urls.append((node_name, fdsnws_url, True)) + parsed_url = urlparse(fdsnws_url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/fdsnws/" + nodes_urls.append((node_name, base_url, True)) if nodes_urls: self.save_nodes_to_cache(nodes_urls) diff --git a/src/a10y/main.py b/src/a10y/main.py index 366c0aa..affa304 100644 --- a/src/a10y/main.py +++ b/src/a10y/main.py @@ -8,22 +8,22 @@ from pathlib import Path from appdirs import user_cache_dir import json - +from urllib.parse import urlparse # Common constants DEFAULT_NODES = [ - ("GFZ", "https://geofon.gfz.de/fdsnws/station/1/", True), - ("ODC", "https://orfeus-eu.org/fdsnws/station/1/", True), - ("ETHZ", "https://eida.ethz.ch/fdsnws/station/1/", True), - ("RESIF", "https://ws.resif.fr/fdsnws/station/1/", True), - ("INGV", "https://webservices.ingv.it/fdsnws/station/1/", True), - ("LMU", "https://erde.geophysik.uni-muenchen.de/fdsnws/station/1/", True), - ("ICGC", "https://ws.icgc.cat/fdsnws/station/1/", True), - ("NOA", "https://eida.gein.noa.gr/fdsnws/station/1/", True), - ("BGR", "https://eida.bgr.de/fdsnws/station/1/", True), - ("BGS", "https://eida.bgs.ac.uk/fdsnws/station/1/", True), - ("NIEP", "https://eida-sc3.infp.ro/fdsnws/station/1/", True), - ("KOERI", "https://eida.koeri.boun.edu.tr/fdsnws/station/1/", True), - ("UIB-NORSAR", "https://eida.geo.uib.no/fdsnws/station/1/", True), + ("GFZ", "https://geofon.gfz.de/fdsnws/", True), + ("ODC", "https://orfeus-eu.org/fdsnws/", True), + ("ETHZ", "https://eida.ethz.ch/fdsnws/", True), + ("RESIF", "https://ws.resif.fr/fdsnws/", True), + ("INGV", "https://webservices.ingv.it/fdsnws/", True), + ("LMU", "https://erde.geophysik.uni-muenchen.de/fdsnws/", True), + ("ICGC", "https://ws.icgc.cat/fdsnws/", True), + ("NOA", "https://eida.gein.noa.gr/fdsnws/", True), + ("BGR", "https://eida.bgr.de/fdsnws/", True), + ("BGS", "https://eida.bgs.ac.uk/fdsnws/", True), + ("NIEP", "https://eida-sc3.infp.ro/fdsnws/", True), + ("KOERI", "https://eida.koeri.boun.edu.tr/fdsnws/", True), + ("UIB-NORSAR", "https://eida.geo.uib.no/fdsnws/", True), ] CACHE_DIR = Path(user_cache_dir("a10y")) @@ -63,8 +63,9 @@ def fetch_nodes_from_api(): break if fdsnws_url: - fdsnws_url = fdsnws_url.rstrip("/") + "/" - nodes_urls.append((node_name, fdsnws_url, True)) + parsed_url = urlparse(fdsnws_url) + base_url = f"{parsed_url.scheme}://{parsed_url.netloc}/fdsnws/" + nodes_urls.append((node_name, base_url, True)) if nodes_urls: save_nodes_to_cache(nodes_urls) From a6a9bf07b20aeb35e12f01fff3bee1e6a420b95f Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Sat, 22 Feb 2025 13:02:15 +0200 Subject: [PATCH 13/15] update --- src/a10y/a10y.tcss | 18 +++++++++--------- src/a10y/app.py | 15 +++++++++++++++ src/a10y/widgets.py | 7 +++++-- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/a10y/a10y.tcss b/src/a10y/a10y.tcss index 22de9d3..2b788cf 100644 --- a/src/a10y/a10y.tcss +++ b/src/a10y/a10y.tcss @@ -18,7 +18,7 @@ Screen { Requests { layout: grid; grid-size: 6 5; - grid-columns: 0.7fr 0.5fr 0.1fr 1fr 1fr 1fr; + grid-columns: 0.7fr 0.5fr 0.5fr 1fr 1fr 1fr; grid-rows: 1 3 3 3 3; max-height: 20; grid-gutter: 1; @@ -29,26 +29,26 @@ Requests { } #nodes-container { - row-span: 4; + row-span: 3; max-width: 24; } #reload-nodes{ - row-span:2; height:100%; align: center middle; - text-align: center; - margin: 1 0; + text-align: center; + } #nodes { height: 100%; } -#timeframe,#nslc{ - column-span: 4; +#send-request{ + column-span:4; + width:100%; } -#options, #send-request { +#options,#nslc,#timeframe{ column-span: 5; - width:100% + width:100%; } .request-label { diff --git a/src/a10y/app.py b/src/a10y/app.py index 42733c4..ac238a3 100644 --- a/src/a10y/app.py +++ b/src/a10y/app.py @@ -101,6 +101,20 @@ def save_nodes_to_cache(self, nodes): with open(CACHE_FILE, "w", encoding="utf-8") as f: json.dump({"nodes": nodes}, f) + def on_checkbox_changed(self, event: Checkbox.Changed) -> None: + """Toggle between 'Select all' and 'Deselect all' when the checkbox is clicked.""" + all_nodes_checkbox = self.query_one("#all-nodes") # Get the checkbox widget + nodes_list = self.query_one("#nodes") # Get the nodes list + + if all_nodes_checkbox.value: + nodes_list.deselect_all() + all_nodes_checkbox.label = "Select all" # ✅ Change to "Select all" + else: + nodes_list.select_all() + all_nodes_checkbox.label = "Deselect all" # ✅ Change to "Deselect all" + + all_nodes_checkbox.refresh() # ✅ Force UI update + @@ -268,6 +282,7 @@ def change_button_disabled(self, disabled: bool) -> None: @work(exclusive=True, thread=True) async def on_button_pressed(self, event: Button.Pressed) -> None: + self.call_from_thread(lambda: self.change_button_disabled(True)) # Disable the button to prevent multiple clicks if event.button.id == "reload-nodes": button = self.query_one("#reload-nodes") diff --git a/src/a10y/widgets.py b/src/a10y/widgets.py index d1e40b2..355a2e7 100644 --- a/src/a10y/widgets.py +++ b/src/a10y/widgets.py @@ -28,7 +28,7 @@ def __init__(self, nodes_urls, config, *args, **kwargs): def compose(self) -> ComposeResult: yield Static("[b]Requests Control[/b]", id="request-title") yield Container( - Checkbox("Select all Nodes", True, id="all-nodes"), + Checkbox("Deselect all ", False, id="all-nodes"), SelectionList(*self.nodes_urls, id="nodes"), id="nodes-container" @@ -58,7 +58,8 @@ def compose(self) -> ComposeResult: id="nslc" ) - yield Button("Reload Nodes\n(Restart the app)", variant="primary", id="reload-nodes", disabled=False) + + yield Horizontal( Label("Start Time:", classes="request-label"), Input(classes="date-input", id="start", value=self.config["default_starttime"]), @@ -89,6 +90,7 @@ def compose(self) -> ComposeResult: Checkbox("M", self.config["default_quality_M"], id="qm"), id="options" ) + yield Button("Reload Nodes\n(Restart the app)", variant="primary", id="reload-nodes", disabled=False) yield Horizontal( Checkbox("Include Restricted", self.config["default_includerestricted"], id="restricted"), Button("Send", variant="primary", id="request-button",disabled=False), @@ -96,6 +98,7 @@ def compose(self) -> ComposeResult: Button("File", variant="primary", id="file-button"), id="send-request" ) + class Status(Static): From a5183c44e1f440b04fd88225dd9f72935424e25d Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Sat, 22 Feb 2025 13:11:02 +0200 Subject: [PATCH 14/15] animations for loading --- src/a10y/main.py | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/a10y/main.py b/src/a10y/main.py index affa304..13fe64d 100644 --- a/src/a10y/main.py +++ b/src/a10y/main.py @@ -9,6 +9,11 @@ from appdirs import user_cache_dir import json from urllib.parse import urlparse +import time +import threading +import itertools +import sys +from urllib.parse import urlparse # Common constants DEFAULT_NODES = [ ("GFZ", "https://geofon.gfz.de/fdsnws/", True), @@ -41,9 +46,28 @@ def ensure_cache_dir(): """Ensure the cache directory exists.""" CACHE_DIR.mkdir(parents=True, exist_ok=True) + + +QUERY_URL = "https://www.orfeus-eu.org/eidaws/routing/1/globalconfig?format=fdsn" + +def loading_animation(stop_event): + """Display a loading animation while nodes are being fetched.""" + for frame in itertools.cycle(['|', '/', '-', '\\']): # Simple animation + if stop_event.is_set(): + break + sys.stdout.write(f"\rPlease wait... {frame} ") # Overwrite the same line + sys.stdout.flush() + time.sleep(0.5) + sys.stdout.write("\rFetching complete! ✅\n") # Clear animation + def fetch_nodes_from_api(): - """Fetch fresh nodes from API and save to cache.""" + """Fetch fresh nodes from API and save to cache, with a loading animation.""" nodes_urls = [] + stop_event = threading.Event() + + # Start loading animation in a separate thread + animation_thread = threading.Thread(target=loading_animation, args=(stop_event,)) + animation_thread.start() try: response = requests.get(QUERY_URL, timeout=60) @@ -69,12 +93,16 @@ def fetch_nodes_from_api(): if nodes_urls: save_nodes_to_cache(nodes_urls) - return nodes_urls except requests.RequestException as e: logging.warning(f"Failed to fetch nodes from API: {e}") - return None + finally: + stop_event.set() # Stop loading animation + animation_thread.join() # Wait for the animation to stop + + return nodes_urls if nodes_urls else None + def save_nodes_to_cache(nodes): """Save nodes to cache file.""" From e09f2ebccfc5b40ac7e638f4b16df9c39785d759 Mon Sep 17 00:00:00 2001 From: Nikolaos Sokos Date: Mon, 10 Mar 2025 13:33:38 +0200 Subject: [PATCH 15/15] input_changed --- pyproject.toml | 2 +- src/a10y/__init__.py | 2 +- src/a10y/a10y.tcss | 29 +++++++++++++++++++++++++---- src/a10y/app.py | 17 ++++++++++++----- src/a10y/widgets.py | 10 ++++++++-- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3141039..568c3b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "eida-a10y" -version = "1.0.1" +version = "1.0.4" readme = "README.md" requires-python = "<4.0,>=3.11" dependencies = [ diff --git a/src/a10y/__init__.py b/src/a10y/__init__.py index 0479b58..19b3649 100644 --- a/src/a10y/__init__.py +++ b/src/a10y/__init__.py @@ -2,4 +2,4 @@ A10y: A terminal-based availability tool with a Textual UI. """ -__version__ = "1.0.0" \ No newline at end of file +__version__ = "1.0.4" \ No newline at end of file diff --git a/src/a10y/a10y.tcss b/src/a10y/a10y.tcss index 2b788cf..53694a4 100644 --- a/src/a10y/a10y.tcss +++ b/src/a10y/a10y.tcss @@ -1,24 +1,45 @@ Screen { - layout: vertical; + max-width:200; + overflow: hidden; } +Header { + max-width: 200; +} + .box { border: solid green; - min-width: 150; + min-width: 160; + max-width:200; } .hide { display: none; } +Explanations { + border: solid gray; + padding: 1; + background: black; + color: white; +} + #explanations-keys { - margin-left: 2; + text-style: bold; + padding: 1; + width: auto; +} + +.version { + color: cyan; + text-style: bold italic; + padding-top: 1; } Requests { layout: grid; grid-size: 6 5; - grid-columns: 0.7fr 0.5fr 0.5fr 1fr 1fr 1fr; + grid-columns: 0.7fr 0.5fr 0.7fr 1fr 1fr 1fr; grid-rows: 1 3 3 3 3; max-height: 20; grid-gutter: 1; diff --git a/src/a10y/app.py b/src/a10y/app.py index ac238a3..ffae9cd 100644 --- a/src/a10y/app.py +++ b/src/a10y/app.py @@ -17,7 +17,7 @@ from pathlib import Path from appdirs import user_cache_dir from urllib.parse import urlparse - +from a10y import __version__ CACHE_DIR = Path(user_cache_dir("a10y")) CACHE_FILE = CACHE_DIR / "nodes_cache.json" QUERY_URL = "https://www.orfeus-eu.org/eidaws/routing/1/globalconfig?format=fdsn" @@ -28,7 +28,7 @@ def __init__(self, nodes_urls, routing, **kwargs): self.routing = routing # Store routing URL self.config = kwargs # Store remaining settings super().__init__() - + def action_quit(self) -> None: """Ensure terminal resets properly when quitting.""" self.exit() @@ -39,15 +39,18 @@ def action_quit(self) -> None: CSS_PATH = "a10y.tcss" BINDINGS = [ + Binding("ctrl+c", "quit", "Quit"), Binding("tab/shift+tab", "navigate", "Navigate"), Binding("ctrl+s", "send_button", "Send Request"), Binding("?", "toggle_help", "Help"), - Binding("Submit Issues", "", "https://github.com/EIDA/a10y/issues"), + Binding("Submit Issues", "", "https://github.com/EIDA/a10y/issues",show=True), + Binding("Version","",f"{__version__}"), Binding("ctrl+t", "first_line", "Move to first line", show=False), Binding("ctrl+b", "last_line", "Move to last line", show=False), Binding("t", "lines_view", "Toggle view to lines", show=False), Binding("escape", "cancel_request", "Cancel request", show=False), + ] req_text = "" @@ -170,7 +173,7 @@ def parallel_requests_autocomplete(self, url, data) -> None: @work(exclusive=True, thread=True) - def on_input_submitted(self, event: Input.Submitted) -> None: + def on_input_changed(self, event: Input.Changed) -> None: """A function to change status when an NSLC input field is submitted (i.e. is typed and enter is hit)""" # COULD BE ON Change # keep app responsive while making requests @@ -386,7 +389,11 @@ async def show_results(self, r): infoBar = Static("Quality: Timestamp: Trace start: Trace end: ", id="info-bar") self.query_one('#lines').mount(infoBar) self.query_one('#lines').mount(ScrollableContainer(id="results-container")) - num_spans = 130 + # Dynamically calculate num_spans based on the results container width + num_spans = self.query_one("#results-widget").size.width // 2 # Scale width properly + num_spans = max(num_spans, 160) # Ensure a reasonable span count + + if not self.query_one("#start").value.strip(): self.query_one("#status-line").update( f"{self.query_one('#status-line').renderable}\n[orange1]⚠️ Please enter a start date![/orange1]" diff --git a/src/a10y/widgets.py b/src/a10y/widgets.py index 355a2e7..b414df2 100644 --- a/src/a10y/widgets.py +++ b/src/a10y/widgets.py @@ -8,15 +8,21 @@ from textual import events from rich.text import Text from rich.cells import get_character_cell_size +from a10y import __version__ + class Explanations(Static): """Explanations box with common key functions""" def compose(self) -> ComposeResult: - yield Static("[b]Useful Keys[/b]") + yield Static("[b]Useful Keys[/b]", id="explanations-title") yield Static( """[gold3]ctrl+c[/gold3]: close app [gold3]tab/shif+tab[/gold3]: cycle through options [gold3]ctrl+s[/gold3]: send request [gold3]esc[/gold3]: cancel request [gold3]up/down/pgUp/pgDown[/gold3]: scroll up/down if in scrollable window""", - id="explanations-keys") + id="explanations-keys" + ) + yield Static(f"[b]Version:[/b] {__version__}", classes="version") # Styled version + + class Requests(Static):