pyvo
Advanced tools
@@ -23,15 +23,16 @@ # This test job is separated out into its own workflow to be able to trigger separately | ||
| runs-on: ubuntu-latest | ||
| name: linux (3.13 py313-test-devdeps-alldeps-cov) | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| - name: Set up Python 3.12 | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - name: Set up Python 3.13 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| with: | ||
| python-version: "3.12" | ||
| python-version: "3.13" | ||
| - name: Install tox | ||
| run: python -m pip install --upgrade tox | ||
| - name: Run tests against dev dependencies | ||
| run: tox -e py312-test-devdeps-alldeps-cov | ||
| run: tox -e py313-test-devdeps-alldeps-cov | ||
| - name: Upload coverage to codecov | ||
| uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 | ||
| uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 | ||
| with: | ||
@@ -41,13 +42,14 @@ file: ./coverage.xml | ||
| py313: | ||
| py314: | ||
| runs-on: ubuntu-latest | ||
| name: linux (3.14 py314-test) | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| - name: Set up Python 3.13 | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - name: Set up Python 3.14 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| with: | ||
| python-version: "3.13-dev" | ||
| python-version: "3.14-dev" | ||
| - name: Install tox | ||
| run: python -m pip install --upgrade tox | ||
| - name: Run tests against dev dependencies | ||
| run: tox -e py313-test | ||
| - name: Run tests | ||
| run: tox -e py314-test |
@@ -23,2 +23,3 @@ # Developer version testing is in separate workflow | ||
| runs-on: ubuntu-latest | ||
| name: linux | ||
| strategy: | ||
@@ -28,19 +29,18 @@ fail-fast: false | ||
| include: | ||
| - name: py39 oldest dependencies, Linux | ||
| python-version: '3.9' | ||
| - python-version: '3.9' | ||
| tox_env: py39-test-oldestdeps-alldeps | ||
| - name: py310 mandatory dependencies only, Linux | ||
| python-version: '3.10' | ||
| - python-version: '3.10' | ||
| tox_env: py310-test | ||
| - name: py311 with online tests, Linux | ||
| python-version: '3.11' | ||
| tox_env: py311-test-alldeps-online | ||
| - python-version: '3.11' | ||
| tox_env: py311-test-alldeps | ||
| - python-version: '3.13' | ||
| tox_env: py313-test-alldeps | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| with: | ||
@@ -55,3 +55,3 @@ python-version: ${{ matrix.python-version }} | ||
| runs-on: ${{ matrix.os }} | ||
| name: ${{ matrix.os }} py310 | ||
| name: ${{ matrix.os }} (3.12 py312-test-alldeps) | ||
| strategy: | ||
@@ -63,7 +63,7 @@ fail-fast: false | ||
| - name: Checkout code | ||
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Set up Python | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| with: | ||
@@ -73,3 +73,3 @@ python-version: '3.12' | ||
| run: python -m pip install --upgrade tox | ||
| - name: Python 3.12 with latest astropy | ||
| - name: Python 3.12 with latest dependencies | ||
| run: tox -e py312-test-alldeps | ||
@@ -83,5 +83,5 @@ | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - name: Set up Python 3.12 | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| with: | ||
@@ -100,5 +100,5 @@ python-version: '3.12' | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||
| - name: Set up Python 3.10 | ||
| uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 | ||
| uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 | ||
| with: | ||
@@ -105,0 +105,0 @@ python-version: '3.10' |
+11
-0
@@ -0,1 +1,12 @@ | ||
| 1.7.1 (2025-10-16) | ||
| ================== | ||
| Bug Fixes | ||
| --------- | ||
| - Fix job result handling to prioritize standard URL structure [#684] | ||
| - Improving error messaging for invalid SIAv2 inputs. [#693] | ||
| 1.7 (2025-06-01) | ||
@@ -2,0 +13,0 @@ ================ |
+325
-4
@@ -1,10 +0,331 @@ | ||
| ********************* | ||
| UWS (``pyvo.io.uws``) | ||
| ********************* | ||
| ****************************************** | ||
| Universal Worker Service (``pyvo.io.uws``) | ||
| ****************************************** | ||
| .. currentmodule:: pyvo.io.uws | ||
| Introduction | ||
| ============ | ||
| The Universal Worker Service (UWS) is an IVOA Recommendation that defines a protocol for managing asynchronous jobs in IVOA services | ||
| through a RESTful API that can be used to submit, monitor and control asynchronous operations. | ||
| Getting Started | ||
| =============== | ||
| UWS job documents can be parsed directly from URLs, files, or strings using the parsing functions provided by this module: | ||
| .. doctest-remote-data:: | ||
| >>> import pyvo as vo | ||
| >>> from pyvo.io.uws import parse_job, parse_job_list | ||
| >>> tap_service = vo.dal.TAPService("http://dc.g-vo.org/tap") | ||
| >>> ex_query = """ | ||
| ... SELECT TOP 5 | ||
| ... source_id, ra, dec, phot_g_mean_mag | ||
| ... FROM gaia.dr3lite | ||
| ... WHERE phot_g_mean_mag BETWEEN 19 AND 20 | ||
| ... ORDER BY phot_g_mean_mag | ||
| ... """ | ||
| >>> async_job = tap_service.submit_job(ex_query) | ||
| >>> async_job.run().wait() | ||
| <pyvo.dal.tap.AsyncTAPJob object at 0x...> | ||
| >>> | ||
| >>> # Parse a single job from a UWS service | ||
| >>> job = parse_job(async_job.url) | ||
| >>> | ||
| >>> print(f"Job {job.jobid} is {job.phase}") | ||
| Job ... is COMPLETED | ||
| >>> jobs = parse_job_list('http://dc.g-vo.org/tap/async') | ||
| >>> # Show jobs | ||
| >>> if len(jobs) >= 1: | ||
| ... print(f"Example job: {jobs[0].jobid} is {jobs[0].phase}") | ||
| Example job: ... is ... | ||
| >>> | ||
| >>> print(f"Successfully parsed {len(jobs)} jobs") # doctest: +SKIP | ||
| UWS is most commonly encountered when working with :ref:`TAP services <pyvo_tap>` | ||
| in asynchronous mode. | ||
| The :class:`pyvo.dal.AsyncTAPJob` class provides a higher-level interface that | ||
| uses the UWS parsing functions internally. | ||
| UWS Job Lifecycle | ||
| ================== | ||
| A UWS job progresses through several phases. Most jobs follow the standard progression through the five common states, while the additional states handle special circumstances: | ||
| **Common Job States**: | ||
| * **PENDING**: The job is accepted by the service but not yet committed for execution by the client. In this state, the job quote can be read and evaluated. This is the state into which a job enters when it is first created. | ||
| * **QUEUED**: The job is committed for execution by the client but the service has not yet assigned it to a processor. No results are produced in this phase. | ||
| * **EXECUTING**: The job has been assigned to a processor. Results may be produced at any time during this phase. | ||
| * **COMPLETED**: The execution of the job is over. The results may be collected. | ||
| * **ERROR**: The job failed to complete. No further work will be done nor results produced. Results may be unavailable or available but invalid; either way the results should not be trusted. | ||
| **Special States**: | ||
| * **ABORTED**: The job has been manually aborted by the user, or the system has aborted the job due to lack of or overuse of resources. | ||
| * **HELD**: The job is HELD pending execution and will not automatically be executed (cf. PENDING). | ||
| * **SUSPENDED**: The job has been suspended by the system during execution. This might be because of temporary lack of resources. The UWS will automatically resume the job into the EXECUTING phase without any intervention when resource becomes available. | ||
| * **UNKNOWN**: The job is in an unknown state. | ||
| * **ARCHIVED**: At destruction time the results associated with a job have been deleted to free up resource, but the metadata associated with the job is retained. | ||
| Core Components | ||
| =============== | ||
| Jobs and Job Lists | ||
| ------------------ | ||
| The fundamental UWS concepts are represented by these classes: | ||
| :class:`~pyvo.io.uws.tree.JobSummary` | ||
| Represents a single UWS job with all its metadata, parameters, and results. | ||
| :class:`~pyvo.io.uws.tree.Jobs` | ||
| Represents a list of jobs, typically returned when querying a job list endpoint. | ||
| :class:`~pyvo.io.uws.endpoint.JobFile` | ||
| Represents a complete UWS job XML document. | ||
| Job Parameters and Results | ||
| -------------------------- | ||
| :class:`~pyvo.io.uws.tree.Parameters` | ||
| Container for job input parameters. | ||
| :class:`~pyvo.io.uws.tree.Parameter` | ||
| Individual parameter with an ID and value, optionally referenced by URL. | ||
| :class:`~pyvo.io.uws.tree.Results` | ||
| Container for job output results. | ||
| :class:`~pyvo.io.uws.tree.Result` | ||
| Individual result with metadata like size and MIME type. | ||
| Working with UWS Jobs | ||
| ===================== | ||
| Setting Up Examples | ||
| ------------------- | ||
| For the following examples, we'll use a sample UWS job document: | ||
| >>> import pyvo.io.uws | ||
| >>> from io import BytesIO | ||
| >>> | ||
| >>> # Load sample UWS job from Appendix A | ||
| >>> sample_job_xml = b'''<?xml version="1.0" encoding="UTF-8"?> | ||
| ... <job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> | ||
| ... <jobId>async-query-12345</jobId> | ||
| ... <runId>query-run-id</runId> | ||
| ... <ownerId>user-1</ownerId> | ||
| ... <phase>COMPLETED</phase> | ||
| ... <quote>2025-06-19T14:35:00.000Z</quote> | ||
| ... <creationTime>2025-06-19T14:30:00.000Z</creationTime> | ||
| ... <startTime>2025-06-19T14:30:05.123Z</startTime> | ||
| ... <endTime>2025-06-19T14:32:18.456Z</endTime> | ||
| ... <executionDuration>600</executionDuration> | ||
| ... <destruction>2025-06-26T14:30:00.000Z</destruction> | ||
| ... <parameters> | ||
| ... <parameter id="LANG">ADQL</parameter> | ||
| ... <parameter id="QUERY">SELECT obj_id, ra, dec, magnitude FROM catalog.objects WHERE 1=CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', 180.0, 45.0, 1.0))</parameter> | ||
| ... <parameter id="FORMAT">votable</parameter> | ||
| ... <parameter id="MAXREC">10000</parameter> | ||
| ... </parameters> | ||
| ... <results> | ||
| ... <result id="result" xlink:href="http://example.com/tap/async/async-query-12345/results/result"/> | ||
| ... </results> | ||
| ... </job>''' | ||
| >>> | ||
| >>> job = pyvo.io.uws.parse_job(BytesIO(sample_job_xml)) | ||
| Parsing and Basic Access | ||
| ------------------------ | ||
| >>> # Access basic job information (job created in testsetup above) | ||
| >>> print(f"Job ID: {job.jobid}") | ||
| Job ID: async-query-12345 | ||
| >>> print(f"Phase: {job.phase}") | ||
| Phase: COMPLETED | ||
| >>> print(f"Owner: {job.ownerid}") | ||
| Owner: user-1 | ||
| >>> print(f"Run ID: {job.runid}") | ||
| Run ID: query-run-id | ||
| Timing and Duration Analysis | ||
| ---------------------------- | ||
| UWS jobs include comprehensive timing information automatically parsed as | ||
| :class:`astropy.time.Time` and :class:`astropy.time.TimeDelta` objects: | ||
| >>> # Job times are automatically parsed as astropy Time objects | ||
| >>> print(f"Created: {job.creationtime}") | ||
| Created: 2025-06-19T14:30:00.000 | ||
| >>> print(f"Started: {job.starttime}") | ||
| Started: 2025-06-19T14:30:05.123 | ||
| >>> print(f"Completed: {job.endtime}") | ||
| Completed: 2025-06-19T14:32:18.456 | ||
| >>> | ||
| >>> # Calculate actual execution time | ||
| >>> if job.starttime and job.endtime: | ||
| ... duration = job.endtime - job.starttime | ||
| ... print(f"Job took {duration.to('second').value:.1f} seconds") | ||
| Job took 133.3 seconds | ||
| >>> | ||
| >>> # Check execution limits | ||
| >>> if job.executionduration: | ||
| ... print(f"Max allowed: {job.executionduration.to('minute').value:.1f} minutes") | ||
| Max allowed: 10.0 minutes | ||
| Parameter Inspection | ||
| -------------------- | ||
| Job parameters include both the query itself and configuration options: | ||
| >>> # Iterate through all parameters | ||
| >>> for param in job.parameters: | ||
| ... if param.byreference: | ||
| ... print(f"Parameter {param.id_}: Referenced from {param.content}") | ||
| ... else: | ||
| ... print(f"Parameter {param.id_}: {param.content}") | ||
| Parameter LANG: ADQL | ||
| Parameter QUERY: SELECT obj_id, ra, dec, magnitude FROM catalog.objects WHERE 1=CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', 180.0, 45.0, 1.0)) | ||
| Parameter FORMAT: votable | ||
| Parameter MAXREC: 10000 | ||
| Result Access and Download | ||
| -------------------------- | ||
| Results include the actual data products and associated metadata: | ||
| >>> # Access all results | ||
| >>> for result in job.results: | ||
| ... print(f"Result '{result.id_}':") | ||
| ... print(f" URL: {result.href}") | ||
| Result 'result': | ||
| URL: http://example.com/tap/async/async-query-12345/results/result | ||
| Job Status Monitoring | ||
| ===================== | ||
| Checking Completion Status | ||
| -------------------------- | ||
| >>> # Check if job completed successfully | ||
| >>> if job.phase == 'COMPLETED': | ||
| ... print("Job completed successfully!") | ||
| ... | ||
| ... # Show summary information | ||
| ... total_time = (job.endtime - job.creationtime).to('minute').value | ||
| ... print(f"Total job runtime: {total_time:.1f} minutes") | ||
| ... | ||
| ... elif job.phase == 'ERROR' and job.errorsummary and job.errorsummary.message: | ||
| ... print(f"Job failed: {job.errorsummary.message.content}") | ||
| ... else: | ||
| ... print(f"Job is currently: {job.phase}") | ||
| Job completed successfully! | ||
| Total job runtime: 2.3 minutes | ||
| Working with Job Lists | ||
| ====================== | ||
| Parsing Job Lists | ||
| ----------------- | ||
| While the examples above focus on individual jobs, you can also parse job lists: | ||
| .. doctest-remote-data:: | ||
| >>> from pyvo.io.uws import parse_job_list | ||
| >>> | ||
| >>> # Parse a job list from a UWS service endpoint | ||
| >>> jobs = parse_job_list('http://dc.g-vo.org/tap/async') | ||
| >>> | ||
| >>> # Or from a local file | ||
| >>> # jobs = parse_job_list('job_list.xml') | ||
| >>> | ||
| >>> # Iterate through jobs (each is a JobSummary object) | ||
| >>> for job in jobs: | ||
| ... print(f"Job {job.jobid}: {job.phase}") # doctest: +IGNORE_OUTPUT | ||
| Job tk7xsqux: PENDING | ||
| Job swmua8pe: ERROR | ||
| Job e58i7yoa: ERROR | ||
| Job 84w2yz8q: COMPLETED | ||
| Job 6r51ymds: COMPLETED | ||
| Job undl67gs: COMPLETED | ||
| Job _wyoqule: COMPLETED | ||
| Job ltct6n8d: COMPLETED | ||
| Job 71kg_stz: COMPLETED | ||
| Job sc9vc_8h: ERROR | ||
| Job psn4i8_s: ERROR | ||
| Job kbrhcstw: COMPLETED | ||
| Job lvzez3fa: COMPLETED | ||
| Job l9pfluab: COMPLETED | ||
| Job lkv6rlxx: COMPLETED | ||
| Job yb77nhg3: COMPLETED | ||
| Job vkf4h48y: PENDING | ||
| Job xr3g9c4d: ERROR | ||
| Job x9xryn2x: COMPLETED | ||
| Job wba5foai: COMPLETED | ||
| Job pni8axcg: ERROR | ||
| Job j6ip1kn_: ERROR | ||
| Error Handling | ||
| ============== | ||
| When working with failed jobs, check the error summary: | ||
| >>> # Example of handling a job with errors | ||
| >>> def check_job_status(job_file): | ||
| ... job = parse_job(job_file) | ||
| ... | ||
| ... if job.phase == 'ERROR': | ||
| ... print(f"Job {job.jobid} failed!") | ||
| ... if job.errorsummary: | ||
| ... print(f"Error type: {job.errorsummary.type_}") | ||
| ... if job.errorsummary.message and job.errorsummary.message.content: | ||
| ... print(f"Message: {job.errorsummary.message.content}") | ||
| Reference/API | ||
| ============= | ||
| .. automodapi:: pyvo.io.uws.tree | ||
| .. automodapi:: pyvo.io.uws.endpoint | ||
| .. automodapi:: pyvo.io.uws.tree | ||
| Appendix A: Example UWS Job Document | ||
| ==================================== | ||
| All examples in this documentation reference this sample UWS job XML document. | ||
| .. code-block:: xml | ||
| <?xml version="1.0" encoding="UTF-8"?> | ||
| <job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"> | ||
| <jobId>async-query-12345</jobId> | ||
| <runId>query-run-id</runId> | ||
| <ownerId>user-1</ownerId> | ||
| <phase>COMPLETED</phase> | ||
| <quote>2025-06-19T14:35:00.000Z</quote> | ||
| <creationTime>2025-06-19T14:30:00.000Z</creationTime> | ||
| <startTime>2025-06-19T14:30:05.123Z</startTime> | ||
| <endTime>2025-06-19T14:32:18.456Z</endTime> | ||
| <executionDuration>600</executionDuration> | ||
| <destruction>2025-06-26T14:30:00.000Z</destruction> | ||
| <parameters> | ||
| <parameter id="LANG">ADQL</parameter> | ||
| <parameter id="QUERY">SELECT obj_id, ra, dec, magnitude FROM catalog.objects WHERE 1=CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', 180.0, 45.0, 1.0))</parameter> | ||
| <parameter id="FORMAT">votable</parameter> | ||
| <parameter id="MAXREC">10000</parameter> | ||
| </parameters> | ||
| <results> | ||
| <result id="result" | ||
| xlink:href="http://example.com/tap/async/async-query-12345/results/result"/> | ||
| </results> | ||
| </job> |
+36
-34
@@ -0,1 +1,3 @@ | ||
| .. _mivot-annoter: | ||
| ****************************************************** | ||
@@ -14,3 +16,3 @@ MIVOT (``pyvo.mivot``): Annotation Writer - Public API | ||
| In this current implementation, only 3 properties are supported (Epoch position, photometry, and color) | ||
| in addition to the data origin that can be added as literal values (not connected with table data). | ||
| in addition to the data origin that can be added as literal values (not connected with table data). | ||
@@ -31,3 +33,3 @@ The pseudo codes below show the way to use this API. | ||
| .. code-block:: python | ||
| builder.add_mango_magnitude(photcal_id=photcal_id, mapping=mapping, semantics=semantics) | ||
@@ -38,8 +40,8 @@ builder.add_mango_magnitude(photcal_id=other_photcal_id, mapping=other_mapping, semantics=other_semantics) | ||
| We can now add the description of the data origin. | ||
| We can now add the description of the data origin. | ||
| .. code-block:: python | ||
| builder.add_query_origin(mapping) | ||
| - The order in which the components are added does not matter. | ||
@@ -52,3 +54,3 @@ - The details of the parameters are described below. | ||
| .. code-block:: python | ||
| builder.pack_into_votable() | ||
@@ -68,22 +70,22 @@ votable.to_xml("MY/ANNOTATED/VOTABLE") | ||
| - ``filter/frame``: Map of the coordinate systems or photometric calibrations that apply to the property. | ||
| All values specified here are considered literal. | ||
| All values specified here are considered literal. | ||
| The corresponding Mivot instances are placed in the GLOBALS block. | ||
| - **Photometric filter**: must be given as a filter profile service identifier (filter id followed with the photometric system) | ||
| - Identifiers can be found on the SVO `page <http://svo2.cab.inta-csic.es/theory/fps/>`_ | ||
| (example: ``GAIA/GAIA3.Grp/AB``) | ||
| - The FPS service returns a full calibration instance, which is then split into a filter object | ||
| (example: ``GAIA/GAIA3.Grp/AB``) | ||
| - The FPS service returns a full calibration instance, which is then split into a filter object | ||
| and a calibration object that refers to that filter. | ||
| - Each one of these components has its own ``dmid`` generated by the API. ``dmid`` can be used to reference them. | ||
| Example for filter ``GAIA/GAIA3.Grp/AB``: | ||
| Example for filter ``GAIA/GAIA3.Grp/AB``: | ||
| - photometric calibration identifer: ``dmid="_photcal_GAIA_GAIA3_Grvs_AB"`` | ||
| - filter identifier ``dmid="_filter_GAIA_GAIA3_Grvs_AB"`` | ||
| - filter identifier ``dmid="_filter_GAIA_GAIA3_Grvs_AB"`` | ||
| - **Coordinate systems**: Can be given either by ``dmid`` or by parameters | ||
| (see `pyvo.mivot.writer.InstancesFromModels.add_simple_space_frame` | ||
| (see `pyvo.mivot.writer.InstancesFromModels.add_simple_space_frame` | ||
| and `pyvo.mivot.writer.InstancesFromModels.add_simple_time_frame`). | ||
| - by ``dmid``: Identifiers are generated by the API when a frame is added in the GLOBALS. | ||
| - by ``dmid``: Identifiers are generated by the API when a frame is added in the GLOBALS. | ||
| (example ``{"dmid": "_spaceframe_spaceRefFrame_equinox_refPosition"}``) | ||
@@ -93,3 +95,3 @@ - by parameters: The mapping parameters are given as dictionaries as for the mapping (see below) | ||
| - ``Mapping``: Mapping of the table data to the property attributes. | ||
| - ``Mapping``: Mapping of the table data to the property attributes. | ||
| The fine structure of these dictionaries is specific to each mapped class, | ||
@@ -99,4 +101,4 @@ but all follow the same pattern. | ||
| unless the string starts with a '*'. In this case, the stripped string is taken as the literal value. | ||
| Other value types (numeric or boolean) are all considered as literals. | ||
| Other value types (numeric or boolean) are all considered as literals. | ||
| +-------------+---------------------------------+ | ||
@@ -117,3 +119,3 @@ | **value** | **attribute value** | | ||
| - ``semantics``: Semantic tags (text + vocabulary entry) that apply to the property. | ||
| +-------------+---------------------------------------------------------+ | ||
@@ -129,3 +131,3 @@ + **key** | **value** + | ||
| All ``semantics`` fields are considered literal values. | ||
| All ``semantics`` fields are considered literal values. | ||
@@ -139,3 +141,3 @@ Add Query origin | ||
| :width: 500 | ||
| DataOrigin package of Mango. | ||
@@ -155,3 +157,3 @@ | ||
| The ``QueryOrigin`` object can be automatically built from the INFO tags of the VOtable. The success of this operation depends | ||
| on the way INFO tags are populated. | ||
| on the way INFO tags are populated. | ||
@@ -169,3 +171,3 @@ The method below, analyzes the INFO tags and insert the resulting query origin into the annotations. | ||
| Add Properties | ||
@@ -177,9 +179,9 @@ -------------- | ||
| - The properties are stored in a container named ``propertyDock``. | ||
| - During he annotation process, properties are added one by one by specific methods. | ||
| - The properties are stored in a container named ``propertyDock``. | ||
| - During he annotation process, properties are added one by one by specific methods. | ||
| .. figure:: _images/mangoProperties.png | ||
| :width: 500 | ||
| Properties supported by Mango. | ||
@@ -194,4 +196,4 @@ | ||
| :width: 500 | ||
| EpochPosition components of Mango. | ||
@@ -206,3 +208,3 @@ | ||
| The detail of the parameters is given with the description of the | ||
| The detail of the parameters is given with the description of the | ||
| :py:meth:`pyvo.mivot.writer.InstancesFromModels.add_mango_epoch_position` method. | ||
@@ -244,3 +246,3 @@ | ||
| The detail of the parameters is given with the | ||
| The detail of the parameters is given with the | ||
| :py:meth:`pyvo.mivot.writer.InstancesFromModels.add_mango_brightness` docstring. | ||
@@ -257,3 +259,3 @@ | ||
| The detail of the parameters is given with the | ||
| The detail of the parameters is given with the | ||
| :py:meth:`pyvo.mivot.writer.InstancesFromModels.add_mango_color` docstring. | ||
@@ -260,0 +262,0 @@ |
+31
-31
@@ -10,3 +10,3 @@ ************************************************************ | ||
| This service exposes the slim `4XMM dr14 catalogue <http://xmmssc.irap.omp.eu/>`_. | ||
| It is able to map query responses on the fly to the MANGO data model. | ||
| It is able to map query responses on the fly to the MANGO data model. | ||
| The annotation process only annotates the columns that are selected by the query. | ||
@@ -27,3 +27,3 @@ | ||
| .. code-block:: python | ||
| import pytest | ||
@@ -35,22 +35,22 @@ from pyvo.utils import activate_features | ||
| from pyvo.mivot.viewer.mivot_viewer import MivotViewer | ||
| # Enable MIVOT-specific features in the pyvo library | ||
| activate_features("MIVOT") | ||
| service = TAPService('https://xcatdb.unistra.fr/xtapdb') | ||
| result = service.run_sync( | ||
| """ | ||
| SELECT TOP 5 * FROM "public".mergedentry | ||
| SELECT TOP 5 * FROM "public".mergedentry | ||
| """, | ||
| format="application/x-votable+xml;content=mivot" | ||
| ) | ||
| # The MIVOT viewer generates the model view of the data | ||
| m_viewer = MivotViewer(result, resolve_ref=True) | ||
| m_viewer = MivotViewer(result, resolve_ref=True) | ||
| # Print out the Mivot annotations read out of the VOtable | ||
| # This statement is just for a pedagogic purpose (access to a private attribute) | ||
| XmlUtils.pretty_print(m_viewer._mapping_block) | ||
| In this first step we just queried the service and we built the object that will process the Mivot annotations. | ||
@@ -67,9 +67,9 @@ The Mivot block printing output is too long to be listed here. However, the screenshot below shows its shallow structure. | ||
| ``MangoObject`` instance which holds all the mapped properties. | ||
| At instantiation time, the viewer reads the first data row, which must exist, | ||
| in order to construct a Python object that reflects the mapped model. | ||
| in order to construct a Python object that reflects the mapped model. | ||
| .. code-block:: python | ||
| # Build a Python object matching the TEMPLATES content and | ||
| # Build a Python object matching the TEMPLATES content and | ||
| # which leaves are set with the values of the first row | ||
@@ -114,8 +114,8 @@ mango_object = m_viewer.dm_instance | ||
| The same code can easily be connected with matplotlib to plot SEDs as shown below (code not provided). | ||
| .. image:: _images/xtapdbSED.png | ||
| :width: 500 | ||
| :alt: XMM SED | ||
| It is to noted that the current table row keeps available through the Mivot viewer. | ||
@@ -141,3 +141,3 @@ | ||
| This example is based on a VOtable resulting on a Vizier cone search. | ||
| This service maps the data to the ``EpochPosition`` MANGO property, | ||
| This service maps the data to the ``EpochPosition`` MANGO property, | ||
| which models a full source's astrometry at a given date. | ||
@@ -150,5 +150,5 @@ | ||
| Therefore, this implementation may differ a little bit from the standard model. | ||
| Vizier does not wrap the source properties in a MANGO object, | ||
| but rather lists them in the Mivot *TEMPLATES*. | ||
| but rather lists them in the Mivot *TEMPLATES*. | ||
| The annotation reader must support both designs. | ||
@@ -159,3 +159,3 @@ | ||
| .. code-block:: python | ||
| import pytest | ||
@@ -173,3 +173,3 @@ import astropy.units as u | ||
| activate_features("MIVOT") | ||
| scs_srv = SCSService("https://vizier.cds.unistra.fr/viz-bin/conesearch/V1.5/I/239/hip_main") | ||
@@ -187,4 +187,4 @@ | ||
| .. code-block:: python | ||
| # Build a Python object matching the TEMPLATES content and | ||
| # Build a Python object matching the TEMPLATES content and | ||
| # which leaves are set with the values of the first row | ||
@@ -201,3 +201,3 @@ mango_property = m_viewer.dm_instance | ||
| .. code-block:: json | ||
| { | ||
@@ -248,10 +248,10 @@ "dmtype": "mango:EpochPosition", | ||
| } | ||
| } | ||
| } | ||
| The reader can transform ``EpochPosition`` instances into ``SkyCoord`` instances. | ||
| These can then be used for further scientific processing. | ||
| .. code-block:: python | ||
| while m_viewer.next_row_view(): | ||
@@ -266,7 +266,7 @@ if mango_property.dmtype == "mango:EpochPosition": | ||
| It contains no features specific to the Vizier output. | ||
| It avoids the need for users to build SkyCoord objects by hand from VOTable fields, | ||
| which is never an easy task. | ||
| The next section provides some tips to use the API documented in the annoter `page <annoter.html>`_. | ||
| The next section provides some tips to use the API documented in the :ref:`annoter page <mivot-annoter>`. |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: pyvo | ||
| Version: 1.7 | ||
| Version: 1.7.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.7 | ||
| Version: 1.7.1 | ||
| Summary: Astropy affiliated package for accessing Virtual Observatory data and services | ||
@@ -5,0 +5,0 @@ Author: the PyVO Developers |
@@ -324,8 +324,10 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| This has probably done already somewhere else | ||
| This has probably been done already somewhere else | ||
| """ | ||
| if isinstance(pos, SkyCoord) and pos.size < 4: | ||
| raise ValueError("radius should be provided in the pos tuple " | ||
| "for CIRCLE searches.") | ||
| if len(pos) == 2: | ||
| if not isinstance(pos[0], SkyCoord): | ||
| raise ValueError | ||
| raise ValueError("a 2-length pos should be a coordinate and a radius") | ||
| if not isinstance(pos[1], Quantity): | ||
@@ -332,0 +334,0 @@ radius = pos[1] * u.deg |
+11
-3
@@ -873,11 +873,19 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| """ | ||
| Returns the UWS result with id='result' if it exists, otherwise None. | ||
| Returns the UWS result that corresponds to the standard TAP result | ||
| endpoint: "results/result". | ||
| If no such result is found it falls back to the old behavior of returning | ||
| the first result with id 'result'. | ||
| """ | ||
| try: | ||
| for r in self._job.results: | ||
| if r.id_ == 'result': | ||
| if r.href and r.href.endswith("results/result"): | ||
| return r | ||
| for r in self._job.results: | ||
| if r.href and r.href.strip() and r.id_ == 'result': | ||
| return r | ||
| return None | ||
| except IndexError: | ||
| except (IndexError, AttributeError): | ||
| return None | ||
@@ -884,0 +892,0 @@ |
@@ -114,2 +114,5 @@ #!/usr/bin/env python | ||
| ERR_POSITIONS = [(SkyCoord(2, 4, unit='deg'), "radius should be provided in the pos tuple"), | ||
| ((1, 2), "a 2-length pos should be a coordinate and a radius")] | ||
| @pytest.mark.usefixtures('sia') | ||
@@ -130,2 +133,11 @@ @pytest.mark.usefixtures('capabilities') | ||
| @pytest.mark.usefixtures('capabilities') | ||
| @pytest.mark.parametrize(("position", "expected_errmsg"), ERR_POSITIONS) | ||
| def test_search_scalar_errors(self, position, expected_errmsg): | ||
| service = SIA2Service('https://example.com/sia') | ||
| with pytest.raises(ValueError, match=expected_errmsg): | ||
| service.search(pos=position) | ||
| @pytest.mark.usefixtures('sia') | ||
| @pytest.mark.usefixtures('capabilities') | ||
| def test_search_vector(self, pos=POSITIONS): | ||
@@ -132,0 +144,0 @@ service = SIA2Service('https://example.com/sia') |
@@ -924,3 +924,127 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| @pytest.mark.usefixtures('async_fixture') | ||
| def test_job_result_with_non_standard_id(self): | ||
| """Test fix for Github issue | ||
| https://github.com/astropy/pyvo/issues/670""" | ||
| status_response = '''<?xml version="1.0" encoding="UTF-8"?> | ||
| <uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" | ||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
| <uws:jobId>1</uws:jobId> | ||
| <uws:phase>COMPLETED</uws:phase> | ||
| <uws:results> | ||
| <uws:result id="main" | ||
| xlink:href="http://example.com/tap/async/1/results/result"/> | ||
| </uws:results> | ||
| </uws:job>''' | ||
| service = TAPService('http://example.com/tap') | ||
| job = service.submit_job("SELECT * FROM ivoa.obscore") | ||
| with requests_mock.Mocker() as rm: | ||
| rm.get(f'http://example.com/tap/async/{job.job_id}', | ||
| text=status_response) | ||
| job._update() | ||
| result = job.result | ||
| assert result is not None | ||
| assert result.id_ == "main" | ||
| assert result.href.endswith("results/result") | ||
| @pytest.mark.usefixtures('async_fixture') | ||
| def test_job_result_fallback_to_id_based_lookup(self): | ||
| status_response = '''<?xml version="1.0" encoding="UTF-8"?> | ||
| <uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" | ||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
| <uws:jobId>1</uws:jobId> | ||
| <uws:phase>COMPLETED</uws:phase> | ||
| <uws:results> | ||
| <uws:result id="result" | ||
| xlink:href="http://example.com/tap/async/1/custom/result"/> | ||
| </uws:results> | ||
| </uws:job>''' | ||
| service = TAPService('http://example.com/tap') | ||
| job = service.submit_job("SELECT * FROM ivoa.obscore") | ||
| with requests_mock.Mocker() as rm: | ||
| rm.get(f'http://example.com/tap/async/{job.job_id}', | ||
| text=status_response) | ||
| job._update() | ||
| result = job.result | ||
| assert result is not None | ||
| assert result.id_ == "result" | ||
| assert not result.href.endswith("results/result") | ||
| @pytest.mark.usefixtures('async_fixture') | ||
| def test_job_result_multiple_results(self): | ||
| """Test that standard URL structure is preferred when multiple results exist.""" | ||
| status_response = '''<?xml version="1.0" encoding="UTF-8"?> | ||
| <uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" | ||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
| <uws:jobId>1</uws:jobId> | ||
| <uws:phase>COMPLETED</uws:phase> | ||
| <uws:results> | ||
| <uws:result id="result" | ||
| xlink:href="http://example.com/tap/async/1/custom/result"/> | ||
| <uws:result id="main" | ||
| xlink:href="http://example.com/tap/async/1/results/result"/> | ||
| </uws:results> | ||
| </uws:job>''' | ||
| # Check that we return the /results/result URL if it exists | ||
| service = TAPService('http://example.com/tap') | ||
| job = service.submit_job("SELECT * FROM ivoa.obscore") | ||
| with requests_mock.Mocker() as rm: | ||
| rm.get(f'http://example.com/tap/async/{job.job_id}', | ||
| text=status_response) | ||
| job._update() | ||
| result = job.result | ||
| assert result is not None | ||
| assert result.id_ == "main" | ||
| assert result.href.endswith("results/result") | ||
| @pytest.mark.usefixtures('async_fixture') | ||
| def test_job_result_empty_href(self): | ||
| status_response = '''<?xml version="1.0" encoding="UTF-8"?> | ||
| <uws:job xmlns:uws="http://www.ivoa.net/xml/UWS/v1.0" | ||
| xmlns:xlink="http://www.w3.org/1999/xlink"> | ||
| <uws:jobId>1</uws:jobId> | ||
| <uws:phase>COMPLETED</uws:phase> | ||
| <uws:results> | ||
| <uws:result id="result" xlink:href=""/> | ||
| </uws:results> | ||
| </uws:job>''' | ||
| service = TAPService('http://example.com/tap') | ||
| job = service.submit_job("SELECT * FROM ivoa.obscore") | ||
| with requests_mock.Mocker() as rm: | ||
| rm.get(f'http://example.com/tap/async/{job.job_id}', | ||
| text=status_response) | ||
| job._update() | ||
| result = job.result | ||
| assert result is None | ||
| @pytest.mark.usefixtures('async_fixture') | ||
| def test_job_result_handles_attribute_error(self): | ||
| service = TAPService('http://example.com/tap') | ||
| job = service.submit_job("SELECT * FROM ivoa.obscore") | ||
| job._job = None | ||
| result = job.result | ||
| assert result is None | ||
| @pytest.mark.usefixtures('async_fixture') | ||
| def test_job_result_handles_malformed_results(self): | ||
| service = TAPService('http://example.com/tap') | ||
| job = service.submit_job("SELECT * FROM ivoa.obscore") | ||
| mock_result = Mock() | ||
| del mock_result.href | ||
| mock_job = Mock() | ||
| mock_job.results = [mock_result] | ||
| job._job = mock_job | ||
| result = job.result | ||
| assert result is None | ||
| @pytest.mark.usefixtures("tapservice") | ||
@@ -927,0 +1051,0 @@ class TestTAPCapabilities: |
@@ -39,3 +39,3 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| 'UWSElement', 'Reference', 'JobSummary', 'Parameters', 'Parameter', | ||
| 'Results', 'Result'] | ||
| 'Results', 'Result', 'Jobs'] | ||
@@ -42,0 +42,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.7' | ||
| version = '1.7.1' |
+3
-1
@@ -6,3 +6,3 @@ [tox] | ||
| envlist = | ||
| py{39,310,311,312,313}-test{,-alldeps,-oldestdeps,-devdeps}{,-online}{,-cov} | ||
| py{39,310,311,312,313,314}-test{,-alldeps,-oldestdeps,-devdeps}{,-online}{,-cov} | ||
| linkcheck | ||
@@ -30,2 +30,4 @@ codestyle | ||
| devdeps: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/scientific-python-nightly-wheels/simple https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/astropy/simple | ||
| # No astropy py314 wheels on pypi yet | ||
| py314: PIP_EXTRA_INDEX_URL = https://pypi.anaconda.org/liberfa/simple https://pypi.anaconda.org/astropy/simple | ||
@@ -32,0 +34,0 @@ deps = |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
4011972
0.46%25978
0.49%