This commit is contained in:
Mark Joshwel 2023-06-16 08:08:23 +00:00 committed by GitHub
commit e61960dcfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 271 additions and 117 deletions

31
.github/workflows/check-quality.yml vendored Normal file
View file

@ -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"

View file

@ -1,6 +1,9 @@
name: release (slsa 3) name: release with slsa 3 compliance
on: on:
push: push:
branches:
main
tags: tags:
- '*' - '*'

View file

@ -8,6 +8,7 @@ Plus Code to iOS-Shortcuts-like shareable text
- [API Reference](#api-reference) - [API Reference](#api-reference)
- [surplus.surplus()](#surplussurplus) - [surplus.surplus()](#surplussurplus)
- [surplus.parse_query()](#surplusparse_query) - [surplus.parse_query()](#surplusparse_query)
- [surplus.handle_query()](#surplushandle_query)
- [surplus.Localcode](#surpluslocalcode) - [surplus.Localcode](#surpluslocalcode)
- [surplus.Latlong](#surpluslatlong) - [surplus.Latlong](#surpluslatlong)
- [Developing](#developing) - [Developing](#developing)
@ -16,11 +17,12 @@ Plus Code to iOS-Shortcuts-like shareable text
```text ```text
$ surplus 9R3J+R9 Singapore $ surplus 9R3J+R9 Singapore
surplus version 1.1.0 surplus version 1.1.1
Thomson Plaza Thomson Plaza
301 Upper Thomson Road, Bishan 301 Upper Thomson Road, Bishan
Sin Ming, Bishan
574408 574408
Singapore Central, Singapore
``` ```
```python ```python
@ -28,7 +30,7 @@ Singapore
>>> Localcode(code="8RPQ+JW", locality="Singapore").full_length() >>> Localcode(code="8RPQ+JW", locality="Singapore").full_length()
(True, '6PH58RPQ+JW') (True, '6PH58RPQ+JW')
>>> surplus("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 ## Installing
@ -44,16 +46,19 @@ pip install git+https://github.com/markjoshwel/surplus
### Command-line Interface ### Command-line Interface
```text ```text
usage: surplus [-h] [-d] query usage: surplus [-h] [-d] query [query ...]
Plus Code to iOS-Shortcuts-like shareable text Plus Code to iOS-Shortcuts-like shareable text
positional arguments: 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: options:
-h, --help show this help message and exit -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 ### API Reference
@ -76,9 +81,12 @@ pluscode to shareable text conversion function
- arguments - arguments
- `query: str | surplus.Localcode | surplus.Latlong` - `query: str | surplus.Localcode | surplus.Latlong`
str - normal longcode (6PH58QMF+FX) - str
surplus.Localcode - shortcode with locality (8QMF+FX Singapore) normal longcode (6PH58QMF+FX)
surplus.Latlong - latlong - [`surplus.Localcode`](#surpluslocalcode)
shortcode with locality (8QMF+FX Singapore)
- [`surplus.Latlong`](#surpluslatlong)
latlong
- `reverser: typing.Callable = geopy.geocoders.Nominatim(user_agent="surplus").reverser` - `reverser: typing.Callable = geopy.geocoders.Nominatim(user_agent="surplus").reverser`
latlong to location function, accesses a dict from .raw attribute of return object 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]` - returns `tuple[bool, str]`
- `(True, <str>)` - `(True, <str>)`
conversion was successful, str is resultant text conversion succeeded, second element is the resultant string
- `(False, <str>)` - `(False, <str>)`
conversion failed, str is error message conversion failed, second element is an error message string
--- ---
#### `surplus.parse_query()` #### `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: - signature:
```python ```python
def parse_query( def parse_query(
query: str, debug: bool = False query: str, debug: bool = False
) -> tuple[bool, str | Localcode | Latlong]: ) -> tuple[Literal[True], str | Localcode | Latlong] | tuple[Literal[False], str]:
``` ```
- arguments: - arguments:
@ -120,12 +128,43 @@ function that parses a string Plus Code, local code or latlong into a str, surpl
- `query: str` - `query: str`
string Plus Code, local code or latlong 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, <str | Localcode | Latlong>)` - `(True, <str | surplus.Localcode | surplus.Latlong>)`
conversion was successful, second element is result conversion succeeded, second element is resultant Plus code string, [`surplus.Localcode`](#surpluslocalcode) or [`surplus.Latlong`](#surpluslatlong)
- `(False, <str>)` - `(False, <str>)`
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, <surplus.Latlong>)`
conversion succeeded, second element is a [`surplus.Latlong`](#surpluslatlong)
- `(False, <str>)`
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: - parameters:
- `code: str` - `code: str`
Plus Code - e.g.: "8QMF+FX" Plus Code - e.g.: `"8QMF+FX"`
- `locality: str` - `locality: str`
e.g.: "Singapore" e.g.: `"Singapore"`
--- ---
@ -162,9 +201,9 @@ method that calculates full-length Plus Code using locality
- returns: - returns:
- `(True, <str>)` - `(True, <str>)`
conversion was successful, str is resultant Plus Code conversion succeeded, second element is the resultant Plus Code string
- `(False, <str>)` - `(False, <str>)`
conversion failed, str is error message conversion failed, second element is an error message string
--- ---

View file

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "surplus" name = "surplus"
version = "1.1.0" version = "1.1.1"
description = "Plus Code to iOS-Shortcuts-like shareable text" description = "Plus Code to iOS-Shortcuts-like shareable text"
authors = ["Mark Joshwel <mark@joshwel.co>"] authors = ["Mark Joshwel <mark@joshwel.co>"]
license = "Unlicence" license = "Unlicence"

View file

@ -30,8 +30,9 @@ For more information, please refer to <http://unlicense.org/>
""" """
from argparse import ArgumentParser from argparse import ArgumentParser
from collections import OrderedDict
from sys import stderr 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 import Location # type: ignore
from geopy.geocoders import Nominatim # 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.openlocationcode import recoverNearest # type: ignore
from pluscodes.validator import Validator # 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): class Localcode(NamedTuple):
@ -130,49 +131,24 @@ def surplus(
(True, <str>) - conversion was successful, str is resultant text (True, <str>) - conversion was successful, str is resultant text
(False, <str>) - conversion failed, str is error message (False, <str>) - conversion failed, str is error message
""" """
lat: float = 0.0
lon: float = 0.0
if isinstance(query, Latlong): def _unique(l: list[str]) -> list[str]:
lat, lon = query.lat, query.long unique: OrderedDict = OrderedDict()
for line in l:
unique.update({line: None})
return list(unique.keys())
else: # instances: str | Localcode _latlong = handle_query(query=query, debug=debug)
str_pcode: str = ""
if isinstance(query, Localcode): if _latlong[0] is False:
result = query.full_length() assert isinstance(_latlong[1], str)
return False, _latlong[1]
if not result[0]: assert isinstance(_latlong[1], Latlong)
return False, result[1] latlong = _latlong[1]
str_pcode = result[1]
else:
str_pcode = query
try: try:
pcode = PlusCode(str_pcode) _reversed: Location | None = reverser(f"{latlong.lat}, {latlong.long}")
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")
try:
_reversed: Location | None = reverser(f"{lat}, {lon}")
if _reversed is None: if _reversed is None:
raise Exception(f"reverser function returned None") raise Exception(f"reverser function returned None")
@ -182,17 +158,21 @@ def surplus(
except Exception as reverr: except Exception as reverr:
return ( return (
False, False,
f"error while reversing latlong ({lat},{lon}): {reverr.__class__.__name__} - {reverr}", f"error while reversing latlong ({Latlong}): {reverr.__class__.__name__} - {reverr}",
) )
if debug: if debug:
stderr.write(f"debug: {location=}\n") stderr.write(f"debug: {location=}\n")
data: list[str] = [ text: list[str] = _unique(
[
( (
", ".join( ", ".join(
[ [
location["address"].get(detail, "") d
for d in _unique(
[
location["address"].get(detail, None)
for detail in ( for detail in (
"emergency, historic, military, natural, landuse, place, railway, " "emergency, historic, military, natural, landuse, place, railway, "
"man_made, aerialway, boundary, amenity, aeroway, club, craft, " "man_made, aerialway, boundary, amenity, aeroway, club, craft, "
@ -200,10 +180,10 @@ def surplus(
).split(", ") ).split(", ")
] ]
) )
if d is not None
]
)
).strip(", "), ).strip(", "),
# location["address"].get("leisure"),
# location["address"].get("shop"),
# location["address"].get("railway"),
( (
location["address"].get("building") location["address"].get("building")
if ( if (
@ -218,35 +198,73 @@ def surplus(
+ (" " + location["address"].get("house_name", "")).strip() + (" " + location["address"].get("house_name", "")).strip()
+ " " + " "
+ location["address"].get("road", "") + location["address"].get("road", "")
+ ( # + (
", " + location["address"].get("suburb", "") # ", " + location["address"].get("suburb", "")
# dont repeat if suburb is mentioned in the road itself # # dont repeat if suburb is mentioned in the road itself
# 'Toa Payoh' in 'Lorong 1A Toa Payoh' # # 'Toa Payoh' in 'Lorong 1A Toa Payoh'
if location["address"].get("suburb", "") # if location["address"].get("suburb", "")
not in location["address"].get("road", "") # not in location["address"].get("road", "")
else "" # else None
) # )
).strip(), ).strip(),
( (
", ".join( ", ".join(
[
d
for d in _unique(
[ [
location["address"].get(detail, "") location["address"].get(detail, "")
for detail in ( for detail in (
"residential, neighbourhood, allotments, quarter" "residential, neighbourhood, allotments, quarter, "
"city_district, district, borough, suburb, subdivision, "
"municipality, city, town, village"
).split(", ") ).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(","), ).strip(","),
location["address"].get("postcode"), location["address"].get("postcode"),
location["address"].get("country"), (
", ".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( def parse_query(
query: str, debug: bool = False 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, function that parses a string Plus Code, local code or latlong into a str,
surplus.Localcode or surplus.Latlong respectively surplus.Localcode or surplus.Latlong respectively
@ -263,7 +281,7 @@ def parse_query(
def _word_match( def _word_match(
oquery: str, squery: list[str] 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 internal helper code reuse function
@ -285,6 +303,9 @@ def parse_query(
locality = oquery.replace(pcode, "") locality = oquery.replace(pcode, "")
locality = locality.strip().strip(",").strip() locality = locality.strip().strip(",").strip()
if debug:
stderr.write(f"debug: {pcode=}, {locality=}\n")
return True, Localcode(code=pcode, locality=locality) return True, Localcode(code=pcode, locality=locality)
return False, "unable to find a pluscode/match to a format" 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) 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, <str>) - 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: def cli() -> None:
parser = ArgumentParser( parser = ArgumentParser(
prog="surplus", prog="surplus",