Repository: astropenguin/azely

Issue:
Update query format of location and object modules

Discussion:

Patch:
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index c1fe2d2..0d103e2 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -5,6 +5,9 @@
         "dockerfile": "Dockerfile"
     },
     "postCreateCommand": "poetry install",
+    "containerEnv": {
+        "AZELY_DIR": "${containerWorkspaceFolder}/.azely/"
+    },
     "customizations": {
         "vscode": {
             "extensions": [
diff --git a/.gitignore b/.gitignore
index f899d85..5d99a78 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,9 +1,7 @@
-# User-defined
+### Azely ###
 .azely/
-.vscode/
-.envrc
-.env
 
+### Python ###
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
@@ -26,9 +24,11 @@ parts/
 sdist/
 var/
 wheels/
+share/python-wheels/
 *.egg-info/
 .installed.cfg
 *.egg
+MANIFEST
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
@@ -43,13 +43,17 @@ pip-delete-this-directory.txt
 # Unit test / coverage reports
 htmlcov/
 .tox/
+.nox/
 .coverage
 .coverage.*
 .cache
 nosetests.xml
 coverage.xml
 *.cover
+*.py,cover
 .hypothesis/
+.pytest_cache/
+cover/
 
 # Translations
 *.mo
@@ -58,6 +62,8 @@ coverage.xml
 # Django stuff:
 *.log
 local_settings.py
+db.sqlite3
+db.sqlite3-journal
 
 # Flask stuff:
 instance/
@@ -72,16 +78,49 @@ docs/_apidoc
 docs/_build/
 
 # PyBuilder
+.pybuilder/
 target/
 
 # Jupyter Notebook
 .ipynb_checkpoints
 
-# pyenv
-.python-version
+# IPython
+profile_default/
+ipython_config.py
 
-# celery beat schedule file
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# poetry
+#   Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+#   This is especially recommended for binary packages to ensure reproducibility, and is more
+#   commonly ignored for libraries.
+#   https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+#   Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+#   pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+#   in version control.
+#   https://pdm.fming.dev/#use-with-ide
+.pdm.toml
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
 celerybeat-schedule
+celerybeat.pid
 
 # SageMath parsed files
 *.sage.py
@@ -92,6 +131,8 @@ celerybeat-schedule
 env/
 venv/
 ENV/
+env.bak/
+venv.bak/
 
 # Spyder project settings
 .spyderproject
@@ -105,3 +146,31 @@ ENV/
 
 # mypy
 .mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+#  JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+#  be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+#  and can be added to the global gitignore or merged into this file.  For a more nuclear
+#  option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+### Python Patch ###
+# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
+poetry.toml
+
+# ruff
+.ruff_cache/
+
+# LSP config files
+pyrightconfig.json
diff --git a/azely/__init__.py b/azely/__init__.py
index d397bbd..583e6a7 100644
--- a/azely/__init__.py
+++ b/azely/__init__.py
@@ -8,7 +8,6 @@ __all__ = [
     "get_time",
     "location",
     "object",
-    "query",
     "time",
     "utils",
 ]
@@ -18,7 +17,6 @@ __version__ = "0.7.0"
 # submodules
 from . import consts
 from . import cache
-from . import query
 from . import utils
 from . import location
 from . import object
diff --git a/azely/consts.py b/azely/consts.py
index 3beab4f..2545a31 100644
--- a/azely/consts.py
+++ b/azely/consts.py
@@ -8,8 +8,8 @@ determined by some environment variables of client.
 __all__ = [
     "AZELY_DIR",
     "AZELY_CONFIG",
-    "AZELY_OBJECT",
-    "AZELY_LOCATION",
+    "AZELY_OBJECTS",
+    "AZELY_LOCATIONS",
     "SOLAR_FRAME",
     "SOLAR_OBJECTS",
     "HERE",
@@ -73,8 +73,8 @@ else:
 
 
 AZELY_CONFIG = ensure(AZELY_DIR / "config.toml")
-AZELY_OBJECT = ensure(AZELY_DIR / "objects.toml")
-AZELY_LOCATION = ensure(AZELY_DIR / "locations.toml")
+AZELY_OBJECTS = ensure(AZELY_DIR / "objects.toml")
+AZELY_LOCATIONS = ensure(AZELY_DIR / "locations.toml")
 
 
 # special values for the solar system ephemeris
diff --git a/azely/location.py b/azely/location.py
index d9bd5d9..563626a 100644
--- a/azely/location.py
+++ b/azely/location.py
@@ -4,7 +4,7 @@ __all__ = ["Location", "get_location"]
 # standard library
 from dataclasses import dataclass
 from datetime import tzinfo
-from typing import ClassVar
+from typing import ClassVar, Optional
 
 
 # dependencies
@@ -15,8 +15,7 @@ from ipinfo import getHandler
 from pytz import timezone
 from timezonefinder import TimezoneFinder
 from .cache import PathLike, cache
-from .consts import AZELY_LOCATION, GOOGLE_API, HERE, IPINFO_API, TIMEOUT
-from .query import parse
+from .consts import AZELY_LOCATIONS, GOOGLE_API, HERE, IPINFO_API, TIMEOUT
 
 
 @dataclass
@@ -65,39 +64,45 @@ class Location:
 
 def get_location(
     query: str,
+    /,
     *,
     google_api: str = GOOGLE_API,
     ipinfo_api: str = IPINFO_API,
+    name: Optional[str] = None,
+    source: PathLike = AZELY_LOCATIONS,
     timeout: int = TIMEOUT,
+    update: bool = False,
 ) -> Location:
     """Get location information."""
-    parsed = parse(query)
-
-    if parsed.query.lower() == HERE:
+    if query.lower() == HERE:
         return get_location_by_ip(
-            query=parsed.query,
+            query,
             ipinfo_api=ipinfo_api,
+            name=name,
             timeout=timeout,
-            source=parsed.source or AZELY_LOCATION,
-            update=parsed.update,
+            source=source,
+            update=update,
         )
     else:
         return get_location_by_name(
-            query=parsed.query,
+            query,
             google_api=google_api,
+            name=name,
             timeout=timeout,
-            source=parsed.source or AZELY_LOCATION,
-            update=parsed.update,
+            source=source,
+            update=update,
         )
 
 
 @cache
 def get_location_by_ip(
     query: str,
+    /,
     *,
     ipinfo_api: str,
-    timeout: int,
+    name: Optional[str],
     source: PathLike,  # consumed by @cache
+    timeout: int,
     update: bool,  # consumed by @cache
 ) -> Location:
     """Get location information by current IP address."""
@@ -114,10 +119,12 @@ def get_location_by_ip(
 @cache
 def get_location_by_name(
     query: str,
+    /,
     *,
     google_api: str,
-    timeout: int,
+    name: Optional[str],
     source: PathLike,  # consumed by @cache
+    timeout: int,
     update: bool,  # consumed by @cache
 ) -> Location:
     """Get location information by a location name."""
@@ -129,7 +136,7 @@ def get_location_by_name(
         )
 
     return Location(
-        name=query,
+        name=name or query,
         longitude=str(response.lon),
         latitude=str(response.lat),
     )
diff --git a/azely/object.py b/azely/object.py
index 02ecc55..1f57ef3 100644
--- a/azely/object.py
+++ b/azely/object.py
@@ -3,7 +3,7 @@ __all__ = ["Object", "get_object"]
 
 # standard library
 from dataclasses import dataclass
-from typing import List
+from typing import Optional
 
 
 # dependent packages
@@ -11,8 +11,7 @@ from astropy.coordinates import Longitude, Latitude, SkyCoord, get_body
 from astropy.time import Time as ObsTime
 from astropy.utils.data import conf
 from .cache import PathLike, cache
-from .consts import AZELY_OBJECT, FRAME, SOLAR_FRAME, SOLAR_OBJECTS, TIMEOUT
-from .query import parse
+from .consts import AZELY_OBJECTS, FRAME, SOLAR_FRAME, SOLAR_OBJECTS, TIMEOUT
 
 
 @dataclass
@@ -64,39 +63,45 @@ class Object:
 
 def get_object(
     query: str,
+    /,
     *,
     frame: str = FRAME,
+    name: Optional[str] = None,
+    source: PathLike = AZELY_OBJECTS,
     timeout: int = TIMEOUT,
+    update: bool = False,
 ) -> Object:
     """Get object information."""
-    parsed = parse(query)
-
-    if parsed.query.lower() in SOLAR_OBJECTS:
+    if query.lower() in SOLAR_OBJECTS:
         return get_object_solar(
-            query=parsed.query,
-            source=parsed.source or AZELY_OBJECT,
-            update=parsed.update,
+            query,
+            name=name,
+            source=source,
+            update=update,
         )
     else:
         return get_object_by_name(
-            query=parsed.query,
+            query,
             frame=frame,
+            name=name,
             timeout=timeout,
-            source=parsed.source or AZELY_OBJECT,
-            update=parsed.update,
+            source=source,
+            update=update,
         )
 
 
 @cache
 def get_object_solar(
     query: str,
+    /,
     *,
+    name: Optional[str],
     source: PathLike,  # consumed by @cache
     update: bool,  # consumed by @cache
 ) -> Object:
     """Get object information in the solar system."""
     return Object(
-        name=query,
+        name=name or query,
         longitude="NA",
         latitude="NA",
         frame=SOLAR_FRAME,
@@ -106,10 +111,12 @@ def get_object_solar(
 @cache
 def get_object_by_name(
     query: str,
+    /,
     *,
     frame: str,
-    timeout: int,
+    name: Optional[str],
     source: PathLike,  # consumed by @cache
+    timeout: int,
     update: bool,  # consumed by @cache
 ) -> Object:
     """Get object information by an object name."""
@@ -122,7 +129,7 @@ def get_object_by_name(
         )
 
     return Object(
-        name=query,
+        name=name or query,
         longitude=str(response.data.lon),  # type: ignore
         latitude=str(response.data.lat),  # type: ignore
         frame=frame,
diff --git a/azely/query.py b/azely/query.py
deleted file mode 100644
index 5dcd3e7..0000000
--- a/azely/query.py
+++ /dev/null
@@ -1,35 +0,0 @@
-__all__ = ["parse"]
-
-
-# standard library
-from dataclasses import dataclass
-from re import compile
-from typing import Optional
-
-
-# constants
-QUERY = compile(r"^\s*((.+):)?([^!]+)(!)?\s*$")
-
-
-@dataclass(frozen=True)
-class Parsed:
-    query: str
-    """Final query to be passed."""
-
-    source: Optional[str] = None
-    """Path of the query source."""
-
-    update: bool = False
-    """Whether to update the query source."""
-
-
-def parse(string: str) -> Parsed:
-    """Parse a string and return a parsed object."""
-    if (match := QUERY.search(string)) is None:
-        raise ValueError(f"{string} is not valid.")
-
-    return Parsed(
-        query=match.group(3),
-        source=match.group(2),
-        update=bool(match.group(4)),
-    )


Test patch:
diff --git a/tests/test_location.py b/tests/test_location.py
index e255d8c..08a7356 100644
--- a/tests/test_location.py
+++ b/tests/test_location.py
@@ -8,7 +8,7 @@ from azely.location import Location, get_location
 from tomlkit import dump
 
 
-# constants
+# test data
 expected = Location(
     name="Array Operations Site",
     longitude="292d14m45.85512974s",
@@ -19,15 +19,12 @@ expected = Location(
 
 # test functions
 def test_location_by_query():
-    assert get_location(f"{expected.name}!") == expected
+    assert get_location(expected.name, update=True) == expected
 
 
 def test_location_by_user():
     with NamedTemporaryFile("w", suffix=".toml") as f:
-        name = "AOS"
-        query = f"{f.name}:{name}"
-
-        dump({name: asdict(expected)}, f)
+        dump({expected.name: asdict(expected)}, f)
         f.seek(0)
 
-        assert get_location(query) == expected
+        assert get_location(expected.name, source=f.name) == expected
diff --git a/tests/test_object.py b/tests/test_object.py
index 6286bee..eceffee 100644
--- a/tests/test_object.py
+++ b/tests/test_object.py
@@ -8,7 +8,7 @@ from azely.object import Object, get_object
 from tomlkit import dump
 
 
-# constants
+# test data
 expected_solar = Object(
     name="Sun",
     longitude="NA",
@@ -17,28 +17,25 @@ expected_solar = Object(
 )
 
 expected_icrs = Object(
-    name="M87",
-    longitude="12h30m49.42338414s",
-    latitude="+12d23m28.0436859s",
+    name="3C 273",
+    longitude="12h29m06.69982572s",
+    latitude="2d03m08.59762998s",
     frame="icrs",
 )
 
 
 # test functions
 def test_object_of_solar():
-    assert get_object(f"{expected_solar.name}!") == expected_solar
+    assert get_object(expected_solar.name, update=True) == expected_solar
 
 
 def test_object_by_query():
-    assert get_object(f"{expected_icrs.name}!") == expected_icrs
+    assert get_object(expected_icrs.name, update=True) == expected_icrs
 
 
 def test_object_by_user():
     with NamedTemporaryFile("w", suffix=".toml") as f:
-        name = "M87"
-        query = f"{f.name}:{name}"
-
-        dump({name: asdict(expected_icrs)}, f)
+        dump({expected_icrs.name: asdict(expected_icrs)}, f)
         f.seek(0)
 
-        assert get_object(query) == expected_icrs
+        assert get_object(expected_icrs.name, source=f.name) == expected_icrs