1.1.1
This commit is contained in:
commit
e61960dcfb
31
.github/workflows/check-quality.yml
vendored
Normal file
31
.github/workflows/check-quality.yml
vendored
Normal 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"
|
|
@ -1,6 +1,9 @@
|
|||
name: release (slsa 3)
|
||||
name: release with slsa 3 compliance
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
main
|
||||
tags:
|
||||
- '*'
|
||||
|
81
README.md
81
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, <str>)`
|
||||
conversion was successful, str is resultant text
|
||||
conversion succeeded, second element is the resultant string
|
||||
- `(False, <str>)`
|
||||
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, <str | Localcode | Latlong>)`
|
||||
conversion was successful, second element is result
|
||||
- `(True, <str | surplus.Localcode | surplus.Latlong>)`
|
||||
conversion succeeded, second element is resultant Plus code string, [`surplus.Localcode`](#surpluslocalcode) or [`surplus.Latlong`](#surpluslatlong)
|
||||
- `(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:
|
||||
|
||||
- `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, <str>)`
|
||||
conversion was successful, str is resultant Plus Code
|
||||
conversion succeeded, second element is the resultant Plus Code string
|
||||
- `(False, <str>)`
|
||||
conversion failed, str is error message
|
||||
conversion failed, second element is an error message string
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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 <mark@joshwel.co>"]
|
||||
license = "Unlicence"
|
||||
|
|
269
surplus.py
269
surplus.py
|
@ -30,8 +30,9 @@ For more information, please refer to <http://unlicense.org/>
|
|||
"""
|
||||
|
||||
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, <str>) - conversion was successful, str is resultant text
|
||||
(False, <str>) - 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, <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:
|
||||
parser = ArgumentParser(
|
||||
prog="surplus",
|
||||
|
|
Loading…
Reference in a new issue