pyvo
Advanced tools
@@ -25,5 +25,5 @@ # This test job is separated out into its own workflow to be able to trigger separately | ||
| steps: | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | ||
| - name: Set up Python 3.13 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 | ||
| with: | ||
@@ -37,3 +37,3 @@ python-version: "3.13" | ||
| - name: Upload coverage to codecov | ||
| uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 | ||
| uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 | ||
| with: | ||
@@ -47,5 +47,5 @@ file: ./coverage.xml | ||
| steps: | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | ||
| - name: Set up Python 3.14 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 | ||
| with: | ||
@@ -52,0 +52,0 @@ python-version: "3.14-dev" |
@@ -39,7 +39,7 @@ # Developer version testing is in separate workflow | ||
| - name: Checkout code | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 | ||
| with: | ||
@@ -61,7 +61,7 @@ python-version: ${{ matrix.python-version }} | ||
| - name: Checkout code | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 | ||
| with: | ||
@@ -80,5 +80,5 @@ python-version: '3.12' | ||
| steps: | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | ||
| - name: Set up Python 3.12 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 | ||
| with: | ||
@@ -97,5 +97,5 @@ python-version: '3.12' | ||
| steps: | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 | ||
| - name: Set up Python 3.10 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 | ||
| with: | ||
@@ -102,0 +102,0 @@ python-version: '3.10' |
+19
-0
@@ -0,1 +1,20 @@ | ||
| 1.8.1 (2026-02-12) | ||
| ================== | ||
| Bug Fixes | ||
| --------- | ||
| - Pass session through to DatalinkService requests. [#716] | ||
| - Add DALRateLimitError for HTTP 429 responses with retry timing info. [#718] | ||
| - Fix a bug in the space frame equinox processing. [#710] | ||
| - Add DEFAULT_JOB_POLL_TIMEOUT constant. [#721] | ||
| - Fix SIA2 overflow warnings. [#727] | ||
| - Add timeout parameter to run_async method in tap module. [#730] | ||
| 1.8 (2025-11-13) | ||
@@ -2,0 +21,0 @@ ================ |
+14
-0
@@ -671,2 +671,16 @@ .. _pyvo-data-access: | ||
| Job polling timeout | ||
| ------------------- | ||
| When polling for job status, pyVO uses a default timeout of 10 seconds for | ||
| each request. Some services may occasionally respond slowly, causing | ||
| ``ReadTimeout`` errors during polling. If you experience intermittent timeout | ||
| failures when polling job status, you can try increasing this timeout:: | ||
| import pyvo.dal.tap | ||
| pyvo.dal.tap.DEFAULT_JOB_POLL_TIMEOUT = 30 # seconds | ||
| This is a module-level setting that affects all follow-up async job operations. | ||
| Note that a response time of 10 or more seconds for a status request | ||
| would likely indicate that the service may be experiencing issues. | ||
| Also, :py:class:`pyvo.dal.AsyncTAPJob` works as a context manager which | ||
@@ -673,0 +687,0 @@ takes care of this automatically: |
+16
-11
@@ -53,2 +53,3 @@ ****************************************************** | ||
| >>> import astropy.units as u | ||
| >>> from astropy.io.votable import parse | ||
| >>> from astropy.coordinates import SkyCoord | ||
@@ -59,3 +60,3 @@ >>> from pyvo.dal.scs import SCSService | ||
| >>> | ||
| >>> scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main") | ||
| >>> scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/311/hip2") | ||
| >>> m_viewer = MivotViewer( | ||
@@ -100,3 +101,3 @@ ... scs_srv.search( | ||
| >>> print(mivot_instance.spaceSys.frame.spaceRefFrame.value) | ||
| ICRS | ||
| FK5 | ||
@@ -107,3 +108,4 @@ .. doctest-skip:: | ||
| >>> print(f"position: {mivot_instance.latitude.value} {mivot_instance.longitude.value}") | ||
| position: 59.94033461 52.26722684 | ||
| position: 59.90665631 52.12106214 | ||
| position: 59.94033468 52.26722736 | ||
| .... | ||
@@ -132,15 +134,18 @@ | ||
| { | ||
| "dmtype": "coords:SpaceSys", | ||
| "dmid": "SpaceFrame_ICRS", | ||
| "frame": { | ||
| "dmtype": "coords:SpaceSys", | ||
| "dmid": "SpaceFrame_ICRS", | ||
| "frame": { | ||
| "dmrole": "coords:PhysicalCoordSys.frame", | ||
| "dmtype": "coords:SpaceFrame", | ||
| "spaceRefFrame": { | ||
| "dmtype": "ivoa:string", | ||
| "value": "ICRS" | ||
| } | ||
| } | ||
| "dmtype": "ivoa:string", | ||
| "value": "FK5" | ||
| }, | ||
| "equinox": { | ||
| "dmtype": "coords:Epoch", | ||
| "value": "J2000" | ||
| } | ||
| } | ||
| } | ||
| As you can see from the previous examples, model leaves (class attributes) are complex types. | ||
@@ -147,0 +152,0 @@ This is because they contain additional metadata as well as values: |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: pyvo | ||
| Version: 1.8 | ||
| Version: 1.8.1 | ||
| Summary: Astropy affiliated package for accessing Virtual Observatory data and services | ||
@@ -5,0 +5,0 @@ Author: the PyVO Developers |
| Metadata-Version: 2.4 | ||
| Name: pyvo | ||
| Version: 1.8 | ||
| Version: 1.8.1 | ||
| Summary: Astropy affiliated package for accessing Virtual Observatory data and services | ||
@@ -5,0 +5,0 @@ Author: the PyVO Developers |
+1
-1
@@ -42,2 +42,2 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| DALAccessError, DALProtocolError, DALFormatError, DALServiceError, | ||
| DALQueryError) | ||
| DALQueryError, DALRateLimitError) |
@@ -21,3 +21,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| DALAccessError, DALProtocolError, DALFormatError, DALServiceError, | ||
| DALQueryError, DALOverflowWarning) | ||
| DALQueryError, DALOverflowWarning, DALRateLimitError) | ||
@@ -35,2 +35,2 @@ __all__ = [ | ||
| "DALAccessError", "DALProtocolError", "DALFormatError", "DALServiceError", | ||
| "DALQueryError", "DALOverflowWarning"] | ||
| "DALQueryError", "DALOverflowWarning", "DALRateLimitError"] |
@@ -513,3 +513,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| return DatalinkQuery( | ||
| self.baseurl, id=id, responseformat=responseformat, **keywords) | ||
| self.baseurl, id=id, responseformat=responseformat, session=self._session, **keywords) | ||
@@ -516,0 +516,0 @@ |
+155
-1
@@ -7,5 +7,7 @@ """ | ||
| "DALAccessError", "DALProtocolError", "DALFormatError", "DALServiceError", | ||
| "DALQueryError", "DALOverflowWarning"] | ||
| "DALQueryError", "DALOverflowWarning", "DALRateLimitError"] | ||
| import re | ||
| from datetime import datetime, timezone | ||
| from email.utils import parsedate_to_datetime | ||
@@ -188,2 +190,6 @@ import requests | ||
| code = response.status_code | ||
| if code == 429: | ||
| return DALRateLimitError.from_response(response, exc, url) | ||
| content_type = response.headers.get('content-type', None) | ||
@@ -203,2 +209,150 @@ if content_type and 'text/plain' in content_type: | ||
| class DALRateLimitError(DALServiceError): | ||
| """ | ||
| Exception for HTTP 429 Too Many Requests responses. | ||
| This exception is raised when a DAL service returns a 429 status code, | ||
| indicating that the client has exceeded a rate limit. It provides | ||
| structured access to retry timing information from the Retry-After header | ||
| via the ``retry_after_seconds``, ``retry_after_raw``, and ``retry_after_date`` | ||
| properties. | ||
| """ | ||
| _defreason = "Rate limit exceeded" | ||
| def __init__(self, reason=None, *, code=429, cause=None, url=None, | ||
| retry_after_seconds=None, retry_after_raw=None, | ||
| retry_after_date=None): | ||
| """ | ||
| Initialize the rate limit exception. | ||
| Parameters | ||
| ---------- | ||
| reason : str | ||
| A message describing the error. | ||
| code : int | ||
| The HTTP status code (default 429). | ||
| cause : Exception | ||
| The underlying exception that caused this error. | ||
| url : str | ||
| The query URL that produced the error. | ||
| retry_after_seconds : int or None | ||
| Seconds to wait before retrying. | ||
| retry_after_raw : str or None | ||
| Raw Retry-After header value. | ||
| retry_after_date : datetime or None | ||
| Parsed datetime if header was HTTP-date format. | ||
| """ | ||
| super().__init__(reason, code, cause, url) | ||
| self._retry_after_seconds = retry_after_seconds | ||
| self._retry_after_raw = retry_after_raw | ||
| self._retry_after_date = retry_after_date | ||
| @property | ||
| def retry_after_seconds(self): | ||
| """ | ||
| Seconds to wait before retrying, or None if not specified. | ||
| """ | ||
| return self._retry_after_seconds | ||
| @property | ||
| def retry_after_raw(self): | ||
| """ | ||
| The raw Retry-After header value, or None if not provided. | ||
| """ | ||
| return self._retry_after_raw | ||
| @property | ||
| def retry_after_date(self): | ||
| """ | ||
| If Retry-After was an HTTP-date, the parsed datetime. | ||
| None if it was an integer or not provided. | ||
| """ | ||
| return self._retry_after_date | ||
| @classmethod | ||
| def from_response(cls, response, cause=None, url=None): | ||
| """ | ||
| Create a DALRateLimitError from an HTTP response. | ||
| Parameters | ||
| ---------- | ||
| response : requests.Response | ||
| The HTTP response object with status code 429. | ||
| cause : Exception | ||
| The underlying exception that caused this error. | ||
| url : str | ||
| The query URL that produced the error. | ||
| Returns | ||
| ------- | ||
| DALRateLimitError | ||
| A new exception instance with parsed retry information. | ||
| """ | ||
| retry_after_raw = None | ||
| for header_name in response.headers: | ||
| if header_name.lower() == 'retry-after': | ||
| retry_after_raw = response.headers[header_name] | ||
| break | ||
| retry_after_seconds = None | ||
| retry_after_date = None | ||
| if retry_after_raw is not None: | ||
| retry_after_seconds, retry_after_date = cls._parse_retry_after( | ||
| retry_after_raw) | ||
| if url: | ||
| message = f"Rate limit exceeded (HTTP 429) for {url}" | ||
| else: | ||
| message = "Rate limit exceeded (HTTP 429)" | ||
| if retry_after_seconds is not None: | ||
| message += f". Retry after {retry_after_seconds} seconds" | ||
| if retry_after_date: | ||
| message += f" (at {retry_after_raw})" | ||
| return cls( | ||
| reason=message, | ||
| code=429, | ||
| cause=cause, | ||
| url=url, | ||
| retry_after_seconds=retry_after_seconds, | ||
| retry_after_raw=retry_after_raw, | ||
| retry_after_date=retry_after_date | ||
| ) | ||
| @staticmethod | ||
| def _parse_retry_after(value): | ||
| """ | ||
| Parse a Retry-After header value. | ||
| Parameters | ||
| ---------- | ||
| value : str | ||
| The Retry-After header value (integer seconds or date). | ||
| Returns | ||
| ------- | ||
| tuple | ||
| (seconds, date) where seconds is an int and date is a datetime. | ||
| For integer format, date is None. For a date format, both are set. | ||
| Returns (None, None) if parsing fails. | ||
| """ | ||
| try: | ||
| seconds = int(value) | ||
| return max(0, seconds), None | ||
| except ValueError: | ||
| pass | ||
| try: | ||
| date = parsedate_to_datetime(value) | ||
| if date.tzinfo is None: | ||
| date = date.replace(tzinfo=timezone.utc) | ||
| now = datetime.now(timezone.utc) | ||
| seconds = max(0, int((date - now).total_seconds())) | ||
| return seconds, date | ||
| except ValueError: | ||
| return None, None | ||
| class DALQueryError(DALAccessError): | ||
@@ -205,0 +359,0 @@ """ |
+17
-4
@@ -453,5 +453,6 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| def maxrec(self, val): | ||
| if not isinstance(val, int): | ||
| if not val: | ||
| return | ||
| if val is None: | ||
| self._maxrec = None | ||
| return | ||
| if not isinstance(val, int) or val < 0: | ||
| raise ValueError(f'maxrec {val} must be non-negative int') | ||
@@ -475,3 +476,5 @@ self._maxrec = val | ||
| """ | ||
| return SIA2Results(self.execute_votable(), url=self.queryurl, session=self._session) | ||
| result = SIA2Results(self.execute_votable(), url=self.queryurl, session=self._session) | ||
| result.check_overflow_warning(self._maxrec) | ||
| return result | ||
@@ -521,2 +524,12 @@ | ||
| def _handle_overflow_warning(self, client_set_maxrec=None): | ||
| """ | ||
| SIA2 overflow warning handling. | ||
| For SIA2 results we suppress the default overflow warning during | ||
| initialization because SIA2Query.execute() will call | ||
| check_overflow_warning() after the results are initialized. | ||
| """ | ||
| pass | ||
| def getrecord(self, index): | ||
@@ -523,0 +536,0 @@ """ |
+20
-5
@@ -32,3 +32,4 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| __all__ = [ | ||
| "search", "escape", "TAPService", "TAPQuery", "AsyncTAPJob", "TAPResults"] | ||
| "search", "escape", "TAPService", "TAPQuery", "AsyncTAPJob", "TAPResults", | ||
| "DEFAULT_JOB_POLL_TIMEOUT", "DEFAULT_JOB_WAIT_TIMEOUT"] | ||
@@ -49,3 +50,9 @@ IVOA_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" | ||
| # Default timeout (in seconds) for job status polling requests. | ||
| DEFAULT_JOB_POLL_TIMEOUT = 10 | ||
| # Default timeout (in seconds) for overall job wait. | ||
| DEFAULT_JOB_WAIT_TIMEOUT = 600. | ||
| def _from_ivoa_format(datetime_str): | ||
@@ -297,3 +304,3 @@ """ | ||
| self, query, *, language="ADQL", maxrec=None, uploads=None, | ||
| delete=True, **keywords): | ||
| delete=True, timeout=DEFAULT_JOB_WAIT_TIMEOUT, **keywords): | ||
| """ | ||
@@ -315,2 +322,4 @@ runs async query and returns its result | ||
| delete the job after fetching the results | ||
| timeout : float | ||
| maximum time to wait for job completion in seconds. Default is 600. | ||
@@ -339,3 +348,3 @@ Returns | ||
| session=self._session, **keywords) | ||
| job = job.run().wait() | ||
| job = job.run().wait(timeout=timeout) | ||
@@ -667,2 +676,6 @@ try: | ||
| response = tapquery.submit() | ||
| try: | ||
| response.raise_for_status() | ||
| except requests.RequestException as ex: | ||
| raise DALServiceError.from_except(ex, tapquery.queryurl) | ||
| job = cls(response.url, session=session) | ||
@@ -709,6 +722,8 @@ job._client_set_maxrec = maxrec | ||
| def _update(self, wait_for_statechange=False, timeout=10.): | ||
| def _update(self, wait_for_statechange=False, timeout=None): | ||
| """ | ||
| updates local job infos with remote values | ||
| """ | ||
| if timeout is None: | ||
| timeout = DEFAULT_JOB_POLL_TIMEOUT | ||
| try: | ||
@@ -970,3 +985,3 @@ if wait_for_statechange: | ||
| def wait(self, *, phases=None, timeout=600.): | ||
| def wait(self, *, phases=None, timeout=DEFAULT_JOB_WAIT_TIMEOUT): | ||
| """ | ||
@@ -973,0 +988,0 @@ waits for the job to reach the given phases. |
@@ -10,2 +10,3 @@ #!/usr/bin/env python | ||
| import requests_mock | ||
| import warnings | ||
@@ -15,3 +16,3 @@ import pytest | ||
| from pyvo.dal.sia2 import search, SIA2Service, SIA2Query, SIAService, SIAQuery | ||
| from pyvo.dal.exceptions import DALServiceError | ||
| from pyvo.dal.exceptions import DALServiceError, DALOverflowWarning | ||
@@ -270,1 +271,68 @@ import astropy.units as u | ||
| SIA2Service('http://example.com/sia') | ||
| @pytest.fixture() | ||
| def sia2_overflow_fixture(mocker): | ||
| """Mock SIA2 service that returns overflow status with exactly 10 records""" | ||
| def callback(request, context): | ||
| votable_content = '''<?xml version="1.0" encoding="UTF-8"?> | ||
| <VOTABLE version="1.3" xmlns="http://www.ivoa.net/xml/VOTable/v1.3"> | ||
| <RESOURCE type="results"> | ||
| <INFO name="QUERY_STATUS" value="OVERFLOW">Result truncated</INFO> | ||
| <TABLE> | ||
| <FIELD name="obs_collection" datatype="char" arraysize="128*"/> | ||
| <FIELD name="obs_id" datatype="char" arraysize="128*"/> | ||
| <FIELD name="facility_name" datatype="char" arraysize="128*"/> | ||
| <FIELD name="instrument_name" datatype="char" arraysize="128*"/> | ||
| <DATA> | ||
| <TABLEDATA>''' + ''.join( | ||
| f'<TR><TD>TEST</TD><TD>obs{i}</TD><TD>TEST</TD><TD>TEST</TD></TR>' | ||
| for i in range(10)) + ''' | ||
| </TABLEDATA> | ||
| </DATA> | ||
| </TABLE> | ||
| </RESOURCE> | ||
| </VOTABLE>''' | ||
| return votable_content.encode('utf-8') | ||
| with mocker.register_uri( | ||
| 'GET', 'https://example.com/sia/capabilities', | ||
| content=get_pkg_data_contents('data/sia2/capabilities.xml') | ||
| ): | ||
| with mocker.register_uri( | ||
| 'GET', sia_re, content=callback | ||
| ): | ||
| yield mocker | ||
| @pytest.mark.usefixtures('sia2_overflow_fixture') | ||
| @pytest.mark.filterwarnings("ignore::astropy.io.votable.exceptions.W06") | ||
| class TestSIA2OverflowWarnings: | ||
| """Test SIA2 overflow warning behavior""" | ||
| def test_no_maxrec_overflow_warning(self): | ||
| service = SIA2Service('https://example.com/sia') | ||
| with pytest.warns(DALOverflowWarning, match="Results truncated due to server limits"): | ||
| results = service.search(pos=(33.3, 4.2, 0.1)) | ||
| assert len(results) == 10 | ||
| def test_maxrec_exact_match_no_warning(self): | ||
| service = SIA2Service('https://example.com/sia') | ||
| with warnings.catch_warnings(record=True) as w: | ||
| warnings.simplefilter("always") | ||
| results = service.search(pos=(33.3, 4.2, 0.1), maxrec=10) | ||
| overflow_warnings = [warning for warning in w | ||
| if issubclass(warning.category, DALOverflowWarning)] | ||
| assert len(overflow_warnings) == 0, f"Unexpected overflow warnings: {overflow_warnings}" | ||
| assert len(results) == 10 | ||
| def test_maxrec_service_truncation_warning(self): | ||
| service = SIA2Service('https://example.com/sia') | ||
| with pytest.warns(DALOverflowWarning, | ||
| match=r"Results truncated at 10 records by service limits" | ||
| r".*you requested maxrec=100"): | ||
| results = service.search(pos=(33.3, 4.2, 0.1), maxrec=100) | ||
| assert len(results) == 10 |
+9
-1
@@ -11,3 +11,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| from .exceptions import DALServiceError | ||
| from .exceptions import DALServiceError, DALRateLimitError | ||
| from ..io import vosi | ||
@@ -66,2 +66,5 @@ from ..utils.url import url_sibling | ||
| if error.response.status_code == 429: | ||
| raise DALRateLimitError.from_response(error.response, error, url) | ||
| if error.response.status_code == 503: | ||
@@ -201,2 +204,7 @@ # Handle Retry-After header, if present | ||
| break | ||
| except requests.HTTPError as ex: | ||
| if ex.response.status_code == 429: | ||
| raise DALRateLimitError.from_response(ex.response, ex, | ||
| tables_url) | ||
| continue | ||
| except requests.RequestException: | ||
@@ -203,0 +211,0 @@ continue |
@@ -92,6 +92,10 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| """ | ||
| # Process complex type "mango:DateTime | ||
| # Process complex type "mango:DateTime" | ||
| if hk_field['dmtype'] == "mango:DateTime": | ||
| representation = hk_field['representation']['value'] | ||
| timestamp = hk_field['dateTime']['value'] | ||
| # Process complex type "coords:epoch" used for the space frame equinox | ||
| elif hk_field['dmtype'] == "coords:Epoch": | ||
| representation = 'yr' if "unit" not in hk_field else hk_field.get("unit") | ||
| timestamp = hk_field['value'] | ||
| # Process simple attribute | ||
@@ -106,5 +110,6 @@ else: | ||
| time_instance = self. _build_time_instance(timestamp, representation, besselian) | ||
| time_instance = self._build_time_instance(timestamp, representation, besselian) | ||
| if not time_instance: | ||
| raise MappingError(f"Cannot build a Time instance from {hk_field}") | ||
| mode = "besselian" if besselian else "julian" | ||
| raise MappingError(f"Cannot build a Time instance from {hk_field} ({mode} date)") | ||
@@ -180,3 +185,2 @@ return time_instance | ||
| frame = coo_sys["spaceRefFrame"]["value"].lower() | ||
| if frame == 'fk4': | ||
@@ -183,0 +187,0 @@ self._map_coord_names = SkyCoordMapping.default_params |
@@ -12,7 +12,6 @@ ''' | ||
| from astropy.utils.data import get_pkg_data_filename | ||
| from astropy import units as u | ||
| from pyvo.mivot.version_checker import check_astropy_version | ||
| from pyvo.mivot.viewer.mivot_instance import MivotInstance | ||
| from pyvo.mivot.features.sky_coord_builder import SkyCoordBuilder | ||
| from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError | ||
| from pyvo.mivot.utils.exceptions import NoMatchingDMTypeError, MappingError | ||
| from pyvo.mivot.viewer.mivot_viewer import MivotViewer | ||
@@ -122,5 +121,4 @@ from pyvo.utils import activate_features | ||
| "equinox": { | ||
| "dmtype": "coords:SpaceFrame.equinox", | ||
| "dmtype": "coords:Epoch", | ||
| "value": "2012", | ||
| "unit": "yr", | ||
| } | ||
@@ -181,3 +179,3 @@ } | ||
| "equinox": { | ||
| "dmtype": "coords:SpaceFrame.equinox", | ||
| "dmtype": "coords:Epoch", | ||
| "value": "2012", | ||
@@ -190,2 +188,23 @@ "unit": "yr", | ||
| def check_skycoo(scoo, ra, dec, distance, pm_ra_cosdec, pm_dec, obstime): | ||
| """ | ||
| Check the SkyCoord instance against the constant values given as parameters | ||
| """ | ||
| try: | ||
| assert scoo.ra.degree == pytest.approx(ra) | ||
| assert scoo.dec.degree == pytest.approx(dec) | ||
| if distance: | ||
| assert scoo.distance.pc == pytest.approx(distance) | ||
| if pm_ra_cosdec: | ||
| assert scoo.pm_ra_cosdec.value == pytest.approx(pm_ra_cosdec) | ||
| if pm_dec: | ||
| assert scoo.pm_dec.value == pytest.approx(pm_dec) | ||
| except AttributeError: | ||
| assert scoo.galactic.l.degree == pytest.approx(ra) | ||
| assert scoo.galactic.b.degree == pytest.approx(dec) | ||
| if obstime: | ||
| assert str(scoo.obstime) == obstime | ||
| def test_no_matching_mapping(): | ||
@@ -208,9 +227,5 @@ """ | ||
| scoo = scb.build_sky_coord() | ||
| assert (str(scoo).replace("\n", "").replace(" ", "") | ||
| == "<SkyCoord (ICRS): (ra, dec) in deg(52.26722684, 59.94033461) " | ||
| "(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>") | ||
| scoo = mivot_instance.get_SkyCoord() | ||
| assert (str(scoo).replace("\n", "").replace(" ", "") | ||
| == "<SkyCoord (ICRS): (ra, dec) in deg(52.26722684, 59.94033461) " | ||
| "(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>") | ||
| check_skycoo(scoo, 52.26722684, 59.94033461, None, | ||
| -0.82, -1.85, | ||
| None) | ||
@@ -223,2 +238,5 @@ vizier_dict["spaceSys"]["frame"]["spaceRefFrame"]["value"] = "Galactic" | ||
| "(pm_l_cosb, pm_b) in mas / yr(-0.82, -1.85)>") | ||
| check_skycoo(scoo, 52.26722684, 59.94033461, None, | ||
| -0.82, -1.85, | ||
| None) | ||
@@ -228,5 +246,5 @@ vizier_dict["spaceSys"]["frame"]["spaceRefFrame"]["value"] = "QWERTY" | ||
| scoo = mivot_instance.get_SkyCoord() | ||
| assert (str(scoo).replace("\n", "").replace(" ", "") | ||
| == "<SkyCoord (ICRS): (ra, dec) in deg(52.26722684, 59.94033461) " | ||
| "(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>") | ||
| check_skycoo(scoo, 52.26722684, 59.94033461, None, | ||
| -0.82, -1.85, | ||
| "J1991.250") | ||
@@ -242,6 +260,5 @@ | ||
| scoo = scb.build_sky_coord() | ||
| assert (str(scoo).replace("\n", "").replace(" ", "") | ||
| == "<SkyCoord (FK5: equinox=J2012.000): (ra, dec, distance) in " | ||
| "(deg, deg, pc)(52.26722684, 59.94033461, 1666.66666667) " | ||
| "(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>") | ||
| check_skycoo(scoo, 52.26722684, 59.94033461, 1666.66666667, | ||
| -0.82, -1.85, | ||
| "J1991.250") | ||
@@ -252,8 +269,14 @@ mydict = deepcopy(vizier_equin_dict) | ||
| scoo = mivot_instance.get_SkyCoord() | ||
| assert (str(scoo).replace("\n", "").replace(" ", "") | ||
| == "<SkyCoord (FK4: equinox=B2012.000, obstime=B1991.250): (ra, dec, distance) in " | ||
| "(deg, deg, pc)(52.26722684, 59.94033461, 1666.66666667) " | ||
| "(pm_ra_cosdec, pm_dec) in mas / yr(-0.82, -1.85)>") | ||
| check_skycoo(scoo, 52.26722684, 59.94033461, 1666.66666667, | ||
| -0.82, -1.85, | ||
| "B1991.250") | ||
| mydict = deepcopy(vizier_equin_dict) | ||
| mydict["spaceSys"]["frame"]["spaceRefFrame"]["value"] = "FK4" | ||
| mydict["spaceSys"]["frame"]["equinox"]["value"] = "J2012" | ||
| with pytest.raises(MappingError, match=r".*besselian date.*"): | ||
| mivot_instance = MivotInstance(**mydict) | ||
| scoo = mivot_instance.get_SkyCoord() | ||
| @pytest.mark.skipif(not check_astropy_version(), reason="need astropy 6+") | ||
@@ -269,12 +292,5 @@ def test_simad_cs_output(): | ||
| assert scoo.ra.degree == pytest.approx(269.45207696) | ||
| assert scoo.dec.degree == pytest.approx(4.69336497) | ||
| assert scoo.distance.pc == pytest.approx(1.82823411) | ||
| x = scoo.pm_ra_cosdec.value | ||
| y = (-801.551 * u.mas/u.yr).value | ||
| assert x == pytest.approx(y) | ||
| x = scoo.pm_dec.value | ||
| y = (10362.394 * u.mas/u.yr).value | ||
| assert x == pytest.approx(y) | ||
| assert str(scoo.obstime) == "J2000.000" | ||
| check_skycoo(scoo, 269.45207696, 4.69336497, 1.82823411, | ||
| -801.551, 10362.394, | ||
| "J2000.000") | ||
@@ -281,0 +297,0 @@ |
+1
-1
@@ -8,2 +8,2 @@ # Note that we need to fall back to the hard-coded version if either | ||
| except Exception: | ||
| version = '1.8' | ||
| version = '1.8.1' |
+2
-0
@@ -18,2 +18,3 @@ [tool:pytest] | ||
| ignore:pyvo.discover:pyvo.utils.prototype.PrototypeWarning | ||
| ignore:leap-second auto-update failed::astropy | ||
@@ -76,2 +77,3 @@ [flake8] | ||
| pyvo.registry.tests = data/*.xml, data/*.desise | ||
| pyvo.mivot.writer = *.xsd | ||
| pyvo.mivot.tests = data/*.xml, data/input/*.xml, data/output/*.xml, data/reference/*json, data/reference/*xml | ||
@@ -78,0 +80,0 @@ pyvo.dal.tests = data/*.xml, data/*/* |
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
4153126
0.41%26884
1.34%