diff --git a/.github/workflows/check-quality.yml b/.github/workflows/check-quality.yml new file mode 100644 index 0000000..c7eef14 --- /dev/null +++ b/.github/workflows/check-quality.yml @@ -0,0 +1,31 @@ +name: qc (black/mypy/isort checks) + +on: + workflow_dispatch: + push: + +jobs: + analyse: + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: install devbox + uses: jetpack-io/devbox-install-action@v0.3.0 + + - name: install dependencies + run: devbox run poetry install + + - name: install dependencies + id: build + run: devbox run poetry build + + - name: analyse with mypy + run: devbox run poetry run mypy surplus.py + + - name: check for black formatting compliance + run: devbox run poetry run "black --check surplus.py" + + - name: analyse isort compliance + run: devbox run poetry run "isort --check surplus.py" diff --git a/.github/workflows/slsa-publish.yml b/.github/workflows/publish-slsa3.yml similarity index 94% rename from .github/workflows/slsa-publish.yml rename to .github/workflows/publish-slsa3.yml index dc633cb..8169e88 100644 --- a/.github/workflows/slsa-publish.yml +++ b/.github/workflows/publish-slsa3.yml @@ -1,6 +1,9 @@ -name: release (slsa 3) +name: release with slsa 3 compliance + on: push: + branches: + main tags: - '*' diff --git a/README.md b/README.md index 16c1000..611d0f0 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Plus Code to iOS-Shortcuts-like shareable text - [API Reference](#api-reference) - [surplus.surplus()](#surplussurplus) - [surplus.parse_query()](#surplusparse_query) + - [surplus.handle_query()](#surplushandle_query) - [surplus.Localcode](#surpluslocalcode) - [surplus.Latlong](#surpluslatlong) - [Developing](#developing) @@ -16,11 +17,12 @@ Plus Code to iOS-Shortcuts-like shareable text ```text $ surplus 9R3J+R9 Singapore -surplus version 1.1.0 +surplus version 1.1.1 Thomson Plaza 301 Upper Thomson Road, Bishan +Sin Ming, Bishan 574408 -Singapore +Central, Singapore ``` ```python @@ -28,7 +30,7 @@ Singapore >>> Localcode(code="8RPQ+JW", locality="Singapore").full_length() (True, '6PH58RPQ+JW') >>> surplus("6PH58RPQ+JW") -(True, 'Caldecott Stn Exit 4\nToa Payoh Link\n298106\nSingapore') +(True, 'Caldecott Stn Exit 4\nToa Payoh Link\n298106\nCentral, Singapore') ``` ## Installing @@ -44,16 +46,19 @@ pip install git+https://github.com/markjoshwel/surplus ### Command-line Interface ```text -usage: surplus [-h] [-d] query +usage: surplus [-h] [-d] query [query ...] Plus Code to iOS-Shortcuts-like shareable text positional arguments: - query full-length Plus Code (6PH58QMF+FX), local codes (8QMF+FX Singapore), or latlong (1.3336875, 103.7749375) + query full-length Plus Code (6PH58QMF+FX), + local code (8QMF+FX Singapore), or + latlong (1.3336875, 103.7749375) options: -h, --help show this help message and exit - -d, --debug prints lat, long and reverser response dict to stderr + -d, --debug prints lat, long and reverser response + dict to stderr ``` ### API Reference @@ -76,9 +81,12 @@ pluscode to shareable text conversion function - arguments - `query: str | surplus.Localcode | surplus.Latlong` - str - normal longcode (6PH58QMF+FX) - surplus.Localcode - shortcode with locality (8QMF+FX Singapore) - surplus.Latlong - latlong + - str + normal longcode (6PH58QMF+FX) + - [`surplus.Localcode`](#surpluslocalcode) + shortcode with locality (8QMF+FX Singapore) + - [`surplus.Latlong`](#surpluslatlong) + latlong - `reverser: typing.Callable = geopy.geocoders.Nominatim(user_agent="surplus").reverser` latlong to location function, accesses a dict from .raw attribute of return object @@ -97,22 +105,22 @@ pluscode to shareable text conversion function - returns `tuple[bool, str]` - `(True, )` - conversion was successful, str is resultant text + conversion succeeded, second element is the resultant string - `(False, )` - conversion failed, str is error message + conversion failed, second element is an error message string --- #### `surplus.parse_query()` -function that parses a string Plus Code, local code or latlong into a str, surplus.Localcode or surplus.Latlong respectively +function that parses a string Plus Code, local code or latlong into a str, [`surplus.Localcode`](#surpluslocalcode) or [`surplus.Latlong`](#surpluslatlong) respectively - signature: ```python def parse_query( query: str, debug: bool = False - ) -> tuple[bool, str | Localcode | Latlong]: + ) -> tuple[Literal[True], str | Localcode | Latlong] | tuple[Literal[False], str]: ``` - arguments: @@ -120,12 +128,43 @@ function that parses a string Plus Code, local code or latlong into a str, surpl - `query: str` string Plus Code, local code or latlong -- returns `tuple[bool, str | Localcode | Latlong]` +- returns `tuple[Literal[True], str | Localcode | Latlong] | tuple[Literal[False], str]` - - `(True, )` - conversion was successful, second element is result + - `(True, )` + conversion succeeded, second element is resultant Plus code string, [`surplus.Localcode`](#surpluslocalcode) or [`surplus.Latlong`](#surpluslatlong) - `(False, )` - conversion failed, str is error message + conversion failed, second element is an error message string + +--- + +#### `surplus.handle_query()` + +function that gets returns a [surplus.Latlong](#surpluslatlong) from a Plus Code string, [`surplus.Localcode`](#surpluslocalcode) or [`surplus.Latlong`](#surpluslatlong) object. +used after [`surplus.parse_query()`](#surplusparse_query). + +- signature: + + ```python + def handle_query( + query: str | Localcode | Latlong, debug: bool = False + ) -> tuple[Literal[True], Latlong] | tuple[Literal[False], str]: + ``` + +- arguments: + + - `query: str | Localcode | Latlong` + - str + normal longcode (6PH58QMF+FX) + - [`surplus.Localcode`](#surpluslocalcode) + shortcode with locality (8QMF+FX Singapore) + - [`surplus.Latlong`](#surpluslatlong) + latlong + +- returns `tuple[Literal[True], Latlong] | tuple[Literal[False], str]` + - `(True, )` + conversion succeeded, second element is a [`surplus.Latlong`](#surpluslatlong) + - `(False, )` + conversion failed, second element is an error message string --- @@ -136,9 +175,9 @@ function that parses a string Plus Code, local code or latlong into a str, surpl - parameters: - `code: str` - Plus Code - e.g.: "8QMF+FX" + Plus Code - e.g.: `"8QMF+FX"` - `locality: str` - e.g.: "Singapore" + e.g.: `"Singapore"` --- @@ -162,9 +201,9 @@ method that calculates full-length Plus Code using locality - returns: - `(True, )` - conversion was successful, str is resultant Plus Code + conversion succeeded, second element is the resultant Plus Code string - `(False, )` - conversion failed, str is error message + conversion failed, second element is an error message string --- diff --git a/pyproject.toml b/pyproject.toml index 8787f3c..13ac65f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "surplus" -version = "1.1.0" +version = "1.1.1" description = "Plus Code to iOS-Shortcuts-like shareable text" authors = ["Mark Joshwel "] license = "Unlicence" diff --git a/surplus.py b/surplus.py index 3d95219..39c05d9 100644 --- a/surplus.py +++ b/surplus.py @@ -30,8 +30,9 @@ For more information, please refer to """ from argparse import ArgumentParser +from collections import OrderedDict from sys import stderr -from typing import Any, Callable, Final, NamedTuple +from typing import Any, Callable, Final, Literal, NamedTuple from geopy import Location # type: ignore from geopy.geocoders import Nominatim # type: ignore @@ -39,7 +40,7 @@ from pluscodes import PlusCode # type: ignore from pluscodes.openlocationcode import recoverNearest # type: ignore from pluscodes.validator import Validator # type: ignore -VERSION: Final[tuple[int, int, int]] = (1, 1, 0) +VERSION: Final[tuple[int, int, int]] = (1, 1, 1) class Localcode(NamedTuple): @@ -130,49 +131,24 @@ def surplus( (True, ) - conversion was successful, str is resultant text (False, ) - conversion failed, str is error message """ - lat: float = 0.0 - lon: float = 0.0 - if isinstance(query, Latlong): - lat, lon = query.lat, query.long + def _unique(l: list[str]) -> list[str]: + unique: OrderedDict = OrderedDict() + for line in l: + unique.update({line: None}) + return list(unique.keys()) - else: # instances: str | Localcode - str_pcode: str = "" + _latlong = handle_query(query=query, debug=debug) - if isinstance(query, Localcode): - result = query.full_length() + if _latlong[0] is False: + assert isinstance(_latlong[1], str) + return False, _latlong[1] - if not result[0]: - return False, result[1] - - str_pcode = result[1] - - else: - str_pcode = query - - try: - pcode = PlusCode(str_pcode) - - except KeyError: - return ( - False, - "enter full-length Plus Code including area code, e.g.: 6PH58QMF+FX", - ) - - except Exception as pcderr: - return ( - False, - f"error while decoding Plus Code: {pcderr.__class__.__name__} - {pcderr}", - ) - - lat = pcode.area.center().lat - lon = pcode.area.center().lon - - if debug: - stderr.write(f"debug: {lat=}, {lon=}\n") + assert isinstance(_latlong[1], Latlong) + latlong = _latlong[1] try: - _reversed: Location | None = reverser(f"{lat}, {lon}") + _reversed: Location | None = reverser(f"{latlong.lat}, {latlong.long}") if _reversed is None: raise Exception(f"reverser function returned None") @@ -182,71 +158,113 @@ def surplus( except Exception as reverr: return ( False, - f"error while reversing latlong ({lat},{lon}): {reverr.__class__.__name__} - {reverr}", + f"error while reversing latlong ({Latlong}): {reverr.__class__.__name__} - {reverr}", ) if debug: stderr.write(f"debug: {location=}\n") - data: list[str] = [ - ( - ",".join( - [ - location["address"].get(detail, "") - for detail in ( - "emergency, historic, military, natural, landuse, place, railway," - "man_made, aerialway, boundary, amenity, aeroway, club, craft," - "leisure, office, mountain_pass, shop, tourism, bridge, tunnel, waterway" - ).split(", ") - ] - ) - ).strip(","), - # location["address"].get("leisure"), - # location["address"].get("shop"), - # location["address"].get("railway"), - ( - location["address"].get("building") - if ( + text: list[str] = _unique( + [ + ( + ", ".join( + [ + d + for d in _unique( + [ + location["address"].get(detail, None) + for detail in ( + "emergency, historic, military, natural, landuse, place, railway, " + "man_made, aerialway, boundary, amenity, aeroway, club, craft, " + "leisure, office, mountain_pass, shop, tourism, bridge, tunnel, waterway" + ).split(", ") + ] + ) + if d is not None + ] + ) + ).strip(", "), + ( location["address"].get("building") - != location["address"].get("house_number") - ) - else None - ), - location["address"].get("highway"), - ( - location["address"].get("house_number", "") - + (" " + location["address"].get("house_name", "")).strip() - + " " - + location["address"].get("road", "") - + ( - ", " + location["address"].get("suburb", "") - # dont repeat if suburb is mentioned in the road itself - # 'Toa Payoh' in 'Lorong 1A Toa Payoh' - if location["address"].get("suburb", "") - not in location["address"].get("road", "") - else "" - ) - ).strip(), - ( - ",".join( - [ - location["address"].get(detail, "") - for detail in ( - "residential, neighbourhood, allotments, quarter" - ).split(", ") - ] - ) - ).strip(","), - location["address"].get("postcode"), - location["address"].get("country"), - ] + if ( + location["address"].get("building") + != location["address"].get("house_number") + ) + else None + ), + location["address"].get("highway"), + ( + location["address"].get("house_number", "") + + (" " + location["address"].get("house_name", "")).strip() + + " " + + location["address"].get("road", "") + # + ( + # ", " + location["address"].get("suburb", "") + # # dont repeat if suburb is mentioned in the road itself + # # 'Toa Payoh' in 'Lorong 1A Toa Payoh' + # if location["address"].get("suburb", "") + # not in location["address"].get("road", "") + # else None + # ) + ).strip(), + ( + ", ".join( + [ + d + for d in _unique( + [ + location["address"].get(detail, "") + for detail in ( + "residential, neighbourhood, allotments, quarter, " + "city_district, district, borough, suburb, subdivision, " + "municipality, city, town, village" + ).split(", ") + ] + ) + if all( + [ + d != "", + d not in location["address"].get("road", ""), + d + not in [ + location["address"].get(detail, "") + for detail in ( + "region, state, state_district, county, " + "state, country, continent" + ).split(", ") + ], + ] + ) + ] + ) + ).strip(","), + location["address"].get("postcode"), + ( + ", ".join( + [ + d + for d in _unique( + [ + location["address"].get(detail, None) + for detail in ( + "region, state, state_district, county, " + "state, country, continent" + ).split(", ") + ] + ) + if d is not None + ] + ) + ), + ] + ) - return True, "\n".join([d for d in data if ((d != None) and d != "")]) + return True, "\n".join([d for d in text if ((d != None) and d != "")]) def parse_query( query: str, debug: bool = False -) -> tuple[bool, str | Localcode | Latlong]: +) -> tuple[Literal[True], str | Localcode | Latlong] | tuple[Literal[False], str]: """ function that parses a string Plus Code, local code or latlong into a str, surplus.Localcode or surplus.Latlong respectively @@ -263,7 +281,7 @@ def parse_query( def _word_match( oquery: str, squery: list[str] - ) -> tuple[bool, str | Localcode | Latlong]: + ) -> tuple[Literal[True], str | Localcode | Latlong] | tuple[Literal[False], str]: """ internal helper code reuse function @@ -285,6 +303,9 @@ def parse_query( locality = oquery.replace(pcode, "") locality = locality.strip().strip(",").strip() + if debug: + stderr.write(f"debug: {pcode=}, {locality=}\n") + return True, Localcode(code=pcode, locality=locality) return False, "unable to find a pluscode/match to a format" @@ -324,6 +345,66 @@ def parse_query( return _word_match(oquery=query, squery=squery) +def handle_query( + query: str | Localcode | Latlong, debug: bool = False +) -> tuple[Literal[True], Latlong] | tuple[Literal[False], str]: + """ + function that gets returns a surplus.Latlong from a Plus Code string, + surplus.Localcode or surplus.Latlong object. + used after surplus.parse_query(). + + query: str | Localcode | Latlong + + debug: bool = False + + returns tuple[bool, str | Latlong] + (True, Latlong) - conversion was successful, second element is latlong + (False, ) - conversion failed, str is error message + """ + lat: float = 0.0 + lon: float = 0.0 + + if isinstance(query, Latlong): + return True, query + + else: # instances: str | Localcode + str_pcode: str = "" + + if isinstance(query, Localcode): + result = query.full_length() + + if not result[0]: + return False, result[1] + + str_pcode = result[1] + + else: + str_pcode = query + + try: + pcode = PlusCode(str_pcode) + + except KeyError: + return ( + False, + "code given is not a full-length Plus Code (including area code), e.g.: 6PH58QMF+FX", + ) + + except Exception as pcderr: + return ( + False, + f"error while decoding Plus Code: {pcderr.__class__.__name__} - {pcderr}", + ) + + lat = pcode.area.center().lat + lon = pcode.area.center().lon + + if debug: + stderr.write(f"debug: {lat=}, {lon=}\n") + + return True, Latlong(lat=lat, long=lon) + + def cli() -> None: parser = ArgumentParser( prog="surplus",