From 22c79d9349f68d3e3645958faebeb95d823cf860 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 14:41:25 +0000 Subject: [PATCH 01/41] meta: prepare for 2.0.0 --- README.md | 233 ++++++------------------------------------------- poetry.lock | 145 +++++++++++++++--------------- pyproject.toml | 2 +- surplus.py | 4 +- 4 files changed, 103 insertions(+), 281 deletions(-) diff --git a/README.md b/README.md index 408f743..c7a0bd0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ surplus is a Python script to convert [Google Map Plus Codes](https://maps.google.com/pluscodes/) -to iOS Shortcuts-like human text. +to iOS Shortcuts-like human-readable text. - [installation](#installation) - [command-line usage](#command-line-usaage) @@ -17,20 +17,13 @@ to iOS Shortcuts-like human text. ```text $ surplus 9R3J+R9 Singapore -surplus version 1.1.3 -Thomson Plaza -301 Upper Thomson Road -Sin Ming, Bishan -574408 -Central, Singapore +TODO CLI DEMO ``` ```python >>> from surplus import surplus, Localcode >>> Localcode(code="8RPQ+JW", locality="Singapore").full_length() -(True, '6PH58RPQ+JW') ->>> surplus("6PH58RPQ+JW") -(True, 'Caldecott Stn Exit 4\nToa Payoh Link\n298106\nCentral, Singapore') +TODO API DEMO ``` ## installation @@ -101,7 +94,7 @@ the command to create an empty commit is `git commit --allow-empty` ### reporting incorrect output -> **Note** +> **Note** > this section is independent from the rest of the contributing section. different output from the iOS Shortcuts app is expected, however incorrect output is not. @@ -110,27 +103,28 @@ different output from the iOS Shortcuts app is expected, however incorrect outpu open an issue in the [repositories issue tracker](https://github.com/markjoshwel/surplus/issues/new), -and include the following: +and do the following: -1. ensure that your issue is not an error of incorrect data returned by your reverser - function, which by default is OpenStreetMap Nominatim. - (_don't know what the above means? then using the default reverser._) +1. ensure that your issue is not an error of incorrect data returned by your reverser + function, which by default is OpenStreetMap Nominatim. + (_don't know what the above means? then you are using the default reverser._) - also look at "[what counts as 'incorrect'](#what-counts-as-incorrect)" before - moving on. + also look at the [what counts as "incorrect"](#what-counts-as-incorrect) section + before moving on. -2. include the erroneous Plus Code, local code, latitude and longitude coordinate, or - query string. +2. include the erroneous query. + (_the Plus Code/local code/latlong coord/query string you passed into surplus_) 3. include output from the teminal with the - [`--debug` flag](#command-line-usage) passed to the surplus CLI or with + [`--debug` flag](#command-line-usage) passed to the surplus CLI or with `debug=True` set in function calls. - > **Note** - > if you are using custom stdout and stderr parameters and redirecting output, - > include that instead. + > **Note** + > if you are using the surplus API and have passed custom stdout and stderr parameters + > to redirect output, include that instead. -4. how it should look like instead, with reasoning if the error not obvious. (e.g., missing details) +4. how it should look like instead, with reasoning if the error is not obvious. (e.g., + missing details) for reference, see how the following issues were written: @@ -140,7 +134,7 @@ and include the following: #### what counts as "incorrect" -- **example 1** +- **example** (correct) - iOS Shortcuts Output @@ -178,40 +172,16 @@ other examples that _should not_ be reported are: (_if so, then consider updating OpenStreetMap to help not just you, but other surplus and OpenStreetMap users!_) -you should report when the output does not make logical sense, or something similar +**you should report** when the output does not make logical sense, or something similar wherein the output of surplus is illogical to read or is not correct in the traditional sense of a correct address. -see the linked issues in [the reporting process](#the-reporting-process) for examples +see the linked issues in [the reporting process](#the-reporting-process) for examples of incorrect outputs. ## the technical details of surplus's output -``` -$ s+ 8QJF+RP --debug -surplus version 1.1.3, debug mode -debug: args.query='8QJF+RP Singapore' -debug: squery=['8QJF+RP', 'Singapore'] -debug: pcode='8QJF+RP', locality='Singapore' -debug: lat=1.3320625, lon=103.7743125 -debug: location={...} -debug: seen_names=['Ngee Ann Polytechnic', '', '', ''] -debug: d='' _dvtm4=[False, False, False, False] _dvcm4=[False, False, False, False] -debug: d='Bukit Timah' _dvtm4=[True, True, True, True] _dvcm4=[True, True, True, True] -debug: d='Singapore' _dvtm4=[True, True, False, True] _dvcm4=[True, True, True, True] -0 Ngee Ann Polytechnic -1 -2 -3 535 Clementi Road -4 Bukit Timah -5 599489 -6 Northwest, Singapore -Ngee Ann Polytechnic -535 Clementi Road -Bukit Timah -599489 -Northwest, Singapore -``` +TODO DEBUG OUTPUT "8QJF+RP Singapore" variables @@ -234,7 +204,7 @@ variables - **variables `pcode` and `locality`** - (_only shown if the query is a local code, not shown on full-length plus codes, + (_only shown if the query is a local code, not shown on full-length plus codes, latlong coordinates or string queries_) represents the plus code and locality portions of a @@ -263,13 +233,13 @@ variables - **variable `d`** - current element in the iteration of the final output line 4 (general regional + current element in the iteration of the final output line 4 (general regional location) nominatim keys - **variable `_dvmt4`** list used in an `all()` check to see if the current nominatim key (variable `d`) can - be wholly found in any of the seen names, in the general regional location, or in + be wholly found in any of the seen names, in the general regional location, or in the road name. reasoning is, if the previous lines wholly state the general regional location of the @@ -321,8 +291,8 @@ breakdown of each output line, accompanied by their nominatim key: - nominatim keys ``` - emergency, historic, military, natural, landuse, place, railway, man_made, - aerialway, boundary, amenity, aeroway, club, craft, leisure, office, mountain_pass, + emergency, historic, military, natural, landuse, place, railway, man_made, + aerialway, boundary, amenity, aeroway, club, craft, leisure, office, mountain_pass, shop, tourism, bridge, tunnel, waterway ``` @@ -386,7 +356,7 @@ breakdown of each output line, accompanied by their nominatim key: - nominatim keys ``` - residential, neighbourhood, allotments, quarter, city_district, district, borough, + residential, neighbourhood, allotments, quarter, city_district, district, borough, suburb, subdivision, municipality, city, town, village ``` @@ -424,152 +394,7 @@ breakdown of each output line, accompanied by their nominatim key: ## api reference -### `surplus.surplus()` - -pluscode to shareable text conversion function - -- signature - - ```python - def surplus( - query: str | Localcode | Latlong, - reverser: typing.Callable = geopy.geocoders.Nominatim(user_agent="surplus").reverse, - debug: bool = False, - ) -> tuple[bool, str]: - ... - ``` - -- arguments - - - `query: str | surplus.Localcode | surplus.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 - function should be able to take a string with two floats and return a `geopy.Location`-like object (None checking is done) - - ```python - # code used by surplus - location: dict[str, Any] = reverser(f"{lat}, {lon}").raw - ``` - - dict should be similar to [nominatim raw dicts](https://nominatim.org/release-docs/latest/api/Output/#addressdetails) - - - `debug: bool = False` - prints lat, long and reverser response dict to stderr - -- returns `tuple[bool, str]` - - - `(True, )` - conversion succeeded, second element is the resultant string - - `(False, )` - 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`](#surpluslocalcode) or [`surplus.Latlong`](#surpluslatlong) respectively - -- signature: - - ```python - def parse_query( - query: str, debug: bool = False - ) -> tuple[typing.Literal[True], str | Localcode | Latlong] | tuple[typing.Literal[False], str]: - ``` - -- arguments: - - - `query: str` - string Plus Code, local code or latlong - -- returns `tuple[typing.Literal[True], str | Localcode | Latlong] | tuple[typing.Literal[False], str]` - - - `(True, )` - conversion succeeded, second element is resultant Plus code string, [`surplus.Localcode`](#surpluslocalcode) or [`surplus.Latlong`](#surpluslatlong) - - `(False, )` - 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[typing.Literal[True], Latlong] | tuple[typing.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[typing.Literal[True], Latlong] | tuple[typing.Literal[False], str]` - - `(True, )` - conversion succeeded, second element is a [`surplus.Latlong`](#surpluslatlong) - - `(False, )` - conversion failed, second element is an error message string - -### `surplus.Localcode` - -`typing.NamedTuple` representing short Plus Code with locality - -- parameters: - - - `code: str` - Plus Code - e.g.: `"8QMF+FX"` - - `locality: str` - e.g.: `"Singapore"` - -#### `surplus.Localcode.full_length()` - -method that calculates full-length Plus Code using locality - -- signature: - - ```python - def full_length( - self, geocoder: Callable = Nominatim(user_agent="surplus").geocode - ) -> tuple[bool, str]: - ``` - -- arguments: - - - `geocoder: typing.Callable = geopy.geocoders.Nominatim(user_agent="surplus").geocode` - place/locality to location function, accesses .longitude and .latitude if returned object is not None - -- returns: - - - `(True, )` - conversion succeeded, second element is the resultant Plus Code string - - `(False, )` - conversion failed, second element is an error message string - -#### `surplus.Latlong` - -`typing.NamedTuple` representing a pair of latitude and longitude coordinates - -- parameters: - - - `lat: float` - latitudinal coordinate - - `long: float` - longitudinal coordinate +TODO API REF ## licence diff --git a/poetry.lock b/poetry.lock index b0a3f97..c999c39 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2,37 +2,34 @@ [[package]] name = "black" -version = "23.3.0" +version = "23.7.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "black-23.3.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9"}, - {file = "black-23.3.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2"}, - {file = "black-23.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c"}, - {file = "black-23.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b"}, - {file = "black-23.3.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d"}, - {file = "black-23.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70"}, - {file = "black-23.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326"}, - {file = "black-23.3.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b"}, - {file = "black-23.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2"}, - {file = "black-23.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331"}, - {file = "black-23.3.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5"}, - {file = "black-23.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961"}, - {file = "black-23.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3"}, - {file = "black-23.3.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266"}, - {file = "black-23.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab"}, - {file = "black-23.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb"}, - {file = "black-23.3.0-py3-none-any.whl", hash = "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4"}, - {file = "black-23.3.0.tar.gz", hash = "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:5c4bc552ab52f6c1c506ccae05681fab58c3f72d59ae6e6639e8885e94fe2587"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:552513d5cd5694590d7ef6f46e1767a4df9af168d449ff767b13b084c020e63f"}, + {file = "black-23.7.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:86cee259349b4448adb4ef9b204bb4467aae74a386bce85d56ba4f5dc0da27be"}, + {file = "black-23.7.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:501387a9edcb75d7ae8a4412bb8749900386eaef258f1aefab18adddea1936bc"}, + {file = "black-23.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb074d8b213749fa1d077d630db0d5f8cc3b2ae63587ad4116e8a436e9bbe995"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b5b0ee6d96b345a8b420100b7d71ebfdd19fab5e8301aff48ec270042cd40ac2"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:893695a76b140881531062d48476ebe4a48f5d1e9388177e175d76234ca247cd"}, + {file = "black-23.7.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:c333286dc3ddca6fdff74670b911cccedacb4ef0a60b34e491b8a67c833b343a"}, + {file = "black-23.7.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831d8f54c3a8c8cf55f64d0422ee875eecac26f5f649fb6c1df65316b67c8926"}, + {file = "black-23.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:7f3bf2dec7d541b4619b8ce526bda74a6b0bffc480a163fed32eb8b3c9aed8ad"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:f9062af71c59c004cd519e2fb8f5d25d39e46d3af011b41ab43b9c74e27e236f"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:01ede61aac8c154b55f35301fac3e730baf0c9cf8120f65a9cd61a81cfb4a0c3"}, + {file = "black-23.7.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:327a8c2550ddc573b51e2c352adb88143464bb9d92c10416feb86b0f5aee5ff6"}, + {file = "black-23.7.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1c6022b86f83b632d06f2b02774134def5d4d4f1dac8bef16d90cda18ba28a"}, + {file = "black-23.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:27eb7a0c71604d5de083757fbdb245b1a4fae60e9596514c6ec497eb63f95320"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:8417dbd2f57b5701492cd46edcecc4f9208dc75529bcf76c514864e48da867d9"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:47e56d83aad53ca140da0af87678fb38e44fd6bc0af71eebab2d1f59b1acf1d3"}, + {file = "black-23.7.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:25cc308838fe71f7065df53aedd20327969d05671bac95b38fdf37ebe70ac087"}, + {file = "black-23.7.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:642496b675095d423f9b8448243336f8ec71c9d4d57ec17bf795b67f08132a91"}, + {file = "black-23.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:ad0014efc7acf0bd745792bd0d8857413652979200ab924fbf239062adc12491"}, + {file = "black-23.7.0-py3-none-any.whl", hash = "sha256:9fd59d418c60c0348505f2ddf9609c1e1de8e7493eab96198fc89d9f865e7a96"}, + {file = "black-23.7.0.tar.gz", hash = "sha256:022a582720b0d9480ed82576c920a8c1dde97cc38ff11d8d8859b3bd6ca9eedb"}, ] [package.dependencies] @@ -51,14 +48,14 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "click" -version = "8.1.3" +version = "8.1.7" description = "Composable command line interface toolkit" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, ] [package.dependencies] @@ -90,14 +87,14 @@ files = [ [[package]] name = "geopy" -version = "2.3.0" +version = "2.4.0" description = "Python Geocoding Toolbox" category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "geopy-2.3.0-py3-none-any.whl", hash = "sha256:4a29a16d41d8e56ba8e07310802a1cbdf098eeb6069cc3d6d3068fc770629ffc"}, - {file = "geopy-2.3.0.tar.gz", hash = "sha256:228cd53b6eef699b2289d1172e462a90d5057779a10388a7366291812601187f"}, + {file = "geopy-2.4.0-py3-none-any.whl", hash = "sha256:d2639a46d0ce4c091e9688b750ba94348a14b898a1e55c68f4b4a07e7d1afa20"}, + {file = "geopy-2.4.0.tar.gz", hash = "sha256:a59392bf17adb486b25dbdd71fbed27733bdf24a2dac588047a619de56695e36"}, ] [package.dependencies] @@ -132,49 +129,49 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "mypy" -version = "1.3.0" +version = "1.5.1" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mypy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c1eb485cea53f4f5284e5baf92902cd0088b24984f4209e25981cc359d64448d"}, - {file = "mypy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c99c3ecf223cf2952638da9cd82793d8f3c0c5fa8b6ae2b2d9ed1e1ff51ba85"}, - {file = "mypy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:550a8b3a19bb6589679a7c3c31f64312e7ff482a816c96e0cecec9ad3a7564dd"}, - {file = "mypy-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cbc07246253b9e3d7d74c9ff948cd0fd7a71afcc2b77c7f0a59c26e9395cb152"}, - {file = "mypy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:a22435632710a4fcf8acf86cbd0d69f68ac389a3892cb23fbad176d1cddaf228"}, - {file = "mypy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6e33bb8b2613614a33dff70565f4c803f889ebd2f859466e42b46e1df76018dd"}, - {file = "mypy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7d23370d2a6b7a71dc65d1266f9a34e4cde9e8e21511322415db4b26f46f6b8c"}, - {file = "mypy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:658fe7b674769a0770d4b26cb4d6f005e88a442fe82446f020be8e5f5efb2fae"}, - {file = "mypy-1.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e42d29e324cdda61daaec2336c42512e59c7c375340bd202efa1fe0f7b8f8ca"}, - {file = "mypy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:d0b6c62206e04061e27009481cb0ec966f7d6172b5b936f3ead3d74f29fe3dcf"}, - {file = "mypy-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:76ec771e2342f1b558c36d49900dfe81d140361dd0d2df6cd71b3db1be155409"}, - {file = "mypy-1.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ebc95f8386314272bbc817026f8ce8f4f0d2ef7ae44f947c4664efac9adec929"}, - {file = "mypy-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:faff86aa10c1aa4a10e1a301de160f3d8fc8703b88c7e98de46b531ff1276a9a"}, - {file = "mypy-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:8c5979d0deb27e0f4479bee18ea0f83732a893e81b78e62e2dda3e7e518c92ee"}, - {file = "mypy-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c5d2cc54175bab47011b09688b418db71403aefad07cbcd62d44010543fc143f"}, - {file = "mypy-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:87df44954c31d86df96c8bd6e80dfcd773473e877ac6176a8e29898bfb3501cb"}, - {file = "mypy-1.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:473117e310febe632ddf10e745a355714e771ffe534f06db40702775056614c4"}, - {file = "mypy-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:74bc9b6e0e79808bf8678d7678b2ae3736ea72d56eede3820bd3849823e7f305"}, - {file = "mypy-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:44797d031a41516fcf5cbfa652265bb994e53e51994c1bd649ffcd0c3a7eccbf"}, - {file = "mypy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddae0f39ca146972ff6bb4399f3b2943884a774b8771ea0a8f50e971f5ea5ba8"}, - {file = "mypy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c4c42c60a8103ead4c1c060ac3cdd3ff01e18fddce6f1016e08939647a0e703"}, - {file = "mypy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e86c2c6852f62f8f2b24cb7a613ebe8e0c7dc1402c61d36a609174f63e0ff017"}, - {file = "mypy-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:f9dca1e257d4cc129517779226753dbefb4f2266c4eaad610fc15c6a7e14283e"}, - {file = "mypy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:95d8d31a7713510685b05fbb18d6ac287a56c8f6554d88c19e73f724a445448a"}, - {file = "mypy-1.3.0-py3-none-any.whl", hash = "sha256:a8763e72d5d9574d45ce5881962bc8e9046bf7b375b0abf031f3e6811732a897"}, - {file = "mypy-1.3.0.tar.gz", hash = "sha256:e1f4d16e296f5135624b34e8fb741eb0eadedca90862405b1f1fde2040b9bd11"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70"}, + {file = "mypy-1.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0"}, + {file = "mypy-1.5.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12"}, + {file = "mypy-1.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d"}, + {file = "mypy-1.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4"}, + {file = "mypy-1.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4"}, + {file = "mypy-1.5.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243"}, + {file = "mypy-1.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275"}, + {file = "mypy-1.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb"}, + {file = "mypy-1.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373"}, + {file = "mypy-1.5.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161"}, + {file = "mypy-1.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a"}, + {file = "mypy-1.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65"}, + {file = "mypy-1.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160"}, + {file = "mypy-1.5.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2"}, + {file = "mypy-1.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb"}, + {file = "mypy-1.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a"}, + {file = "mypy-1.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14"}, + {file = "mypy-1.5.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb"}, + {file = "mypy-1.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693"}, + {file = "mypy-1.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770"}, + {file = "mypy-1.5.1-py3-none-any.whl", hash = "sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5"}, + {file = "mypy-1.5.1.tar.gz", hash = "sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92"}, ] [package.dependencies] mypy-extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = ">=3.10" +typing-extensions = ">=4.1.0" [package.extras] dmypy = ["psutil (>=4.0)"] install-types = ["pip"] -python2 = ["typed-ast (>=1.4.0,<2)"] reports = ["lxml"] [[package]] @@ -203,31 +200,31 @@ files = [ [[package]] name = "pathspec" -version = "0.11.1" +version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, - {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] [[package]] name = "platformdirs" -version = "3.5.1" +version = "3.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "platformdirs-3.5.1-py3-none-any.whl", hash = "sha256:e2378146f1964972c03c085bb5662ae80b2b8c06226c54b2ff4aa9483e8a13a5"}, - {file = "platformdirs-3.5.1.tar.gz", hash = "sha256:412dae91f52a6f84830f39a8078cecd0e866cb72294a5c66808e74d5e88d251f"}, + {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"}, + {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"}, ] [package.extras] -docs = ["furo (>=2023.3.27)", "proselint (>=0.13)", "sphinx (>=6.2.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] [[package]] name = "pluscodes" @@ -257,14 +254,14 @@ files = [ [[package]] name = "typing-extensions" -version = "4.6.3" +version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "typing_extensions-4.6.3-py3-none-any.whl", hash = "sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26"}, - {file = "typing_extensions-4.6.3.tar.gz", hash = "sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5"}, + {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"}, + {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] [metadata] diff --git a/pyproject.toml b/pyproject.toml index 82c23fe..e63bf25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "surplus" -version = "1.1.3" +version = "2.0.0" description = "Plus Code to iOS-Shortcuts-like shareable text" authors = ["Mark Joshwel "] license = "Unlicence" diff --git a/surplus.py b/surplus.py index 89af89d..209040f 100644 --- a/surplus.py +++ b/surplus.py @@ -40,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, 3) +VERSION: Final[tuple[int, int, int]] = (2, 0, 0) class Localcode(NamedTuple): @@ -222,7 +222,7 @@ def surplus( if debug: stderr.write(f"debug: {d=}\t{_dvtm4=}\t{_dvcm4=}\n") - + text[-1] += ", ".join(basket) text.append(("5\t" if debug else "") + address.get("postcode", "")) From dbcf659801f3f5fe4026cb6c0c390b59c4908e85 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 14:43:28 +0000 Subject: [PATCH 02/41] docs: pip install from git@main, frame todo --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c7a0bd0..664f535 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ TODO API DEMO install surplus directly from the repository using pip: ```text -pip install git+https://github.com/markjoshwel/surplus +pip install git+https://github.com/markjoshwel/surplus.git@main ``` ## command-line usage @@ -181,7 +181,10 @@ of incorrect outputs. ## the technical details of surplus's output -TODO DEBUG OUTPUT "8QJF+RP Singapore" +``` +$ s+ --debug 8QJF+RP Singapore +TODO DEBUG OUTPUT +``` variables From 3355f6a27995bf94003f46b53a0638e164761bde Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 16:41:19 +0000 Subject: [PATCH 03/41] docs: be wary, this is the future --- README.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 664f535..3bec5d7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,20 @@ # surplus +> **Warning** +> +> _**this is surplus 2.0.0.**_ +> +> surplus is being rewritten to better incorporate with +> [sandplus](https://github.com/markjoshwel/sandplus.git). +> sandplus is surplus's Android application accompaniment, written in Kotlin with Jetpack +> Compose. +> +> you are on the `future` branch. if you see this warning, that means code is not +> finalised and ready to be used. +> +> want the old, stable, working codebase? see the +> [`main`](https://github.com/markjoshwel/surplus/tree/main) branch. + surplus is a Python script to convert [Google Map Plus Codes](https://maps.google.com/pluscodes/) to iOS Shortcuts-like human-readable text. @@ -31,7 +46,7 @@ TODO API DEMO install surplus directly from the repository using pip: ```text -pip install git+https://github.com/markjoshwel/surplus.git@main +pip install git+https://github.com/markjoshwel/surplus.git@future ``` ## command-line usage From 4e612eb92ea45ca416b7ec3c729c29786ad70279 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 16:57:32 +0000 Subject: [PATCH 04/41] surplus: 2.0.0 rewrite groundwork --- surplus.future.ipynb | 58 +++++ surplus.py | 502 ++++++------------------------------------- test.py | 2 + 3 files changed, 129 insertions(+), 433 deletions(-) create mode 100644 surplus.future.ipynb diff --git a/surplus.future.ipynb b/surplus.future.ipynb new file mode 100644 index 0000000..a182966 --- /dev/null +++ b/surplus.future.ipynb @@ -0,0 +1,58 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# surplus 2.0.0 playground notebook" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "OUTPUT_LINE_X_KEYS: Final[tuple[str, ...]] = (\"region\",\"county\",\"state\",\"state_district\",\"country\",\"continent\",)\n" + ] + } + ], + "source": [ + "# converting nominatim keys to OUTPUT_LINE_X_KEYS format\n", + "\n", + "keys = \"\"\"\n", + "region, county, state, state_district, country, continent\n", + "\"\"\"\n", + "\n", + "split_keys = [f'\"{key.strip()}\"' for key in keys.strip().split(\",\")]\n", + "\n", + "print(f\"OUTPUT_LINE_X_KEYS: Final[tuple[str, ...]] = ({','.join(split_keys)},)\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/surplus.py b/surplus.py index 209040f..ee9b78c 100644 --- a/surplus.py +++ b/surplus.py @@ -40,441 +40,77 @@ from pluscodes import PlusCode # type: ignore from pluscodes.openlocationcode import recoverNearest # type: ignore from pluscodes.validator import Validator # type: ignore + +# constants + VERSION: Final[tuple[int, int, int]] = (2, 0, 0) - -class Localcode(NamedTuple): - """ - typing.NamedTuple representing short Plus Code with locality - - code: str - Plus Code - e.g.: "8QMF+FX" - locality: str - e.g.: "Singapore" - """ - - code: str - locality: str - - def full_length( - self, geocoder: Callable = Nominatim(user_agent="surplus").geocode - ) -> tuple[bool, str]: - """ - method that calculates full-length Plus Code using locality - - geocoder: typing.Callable = geopy.geocoders.Nominatim(user_agent="surplus").geocode - place/locality to location function, accesses .longitude and .latitude if - returned object is not None - - returns tuple[bool, str] - (True, ) - conversion was successful, str is resultant Plus Code - (False, ) - conversion failed, str is error message - """ - location: Location | None = geocoder(self.locality) - lat: float = 0.0 - lon: float = 0.0 - - if location is None: - return False, f"no coordinates found for '{self.locality}'" - - recv_pcode = recoverNearest( - code=self.code, - referenceLongitude=location.longitude, - referenceLatitude=location.latitude, - ) - - return True, recv_pcode - - -class Latlong(NamedTuple): - """ - typing.NamedTuple representing a pair of latitude and longitude coordinates - - lat: float - latitudinal coordinate - long: float - longitudinal coordinate - """ - - lat: float - long: float - - -def surplus( - query: str | Localcode | Latlong, - reverser: Callable = Nominatim(user_agent="surplus").reverse, - debug: bool = False, -) -> tuple[bool, str]: - """ - pluscode to shareable text conversion function - - query: str | surplus.Localcode | surplus.Latlong - str - normal longcode (6PH58QMF+FX) - surplus.Localcode - shortcode with locality (8QMF+FX Singapore) - surplus.Latlong - latlong - - reverser: typing.Callable = geopy.geocoders.Nominatim(user_agent="surplus").reverser - latlong to data function, accesses a dict from .raw attribute of return object - function should be able to take a string with two floats and return a - geopy.Location-like object (None checking is done) - - # code used by surplus - location: dict[str, Any] = reverser(f"{lat}, {lon}").raw - - dict should be similar to nominatim raw dicts, see - - - debug: bool = False - prints lat, long and reverser response dict to stderr - - returns tuple[bool, str] - (True, ) - conversion was successful, str is resultant text - (False, ) - conversion failed, str is error message - """ - - def _unique(l: list[str]) -> list[str]: - """(internal function) returns a in-order unique list from list""" - unique: OrderedDict = OrderedDict() - for line in l: - unique.update({line: None}) - return list(unique.keys()) - - def _generate_text(address: dict[str, str], debug: bool = False) -> list[str]: - """(internal function) separation of concern function for text generation""" - - text: list[str] = [] - seen_names: list[str] = [] - - text.append( - ("0\t" if debug else "") - + ", ".join( - seen_names := [ - d - for d in _unique( - [ - 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(", ") - ] - ) - if d != "" - ] - ) - ) - - if address.get("building") != address.get("house_number"): - seen_names += [address.get("building", "")] - text.append(("1\t" if debug else "") + address.get("building", "")) - - seen_names += [address.get("highway", "")] - text.append(("2\t" if debug else "") + address.get("highway", "")) - - seen_names += [address.get("house_name", "")] - text.append( - ("3\t" if debug else "") - + ( - address.get("house_number", "") - + (" " + address.get("house_name", "")).strip() - + " " - + address.get("road", "") - ).strip() - ) - - if debug: - stderr.write(f"debug: {seen_names=}\n") - - text.append("4\t" if debug else "") - basket: list[str] = [] - for d in _unique( - [ - address.get(detail, "") - for detail in ( - "residential, neighbourhood, allotments, quarter, " - "city_district, district, borough, suburb, subdivision, " - "municipality, city, town, village" - ).split(", ") - ] - ): - if all( - _dvtm4 := [ - d != "", - d not in address.get("road", ""), - d - not in [ - address.get(detail, "") - for detail in ( - "region, state, state_district, county, " - "state, country, continent" - ).split(", ") - ], - any( - _dvcm4 := [ - True if (d not in sn) else False for sn in seen_names - ] - ), - ] - ): - basket.append(d) - - if debug: - stderr.write(f"debug: {d=}\t{_dvtm4=}\t{_dvcm4=}\n") - - text[-1] += ", ".join(basket) - - text.append(("5\t" if debug else "") + address.get("postcode", "")) - - text.append( - ("6\t" if debug else "") - + ", ".join( - [ - d - for d in _unique( - [ - address.get(detail, "") - for detail in ( - "region, county, state, state_district, " - "country, continent" - ).split(", ") - ] - ) - if d != "" - ] - ) - ) - - return [d for d in _unique(text) if all([d != None, d != ""])] - - _latlong = handle_query(query=query, debug=debug) - - if _latlong[0] is False: - assert isinstance(_latlong[1], str) - return False, _latlong[1] - - assert isinstance(_latlong[1], Latlong) - latlong = _latlong[1] - - try: - _reversed: Location | None = reverser(f"{latlong.lat}, {latlong.long}") - - if _reversed is None: - raise Exception(f"reverser function returned None") - - location: dict[str, Any] = _reversed.raw - - except Exception as reverr: - return ( - False, - f"error while reversing latlong ({Latlong}): {reverr.__class__.__name__} - {reverr}", - ) - - if debug: - stderr.write(f"debug: {location=}\n") - return True, "\n".join( - _generate_text(address=location.get("address", {}), debug=debug) - + _generate_text(address=location.get("address", {})) - ) - - return True, "\n".join(_generate_text(address=location.get("address", {}))) - - -def parse_query( - query: str, debug: bool = False -) -> 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 - - query: str - string Plus Code, local code or latlong - debug: bool = False - prints query parsing information to stderr - - returns tuple[bool, str | Localcode | Latlong] - (True, ) - conversion was successful, second element is result - (False, ) - conversion failed, str is error message - """ - - def _word_match( - oquery: str, squery: list[str] - ) -> tuple[Literal[True], str | Localcode | Latlong] | tuple[Literal[False], str]: - """ - internal helper code reuse function - - looks through each 'word' and attempts to match to a Plus Code - if found, remove from original query and strip of whitespace and commas - use resulting stripped query as locality - """ - - pcode: str = "" - - for word in squery: - if Validator().is_valid(word): - pcode = word - - if Validator().is_full(word): - return True, word - - if pcode != "": # found a pluscode - 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" - - squery = [word.strip(",").strip() for word in query.split()] - - if debug: - stderr.write(f"debug: {squery=}\n") - - match squery: - # attempt to match to conjoined latlong ('lat,long') - case [a]: - try: - plat, plong = a.split(",") - lat = float(plat) - long = float(plong) - - except ValueError: - return _word_match(oquery=query, squery=squery) - - else: - return True, Latlong(lat=lat, long=long) - - # attempt to match to latlong ('lat, long') - case [a, b]: - try: - lat = float(a) - long = float(b) - - except ValueError: - return _word_match(oquery=query, squery=squery) - - else: - return True, Latlong(lat=lat, long=long) - - case _: - 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", - description=__doc__[__doc__.find(":") + 2 : __doc__.find("\n", 1)], - ) - parser.add_argument( - "query", - type=str, - help="full-length Plus Code (6PH58QMF+FX), local code (8QMF+FX Singapore), or latlong (1.3336875, 103.7749375)", - nargs="*", - ) - parser.add_argument( - "-d", - "--debug", - action="store_true", - default=False, - help="prints lat, long and reverser response dict to stderr", - ) - parser.add_argument( - "-v", - "--version", - action="store_true", - default=False, - help="prints version information to stderr and exits", - ) - args = parser.parse_args() - - stderr.write( - f"surplus version {'.'.join([str(v) for v in VERSION])}" - + (f", debug mode" if args.debug else "") - + "\n" - ) - - if args.version: - exit() - - if args.debug: - stderr.write("debug: args.query='" + " ".join(args.query) + "'\n") - - query = parse_query(" ".join(args.query), debug=args.debug) - if not query[0]: - stderr.write(f"{query[-1]}\n") - exit(1) - - result: tuple[bool, str] = surplus(query[-1], debug=args.debug) - if not result[0]: - stderr.write(f"{result[-1]}\n") - exit(2) - - print(result[-1]) +OUTPUT_LINE_0_KEYS: Final[tuple[str, ...]] = ( + "emergency", + "historic", + "military", + "natural", + "landuse", + "place", + "railway", + "man_made", + "aerialway", + "boundary", + "amenity", + "aeroway", + "club", + "craft", + "leisure", + "office", + "mountain_pass", + "shop", + "tourism", + "bridge", + "tunnel", + "waterway", +) +OUTPUT_LINE_1_KEYS: Final[tuple[str, ...]] = ("building",) +OUTPUT_LINE_2_KEYS: Final[tuple[str, ...]] = ("highway",) +OUTPUT_LINE_3_KEYS: Final[tuple[str, ...]] = ( + "house_number", + "house_name", + "road", +) +OUTPUT_LINE_4_KEYS: Final[tuple[str, ...]] = ( + "residential", + "neighbourhood", + "allotments", + "quarter", + "city_district", + "district", + "borough", + "suburb", + "subdivision", + "municipality", + "city", + "town", + "village", +) +OUTPUT_LINE_5_KEYS: Final[tuple[str, ...]] = ("postcode",) +OUTPUT_LINE_6_KEYS: Final[tuple[str, ...]] = ( + "region", + "county", + "state", + "state_district", + "country", + "continent", +) + +# program body + +... + +# program entry + + +def main(): + pass if __name__ == "__main__": - cli() + main() diff --git a/test.py b/test.py index aa97476..1341b8f 100644 --- a/test.py +++ b/test.py @@ -1,3 +1,5 @@ +# type: ignore + """ surplus test runner ------------------- From d5ea1f7af449005a40745c78a3d26ac0ed904c4e Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 16:58:28 +0000 Subject: [PATCH 05/41] meta: use black[jupyter] + update group deps --- poetry.lock | 319 ++++++++++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 4 +- 2 files changed, 320 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index c999c39..803e3fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,5 +1,47 @@ # This file is automatically @generated by Poetry and should not be changed by hand. +[[package]] +name = "appnope" +version = "0.1.3" +description = "Disable App Nap on macOS >= 10.9" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "appnope-0.1.3-py2.py3-none-any.whl", hash = "sha256:265a455292d0bd8a72453494fa24df5a11eb18373a60c7c0430889f22548605e"}, + {file = "appnope-0.1.3.tar.gz", hash = "sha256:02bd91c4de869fbb1e1c50aafc4098827a7a54ab2f39d9dcba6c9547ed920e24"}, +] + +[[package]] +name = "asttokens" +version = "2.2.1" +description = "Annotate AST trees with source code positions" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, + {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, +] + +[package.dependencies] +six = "*" + +[package.extras] +test = ["astroid", "pytest"] + +[[package]] +name = "backcall" +version = "0.2.0" +description = "Specifications for callback functions passed in to an API" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "backcall-0.2.0-py2.py3-none-any.whl", hash = "sha256:fbbce6a29f263178a1f7915c1940bde0ec2b2a967566fe1c65c1dfb7422bd255"}, + {file = "backcall-0.2.0.tar.gz", hash = "sha256:5cbdbf27be5e7cfadb448baf0aa95508f91f2bbc6c6437cd9cd06e2a4c215e1e"}, +] + [[package]] name = "black" version = "23.7.0" @@ -34,10 +76,12 @@ files = [ [package.dependencies] click = ">=8.0.0" +ipython = {version = ">=7.8.0", optional = true, markers = "extra == \"jupyter\""} mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" +tokenize-rt = {version = ">=3.2.0", optional = true, markers = "extra == \"jupyter\""} tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] @@ -73,6 +117,33 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "decorator" +version = "5.1.1" +description = "Decorators for Humans" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, + {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, +] + +[[package]] +name = "executing" +version = "1.2.0" +description = "Get the currently executing AST node of a frame, and other information" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "executing-1.2.0-py2.py3-none-any.whl", hash = "sha256:0314a69e37426e3608aada02473b4161d4caf5a4b244d1d0c48072b8fee7bacc"}, + {file = "executing-1.2.0.tar.gz", hash = "sha256:19da64c18d2d851112f09c287f8d3dbbdf725ab0e569077efb6cdcbd3497c107"}, +] + +[package.extras] +tests = ["asttokens", "littleutils", "pytest", "rich"] + [[package]] name = "geographiclib" version = "2.0" @@ -109,6 +180,45 @@ dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (< requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"] timezone = ["pytz"] +[[package]] +name = "ipython" +version = "8.14.0" +description = "IPython: Productive Interactive Computing" +category = "dev" +optional = false +python-versions = ">=3.9" +files = [ + {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, + {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, +] + +[package.dependencies] +appnope = {version = "*", markers = "sys_platform == \"darwin\""} +backcall = "*" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\""} +pickleshare = "*" +prompt-toolkit = ">=3.0.30,<3.0.37 || >3.0.37,<3.1.0" +pygments = ">=2.4.0" +stack-data = "*" +traitlets = ">=5" + +[package.extras] +all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +black = ["black"] +doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +kernel = ["ipykernel"] +nbconvert = ["nbconvert"] +nbformat = ["nbformat"] +notebook = ["ipywidgets", "notebook"] +parallel = ["ipyparallel"] +qtconsole = ["qtconsole"] +test = ["pytest (<7.1)", "pytest-asyncio", "testpath"] +test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pandas", "pytest (<7.1)", "pytest-asyncio", "testpath", "trio"] + [[package]] name = "isort" version = "5.12.0" @@ -127,6 +237,41 @@ pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib" plugins = ["setuptools"] requirements-deprecated-finder = ["pip-api", "pipreqs"] +[[package]] +name = "jedi" +version = "0.19.0" +description = "An autocompletion tool for Python that can be used for text editors." +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "jedi-0.19.0-py2.py3-none-any.whl", hash = "sha256:cb8ce23fbccff0025e9386b5cf85e892f94c9b822378f8da49970471335ac64e"}, + {file = "jedi-0.19.0.tar.gz", hash = "sha256:bcf9894f1753969cbac8022a8c2eaee06bfa3724e4192470aaffe7eb6272b0c4"}, +] + +[package.dependencies] +parso = ">=0.8.3,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] + +[[package]] +name = "matplotlib-inline" +version = "0.1.6" +description = "Inline Matplotlib backend for Jupyter" +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "matplotlib-inline-0.1.6.tar.gz", hash = "sha256:f887e5f10ba98e8d2b150ddcf4702c1e5f8b3a20005eb0f74bfdbd360ee6f304"}, + {file = "matplotlib_inline-0.1.6-py3-none-any.whl", hash = "sha256:f1f41aab5328aa5aaea9b16d083b128102f8712542f819fe7e6a420ff581b311"}, +] + +[package.dependencies] +traitlets = "*" + [[package]] name = "mypy" version = "1.5.1" @@ -198,6 +343,22 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] +[[package]] +name = "parso" +version = "0.8.3" +description = "A Python Parser" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "parso-0.8.3-py2.py3-none-any.whl", hash = "sha256:c001d4636cd3aecdaf33cbb40aebb59b094be2a74c556778ef5576c175e19e75"}, + {file = "parso-0.8.3.tar.gz", hash = "sha256:8c07be290bb59f03588915921e29e8a50002acaf2cdc5fa0e0114f91709fafa0"}, +] + +[package.extras] +qa = ["flake8 (==3.8.3)", "mypy (==0.782)"] +testing = ["docopt", "pytest (<6.0.0)"] + [[package]] name = "pathspec" version = "0.11.2" @@ -210,6 +371,33 @@ files = [ {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, ] +[[package]] +name = "pexpect" +version = "4.8.0" +description = "Pexpect allows easy control of interactive console applications." +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pexpect-4.8.0-py2.py3-none-any.whl", hash = "sha256:0b48a55dcb3c05f3329815901ea4fc1537514d6ba867a152b581d69ae3710937"}, + {file = "pexpect-4.8.0.tar.gz", hash = "sha256:fc65a43959d153d0114afe13997d439c22823a27cefceb5ff35c2178c6784c0c"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "pickleshare" +version = "0.7.5" +description = "Tiny 'shelve'-like database with concurrency support" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pickleshare-0.7.5-py2.py3-none-any.whl", hash = "sha256:9649af414d74d4df115d5d718f82acb59c9d418196b7b4290ed47a12ce62df56"}, + {file = "pickleshare-0.7.5.tar.gz", hash = "sha256:87683d47965c1da65cdacaf31c8441d12b8044cdec9aca500cd78fc2c683afca"}, +] + [[package]] name = "platformdirs" version = "3.10.0" @@ -240,6 +428,107 @@ files = [ [package.extras] dev = ["black (==22.3.0)", "build (==0.8.0)", "coverage (==6.4)", "isort (==5.8.0)", "pylintv (==2.15.0)", "pytest (==7.1.1)", "pytest-cov (==3.0.0)", "twine (==4.0.1)"] +[[package]] +name = "prompt-toolkit" +version = "3.0.39" +description = "Library for building powerful interactive command lines in Python" +category = "dev" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, + {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.2" +description = "Safely evaluate AST nodes without side effects" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "pure_eval-0.2.2-py3-none-any.whl", hash = "sha256:01eaab343580944bc56080ebe0a674b39ec44a945e6d09ba7db3cb8cec289350"}, + {file = "pure_eval-0.2.2.tar.gz", hash = "sha256:2b45320af6dfaa1750f543d714b6d1c520a1688dec6fd24d339063ce0aaa9ac3"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pygments" +version = "2.16.1" +description = "Pygments is a syntax highlighting package written in Python." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Pygments-2.16.1-py3-none-any.whl", hash = "sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692"}, + {file = "Pygments-2.16.1.tar.gz", hash = "sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29"}, +] + +[package.extras] +plugins = ["importlib-metadata"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "stack-data" +version = "0.6.2" +description = "Extract data from python stack frames and tracebacks for informative displays" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "stack_data-0.6.2-py3-none-any.whl", hash = "sha256:cbb2a53eb64e5785878201a97ed7c7b94883f48b87bfb0bbe8b623c74679e4a8"}, + {file = "stack_data-0.6.2.tar.gz", hash = "sha256:32d2dd0376772d01b6cb9fc996f3c8b57a357089dec328ed4b6553d037eaf815"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "tokenize-rt" +version = "5.2.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"}, + {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -252,6 +541,22 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "traitlets" +version = "5.9.0" +description = "Traitlets Python configuration system" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "traitlets-5.9.0-py3-none-any.whl", hash = "sha256:9e6ec080259b9a5940c797d58b613b5e31441c2257b87c2e795c5228ae80d2d8"}, + {file = "traitlets-5.9.0.tar.gz", hash = "sha256:f6cde21a9c68cf756af02035f72d5a723bf607e862e7be33ece505abf4a3bad9"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] + [[package]] name = "typing-extensions" version = "4.7.1" @@ -264,7 +569,19 @@ files = [ {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"}, ] +[[package]] +name = "wcwidth" +version = "0.2.6" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "wcwidth-0.2.6-py2.py3-none-any.whl", hash = "sha256:795b138f6875577cd91bba52baf9e445cd5118fd32723b460e30a0af30ea230e"}, + {file = "wcwidth-0.2.6.tar.gz", hash = "sha256:a5220780a404dbe3353789870978e472cfe477761f06ee55077256e509b156d0"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "9837c2f9210fa01e5287a85b36c2c95030cc6be0e4ecffa7fc2a5285970b41d3" +content-hash = "8abdfe54d3a4b6e9a68de3e449f56ee704b9c439a23e1ef80bd3bc70a39b4137" diff --git a/pyproject.toml b/pyproject.toml index e63bf25..3534302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ pluscodes = "^2022.1.3" geopy = "^2.3.0" [tool.poetry.group.dev.dependencies] -black = "^23.3.0" -mypy = "^1.3.0" +black = {extras = ["jupyter"], version = "^23.7.0"} +mypy = "^1.5.1" isort = "^5.12.0" [tool.poetry.scripts] From e41c2ccf8174700bfaabfb9936207d5caef14559 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 20:27:20 +0000 Subject: [PATCH 06/41] docs: shorten warning and rephrase --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 3bec5d7..485aeb6 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,14 @@ > **Warning** > -> _**this is surplus 2.0.0.**_ -> +> **this is surplus `2.0.0`.** > surplus is being rewritten to better incorporate with > [sandplus](https://github.com/markjoshwel/sandplus.git). > sandplus is surplus's Android application accompaniment, written in Kotlin with Jetpack > Compose. > > you are on the `future` branch. if you see this warning, that means code is not -> finalised and ready to be used. -> +> finalised and ready to be used. > want the old, stable, working codebase? see the > [`main`](https://github.com/markjoshwel/surplus/tree/main) branch. From ba376466fff42cbdc839e28641604dd4c82ceb99 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 20:28:44 +0000 Subject: [PATCH 07/41] pyproject: add black and isort tooling configs --- pyproject.toml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 3534302..d52accf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,13 @@ isort = "^5.12.0" surplus = 'surplus:cli' "s+" = 'surplus:cli' +[tool.black] +line-length = 90 + +[tool.isort] +line_length = 90 +profile = "black" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" From 1d66e7ca767ee6d68172816735bc4317dbe8910d Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 20:28:57 +0000 Subject: [PATCH 08/41] meta: update requirements.txt --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 181f69c..29a14d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ geographiclib==2.0 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734 \ --hash=sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859 -geopy==2.3.0 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:228cd53b6eef699b2289d1172e462a90d5057779a10388a7366291812601187f \ - --hash=sha256:4a29a16d41d8e56ba8e07310802a1cbdf098eeb6069cc3d6d3068fc770629ffc +geopy==2.4.0 ; python_version >= "3.10" and python_version < "4.0" \ + --hash=sha256:a59392bf17adb486b25dbdd71fbed27733bdf24a2dac588047a619de56695e36 \ + --hash=sha256:d2639a46d0ce4c091e9688b750ba94348a14b898a1e55c68f4b4a07e7d1afa20 pluscodes==2022.1.3 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:50625f472f8d4e8822e005180c2eb41bf09e45e429f362d3cded346f1169dae8 From 19c0d9d9101591cc1f92c6d9d407777dd3eb9087 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 20:35:50 +0000 Subject: [PATCH 09/41] surplus: query structs and geocoder/reverser funcs --- surplus.future.ipynb | 97 +++++++++++++- surplus.py | 294 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 378 insertions(+), 13 deletions(-) diff --git a/surplus.future.ipynb b/surplus.future.ipynb index a182966..53123d5 100644 --- a/surplus.future.ipynb +++ b/surplus.future.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -31,6 +31,101 @@ "\n", "print(f\"OUTPUT_LINE_X_KEYS: Final[tuple[str, ...]] = ({','.join(split_keys)},)\")" ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "from surplus import PlusCodeQuery, LocalCodeQuery, LatlongQuery, StringQuery\n", + "from surplus import default_geocoder, default_reverser\n", + "from surplus import Latlong" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Result(value=Latlong(latitude=1.3336875, longitude=103.7746875), error=None)" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "PlusCodeQuery(code=\"6PH58QMF+FV\").to_lat_long_coord(geocoder=default_geocoder)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Result(value=Latlong(latitude=1.3336875, longitude=103.7746875), error=None)" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "LocalCodeQuery(code=\"8QMF+FV\", locality=\"Singapore\").to_lat_long_coord(\n", + " geocoder=default_geocoder\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Result(value=Latlong(latitude=1.33318835, longitude=103.77461234638255), error=None)" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "LatlongQuery(\n", + " latlong=Latlong(latitude=1.33318835, longitude=103.77461234638255)\n", + ").to_lat_long_coord(geocoder=default_geocoder)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Result(value=Latlong(latitude=1.33318835, longitude=103.77461234638255), error=None)" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "StringQuery(query=\"Ngee Ann Polytechnic\").to_lat_long_coord(geocoder=default_geocoder)" + ] } ], "metadata": { diff --git a/surplus.py b/surplus.py index ee9b78c..f6e846c 100644 --- a/surplus.py +++ b/surplus.py @@ -31,20 +31,23 @@ For more information, please refer to from argparse import ArgumentParser from collections import OrderedDict -from sys import stderr -from typing import Any, Callable, Final, Literal, NamedTuple +from dataclasses import dataclass +from sys import stderr, stdout +from typing import Any, Callable, Final, Generic, Literal, NamedTuple, TextIO, TypeVar -from geopy import Location # type: ignore -from geopy.geocoders import Nominatim # type: ignore -from pluscodes import PlusCode # type: ignore -from pluscodes.openlocationcode import recoverNearest # type: ignore -from pluscodes.validator import Validator # type: ignore +from geopy import Location as _geopy_Location # type: ignore +from geopy.geocoders import Nominatim as _geopy_Nominatim # type: ignore +from pluscodes import PlusCode as _PlusCode # type: ignore +from pluscodes.validator import Validator as _PlusCode_Validator # type: ignore +from pluscodes.openlocationcode import ( # type: ignore # isort: skip + recoverNearest as _PlusCode_recoverNearest, +) # constants VERSION: Final[tuple[int, int, int]] = (2, 0, 0) - +USER_AGENT: Final[str] = "surplus" OUTPUT_LINE_0_KEYS: Final[tuple[str, ...]] = ( "emergency", "historic", @@ -101,15 +104,282 @@ OUTPUT_LINE_6_KEYS: Final[tuple[str, ...]] = ( "continent", ) -# program body +# exceptions -... -# program entry +class InvalidPlusCodeError(Exception): + ... + + +class NoSuitableLocationError(Exception): + ... + + +# data structures + +ResultType = TypeVar("ResultType") + + +@dataclass +class Result(Generic[ResultType]): + """ + typing.NamedTuple representing a result for safe value handling + + arguments + value: ResultType + value to return or fallback value if erroneous + error: Exception | None = None + exception if any + + methods + def __bool__(self) -> bool: ... + def get(self) -> ResultType: ... + + example + int_result = Result[int](0) + str_err_result = Result[str]("", FileNotFoundError(...)) + """ + + value: ResultType + error: BaseException | None = None + + def __bool__(self) -> bool: + """method that returns True if self.error is not None""" + return self.error is None + + def get(self) -> ResultType: + """method that returns self.value if Result is non-erroneous else raises error""" + if self.error is not None: + raise self.error + + return self.value + + +class Latlong(NamedTuple): + """ + typing.NamedTuple representing a latitude-longitude coordinate pair + + arguments + latitude: float + longitude: float + + methods + def __str__(self) -> str: ... + """ + + latitude: float + longitude: float + + def __str__(self) -> str: + return f"{self.latitude}, {self.longitude}" + + +EMPTY_LATLONG: Final[Latlong] = Latlong(latitude=0.0, longitude=0.0) + + +class PlusCodeQuery(NamedTuple): + """ + typing.NamedTuple representing a complete Plus Code + + arguments + code: str + + methods + def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + """ + + code: str + + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + """ + method that returns a latitude-longitude coordinate pair + + arguments + geocoder: typing.Callable[[str], Latlong] + name string to location function, must take in a string and return a + Latlong. exceptions are handled. + + returns Result[Latlong] + """ + + latitude: float = 0.0 + longitude: float = 0.0 + + try: + plus_code = _PlusCode(self.code) + latitude = plus_code.area.center().lat + longitude = plus_code.area.center().lon + + except KeyError: + return Result[Latlong]( + EMPTY_LATLONG, + error=InvalidPlusCodeError( + "Plus Code is not full-length, e.g, 6PH58QMF+FX" + ), + ) + + except Exception as err: + return Result[Latlong](EMPTY_LATLONG, error=err) + + return Result[Latlong](Latlong(latitude=latitude, longitude=longitude)) + + +class LocalCodeQuery(NamedTuple): + """ + typing.NamedTuple representing a complete shortened Plus Code with locality, referred + to by surplus as a "local code" + + arguments + code: str + Plus Code portion of local code, e.g., "8QMF+FX" + locality: str + remaining string of local code, e.g., "Singapore" + + methods + def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + """ + + code: str + locality: str + + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + """ + method that returns a latitude-longitude coordinate pair + + arguments + geocoder: typing.Callable[[str], Latlong] + name string to location function, must take in a string and return a + Latlong. exceptions are handled. + + returns Result[Latlong] + """ + + try: + locality_location = geocoder(self.locality) + + recovered_pluscode = _PlusCode_recoverNearest( + code=self.code, + referenceLatitude=locality_location.latitude, + referenceLongitude=locality_location.longitude, + ) + + return PlusCodeQuery(recovered_pluscode).to_lat_long_coord(geocoder=geocoder) + + except Exception as err: + return Result[Latlong](EMPTY_LATLONG, error=err) + + +class LatlongQuery(NamedTuple): + """ + typing.NamedTuple representing a latitude-longitude coordinate pair + + arguments + latlong: Latlong + + methods + def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + """ + + latlong: Latlong + + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + """ + method that returns a latitude-longitude coordinate pair + + arguments + geocoder: typing.Callable[[str], Latlong] + name string to location function, must take in a string and return a + Latlong. exceptions are handled. + + returns Result[Latlong] + """ + + return Result[Latlong](self.latlong) + + +class StringQuery(NamedTuple): + """ + typing.NamedTuple representing a complete Plus Code + + arguments + code: str + + methods + def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + """ + + query: str + + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + """ + method that returns a latitude-longitude coordinate pair + + arguments + geocoder: typing.Callable[[str], Latlong] + name string to location function, must take in a string and return a + Latlong. exceptions are handled. + + returns Result[Latlong] + """ + + try: + return Result[Latlong](geocoder(self.query)) + + except Exception as err: + return Result[Latlong](EMPTY_LATLONG, error=err) + + +# functions + + +def default_geocoder(place: str) -> Latlong: + """default geocoder for surplus, uses OpenStreetMap Nominatim""" + + location: _geopy_Location | None = _geopy_Nominatim(user_agent=USER_AGENT).geocode( + place + ) + + if location is None: + raise NoSuitableLocationError( + f"No suitable location could be geolocated from '{place}'" + ) + + return Latlong( + latitude=location.latitude, + longitude=location.longitude, + ) + + +def default_reverser(latlong: Latlong) -> dict[str, Any]: + """default geocoder for surplus, uses OpenStreetMap Nominatim""" + location: _geopy_Location | None = _geopy_Nominatim(user_agent=USER_AGENT).reverse( + str(latlong) + ) + + if location is None: + raise NoSuitableLocationError( + f"No suitable location could be reversed from '{str(latlong)}'" + ) + + return location.raw + + +def surplus( + query: PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery, + geocoder: Callable[[str], Latlong], + reverser: Callable[[str], dict[str, Any]], + stderr: TextIO = stderr, + stdout: TextIO = stdout, + debug: bool = False, +) -> Result[str]: + return Result[str]("", error=NotImplementedError()) + + +# command-line entry def main(): - pass + ... if __name__ == "__main__": From 4ca4a7c8a852491b6eaed5304f8c006cfee700f4 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Thu, 31 Aug 2023 20:50:46 +0000 Subject: [PATCH 10/41] surplus: add and use local to full length converter --- surplus.future.ipynb | 30 ++++++++++++++++++++++++++--- surplus.py | 45 +++++++++++++++++++++++++++++++++----------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/surplus.future.ipynb b/surplus.future.ipynb index 53123d5..2dff9eb 100644 --- a/surplus.future.ipynb +++ b/surplus.future.ipynb @@ -34,7 +34,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -45,7 +45,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -54,7 +54,7 @@ "Result(value=Latlong(latitude=1.3336875, longitude=103.7746875), error=None)" ] }, - "execution_count": 11, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -63,6 +63,30 @@ "PlusCodeQuery(code=\"6PH58QMF+FV\").to_lat_long_coord(geocoder=default_geocoder)" ] }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Result(value=Latlong(latitude=1.3336875, longitude=103.7746875), error=None)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plus_code = LocalCodeQuery(code=\"8QMF+FV\", locality=\"Singapore\").to_full_plus_code(\n", + " geocoder=default_geocoder\n", + ")\n", + "\n", + "PlusCodeQuery(code=plus_code.get()).to_lat_long_coord(geocoder=default_geocoder)" + ] + }, { "cell_type": "code", "execution_count": 6, diff --git a/surplus.py b/surplus.py index f6e846c..3c91c30 100644 --- a/surplus.py +++ b/surplus.py @@ -242,6 +242,32 @@ class LocalCodeQuery(NamedTuple): code: str locality: str + def to_full_plus_code(self, geocoder: Callable[[str], Latlong]) -> Result[str]: + """ + method that returns a full-length Plus Code + + arguments + geocoder: typing.Callable[[str], Latlong] + name string to location function, must take in a string and return a + Latlong. exceptions are handled. + + returns Result[str] + """ + + try: + locality_location = geocoder(self.locality) + + recovered_pluscode = _PlusCode_recoverNearest( + code=self.code, + referenceLatitude=locality_location.latitude, + referenceLongitude=locality_location.longitude, + ) + + return Result[str](recovered_pluscode) + + except Exception as err: + return Result[str]("", error=err) + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: """ method that returns a latitude-longitude coordinate pair @@ -254,19 +280,16 @@ class LocalCodeQuery(NamedTuple): returns Result[Latlong] """ - try: - locality_location = geocoder(self.locality) + recovered_pluscode = self.to_full_plus_code(geocoder=geocoder) - recovered_pluscode = _PlusCode_recoverNearest( - code=self.code, - referenceLatitude=locality_location.latitude, - referenceLongitude=locality_location.longitude, - ) + if not recovered_pluscode: + return Result[Latlong](EMPTY_LATLONG, error=recovered_pluscode.error) - return PlusCodeQuery(recovered_pluscode).to_lat_long_coord(geocoder=geocoder) - - except Exception as err: - return Result[Latlong](EMPTY_LATLONG, error=err) + return Result[Latlong]( + PlusCodeQuery(recovered_pluscode.get()) + .to_lat_long_coord(geocoder=geocoder) + .get() # PlusCodeQuery can get latlong coord offline, so no need to handle + ) class LatlongQuery(NamedTuple): From 348e1f43c919e7fe22eed1483a45474c4f194aec Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 06:17:47 +0000 Subject: [PATCH 11/41] meta: >=py3.11 --- README.md | 6 +++++- poetry.lock | 18 ++---------------- pyproject.toml | 2 +- 3 files changed, 8 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 485aeb6..aaa793b 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ TODO API DEMO ## installation +> **Note** +> python 3.11 or later is required due to a bug in earlier versions. +> [(python/cpython#88089)](https://github.com/python/cpython/issues/88089) + install surplus directly from the repository using pip: ```text @@ -71,7 +75,7 @@ options: prerequisites: -- [Python >=3.10](https://www.python.org/) +- [Python >=3.11](https://www.python.org/) - [Poetry](https://python-poetry.org/) alternatively, use [devbox](https://get.jetpack.io/devbox) for a hermetic development environment powered by [Nix](https://nixos.org/). diff --git a/poetry.lock b/poetry.lock index 803e3fb..4f2aa86 100644 --- a/poetry.lock +++ b/poetry.lock @@ -82,7 +82,6 @@ packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tokenize-rt = {version = ">=3.2.0", optional = true, markers = "extra == \"jupyter\""} -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] @@ -311,7 +310,6 @@ files = [ [package.dependencies] mypy-extensions = ">=1.0.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = ">=4.1.0" [package.extras] @@ -529,18 +527,6 @@ files = [ {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"}, ] -[[package]] -name = "tomli" -version = "2.0.1" -description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" -files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] - [[package]] name = "traitlets" version = "5.9.0" @@ -583,5 +569,5 @@ files = [ [metadata] lock-version = "2.0" -python-versions = "^3.10" -content-hash = "8abdfe54d3a4b6e9a68de3e449f56ee704b9c439a23e1ef80bd3bc70a39b4137" +python-versions = "^3.11" +content-hash = "f9270d7fb45c708e923f4afe041b26f77becb8e4adfcd517742e67f6b8f9d6b9" diff --git a/pyproject.toml b/pyproject.toml index d52accf..63c17c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ readme = "README.md" include = ["surplus.py"] [tool.poetry.dependencies] -python = "^3.10" +python = "^3.11" pluscodes = "^2022.1.3" geopy = "^2.3.0" From 3a67ad8a1d7f0ca1129d47d76b01cc455b66d468 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 07:49:24 +0000 Subject: [PATCH 12/41] meta: add ipynb to devbox deps --- devbox.json | 3 ++- devbox.lock | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/devbox.json b/devbox.json index 1dab9c9..581bae2 100644 --- a/devbox.json +++ b/devbox.json @@ -1,6 +1,7 @@ { "packages": [ "python311", + "python311Packages.ipykernel", "poetry" ], "shell": { @@ -9,4 +10,4 @@ "nixpkgs": { "commit": "f80ac848e3d6f0c12c52758c0f25c10c97ca3b62" } -} \ No newline at end of file +} diff --git a/devbox.lock b/devbox.lock index baa25f4..7e3b009 100644 --- a/devbox.lock +++ b/devbox.lock @@ -7,6 +7,9 @@ "python311": { "plugin_version": "0.0.1", "resolved": "github:NixOS/nixpkgs/f80ac848e3d6f0c12c52758c0f25c10c97ca3b62#python311" + }, + "python311Packages.ipykernel": { + "resolved": "github:NixOS/nixpkgs/f80ac848e3d6f0c12c52758c0f25c10c97ca3b62#python311Packages.ipykernel" } } } \ No newline at end of file From 70b7cb6f8c209c681945dd1070aeaf6489a85e0b Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 07:50:17 +0000 Subject: [PATCH 13/41] surplus: revert Result to typing.NamedTuple dataclasses cannot be frozen and have slots, a bug that probably will never be fixed --- surplus.future.ipynb | 61 +++++++++++++++++++++++++++++++++++++++----- surplus.py | 5 ++-- 2 files changed, 56 insertions(+), 10 deletions(-) diff --git a/surplus.future.ipynb b/surplus.future.ipynb index 2dff9eb..bc8ad90 100644 --- a/surplus.future.ipynb +++ b/surplus.future.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -34,13 +34,60 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ "from surplus import PlusCodeQuery, LocalCodeQuery, LatlongQuery, StringQuery\n", - "from surplus import default_geocoder, default_reverser\n", - "from surplus import Latlong" + "from surplus import Latlong, Result\n", + "from surplus import default_geocoder, default_reverser" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Generic Result NamedTuple" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "3\n" + ] + }, + { + "ename": "Exception", + "evalue": "test", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/home/m/works/surplus/surplus.future.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2\u001b[0m err_result \u001b[39m=\u001b[39m surplus\u001b[39m.\u001b[39mResult[\u001b[39mint\u001b[39m](\u001b[39m3\u001b[39m, error\u001b[39m=\u001b[39m\u001b[39mException\u001b[39;00m(\u001b[39m\"\u001b[39m\u001b[39mtest\u001b[39m\u001b[39m\"\u001b[39m))\n\u001b[1;32m 4\u001b[0m \u001b[39mprint\u001b[39m(nom_result\u001b[39m.\u001b[39mget())\n\u001b[0;32m----> 5\u001b[0m \u001b[39mprint\u001b[39m(err_result\u001b[39m.\u001b[39;49mget())\n", + "File \u001b[0;32m~/works/surplus/surplus.py:152\u001b[0m, in \u001b[0;36mResult.get\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 150\u001b[0m \u001b[39m\"\"\"method that returns self.value if Result is non-erroneous else raises error\"\"\"\u001b[39;00m\n\u001b[1;32m 151\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m--> 152\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror\n\u001b[1;32m 154\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mvalue\n", + "\u001b[0;31mException\u001b[0m: test" + ] + } + ], + "source": [ + "nom_result = Result[int](3)\n", + "err_result = Result[int](3, error=Exception(\"test\"))\n", + "\n", + "print(nom_result.get())\n", + "print(err_result.get())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Query Types" ] }, { @@ -65,7 +112,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 5, "metadata": {}, "outputs": [ { @@ -74,7 +121,7 @@ "Result(value=Latlong(latitude=1.3336875, longitude=103.7746875), error=None)" ] }, - "execution_count": 13, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -168,7 +215,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.12" + "version": "3.11.1" }, "orig_nbformat": 4 }, diff --git a/surplus.py b/surplus.py index 3c91c30..f6dc749 100644 --- a/surplus.py +++ b/surplus.py @@ -26,7 +26,7 @@ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -For more information, please refer to +For more information, please refer to """ from argparse import ArgumentParser @@ -120,8 +120,7 @@ class NoSuitableLocationError(Exception): ResultType = TypeVar("ResultType") -@dataclass -class Result(Generic[ResultType]): +class Result(NamedTuple, Generic[ResultType]): """ typing.NamedTuple representing a result for safe value handling From cdc5030baa5da697282ceff997c80727e886b769 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 13:35:30 +0000 Subject: [PATCH 14/41] surplus: crying Results, query parsing --- surplus.future.ipynb | 55 +++++-- surplus.py | 362 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 375 insertions(+), 42 deletions(-) diff --git a/surplus.future.ipynb b/surplus.future.ipynb index bc8ad90..e0e683c 100644 --- a/surplus.future.ipynb +++ b/surplus.future.ipynb @@ -4,12 +4,20 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# surplus 2.0.0 playground notebook" + "# surplus 2.0.0 playground notebook\n", + "\n", + "wrangling with environments for devbox users using codium/vs code:\n", + "\n", + "```text\n", + "$ devbox shell # enter devbox env\n", + "(surplus-py3.11) (devbox) $ exit # leave poetry env\n", + "(devbox) $ codium . # open ide\n", + "```" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [ { @@ -34,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -52,35 +60,50 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "3\n" + "True\tNone \t3\n", + "False\t'stest' \t-1\n", + "False\tZeroDivisionError('division by zero') \tdivision by zero (ZeroDivisionError)\n" ] }, { - "ename": "Exception", - "evalue": "test", + "ename": "ZeroDivisionError", + "evalue": "division by zero", "output_type": "error", "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/home/m/works/surplus/surplus.future.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 2\u001b[0m err_result \u001b[39m=\u001b[39m surplus\u001b[39m.\u001b[39mResult[\u001b[39mint\u001b[39m](\u001b[39m3\u001b[39m, error\u001b[39m=\u001b[39m\u001b[39mException\u001b[39;00m(\u001b[39m\"\u001b[39m\u001b[39mtest\u001b[39m\u001b[39m\"\u001b[39m))\n\u001b[1;32m 4\u001b[0m \u001b[39mprint\u001b[39m(nom_result\u001b[39m.\u001b[39mget())\n\u001b[0;32m----> 5\u001b[0m \u001b[39mprint\u001b[39m(err_result\u001b[39m.\u001b[39;49mget())\n", - "File \u001b[0;32m~/works/surplus/surplus.py:152\u001b[0m, in \u001b[0;36mResult.get\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 150\u001b[0m \u001b[39m\"\"\"method that returns self.value if Result is non-erroneous else raises error\"\"\"\u001b[39;00m\n\u001b[1;32m 151\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror \u001b[39mis\u001b[39;00m \u001b[39mnot\u001b[39;00m \u001b[39mNone\u001b[39;00m:\n\u001b[0;32m--> 152\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror\n\u001b[1;32m 154\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mvalue\n", - "\u001b[0;31mException\u001b[0m: test" + "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", + "\u001b[1;32m/home/m/works/surplus/surplus.future.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\u001b[39mbool\u001b[39m(err_result), \u001b[39mrepr\u001b[39m(err_result\u001b[39m.\u001b[39merror), err_result\u001b[39m.\u001b[39mget()))\n\u001b[1;32m 12\u001b[0m \u001b[39mprint\u001b[39m(\n\u001b[1;32m 13\u001b[0m \u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\n\u001b[1;32m 14\u001b[0m \u001b[39mbool\u001b[39m(exc_result), \u001b[39mrepr\u001b[39m(exc_result\u001b[39m.\u001b[39merror), exc_result\u001b[39m.\u001b[39mcry(string\u001b[39m=\u001b[39m\u001b[39mTrue\u001b[39;00m)\n\u001b[1;32m 15\u001b[0m )\n\u001b[1;32m 16\u001b[0m )\n\u001b[0;32m---> 17\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\u001b[39mbool\u001b[39m(exc_result), \u001b[39mrepr\u001b[39m(exc_result\u001b[39m.\u001b[39merror), exc_result\u001b[39m.\u001b[39;49mget()))\n", + "File \u001b[0;32m~/works/surplus/surplus.py:202\u001b[0m, in \u001b[0;36mResult.get\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 200\u001b[0m \u001b[39m\"\"\"method that returns self.value if Result is non-erroneous else raises error\"\"\"\u001b[39;00m\n\u001b[1;32m 201\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39misinstance\u001b[39m(\u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror, \u001b[39mBaseException\u001b[39;00m):\n\u001b[0;32m--> 202\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror\n\u001b[1;32m 204\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mvalue\n", + "\u001b[1;32m/home/m/works/surplus/surplus.future.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 3\u001b[0m err_result \u001b[39m=\u001b[39m Result[\u001b[39mint\u001b[39m](\u001b[39m-\u001b[39m\u001b[39m1\u001b[39m, error\u001b[39m=\u001b[39m\u001b[39m\"\u001b[39m\u001b[39mstest\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[1;32m 5\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m----> 6\u001b[0m \u001b[39m1\u001b[39;49m \u001b[39m/\u001b[39;49m \u001b[39m0\u001b[39;49m\n\u001b[1;32m 7\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m \u001b[39mas\u001b[39;00m exc:\n\u001b[1;32m 8\u001b[0m exc_result \u001b[39m=\u001b[39m Result[\u001b[39mint\u001b[39m](\u001b[39m-\u001b[39m\u001b[39m1\u001b[39m, error\u001b[39m=\u001b[39mexc)\n", + "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" ] } ], "source": [ "nom_result = Result[int](3)\n", - "err_result = Result[int](3, error=Exception(\"test\"))\n", "\n", - "print(nom_result.get())\n", - "print(err_result.get())" + "err_result = Result[int](-1, error=\"stest\")\n", + "\n", + "try:\n", + " 1 / 0\n", + "except Exception as exc:\n", + " exc_result = Result[int](-1, error=exc)\n", + "\n", + "print(\"{}\\t{:<40}\\t{}\".format(bool(nom_result), repr(nom_result.error), nom_result.get()))\n", + "print(\"{}\\t{:<40}\\t{}\".format(bool(err_result), repr(err_result.error), err_result.get()))\n", + "print(\n", + " \"{}\\t{:<40}\\t{}\".format(\n", + " bool(exc_result), repr(exc_result.error), exc_result.cry(string=True)\n", + " )\n", + ")\n", + "print(\"{}\\t{:<40}\\t{}\".format(bool(exc_result), repr(exc_result.error), exc_result.get()))" ] }, { @@ -180,7 +203,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -189,7 +212,7 @@ "Result(value=Latlong(latitude=1.33318835, longitude=103.77461234638255), error=None)" ] }, - "execution_count": 8, + "execution_count": 2, "metadata": {}, "output_type": "execute_result" } diff --git a/surplus.py b/surplus.py index f6dc749..860c297 100644 --- a/surplus.py +++ b/surplus.py @@ -1,6 +1,6 @@ """ -surplus: Plus Code to iOS-Shortcuts-like shareable text -------------------------------------------------------- +surplus: Google Maps Plus Code to iOS Shortcuts-like shareable text +------------------------------------------------------------------- by mark and contributors This is free and unencumbered software released into the public domain. @@ -33,7 +33,17 @@ from argparse import ArgumentParser from collections import OrderedDict from dataclasses import dataclass from sys import stderr, stdout -from typing import Any, Callable, Final, Generic, Literal, NamedTuple, TextIO, TypeVar +from typing import ( + Any, + Callable, + Final, + Generic, + Literal, + NamedTuple, + TextIO, + TypeAlias, + TypeVar, +) from geopy import Location as _geopy_Location # type: ignore from geopy.geocoders import Nominatim as _geopy_Nominatim # type: ignore @@ -115,6 +125,10 @@ class NoSuitableLocationError(Exception): ... +class InvalidQueryError(Exception): + ... + + # data structures ResultType = TypeVar("ResultType") @@ -127,30 +141,67 @@ class Result(NamedTuple, Generic[ResultType]): arguments value: ResultType value to return or fallback value if erroneous - error: Exception | None = None - exception if any + error: BaseException | str | None = None + exception if any, or an error message methods def __bool__(self) -> bool: ... def get(self) -> ResultType: ... + def cry(self) -> str: ... example - int_result = Result[int](0) - str_err_result = Result[str]("", FileNotFoundError(...)) + # do something + try: + file_contents = Path(...).read_text() + except Exception as exc: + result = Result[str]("", error=exc) + else: + result = Result[str] + + # handle result + if not result: + # .cry() either raises an exception or returns an error message + error_message = result.cry() + ... + else: + data = result.get() # raises exception or returns value """ value: ResultType - error: BaseException | None = None + error: BaseException | str | None = None def __bool__(self) -> bool: """method that returns True if self.error is not None""" return self.error is None - def get(self) -> ResultType: - """method that returns self.value if Result is non-erroneous else raises error""" - if self.error is not None: + def cry(self, string: bool = False) -> str: + """ + method that raises self.error if is an instance of BaseException, + returns self.error if is an instance of str, or returns an empty string if + self.error is None. + + arguments + string: bool = False + if self.error is an instance Exception, returns it as a string. + """ + + if isinstance(self.error, BaseException): + if string: + message = f"{self.error}" + name = self.error.__class__.__name__ + return f"{message} ({name})" if (message != "") else name + raise self.error + if isinstance(self.error, str): + return self.error + + return "" + + def get(self) -> ResultType: + """method that returns self.value if Result is non-erroneous else raises error""" + if isinstance(self.error, BaseException): + raise self.error return self.value @@ -217,8 +268,8 @@ class PlusCodeQuery(NamedTuple): ), ) - except Exception as err: - return Result[Latlong](EMPTY_LATLONG, error=err) + except Exception as exc: + return Result[Latlong](EMPTY_LATLONG, error=exc) return Result[Latlong](Latlong(latitude=latitude, longitude=longitude)) @@ -264,8 +315,8 @@ class LocalCodeQuery(NamedTuple): return Result[str](recovered_pluscode) - except Exception as err: - return Result[str]("", error=err) + except Exception as exc: + return Result[str]("", error=exc) def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: """ @@ -347,11 +398,11 @@ class StringQuery(NamedTuple): try: return Result[Latlong](geocoder(self.query)) - except Exception as err: - return Result[Latlong](EMPTY_LATLONG, error=err) + except Exception as exc: + return Result[Latlong](EMPTY_LATLONG, error=exc) -# functions +Query: TypeAlias = PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery def default_geocoder(place: str) -> Latlong: @@ -386,13 +437,252 @@ def default_reverser(latlong: Latlong) -> dict[str, Any]: return location.raw +class Behaviour(NamedTuple): + """ + typing.NamedTuple representing program behaviour + + arguments + query: list[str] + original user-passed query string split by spaces + geocoder: Callable[[str], Latlong] + name string to location function, must take in a string and return a Latlong. + exceptions are handled by the caller. + reverser: Callable[[str], dict[str, Any]] + Latlong object to dictionary function, must take in a string and return a + dict. exceptions are handled by the caller. + stderr: TextIO = stderr + TextIO-like object representing a writeable file. defaults to sys.stderr. + stdout: TextIO = stdout + TextIO-like object representing a writeable file. defaults to sys.stdout. + debug: bool = False + whether to print debug information to stderr + """ + + query: list[str] + geocoder: Callable[[str], Latlong] = default_geocoder + reverser: Callable[[Latlong], dict[str, Any]] = default_reverser + stderr: TextIO = stderr + stdout: TextIO = stdout + debug: bool = False + + +# functions + + +def parse_query( + behaviour: Behaviour, +) -> Result[Query]: + """ + function that parses a query string into a query object + + arguments + behaviour: Behaviour + + returns Result[Query] + """ + + def _match_plus_code( + behaviour: Behaviour, + ) -> Result[Query]: + """ + internal helper code reuse function + + looks through each 'word' and attempts to match to a Plus Code + if found, remove from original query and strip of whitespace and commas + use resulting stripped query as locality + """ + + validator = _PlusCode_Validator() + portion_plus_code: str = "" + portion_locality: str = "" + + original_query = " ".join(behaviour.query) + split_query = behaviour.query + + for word in split_query: + if validator.is_valid(word): + portion_plus_code = word + + if validator.is_full(word): + return Result[Query](PlusCodeQuery(portion_plus_code)) + + break + + # didn't find a plus code. :( + if portion_plus_code == "": + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error="unable to find a pluscode", + ) + + # found a plus code! + portion_locality = original_query.replace(portion_plus_code, "") + portion_locality = portion_locality.strip().strip(",").strip() + + if behaviour.debug: + behaviour.stderr.write(f"debug: {portion_plus_code=}, {portion_locality=}\n") + + return Result[Query]( + LocalCodeQuery( + code=portion_plus_code, + locality=portion_locality, + ) + ) + + # types to handle: + # + # plus codes + # 6PH58R3M+F8 + # local codes + # 8RQQ+4Q Singapore (single-word-long locality suffix) + # St Lucia, Queensland, Australia G227+XF (multi-word-long locality prefix) + # latlong coords + # 1.3521,103.8198 (single-word-long with comma) + # 1.3521, 103.8198 (space-seperated with comma) + # 1.3521 103.8198 (space-seperated without comma) + # string queries + # Ngee Ann Polytechnic, Singapore (has a comma) + # Toa Payoh North (no commas) + + if behaviour.debug: + behaviour.stderr.write(f"debug: {behaviour.query=}\n") + + # check if empty + if behaviour.query == []: + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error="query is empty", + ) + + # try to find a plus/local code + if mpc_result := _match_plus_code(behaviour=behaviour): + # found one! + return Result[Query](mpc_result.get()) + + match behaviour.query: + case [single]: + # possibly a: + # comma-seperated single-word-long latlong coord + # (fallback) single word string query + + if "," not in single: # no comma, not a latlong coord + return Result[Query](StringQuery(" ".join(behaviour.query))) + + else: # has comma, possibly a latlong coord + split_query: list[str] = single.split(",") + + if len(split_query) > 2: + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error="unable to parse latlong coord", + ) + + try: # try to type cast query + latitude = float(split_query[0].strip(",")) + longitude = float(split_query[-1].strip(",")) + + except ValueError: # not a latlong coord, fallback + return Result[Query](StringQuery(single)) + + else: # are floats, so is a latlong coord + return Result[Query]( + LatlongQuery( + Latlong( + latitude=latitude, + longitude=longitude, + ) + ) + ) + + case [left_single, right_single]: + # possibly a: + # space-seperated latlong coord + # (fallback) space-seperated string query + + try: # try to type cast query + latitude = float(left_single.strip(",")) + longitude = float(right_single.strip(",")) + + except ValueError: # not a latlong coord, fallback + return Result[Query](StringQuery(" ".join(behaviour.query))) + + else: # are floats, so is a latlong coord + return Result[Query]( + LatlongQuery(Latlong(latitude=latitude, longitude=longitude)) + ) + + case _: + # possibly a: + # (fallback) space-seperated string query + + return Result[Query](StringQuery(" ".join(behaviour.query))) + + +def handle_args() -> Behaviour: + """ + internal function that handles command-line arguments + + returns Behaviour + program behaviour namedtuple + """ + + parser = ArgumentParser( + prog="surplus", + description=__doc__[__doc__.find(":") + 2 : __doc__.find("\n", 1)], + ) + parser.add_argument( + "query", + type=str, + help=( + "full-length Plus Code (6PH58QMF+FX), " + "shortened Plus Code/'local code' (8QMF+FX Singapore), " + "latlong (1.3336875, 103.7749375), " + "or string query (e.g., 'Wisma Atria')" + ), + nargs="*", + ) + parser.add_argument( + "-d", + "--debug", + action="store_true", + default=False, + help="prints lat, long and reverser response dict to stderr", + ) + parser.add_argument( + "-v", + "--version", + action="store_true", + default=False, + help="prints version information to stderr and exits", + ) + + args = parser.parse_args() + behaviour = Behaviour( + query=args.query, + geocoder=default_geocoder, + reverser=default_reverser, + stderr=stderr, + stdout=stdout, + debug=args.debug, + ) + + # print header + + (behaviour.stdout if behaviour.debug else behaviour.stderr).write( + f"surplus version {'.'.join([str(v) for v in VERSION])}" + + (f", debug mode" if behaviour.debug else "") + + "\n" + ) + + if args.version: + exit(0) + + return behaviour + + def surplus( query: PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery, - geocoder: Callable[[str], Latlong], - reverser: Callable[[str], dict[str, Any]], - stderr: TextIO = stderr, - stdout: TextIO = stdout, - debug: bool = False, + behaviour: Behaviour, ) -> Result[str]: return Result[str]("", error=NotImplementedError()) @@ -400,9 +690,29 @@ def surplus( # command-line entry -def main(): - ... +def cli() -> int: + behaviour = handle_args() + query = parse_query(behaviour=behaviour) + + if not query: + behaviour.stderr.write(f"error: {query.cry(string=True)}\n") + return -1 + + if behaviour.debug: + behaviour.stderr.write(f"debug: {query.get()=}\n") + + text = surplus( + query=query.get(), + behaviour=behaviour, + ) + + if not text: + behaviour.stderr.write(f"error: {text.cry(string=True)}\n") + return -2 + + behaviour.stdout.write(text.get() + "\n") + return 0 if __name__ == "__main__": - main() + exit(cli()) From d9c255b089de0319b6e920413fd0aaa4fdc4a1e3 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 13:38:03 +0000 Subject: [PATCH 15/41] docs: update description and add cli usage --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index aaa793b..3192926 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ > [`main`](https://github.com/markjoshwel/surplus/tree/main) branch. surplus is a Python script to convert -[Google Map Plus Codes](https://maps.google.com/pluscodes/) -to iOS Shortcuts-like human-readable text. +[Google Maps Plus Codes](https://maps.google.com/pluscodes/) +to iOS Shortcuts-like shareable text. - [installation](#installation) - [command-line usage](#command-line-usaage) @@ -56,19 +56,19 @@ pip install git+https://github.com/markjoshwel/surplus.git@future ```text usage: surplus [-h] [-d] [-v] [query ...] -Plus Code to iOS-Shortcuts-like shareable text +Google Maps Plus Code to iOS Shortcuts-like shareable text positional arguments: - query full-length Plus Code (6PH58QMF+FX), - local code (8QMF+FX Singapore), or - latlong (1.3336875, 103.7749375) + query full-length Plus Code (6PH58QMF+FX), shortened + Plus Code/'local code' (8QMF+FX Singapore), + latlong (1.3336875, 103.7749375), or string + query (e.g., 'Wisma Atria') options: -h, --help show this help message and exit - -d, --debug prints lat, long and reverser - response dict to stderr - -v, --version prints version information to stderr -     and exits + -d, --debug prints lat, long and reverser response dict to + stderr + -v, --version prints version information to stderr and exits ``` ## developer's guide From 94191a6225ed702c88bf37cb38f596074587a14c Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 13:38:17 +0000 Subject: [PATCH 16/41] devbox: add poetry env use to init hook --- devbox.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/devbox.json b/devbox.json index 581bae2..63a0c48 100644 --- a/devbox.json +++ b/devbox.json @@ -5,7 +5,10 @@ "poetry" ], "shell": { - "init_hook": "poetry shell" + "init_hook": [ + "poetry env use $(which python)", + "poetry shell" + ] }, "nixpkgs": { "commit": "f80ac848e3d6f0c12c52758c0f25c10c97ca3b62" From d67d72cdd51f281835c967097776369e74b80dd7 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 13:38:38 +0000 Subject: [PATCH 17/41] meta: update description in pyproject --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 63c17c6..550c371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = "surplus" version = "2.0.0" -description = "Plus Code to iOS-Shortcuts-like shareable text" +description = "Python script to convert Google Maps Plus Codes to iOS Shortcuts-like shareable text." authors = ["Mark Joshwel "] license = "Unlicence" readme = "README.md" From a7803ec3ca102f227dd4e2901bf58557efea36aa Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 13:54:43 +0000 Subject: [PATCH 18/41] surplus: remove unnecessary exceptions --- surplus.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/surplus.py b/surplus.py index 860c297..44fea5e 100644 --- a/surplus.py +++ b/surplus.py @@ -31,14 +31,12 @@ For more information, please refer to from argparse import ArgumentParser from collections import OrderedDict -from dataclasses import dataclass from sys import stderr, stdout from typing import ( Any, Callable, Final, Generic, - Literal, NamedTuple, TextIO, TypeAlias, @@ -117,18 +115,10 @@ OUTPUT_LINE_6_KEYS: Final[tuple[str, ...]] = ( # exceptions -class InvalidPlusCodeError(Exception): - ... - - class NoSuitableLocationError(Exception): ... -class InvalidQueryError(Exception): - ... - - # data structures ResultType = TypeVar("ResultType") @@ -262,10 +252,7 @@ class PlusCodeQuery(NamedTuple): except KeyError: return Result[Latlong]( - EMPTY_LATLONG, - error=InvalidPlusCodeError( - "Plus Code is not full-length, e.g, 6PH58QMF+FX" - ), + EMPTY_LATLONG, error="Plus Code is not full-length, e.g, 6PH58QMF+FX" ) except Exception as exc: From fd372fb483dd83bc11ae7ac5921aadd283fcd0f5 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 14:44:50 +0000 Subject: [PATCH 19/41] surplus: fix #17 (local codes must be full-length) --- surplus.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/surplus.py b/surplus.py index 44fea5e..aed3768 100644 --- a/surplus.py +++ b/surplus.py @@ -119,6 +119,10 @@ class NoSuitableLocationError(Exception): ... +class IncompletePlusCodeError(Exception): + ... + + # data structures ResultType = TypeVar("ResultType") @@ -252,7 +256,7 @@ class PlusCodeQuery(NamedTuple): except KeyError: return Result[Latlong]( - EMPTY_LATLONG, error="Plus Code is not full-length, e.g, 6PH58QMF+FX" + EMPTY_LATLONG, error=IncompletePlusCodeError("Plus Code is not full-length (e.g., 6PH58QMF+FX)"), ) except Exception as exc: @@ -499,7 +503,14 @@ def parse_query( if portion_plus_code == "": return Result[Query]( LatlongQuery(EMPTY_LATLONG), - error="unable to find a pluscode", + error="unable to find a Plus Code", + ) + + # did find plus code, but not full-length. :( + if not validator.is_full(portion_plus_code): + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error=IncompletePlusCodeError("Plus Code is not full-length (e.g., 6PH58QMF+FX)"), ) # found a plus code! @@ -546,6 +557,9 @@ def parse_query( # found one! return Result[Query](mpc_result.get()) + if isinstance(mpc_result.error, IncompletePlusCodeError): + return mpc_result + match behaviour.query: case [single]: # possibly a: From 3b136c30bab66515489cc34e7baa92c5fea601e9 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Fri, 1 Sep 2023 16:43:59 +0000 Subject: [PATCH 20/41] surplus: implement groundwork for #18 --- README.md | 22 ++++---- surplus.py | 144 ++++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 138 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 3192926..ee4b008 100644 --- a/README.md +++ b/README.md @@ -54,21 +54,25 @@ pip install git+https://github.com/markjoshwel/surplus.git@future ## command-line usage ```text -usage: surplus [-h] [-d] [-v] [query ...] +usage: surplus [-h] [-d] [-v] [-c {pluscode,localcode,latlong,string}] + [query ...] Google Maps Plus Code to iOS Shortcuts-like shareable text positional arguments: - query full-length Plus Code (6PH58QMF+FX), shortened - Plus Code/'local code' (8QMF+FX Singapore), - latlong (1.3336875, 103.7749375), or string - query (e.g., 'Wisma Atria') + query full-length Plus Code (6PH58QMF+FX), shortened + Plus Code/'local code' (8QMF+FX Singapore), + latlong (1.3336875, 103.7749375), or string + query (e.g., 'Wisma Atria') options: - -h, --help show this help message and exit - -d, --debug prints lat, long and reverser response dict to - stderr - -v, --version prints version information to stderr and exits + -h, --help show this help message and exit + -d, --debug prints lat, long and reverser response dict to + stderr + -v, --version prints version information to stderr and exits + -c {pluscode,localcode,latlong,string}, + --convert-to {pluscode,localcode,latlong,string} + converts query to another type ``` ## developer's guide diff --git a/surplus.py b/surplus.py index aed3768..27a3343 100644 --- a/surplus.py +++ b/surplus.py @@ -26,11 +26,12 @@ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -For more information, please refer to +For more information, please refer to """ from argparse import ArgumentParser from collections import OrderedDict +from enum import Enum from sys import stderr, stdout from typing import ( Any, @@ -41,6 +42,7 @@ from typing import ( TextIO, TypeAlias, TypeVar, + Sequence, ) from geopy import Location as _geopy_Location # type: ignore @@ -125,6 +127,23 @@ class IncompletePlusCodeError(Exception): # data structures + +class SurplusQueryTypes(Enum): + """enum representing the mode of surplus""" + + PLUS_CODE = "pluscode" + LOCAL_CODE = "localcode" + LATLONG = "latlong" + STRING = "string" + + +class SurplusOperationMode(Enum): + """enum representing the mode of surplus""" + + GENERATE_TEXT = "generate" + CONVERT_TYPES = "convert" + + ResultType = TypeVar("ResultType") @@ -215,6 +234,10 @@ class Latlong(NamedTuple): longitude: float def __str__(self) -> str: + """ + method that returns a comma-and-space-seperated string of self.latitude and + self.longitude + """ return f"{self.latitude}, {self.longitude}" @@ -256,7 +279,10 @@ class PlusCodeQuery(NamedTuple): except KeyError: return Result[Latlong]( - EMPTY_LATLONG, error=IncompletePlusCodeError("Plus Code is not full-length (e.g., 6PH58QMF+FX)"), + EMPTY_LATLONG, + error=IncompletePlusCodeError( + "Plus Code is not full-length (e.g., 6PH58QMF+FX)" + ), ) except Exception as exc: @@ -447,6 +473,11 @@ class Behaviour(NamedTuple): TextIO-like object representing a writeable file. defaults to sys.stdout. debug: bool = False whether to print debug information to stderr + operation_mode: SurplusOperationMode = SurplusOperationMode.GENERATE_TEXT + surplus operation mode enum value + convert_to_type: SurplusQueryTypes | None = None + surplus query type enum value for when + operation_mode = SurplusOperationMode.CONVERT_TYPES """ query: list[str] @@ -455,6 +486,9 @@ class Behaviour(NamedTuple): stderr: TextIO = stderr stdout: TextIO = stdout debug: bool = False + version_header: bool = False + operation_mode: SurplusOperationMode = SurplusOperationMode.GENERATE_TEXT + convert_to_type: SurplusQueryTypes | None = None # functions @@ -505,12 +539,14 @@ def parse_query( LatlongQuery(EMPTY_LATLONG), error="unable to find a Plus Code", ) - + # did find plus code, but not full-length. :( if not validator.is_full(portion_plus_code): return Result[Query]( LatlongQuery(EMPTY_LATLONG), - error=IncompletePlusCodeError("Plus Code is not full-length (e.g., 6PH58QMF+FX)"), + error=IncompletePlusCodeError( + "Plus Code is not full-length (e.g., 6PH58QMF+FX)" + ), ) # found a plus code! @@ -557,9 +593,11 @@ def parse_query( # found one! return Result[Query](mpc_result.get()) + # is a plus/local code, but missing details if isinstance(mpc_result.error, IncompletePlusCodeError): - return mpc_result + return mpc_result # propagate back up to caller + # not a plus/local code, try to match for latlong or string query match behaviour.query: case [single]: # possibly a: @@ -656,8 +694,27 @@ def handle_args() -> Behaviour: default=False, help="prints version information to stderr and exits", ) + parser.add_argument( + "-c", + "--convert-to", + type=str, + choices=[str(v.value) for v in SurplusQueryTypes], + help="converts query to another type", + default="", + ) args = parser.parse_args() + + convert_to_type: SurplusQueryTypes | None = None + if args.convert_to != "": + convert_to_type = SurplusQueryTypes(args.convert_to) + + operation_mode: SurplusOperationMode = ( + SurplusOperationMode.GENERATE_TEXT + if convert_to_type is None + else SurplusOperationMode.CONVERT_TYPES + ) + behaviour = Behaviour( query=args.query, geocoder=default_geocoder, @@ -665,19 +722,11 @@ def handle_args() -> Behaviour: stderr=stderr, stdout=stdout, debug=args.debug, + version_header=args.version, + operation_mode=operation_mode, + convert_to_type=convert_to_type, ) - # print header - - (behaviour.stdout if behaviour.debug else behaviour.stderr).write( - f"surplus version {'.'.join([str(v) for v in VERSION])}" - + (f", debug mode" if behaviour.debug else "") - + "\n" - ) - - if args.version: - exit(0) - return behaviour @@ -685,30 +734,87 @@ def surplus( query: PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery, behaviour: Behaviour, ) -> Result[str]: - return Result[str]("", error=NotImplementedError()) + """ + query to shareable text conversion function + + query: PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery + query object to convert, see respective docstrings for more information on each + type of query object + behaviour: Behaviour + program behaviour namedtuple + """ + + def _unique(l: Sequence[str]) -> list[str]: + """(internal function) returns a in-order unique list from list""" + unique: OrderedDict = OrderedDict() + for line in l: + unique.update({line: None}) + return list(unique.keys()) + + def _generate_text(line_keys: Sequence[str]) -> str: + """(internal function) TODO DOCSTRING""" + # TODO + return "" + + # get latlong and handle result + latlong = query.to_lat_long_coord(geocoder=behaviour.geocoder) + + if not latlong: + return Result[str]("", error=latlong.error) + + if behaviour.debug: + behaviour.stderr.write(f"debug: {latlong.get()=}\n") + + # operate on query + text: str = "" + + if behaviour.operation_mode == SurplusOperationMode.GENERATE_TEXT: + # TODO + return Result[str]("", error="not fully implemented yet") + + elif behaviour.operation_mode == SurplusOperationMode.CONVERT_TYPES: + # TODO: https://github.com/markjoshwel/surplus/issues/18 + return Result[str]("", error="conversion functionality is not implemented yet") + + else: + return Result[str]("", error="unknown operation mode") # command-line entry def cli() -> int: + # handle arguments and print version header behaviour = handle_args() + + (behaviour.stdout if behaviour.version_header else behaviour.stderr).write( + f"surplus version {'.'.join([str(v) for v in VERSION])}" + + (f", debug mode" if behaviour.debug else "") + + "\n" + ) + + if behaviour.version_header: + exit(0) + + # parse query and handle result query = parse_query(behaviour=behaviour) if not query: - behaviour.stderr.write(f"error: {query.cry(string=True)}\n") + behaviour.stderr.write(f"error: {query.cry(string=not behaviour.debug)}\n") return -1 if behaviour.debug: behaviour.stderr.write(f"debug: {query.get()=}\n") + # run surplus text = surplus( query=query.get(), behaviour=behaviour, ) + # handle and display surplus result if not text: - behaviour.stderr.write(f"error: {text.cry(string=True)}\n") + behaviour.stderr.write(f"error: {text.cry(string=not behaviour.debug)}\n") return -2 behaviour.stdout.write(text.get() + "\n") From fd66a53c112c08ba840856f74320325f1218c9e1 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 04:22:19 +0000 Subject: [PATCH 21/41] s+/#17: better conversion framework code --- surplus.py | 63 ++++++++++++++++++------------------------------------ 1 file changed, 21 insertions(+), 42 deletions(-) diff --git a/surplus.py b/surplus.py index 27a3343..9a27d55 100644 --- a/surplus.py +++ b/surplus.py @@ -39,10 +39,10 @@ from typing import ( Final, Generic, NamedTuple, + Sequence, TextIO, TypeAlias, TypeVar, - Sequence, ) from geopy import Location as _geopy_Location # type: ignore @@ -128,20 +128,13 @@ class IncompletePlusCodeError(Exception): # data structures -class SurplusQueryTypes(Enum): - """enum representing the mode of surplus""" +class ConversionResultTypeEnum(Enum): + """enum representing what the result type of conversion should be""" PLUS_CODE = "pluscode" LOCAL_CODE = "localcode" LATLONG = "latlong" - STRING = "string" - - -class SurplusOperationMode(Enum): - """enum representing the mode of surplus""" - - GENERATE_TEXT = "generate" - CONVERT_TYPES = "convert" + SHAREABLE_TEXT = "shareabletext" ResultType = TypeVar("ResultType") @@ -456,7 +449,7 @@ def default_reverser(latlong: Latlong) -> dict[str, Any]: class Behaviour(NamedTuple): """ - typing.NamedTuple representing program behaviour + typing.NamedTuple representing expected behaviour of surplus arguments query: list[str] @@ -473,11 +466,10 @@ class Behaviour(NamedTuple): TextIO-like object representing a writeable file. defaults to sys.stdout. debug: bool = False whether to print debug information to stderr - operation_mode: SurplusOperationMode = SurplusOperationMode.GENERATE_TEXT - surplus operation mode enum value - convert_to_type: SurplusQueryTypes | None = None - surplus query type enum value for when - operation_mode = SurplusOperationMode.CONVERT_TYPES + version_header: bool = False + whether to print version information and exit + convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT + what type to convert query to """ query: list[str] @@ -487,8 +479,7 @@ class Behaviour(NamedTuple): stdout: TextIO = stdout debug: bool = False version_header: bool = False - operation_mode: SurplusOperationMode = SurplusOperationMode.GENERATE_TEXT - convert_to_type: SurplusQueryTypes | None = None + convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT # functions @@ -698,23 +689,12 @@ def handle_args() -> Behaviour: "-c", "--convert-to", type=str, - choices=[str(v.value) for v in SurplusQueryTypes], + choices=[str(v.value) for v in ConversionResultTypeEnum], help="converts query to another type", default="", ) args = parser.parse_args() - - convert_to_type: SurplusQueryTypes | None = None - if args.convert_to != "": - convert_to_type = SurplusQueryTypes(args.convert_to) - - operation_mode: SurplusOperationMode = ( - SurplusOperationMode.GENERATE_TEXT - if convert_to_type is None - else SurplusOperationMode.CONVERT_TYPES - ) - behaviour = Behaviour( query=args.query, geocoder=default_geocoder, @@ -723,8 +703,7 @@ def handle_args() -> Behaviour: stdout=stdout, debug=args.debug, version_header=args.version, - operation_mode=operation_mode, - convert_to_type=convert_to_type, + convert_to_type=ConversionResultTypeEnum(args.convert_to), ) return behaviour @@ -768,16 +747,16 @@ def surplus( # operate on query text: str = "" - if behaviour.operation_mode == SurplusOperationMode.GENERATE_TEXT: - # TODO - return Result[str]("", error="not fully implemented yet") + match behaviour.convert_to_type: + case ConversionResultTypeEnum.SHAREABLE_TEXT: + # TODO + return Result[str]("", error="TODO") - elif behaviour.operation_mode == SurplusOperationMode.CONVERT_TYPES: - # TODO: https://github.com/markjoshwel/surplus/issues/18 - return Result[str]("", error="conversion functionality is not implemented yet") - - else: - return Result[str]("", error="unknown operation mode") + case _: + # TODO: https://github.com/markjoshwel/surplus/issues/18 + return Result[str]( + "", error="conversion functionality is not implemented yet" + ) # command-line entry From 30e6c009ccc82a0a89aa70001b59111beffefaec Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 04:37:41 +0000 Subject: [PATCH 22/41] s+/#17: fix cli and better help message --- surplus.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/surplus.py b/surplus.py index 9a27d55..6c44558 100644 --- a/surplus.py +++ b/surplus.py @@ -153,16 +153,17 @@ class Result(NamedTuple, Generic[ResultType]): methods def __bool__(self) -> bool: ... def get(self) -> ResultType: ... - def cry(self) -> str: ... + def cry(self, string: bool = False) -> str: ... example # do something try: file_contents = Path(...).read_text() except Exception as exc: + # must pass a default value result = Result[str]("", error=exc) else: - result = Result[str] + result = Result[str](file_contents) # handle result if not result: @@ -690,8 +691,11 @@ def handle_args() -> Behaviour: "--convert-to", type=str, choices=[str(v.value) for v in ConversionResultTypeEnum], - help="converts query to another type", - default="", + help=( + "converts query a specific output type, defaults to " + f"'{Behaviour([]).convert_to_type.value}'" + ), + default=Behaviour([]).convert_to_type.value, ) args = parser.parse_args() @@ -721,6 +725,8 @@ def surplus( type of query object behaviour: Behaviour program behaviour namedtuple + + returns Result[str] """ def _unique(l: Sequence[str]) -> list[str]: @@ -735,6 +741,7 @@ def surplus( # TODO return "" + """ # get latlong and handle result latlong = query.to_lat_long_coord(geocoder=behaviour.geocoder) @@ -743,6 +750,7 @@ def surplus( if behaviour.debug: behaviour.stderr.write(f"debug: {latlong.get()=}\n") + """ # operate on query text: str = "" @@ -750,12 +758,27 @@ def surplus( match behaviour.convert_to_type: case ConversionResultTypeEnum.SHAREABLE_TEXT: # TODO - return Result[str]("", error="TODO") + return Result[str](text, error="TODO") - case _: + case ConversionResultTypeEnum.PLUS_CODE: # TODO: https://github.com/markjoshwel/surplus/issues/18 return Result[str]( - "", error="conversion functionality is not implemented yet" + text, error="converting to Plus Code is not implemented yet" + ) + + case ConversionResultTypeEnum.LOCAL_CODE: + # TODO: https://github.com/markjoshwel/surplus/issues/18 + return Result[str]( + text, error="converting to Plus Code is not implemented yet" + ) + + case ConversionResultTypeEnum.LATLONG: + # TODO: https://github.com/markjoshwel/surplus/issues/18 + return Result[str](text, error="converting to Latlong is not implemented yet") + + case _: + return Result[str]( + "", error=f"unknown conversion result type '{behaviour.convert_to_type}'" ) @@ -763,6 +786,8 @@ def surplus( def cli() -> int: + """command-line entry point, returns an exit code int""" + # handle arguments and print version header behaviour = handle_args() From 528255a8cb12b5f6031de13fc32317be78da6cbb Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 06:22:51 +0000 Subject: [PATCH 23/41] s+: semi-working conversion to shareabletext --- surplus.future.ipynb | 29 +++++++ surplus.py | 179 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 176 insertions(+), 32 deletions(-) diff --git a/surplus.future.ipynb b/surplus.future.ipynb index e0e683c..7b3121f 100644 --- a/surplus.future.ipynb +++ b/surplus.future.ipynb @@ -220,6 +220,35 @@ "source": [ "StringQuery(query=\"Ngee Ann Polytechnic\").to_lat_long_coord(geocoder=default_geocoder)" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reverser Dictionary Output" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "{\n", + " \"amenity\": \"\",\n", + " \"house_number\": \"\",\n", + " \"road\": \"\",\n", + " \"suburb\": \"\",\n", + " \"city\": \"\",\n", + " \"county\": \"\",\n", + " \"ISO3166-2-lvl6\": \"\",\n", + " \"postcode\": \"\",\n", + " \"country\": \"\",\n", + " \"country_code\": \"\",\n", + " \"original response\": \"{'place_id': 297946059, 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright', 'osm_type': 'relation', 'osm_id': 2535118, 'lat': '1.33318835', 'lon': '103.77461234638255', 'class': 'amenity', 'type': 'university', 'place_rank': 30, 'importance': 0.34662169301918117, 'addresstype': 'amenity', 'name': 'Ngee Ann Polytechnic', 'display_name': 'Ngee Ann Polytechnic, 535, Clementi Road, Bukit Timah, Singapore, Northwest, 599489, Singapore', 'address': {'amenity': 'Ngee Ann Polytechnic', 'house_number': '535', 'road': 'Clementi Road', 'suburb': 'Bukit Timah', 'city': 'Singapore', 'county': 'Northwest', 'ISO3166-2-lvl6': 'SG-03', 'postcode': '599489', 'country': 'Singapore', 'country_code': 'sg'}, 'boundingbox': ['1.3289692', '1.3372184', '103.7701481', '103.7783945']}\",\n", + " \"latitude\": \"1.33318835\",\n", + "}" + ] } ], "metadata": { diff --git a/surplus.py b/surplus.py index 6c44558..4c33992 100644 --- a/surplus.py +++ b/surplus.py @@ -84,6 +84,7 @@ OUTPUT_LINE_0_KEYS: Final[tuple[str, ...]] = ( ) OUTPUT_LINE_1_KEYS: Final[tuple[str, ...]] = ("building",) OUTPUT_LINE_2_KEYS: Final[tuple[str, ...]] = ("highway",) +OUTPUT_LINE_3_NAME: Final[tuple[str, ...]] = ("house_name", "road") OUTPUT_LINE_3_KEYS: Final[tuple[str, ...]] = ( "house_number", "house_name", @@ -434,18 +435,32 @@ def default_geocoder(place: str) -> Latlong: ) -def default_reverser(latlong: Latlong) -> dict[str, Any]: +def default_reverser(latlong: Latlong) -> dict[str, str]: """default geocoder for surplus, uses OpenStreetMap Nominatim""" location: _geopy_Location | None = _geopy_Nominatim(user_agent=USER_AGENT).reverse( str(latlong) ) if location is None: - raise NoSuitableLocationError( - f"No suitable location could be reversed from '{str(latlong)}'" - ) + raise NoSuitableLocationError(f"could not reverse '{str(latlong)}'") - return location.raw + location_dict: dict[str, str] = {} + + for key in (address := location.raw.get("address", {})): + location_dict[key] = str(address.get(key, "")) + + _bounding_box_default: list[float] = [0.0] * 4 + + location_dict["original response"] = str(location.raw) + location_dict["latitude"] = str(location.longitude) + location_dict["latitude"] = str(location.latitude) + + # location_dict["boundingbox1"] = str(location.raw.get("boundingbox", _bounding_box_default)[0]) + # location_dict["boundingbox2"] = str(location.raw.get("boundingbox", _bounding_box_default)[1]) + # location_dict["boundingbox3"] = str(location.raw.get("boundingbox", _bounding_box_default)[2]) + # location_dict["boundingbox4"] = str(location.raw.get("boundingbox", _bounding_box_default)[3]) + + return location_dict class Behaviour(NamedTuple): @@ -458,7 +473,7 @@ class Behaviour(NamedTuple): geocoder: Callable[[str], Latlong] name string to location function, must take in a string and return a Latlong. exceptions are handled by the caller. - reverser: Callable[[str], dict[str, Any]] + reverser: Callable[[str], dict[str, str]] Latlong object to dictionary function, must take in a string and return a dict. exceptions are handled by the caller. stderr: TextIO = stderr @@ -475,7 +490,7 @@ class Behaviour(NamedTuple): query: list[str] geocoder: Callable[[str], Latlong] = default_geocoder - reverser: Callable[[Latlong], dict[str, Any]] = default_reverser + reverser: Callable[[Latlong], dict[str, str]] = default_reverser stderr: TextIO = stderr stdout: TextIO = stdout debug: bool = False @@ -713,6 +728,100 @@ def handle_args() -> Behaviour: return behaviour +def _unique(l: Sequence[str]) -> list[str]: + """(internal function) returns a in-order unique list from list""" + unique: OrderedDict = OrderedDict() + for line in l: + unique.update({line: None}) + return list(unique.keys()) + + +def _generate_text(location: dict[str, str], debug: bool = False) -> str: + """(internal function) generate shareable text from location dict""" + + def _generate_text_line( + line_number: int, + line_keys: Sequence[str], + seperator: str = ", ", + element_check: Callable[[str], bool] = lambda e: True, + debug: bool = False, + ) -> str: + """ + (internal function) generate a line of shareable text from a list of keys + + arguments + line_number: int + line number to prefix with + line_keys: Sequence[str] + list of keys to .get() from location dict + element_check: Callable[[str], bool] = lambda e: True + function that takes in a string and returns a bool, used to filter + elements from line_keys + debug: bool = False + whether to prefix line with line_number + + returns str + """ + + line_prefix: str = f"{line_number}\t" if debug else "" + basket: list[str] = [] + + for detail in _unique([location.get(detail, "") for detail in line_keys]): + if detail == "": + continue + + if element_check(detail): + basket.append(detail) + + line = line_prefix + seperator.join(basket) + + return (line + "\n") if (line != "") else "" + + text: list[str] = [] + + seen_names: list[str] = [ + detail + for detail in _unique( + [ + location.get(location_key, "") + for location_key in ( + OUTPUT_LINE_0_KEYS + + OUTPUT_LINE_1_KEYS + + OUTPUT_LINE_2_KEYS + + OUTPUT_LINE_3_NAME + ) + ] + ) + if detail != "" + ] + + general_global_info: list[str] = [ + location.get(detail, "") for detail in OUTPUT_LINE_6_KEYS + ] + + text.append(_generate_text_line(0, OUTPUT_LINE_0_KEYS, debug=debug)) + text.append(_generate_text_line(1, OUTPUT_LINE_1_KEYS, debug=debug)) + text.append(_generate_text_line(2, OUTPUT_LINE_2_KEYS, debug=debug)) + text.append(_generate_text_line(3, OUTPUT_LINE_3_KEYS, seperator=" ", debug=debug)) + text.append( + _generate_text_line( + 4, + OUTPUT_LINE_4_KEYS, + element_check=lambda ak: all( + [ + ak not in general_global_info, + any(True if (ak not in sn) else False for sn in seen_names), + ] + ), + debug=debug, + ) + ) + text.append(_generate_text_line(5, OUTPUT_LINE_5_KEYS, debug=debug)) + text.append(_generate_text_line(6, OUTPUT_LINE_6_KEYS, debug=debug)) + + return "".join(text) + + def surplus( query: PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery, behaviour: Behaviour, @@ -729,36 +838,42 @@ def surplus( returns Result[str] """ - def _unique(l: Sequence[str]) -> list[str]: - """(internal function) returns a in-order unique list from list""" - unique: OrderedDict = OrderedDict() - for line in l: - unique.update({line: None}) - return list(unique.keys()) - - def _generate_text(line_keys: Sequence[str]) -> str: - """(internal function) TODO DOCSTRING""" - # TODO - return "" - - """ - # get latlong and handle result - latlong = query.to_lat_long_coord(geocoder=behaviour.geocoder) - - if not latlong: - return Result[str]("", error=latlong.error) - - if behaviour.debug: - behaviour.stderr.write(f"debug: {latlong.get()=}\n") - """ - # operate on query text: str = "" match behaviour.convert_to_type: case ConversionResultTypeEnum.SHAREABLE_TEXT: - # TODO - return Result[str](text, error="TODO") + # get latlong and handle result + latlong = query.to_lat_long_coord(geocoder=behaviour.geocoder) + + if not latlong: + return Result[str]("", error=latlong.error) + + if behaviour.debug: + behaviour.stderr.write(f"debug: {latlong.get()=}\n") + + try: + location: dict[str, str] = behaviour.reverser(latlong.get()) + + except Exception as exc: + return Result[str]("", error=exc) + + if behaviour.debug: + behaviour.stderr.write(f"debug: {location=}\n") + + if behaviour.debug: + behaviour.stderr.write( + _generate_text( + location=location, + debug=behaviour.debug, + ) + ) + + text = _generate_text( + location=location, + ).rstrip() + + return Result[str](text) case ConversionResultTypeEnum.PLUS_CODE: # TODO: https://github.com/markjoshwel/surplus/issues/18 From f62c31d685d265c89222bcfc9c88ed75f0868397 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 06:32:26 +0000 Subject: [PATCH 24/41] docs: change usage to reflect new cli args --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ee4b008..660d02d 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,10 @@ options: -d, --debug prints lat, long and reverser response dict to stderr -v, --version prints version information to stderr and exits - -c {pluscode,localcode,latlong,string}, - --convert-to {pluscode,localcode,latlong,string} - converts query to another type + -c {pluscode,localcode,latlong,shareabletext}, + --convert-to {pluscode,localcode,latlong,shareabletext} + converts query a specific output type, defaults + to 'shareabletext' ``` ## developer's guide From cc90b5e196168d9d5208376508fe915b54cd8e5e Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 09:35:33 +0000 Subject: [PATCH 25/41] s+: touch up conversion to shareabletext + tests --- surplus.future.ipynb | 191 ++++++++++++++++++++++++++--------- surplus.py | 234 +++++++++++++++++++++++++++---------------- test.py | 78 ++++++++++----- 3 files changed, 344 insertions(+), 159 deletions(-) diff --git a/surplus.future.ipynb b/surplus.future.ipynb index 7b3121f..9eb924c 100644 --- a/surplus.future.ipynb +++ b/surplus.future.ipynb @@ -113,6 +113,26 @@ "## Query Types" ] }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Result(value=Latlong(latitude=1.3336875, longitude=103.7746875), error=None)" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "PlusCodeQuery(code=\"6PH58QMF+FV\").to_lat_long_coord(geocoder=default_geocoder)" + ] + }, { "cell_type": "code", "execution_count": 4, @@ -130,7 +150,11 @@ } ], "source": [ - "PlusCodeQuery(code=\"6PH58QMF+FV\").to_lat_long_coord(geocoder=default_geocoder)" + "plus_code = LocalCodeQuery(code=\"8QMF+FV\", locality=\"Singapore\").to_full_plus_code(\n", + " geocoder=default_geocoder\n", + ")\n", + "\n", + "PlusCodeQuery(code=plus_code.get()).to_lat_long_coord(geocoder=default_geocoder)" ] }, { @@ -150,11 +174,9 @@ } ], "source": [ - "plus_code = LocalCodeQuery(code=\"8QMF+FV\", locality=\"Singapore\").to_full_plus_code(\n", + "LocalCodeQuery(code=\"8QMF+FV\", locality=\"Singapore\").to_lat_long_coord(\n", " geocoder=default_geocoder\n", - ")\n", - "\n", - "PlusCodeQuery(code=plus_code.get()).to_lat_long_coord(geocoder=default_geocoder)" + ")" ] }, { @@ -165,7 +187,7 @@ { "data": { "text/plain": [ - "Result(value=Latlong(latitude=1.3336875, longitude=103.7746875), error=None)" + "Result(value=Latlong(latitude=1.33318835, longitude=103.77461234638255), error=None)" ] }, "execution_count": 6, @@ -174,9 +196,9 @@ } ], "source": [ - "LocalCodeQuery(code=\"8QMF+FV\", locality=\"Singapore\").to_lat_long_coord(\n", - " geocoder=default_geocoder\n", - ")" + "LatlongQuery(\n", + " latlong=Latlong(latitude=1.33318835, longitude=103.77461234638255)\n", + ").to_lat_long_coord(geocoder=default_geocoder)" ] }, { @@ -195,28 +217,6 @@ "output_type": "execute_result" } ], - "source": [ - "LatlongQuery(\n", - " latlong=Latlong(latitude=1.33318835, longitude=103.77461234638255)\n", - ").to_lat_long_coord(geocoder=default_geocoder)" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Result(value=Latlong(latitude=1.33318835, longitude=103.77461234638255), error=None)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ "StringQuery(query=\"Ngee Ann Polytechnic\").to_lat_long_coord(geocoder=default_geocoder)" ] @@ -225,7 +225,13 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## Reverser Dictionary Output" + "## return dictionary of `reverser` function\n", + "\n", + "all the necessary keys (see `SHAREABLE_TEXT_LINE_*` constants) should be at the top-level of the dictionary.\n", + "these keys will be casted into strings for safety guarantee when shareable text lines are being generated.\n", + "\n", + "while not necessary, consider keeping the original response dict under the \"raw\" key.\n", + "helps with debugging using `-d/--debug`!" ] }, { @@ -234,21 +240,116 @@ "metadata": {}, "outputs": [], "source": [ - "{\n", - " \"amenity\": \"\",\n", - " \"house_number\": \"\",\n", - " \"road\": \"\",\n", - " \"suburb\": \"\",\n", - " \"city\": \"\",\n", - " \"county\": \"\",\n", - " \"ISO3166-2-lvl6\": \"\",\n", - " \"postcode\": \"\",\n", - " \"country\": \"\",\n", - " \"country_code\": \"\",\n", - " \"original response\": \"{'place_id': 297946059, 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright', 'osm_type': 'relation', 'osm_id': 2535118, 'lat': '1.33318835', 'lon': '103.77461234638255', 'class': 'amenity', 'type': 'university', 'place_rank': 30, 'importance': 0.34662169301918117, 'addresstype': 'amenity', 'name': 'Ngee Ann Polytechnic', 'display_name': 'Ngee Ann Polytechnic, 535, Clementi Road, Bukit Timah, Singapore, Northwest, 599489, Singapore', 'address': {'amenity': 'Ngee Ann Polytechnic', 'house_number': '535', 'road': 'Clementi Road', 'suburb': 'Bukit Timah', 'city': 'Singapore', 'county': 'Northwest', 'ISO3166-2-lvl6': 'SG-03', 'postcode': '599489', 'country': 'Singapore', 'country_code': 'sg'}, 'boundingbox': ['1.3289692', '1.3372184', '103.7701481', '103.7783945']}\",\n", - " \"latitude\": \"1.33318835\",\n", + "reverser_return = {\n", + " \"amenity\": \"Ngee Ann Polytechnic\",\n", + " \"house_number\": \"535\",\n", + " \"road\": \"Clementi Road\",\n", + " \"suburb\": \"Bukit Timah\",\n", + " \"city\": \"Singapore\",\n", + " \"county\": \"Northwest\",\n", + " \"ISO3166-2-lvl6\": \"SG-03\",\n", + " \"postcode\": \"599489\",\n", + " \"country\": \"Singapore\",\n", + " \"country_code\": \"sg\",\n", + " \"raw\": {\n", + " \"place_id\": 297946059,\n", + " \"licence\": \"Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright\",\n", + " \"osm_type\": \"relation\",\n", + " \"osm_id\": 2535118,\n", + " \"lat\": \"1.33318835\",\n", + " \"lon\": \"103.77461234638255\",\n", + " \"class\": \"amenity\",\n", + " \"type\": \"university\",\n", + " \"place_rank\": 30,\n", + " \"importance\": 0.34662169301918117,\n", + " \"addresstype\": \"amenity\",\n", + " \"name\": \"Ngee Ann Polytechnic\",\n", + " \"display_name\": \"Ngee Ann Polytechnic, 535, Clementi Road, Bukit Timah, Singapore, Northwest, 599489, Singapore\",\n", + " \"address\": {\n", + " \"amenity\": \"Ngee Ann Polytechnic\",\n", + " \"house_number\": \"535\",\n", + " \"road\": \"Clementi Road\",\n", + " \"suburb\": \"Bukit Timah\",\n", + " \"city\": \"Singapore\",\n", + " \"county\": \"Northwest\",\n", + " \"ISO3166-2-lvl6\": \"SG-03\",\n", + " \"postcode\": \"599489\",\n", + " \"country\": \"Singapore\",\n", + " \"country_code\": \"sg\",\n", + " },\n", + " \"boundingbox\": [\"1.3289692\", \"1.3372184\", \"103.7701481\", \"103.7783945\"],\n", + " },\n", + " \"latitude\": 1.33318835,\n", + " \"longitude\": 103.77461234638255,\n", "}" ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'ISO3166-2-lvl6': 'SG-03',\n", + " 'amenity': 'Ngee Ann Polytechnic',\n", + " 'city': 'Singapore',\n", + " 'country': 'Singapore',\n", + " 'country_code': 'sg',\n", + " 'county': 'Northwest',\n", + " 'house_number': '535',\n", + " 'latitude': 1.33318835,\n", + " 'longitude': 103.77461234638255,\n", + " 'postcode': '599489',\n", + " 'raw': {'address': {'ISO3166-2-lvl6': 'SG-03',\n", + " 'amenity': 'Ngee Ann Polytechnic',\n", + " 'city': 'Singapore',\n", + " 'country': 'Singapore',\n", + " 'country_code': 'sg',\n", + " 'county': 'Northwest',\n", + " 'house_number': '535',\n", + " 'postcode': '599489',\n", + " 'road': 'Clementi Road',\n", + " 'suburb': 'Bukit Timah'},\n", + " 'addresstype': 'amenity',\n", + " 'boundingbox': ['1.3289692',\n", + " '1.3372184',\n", + " '103.7701481',\n", + " '103.7783945'],\n", + " 'class': 'amenity',\n", + " 'display_name': 'Ngee Ann Polytechnic, 535, Clementi Road, Bukit '\n", + " 'Timah, Singapore, Northwest, 599489, Singapore',\n", + " 'importance': 0.34662169301918117,\n", + " 'lat': '1.33318835',\n", + " 'licence': 'Data © OpenStreetMap contributors, ODbL 1.0. '\n", + " 'http://osm.org/copyright',\n", + " 'lon': '103.77461234638255',\n", + " 'name': 'Ngee Ann Polytechnic',\n", + " 'osm_id': 2535118,\n", + " 'osm_type': 'relation',\n", + " 'place_id': 297946059,\n", + " 'place_rank': 30,\n", + " 'type': 'university'},\n", + " 'road': 'Clementi Road',\n", + " 'suburb': 'Bukit Timah'}\n" + ] + } + ], + "source": [ + "import pprint\n", + "\n", + "latlong = LocalCodeQuery(code=\"8QMF+FV\", locality=\"Singapore\").to_lat_long_coord(\n", + " default_geocoder\n", + ")\n", + "if not latlong:\n", + " latlong.cry()\n", + "\n", + "else:\n", + " location = default_reverser(latlong.get())\n", + " pprint.pprint(location)" + ] } ], "metadata": { diff --git a/surplus.py b/surplus.py index 4c33992..df3160b 100644 --- a/surplus.py +++ b/surplus.py @@ -58,7 +58,7 @@ from pluscodes.openlocationcode import ( # type: ignore # isort: skip VERSION: Final[tuple[int, int, int]] = (2, 0, 0) USER_AGENT: Final[str] = "surplus" -OUTPUT_LINE_0_KEYS: Final[tuple[str, ...]] = ( +SHAREABLE_TEXT_LINE_0_KEYS: Final[tuple[str, ...]] = ( "emergency", "historic", "military", @@ -82,15 +82,14 @@ OUTPUT_LINE_0_KEYS: Final[tuple[str, ...]] = ( "tunnel", "waterway", ) -OUTPUT_LINE_1_KEYS: Final[tuple[str, ...]] = ("building",) -OUTPUT_LINE_2_KEYS: Final[tuple[str, ...]] = ("highway",) -OUTPUT_LINE_3_NAME: Final[tuple[str, ...]] = ("house_name", "road") -OUTPUT_LINE_3_KEYS: Final[tuple[str, ...]] = ( +SHAREABLE_TEXT_LINE_1_KEYS: Final[tuple[str, ...]] = ("building",) +SHAREABLE_TEXT_LINE_2_KEYS: Final[tuple[str, ...]] = ("highway",) +SHAREABLE_TEXT_LINE_3_KEYS: Final[tuple[str, ...]] = ( "house_number", "house_name", "road", ) -OUTPUT_LINE_4_KEYS: Final[tuple[str, ...]] = ( +SHAREABLE_TEXT_LINE_4_KEYS: Final[tuple[str, ...]] = ( "residential", "neighbourhood", "allotments", @@ -105,8 +104,8 @@ OUTPUT_LINE_4_KEYS: Final[tuple[str, ...]] = ( "town", "village", ) -OUTPUT_LINE_5_KEYS: Final[tuple[str, ...]] = ("postcode",) -OUTPUT_LINE_6_KEYS: Final[tuple[str, ...]] = ( +SHAREABLE_TEXT_LINE_5_KEYS: Final[tuple[str, ...]] = ("postcode",) +SHAREABLE_TEXT_LINE_6_KEYS: Final[tuple[str, ...]] = ( "region", "county", "state", @@ -114,6 +113,12 @@ OUTPUT_LINE_6_KEYS: Final[tuple[str, ...]] = ( "country", "continent", ) +SHAREABLE_TEXT_NAMES: Final[tuple[str, ...]] = ( + SHAREABLE_TEXT_LINE_0_KEYS + + SHAREABLE_TEXT_LINE_1_KEYS + + SHAREABLE_TEXT_LINE_2_KEYS + + ("house_name", "road") +) # exceptions @@ -276,6 +281,7 @@ class PlusCodeQuery(NamedTuple): return Result[Latlong]( EMPTY_LATLONG, error=IncompletePlusCodeError( + "PlusCodeQuery.to_lat_long_coord: " "Plus Code is not full-length (e.g., 6PH58QMF+FX)" ), ) @@ -435,7 +441,7 @@ def default_geocoder(place: str) -> Latlong: ) -def default_reverser(latlong: Latlong) -> dict[str, str]: +def default_reverser(latlong: Latlong) -> dict[str, Any]: """default geocoder for surplus, uses OpenStreetMap Nominatim""" location: _geopy_Location | None = _geopy_Nominatim(user_agent=USER_AGENT).reverse( str(latlong) @@ -444,21 +450,14 @@ def default_reverser(latlong: Latlong) -> dict[str, str]: if location is None: raise NoSuitableLocationError(f"could not reverse '{str(latlong)}'") - location_dict: dict[str, str] = {} + location_dict: dict[str, Any] = {} for key in (address := location.raw.get("address", {})): - location_dict[key] = str(address.get(key, "")) + location_dict[key] = address.get(key, "") - _bounding_box_default: list[float] = [0.0] * 4 - - location_dict["original response"] = str(location.raw) - location_dict["latitude"] = str(location.longitude) - location_dict["latitude"] = str(location.latitude) - - # location_dict["boundingbox1"] = str(location.raw.get("boundingbox", _bounding_box_default)[0]) - # location_dict["boundingbox2"] = str(location.raw.get("boundingbox", _bounding_box_default)[1]) - # location_dict["boundingbox3"] = str(location.raw.get("boundingbox", _bounding_box_default)[2]) - # location_dict["boundingbox4"] = str(location.raw.get("boundingbox", _bounding_box_default)[3]) + location_dict["raw"] = location.raw + location_dict["latitude"] = location.latitude + location_dict["longitude"] = location.longitude return location_dict @@ -468,12 +467,13 @@ class Behaviour(NamedTuple): typing.NamedTuple representing expected behaviour of surplus arguments - query: list[str] - original user-passed query string split by spaces + query: str | list[str] + str: original user-passed query string + list[str]: original user-passed query string split by spaces geocoder: Callable[[str], Latlong] name string to location function, must take in a string and return a Latlong. exceptions are handled by the caller. - reverser: Callable[[str], dict[str, str]] + reverser: Callable[[str], dict[str, Any]] Latlong object to dictionary function, must take in a string and return a dict. exceptions are handled by the caller. stderr: TextIO = stderr @@ -488,9 +488,9 @@ class Behaviour(NamedTuple): what type to convert query to """ - query: list[str] + query: str | list[str] geocoder: Callable[[str], Latlong] = default_geocoder - reverser: Callable[[Latlong], dict[str, str]] = default_reverser + reverser: Callable[[Latlong], dict[str, Any]] = default_reverser stderr: TextIO = stderr stdout: TextIO = stdout debug: bool = False @@ -527,9 +527,16 @@ def parse_query( validator = _PlusCode_Validator() portion_plus_code: str = "" portion_locality: str = "" + original_query: str = "" + split_query: list[str] = [] - original_query = " ".join(behaviour.query) - split_query = behaviour.query + if isinstance(behaviour.query, str): + original_query = behaviour.query + split_query = behaviour.query.split(" ") + + else: + original_query = " ".join(behaviour.query) + split_query = behaviour.query for word in split_query: if validator.is_valid(word): @@ -547,19 +554,19 @@ def parse_query( error="unable to find a Plus Code", ) - # did find plus code, but not full-length. :( - if not validator.is_full(portion_plus_code): - return Result[Query]( - LatlongQuery(EMPTY_LATLONG), - error=IncompletePlusCodeError( - "Plus Code is not full-length (e.g., 6PH58QMF+FX)" - ), - ) - # found a plus code! portion_locality = original_query.replace(portion_plus_code, "") portion_locality = portion_locality.strip().strip(",").strip() + # did find plus code, but not full-length. :( + if (portion_locality == "") and (not validator.is_full(portion_plus_code)): + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error=IncompletePlusCodeError( + "_match_plus_code: Plus Code is not full-length (e.g., 6PH58QMF+FX)" + ), + ) + if behaviour.debug: behaviour.stderr.write(f"debug: {portion_plus_code=}, {portion_locality=}\n") @@ -589,7 +596,7 @@ def parse_query( behaviour.stderr.write(f"debug: {behaviour.query=}\n") # check if empty - if behaviour.query == []: + if (behaviour.query == []) or (behaviour.query == ""): return Result[Query]( LatlongQuery(EMPTY_LATLONG), error="query is empty", @@ -604,28 +611,43 @@ def parse_query( if isinstance(mpc_result.error, IncompletePlusCodeError): return mpc_result # propagate back up to caller + # handle query + original_query: str = "" + split_query: list[str] = [] + + if isinstance(behaviour.query, str): + original_query = behaviour.query + split_query = behaviour.query.split(" ") + + else: + original_query = " ".join(behaviour.query) + split_query = behaviour.query + + if behaviour.debug: + behaviour.stderr.write(f"debug: {split_query=}\ndebug: {original_query=}\n") + # not a plus/local code, try to match for latlong or string query - match behaviour.query: + match split_query: case [single]: # possibly a: # comma-seperated single-word-long latlong coord # (fallback) single word string query if "," not in single: # no comma, not a latlong coord - return Result[Query](StringQuery(" ".join(behaviour.query))) + return Result[Query](StringQuery(original_query)) else: # has comma, possibly a latlong coord - split_query: list[str] = single.split(",") + comma_split_single: list[str] = single.split(",") - if len(split_query) > 2: + if len(comma_split_single) > 2: return Result[Query]( LatlongQuery(EMPTY_LATLONG), error="unable to parse latlong coord", ) try: # try to type cast query - latitude = float(split_query[0].strip(",")) - longitude = float(split_query[-1].strip(",")) + latitude = float(comma_split_single[0].strip(",")) + longitude = float(comma_split_single[-1].strip(",")) except ValueError: # not a latlong coord, fallback return Result[Query](StringQuery(single)) @@ -650,7 +672,7 @@ def parse_query( longitude = float(right_single.strip(",")) except ValueError: # not a latlong coord, fallback - return Result[Query](StringQuery(" ".join(behaviour.query))) + return Result[Query](StringQuery(original_query)) else: # are floats, so is a latlong coord return Result[Query]( @@ -661,7 +683,7 @@ def parse_query( # possibly a: # (fallback) space-seperated string query - return Result[Query](StringQuery(" ".join(behaviour.query))) + return Result[Query](StringQuery(original_query)) def handle_args() -> Behaviour: @@ -736,15 +758,16 @@ def _unique(l: Sequence[str]) -> list[str]: return list(unique.keys()) -def _generate_text(location: dict[str, str], debug: bool = False) -> str: +def _generate_text( + location: dict[str, Any], behaviour: Behaviour, debug: bool = False +) -> str: """(internal function) generate shareable text from location dict""" def _generate_text_line( line_number: int, line_keys: Sequence[str], seperator: str = ", ", - element_check: Callable[[str], bool] = lambda e: True, - debug: bool = False, + filter: Callable[[str], list[bool]] = lambda e: [True], ) -> str: """ (internal function) generate a line of shareable text from a list of keys @@ -754,11 +777,10 @@ def _generate_text(location: dict[str, str], debug: bool = False) -> str: line number to prefix with line_keys: Sequence[str] list of keys to .get() from location dict - element_check: Callable[[str], bool] = lambda e: True - function that takes in a string and returns a bool, used to filter - elements from line_keys - debug: bool = False - whether to prefix line with line_number + filter: Callable[[str], list[bool]] = lambda e: True + function that takes in a string and returns a list of bools, used to + filter elements from line_keys. list will be passed to all(). if all + returns True, then the element is kept. returns str """ @@ -766,60 +788,93 @@ def _generate_text(location: dict[str, str], debug: bool = False) -> str: line_prefix: str = f"{line_number}\t" if debug else "" basket: list[str] = [] - for detail in _unique([location.get(detail, "") for detail in line_keys]): + for detail in _unique([str(location.get(detail, "")) for detail in line_keys]): if detail == "": continue - if element_check(detail): + # filtering: if all(filter(detail)) returns True, + # then the element is kept/added to 'basket' + + if filter_status := all(detail_check := filter(detail)) is True: + if debug: + behaviour.stderr.write( + "debug: _generate_line_text: " + f"{str(detail_check):<20} -> {str(filter_status):<5} " + f"-------- '{detail}'\n" + ) + basket.append(detail) - line = line_prefix + seperator.join(basket) + else: # filter function returned False, so element is filtered/skipped + if debug: + behaviour.stderr.write( + "debug: _generate_line_text: " + f"{str(detail_check):<20} -> {str(filter_status):<5}" + f" filtered '{detail}'\n" + ) + continue + line = line_prefix + seperator.join(basket) return (line + "\n") if (line != "") else "" + # iso3166-2 handling: this allows surplus to have special key arrangements for a + # specific iso3166-2 code for edge cases + # (https://en.wikipedia.org/wiki/ISO_3166-2) + + # get iso3166-2 before doing anything + iso3166_2: str = "" + for key in location: + if key.startswith("iso3166"): + iso3166_2 = location.get(key, "") + + # skeleton code to allow for changing keys based on iso3166-2 code + st_line0_keys = SHAREABLE_TEXT_LINE_0_KEYS + st_line1_keys = SHAREABLE_TEXT_LINE_1_KEYS + st_line2_keys = SHAREABLE_TEXT_LINE_2_KEYS + st_line3_keys = SHAREABLE_TEXT_LINE_3_KEYS + st_line4_keys = SHAREABLE_TEXT_LINE_4_KEYS + st_line5_keys = SHAREABLE_TEXT_LINE_5_KEYS + st_line6_keys = SHAREABLE_TEXT_LINE_6_KEYS + st_names = SHAREABLE_TEXT_NAMES + + match iso3166_2.split("-"): + case _: + pass + + # start generating text text: list[str] = [] seen_names: list[str] = [ detail for detail in _unique( - [ - location.get(location_key, "") - for location_key in ( - OUTPUT_LINE_0_KEYS - + OUTPUT_LINE_1_KEYS - + OUTPUT_LINE_2_KEYS - + OUTPUT_LINE_3_NAME - ) - ] + [str(location.get(location_key, "")) for location_key in st_names] ) if detail != "" ] general_global_info: list[str] = [ - location.get(detail, "") for detail in OUTPUT_LINE_6_KEYS + str(location.get(detail, "")) for detail in st_line6_keys ] - text.append(_generate_text_line(0, OUTPUT_LINE_0_KEYS, debug=debug)) - text.append(_generate_text_line(1, OUTPUT_LINE_1_KEYS, debug=debug)) - text.append(_generate_text_line(2, OUTPUT_LINE_2_KEYS, debug=debug)) - text.append(_generate_text_line(3, OUTPUT_LINE_3_KEYS, seperator=" ", debug=debug)) + text.append(_generate_text_line(0, st_line0_keys)) + text.append(_generate_text_line(1, st_line1_keys)) + text.append(_generate_text_line(2, st_line2_keys)) + text.append(_generate_text_line(3, st_line3_keys, seperator=" ")) text.append( _generate_text_line( 4, - OUTPUT_LINE_4_KEYS, - element_check=lambda ak: all( - [ - ak not in general_global_info, - any(True if (ak not in sn) else False for sn in seen_names), - ] - ), - debug=debug, + st_line4_keys, + filter=lambda ak: [ + # everything here should be True if the element is to be kept + ak not in general_global_info, + not any(True if (ak in sn) else False for sn in seen_names), + ], ) ) - text.append(_generate_text_line(5, OUTPUT_LINE_5_KEYS, debug=debug)) - text.append(_generate_text_line(6, OUTPUT_LINE_6_KEYS, debug=debug)) + text.append(_generate_text_line(5, st_line5_keys)) + text.append(_generate_text_line(6, st_line6_keys)) - return "".join(text) + return "".join(_unique(text)).rstrip() def surplus( @@ -852,8 +907,9 @@ def surplus( if behaviour.debug: behaviour.stderr.write(f"debug: {latlong.get()=}\n") + # reverse location and handle result try: - location: dict[str, str] = behaviour.reverser(latlong.get()) + location: dict[str, Any] = behaviour.reverser(latlong.get()) except Exception as exc: return Result[str]("", error=exc) @@ -861,17 +917,21 @@ def surplus( if behaviour.debug: behaviour.stderr.write(f"debug: {location=}\n") + # generate text if behaviour.debug: behaviour.stderr.write( _generate_text( location=location, + behaviour=behaviour, debug=behaviour.debug, ) + + "\n" ) text = _generate_text( location=location, - ).rstrip() + behaviour=behaviour, + ) return Result[str](text) @@ -918,13 +978,13 @@ def cli() -> int: # parse query and handle result query = parse_query(behaviour=behaviour) + if behaviour.debug: + behaviour.stderr.write(f"debug: {query=}\n") + if not query: behaviour.stderr.write(f"error: {query.cry(string=not behaviour.debug)}\n") return -1 - if behaviour.debug: - behaviour.stderr.write(f"debug: {query.get()=}\n") - # run surplus text = surplus( query=query.get(), diff --git a/test.py b/test.py index 1341b8f..af0f2ab 100644 --- a/test.py +++ b/test.py @@ -39,6 +39,7 @@ from typing import Final, NamedTuple import surplus INDENT: Final[int] = 3 +MINIMUM_PASS_RATE: Final[float] = 0.7 # because results can be flaky class ContinuityTest(NamedTuple): @@ -54,10 +55,8 @@ class TestFailure(NamedTuple): tests: list[ContinuityTest] = [ ContinuityTest( - query="8RPQ+JW Singapore", - expected=( - "Caldecott Stn Exit 4\n" "Toa Payoh Link\n" "298106\n" "Central, Singapore" - ), + query="8R3M+F8 Singapore", + expected=("Wisma Atria\n" "435 Orchard Road\n" "238877\n" "Central, Singapore"), ), ContinuityTest( query="9R3J+R9 Singapore", @@ -73,18 +72,32 @@ tests: list[ContinuityTest] = [ query="3RQ3+HW3 Pemping, Batam City, Riau Islands, Indonesia", expected=("Batam\n" "Kepulauan Riau, Indonesia"), ), - # ContinuityTest( - # query="CQPP+QM9 Singapore", - # expected=( - # "Woodlands Integrated Transport Hub\n" "738343\n" "Northwest, Singapore" - # ), - # ), ContinuityTest( - query="8RRX+75Q Singapore", + query="St Lucia, Queensland, Australia G227+XF", expected=( - "Braddell Station/Blk 106\n" - "Lorong 1 Toa Payoh\n" - "319758\n" + "The University of Queensland\n" + "Macquarie Street\n" + "St Lucia, Greater Brisbane\n" + "4072\n" + "Queensland, Australia" + ), + ), + ContinuityTest( + query="Ngee Ann Polytechnic, Singapore", + expected=( + "Ngee Ann Polytechnic\n" + "535 Clementi Road\n" + "Bukit Timah\n" + "599489\n" + "Northwest, Singapore" + ), + ), + ContinuityTest( + query="1.3521, 103.8198", + expected=( + "MacRitchie Nature Trail" + "Central Water Catchment\n" + "574325\n" "Central, Singapore" ), ), @@ -108,29 +121,29 @@ def main() -> int: for idx, test in enumerate(tests, start=1): print(f"[{idx}/{len(tests)}] {test.query}") + output: str = "" + behaviour = surplus.Behaviour(test.query) try: - query = surplus.parse_query(query=test.query) + query = surplus.parse_query(behaviour) - if query[0] is False: - raise QueryParseFailure(f"test query parse result returned False") + if not query: + raise QueryParseFailure(query.cry()) - result = surplus.surplus(query=query[1]) + result = surplus.surplus(query.get(), behaviour) - if result[0] is False: - raise SurplusFailure(result[1]) + if not result: + raise SurplusFailure(result.cry()) - output = result[1] - - print(indent(text=output, prefix=INDENT * " ")) + output = result.get() if output != test.expected: - raise ContinuityFailure(f"test did not match output") + raise ContinuityFailure("did not match output") except Exception as exc: failures.append(TestFailure(test=test, exception=exc, output=output)) - stderr.write(indent(text="(fail)", prefix=INDENT * " ") + "\n") + stderr.write(indent(text="(fail)", prefix=INDENT * " ") + "\n\n") else: stderr.write(indent(text="(pass)", prefix=INDENT * " ") + "\n\n") @@ -153,9 +166,20 @@ def main() -> int: + (indent(text=fail.output, prefix=(2 * INDENT) * " ")) ) - print(f"\ncomplete: {len(tests) - len(failures)} passed, {len(failures)} failed") + passes = len(tests) - len(failures) + pass_rate = round(passes / len(tests), 2) - return len(failures) + print( + f"complete: {passes} passed, {len(failures)} failed " + f"({pass_rate * 100:.0f}%/{pass_rate * 100:.0f}%)" + ) + + if passes < MINIMUM_PASS_RATE: + print("continuity pass rate is under minimum, test suite failed ;<") + return 1 + + print("continuity tests passed :>") + return 0 if __name__ == "__main__": From 592a2c661cef8e7dc2158f3c9dd7421dce9b727d Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:00:54 +0000 Subject: [PATCH 26/41] tests: add check for multiple expected outputs --- test.py | 73 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/test.py b/test.py index af0f2ab..507b078 100644 --- a/test.py +++ b/test.py @@ -35,6 +35,7 @@ from sys import stderr from textwrap import indent from traceback import format_exception from typing import Final, NamedTuple +from io import StringIO import surplus @@ -44,13 +45,14 @@ MINIMUM_PASS_RATE: Final[float] = 0.7 # because results can be flaky class ContinuityTest(NamedTuple): query: str - expected: str + expected: list[str] class TestFailure(NamedTuple): test: ContinuityTest exception: Exception output: str + test_stderr: StringIO tests: list[ContinuityTest] = [ @@ -60,27 +62,41 @@ tests: list[ContinuityTest] = [ ), ContinuityTest( query="9R3J+R9 Singapore", - expected=( - "Thomson Plaza\n" - "301 Upper Thomson Road\n" - "Sin Ming, Bishan\n" - "574408\n" - "Central, Singapore" - ), + expected=[ + ( + "Thomson Plaza\n" + "301 Upper Thomson Road\n" + "Sin Ming, Bishan\n" + "574408\n" + "Central, Singapore" + ) + ], ), ContinuityTest( query="3RQ3+HW3 Pemping, Batam City, Riau Islands, Indonesia", - expected=("Batam\n" "Kepulauan Riau, Indonesia"), + expected=[ + ("Batam\n" "Kepulauan Riau, Indonesia"), + ("Batam\n" "Sumatera, Kepulauan Riau, Indonesia"), + ], ), ContinuityTest( query="St Lucia, Queensland, Australia G227+XF", - expected=( - "The University of Queensland\n" - "Macquarie Street\n" - "St Lucia, Greater Brisbane\n" - "4072\n" - "Queensland, Australia" - ), + expected=[ + ( + "The University of Queensland\n" + "Macquarie Street\n" + "St Lucia, Greater Brisbane\n" + "4072\n" + "Queensland, Australia" + ), + ( + "The University of Queensland\n" + "Macquarie Street\n" + "St Lucia, Greater Brisbane, Dutton Park\n" + "4072\n" + "Queensland, Australia" + ) + ], ), ContinuityTest( query="Ngee Ann Polytechnic, Singapore", @@ -95,7 +111,7 @@ tests: list[ContinuityTest] = [ ContinuityTest( query="1.3521, 103.8198", expected=( - "MacRitchie Nature Trail" + "MacRitchie Nature Trail\n" "Central Water Catchment\n" "574325\n" "Central, Singapore" @@ -122,8 +138,13 @@ def main() -> int: for idx, test in enumerate(tests, start=1): print(f"[{idx}/{len(tests)}] {test.query}") + test_stderr = StringIO() + output: str = "" - behaviour = surplus.Behaviour(test.query) + behaviour = surplus.Behaviour( + test.query, + stderr=test_stderr, + ) try: query = surplus.parse_query(behaviour) @@ -138,11 +159,11 @@ def main() -> int: output = result.get() - if output != test.expected: - raise ContinuityFailure("did not match output") + if output not in test.expected: + raise ContinuityFailure("did not match any expected outputs") except Exception as exc: - failures.append(TestFailure(test=test, exception=exc, output=output)) + failures.append(TestFailure(test=test, exception=exc, output=output, stderr=test_stderr)) stderr.write(indent(text="(fail)", prefix=INDENT * " ") + "\n\n") else: @@ -163,18 +184,20 @@ def main() -> int: + (indent(text=fail.test.expected, prefix=(2 * INDENT) * " ") + "\n\n") + (indent(text="Actual:", prefix=INDENT * " ") + "\n") + (indent(text=repr(fail.output), prefix=(2 * INDENT) * " ") + "\n") - + (indent(text=fail.output, prefix=(2 * INDENT) * " ")) + + (indent(text=fail.output, prefix=(2 * INDENT) * " ") + "\n\n") + + (indent(text="stderr:", prefix=INDENT * " ") + "\n") + + (indent(text=fail.test_stderr.getvalue(), prefix=(2 * INDENT) * " ")) ) passes = len(tests) - len(failures) - pass_rate = round(passes / len(tests), 2) + pass_rate = passes / len(tests) print( f"complete: {passes} passed, {len(failures)} failed " - f"({pass_rate * 100:.0f}%/{pass_rate * 100:.0f}%)" + f"({pass_rate * 100:.0f}%/{MINIMUM_PASS_RATE * 100:.0f}%)" ) - if passes < MINIMUM_PASS_RATE: + if pass_rate < MINIMUM_PASS_RATE: print("continuity pass rate is under minimum, test suite failed ;<") return 1 From af083fb49ac794c15ce123e7490aba57cf753694 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:08:49 +0000 Subject: [PATCH 27/41] tests: fix attempt 0 --- test.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/test.py b/test.py index 507b078..503b6c8 100644 --- a/test.py +++ b/test.py @@ -31,11 +31,11 @@ OTHER DEALINGS IN THE SOFTWARE. For more information, please refer to """ +from io import StringIO from sys import stderr from textwrap import indent from traceback import format_exception from typing import Final, NamedTuple -from io import StringIO import surplus @@ -52,7 +52,7 @@ class TestFailure(NamedTuple): test: ContinuityTest exception: Exception output: str - test_stderr: StringIO + stderr: StringIO tests: list[ContinuityTest] = [ @@ -95,7 +95,7 @@ tests: list[ContinuityTest] = [ "St Lucia, Greater Brisbane, Dutton Park\n" "4072\n" "Queensland, Australia" - ) + ), ], ), ContinuityTest( @@ -163,7 +163,9 @@ def main() -> int: raise ContinuityFailure("did not match any expected outputs") except Exception as exc: - failures.append(TestFailure(test=test, exception=exc, output=output, stderr=test_stderr)) + failures.append( + TestFailure(test=test, exception=exc, output=output, stderr=test_stderr) + ) stderr.write(indent(text="(fail)", prefix=INDENT * " ") + "\n\n") else: @@ -186,7 +188,7 @@ def main() -> int: + (indent(text=repr(fail.output), prefix=(2 * INDENT) * " ") + "\n") + (indent(text=fail.output, prefix=(2 * INDENT) * " ") + "\n\n") + (indent(text="stderr:", prefix=INDENT * " ") + "\n") - + (indent(text=fail.test_stderr.getvalue(), prefix=(2 * INDENT) * " ")) + + (indent(text=fail.stderr.getvalue(), prefix=(2 * INDENT) * " ")) ) passes = len(tests) - len(failures) From 52e23e7ee4fdb60bcec736ead83aec593ed8f6e2 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:16:45 +0000 Subject: [PATCH 28/41] s+: rename notebook to playground --- surplus.future.ipynb => playground.ipynb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename surplus.future.ipynb => playground.ipynb (100%) diff --git a/surplus.future.ipynb b/playground.ipynb similarity index 100% rename from surplus.future.ipynb rename to playground.ipynb From 9d1b0d638fc0bce13a2596691aa5c8dd30b29e4f Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:17:05 +0000 Subject: [PATCH 29/41] tests: fix attempt 1 --- test.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/test.py b/test.py index 503b6c8..8786427 100644 --- a/test.py +++ b/test.py @@ -181,10 +181,17 @@ def main() -> int: indent("\n".join(format_exception(fail.exception)), prefix=INDENT * " ") + "\n" ) - + (indent(text="Expected:", prefix=INDENT * " ") + "\n") - + (indent(text=repr(fail.test.expected), prefix=(2 * INDENT) * " ") + "\n") - + (indent(text=fail.test.expected, prefix=(2 * INDENT) * " ") + "\n\n") - + (indent(text="Actual:", prefix=INDENT * " ") + "\n") + + (indent(text="Expected:", prefix=INDENT * " ")) + ) + + for expected_output in fail.test.expected: + print( + +(indent(text=repr(expected_output), prefix=(2 * INDENT) * " ") + "\n") + + (indent(text=expected_output, prefix=(2 * INDENT) * " ") + "\n") + ) + + print( + +(indent(text="Actual:", prefix=INDENT * " ") + "\n") + (indent(text=repr(fail.output), prefix=(2 * INDENT) * " ") + "\n") + (indent(text=fail.output, prefix=(2 * INDENT) * " ") + "\n\n") + (indent(text="stderr:", prefix=INDENT * " ") + "\n") From 2ee8e4a3f5c1e1e71ded80eeae4bcad33666d63f Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:24:44 +0000 Subject: [PATCH 30/41] tests: fix attempt 2 --- test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test.py b/test.py index 8786427..38aa439 100644 --- a/test.py +++ b/test.py @@ -186,12 +186,12 @@ def main() -> int: for expected_output in fail.test.expected: print( - +(indent(text=repr(expected_output), prefix=(2 * INDENT) * " ") + "\n") + indent(text=repr(expected_output), prefix=(2 * INDENT) * " ") + "\n" + (indent(text=expected_output, prefix=(2 * INDENT) * " ") + "\n") ) print( - +(indent(text="Actual:", prefix=INDENT * " ") + "\n") + indent(text="Actual:", prefix=INDENT * " ") + "\n" + (indent(text=repr(fail.output), prefix=(2 * INDENT) * " ") + "\n") + (indent(text=fail.output, prefix=(2 * INDENT) * " ") + "\n\n") + (indent(text="stderr:", prefix=INDENT * " ") + "\n") From e1013c2de758948a6b367f9a25e9580d71340d0b Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:24:57 +0000 Subject: [PATCH 31/41] s+: add seen_names to debug prints --- surplus.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/surplus.py b/surplus.py index df3160b..98da935 100644 --- a/surplus.py +++ b/surplus.py @@ -475,7 +475,9 @@ class Behaviour(NamedTuple): exceptions are handled by the caller. reverser: Callable[[str], dict[str, Any]] Latlong object to dictionary function, must take in a string and return a - dict. exceptions are handled by the caller. + dict. keys found in SHAREABLE_TEXT_LINE_*_KEYS used to access address details + are placed top-level in the dict. exceptions are handled by the caller. see + the playground notebook for example output. stderr: TextIO = stderr TextIO-like object representing a writeable file. defaults to sys.stderr. stdout: TextIO = stdout @@ -852,6 +854,9 @@ def _generate_text( if detail != "" ] + if debug: + behaviour.stderr.write(f"debug: {seen_names=}\n") + general_global_info: list[str] = [ str(location.get(detail, "")) for detail in st_line6_keys ] From 9549097d6c5fd135854876d888298297af41f266 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:56:13 +0000 Subject: [PATCH 32/41] s+: fix '_generate_line_text' typo --- surplus.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/surplus.py b/surplus.py index df3160b..dfe7acc 100644 --- a/surplus.py +++ b/surplus.py @@ -475,7 +475,9 @@ class Behaviour(NamedTuple): exceptions are handled by the caller. reverser: Callable[[str], dict[str, Any]] Latlong object to dictionary function, must take in a string and return a - dict. exceptions are handled by the caller. + dict. keys found in SHAREABLE_TEXT_LINE_*_KEYS used to access address details + are placed top-level in the dict. exceptions are handled by the caller. see + the playground notebook for example output. stderr: TextIO = stderr TextIO-like object representing a writeable file. defaults to sys.stderr. stdout: TextIO = stdout @@ -798,7 +800,7 @@ def _generate_text( if filter_status := all(detail_check := filter(detail)) is True: if debug: behaviour.stderr.write( - "debug: _generate_line_text: " + "debug: _generate_text_line: " f"{str(detail_check):<20} -> {str(filter_status):<5} " f"-------- '{detail}'\n" ) @@ -808,7 +810,7 @@ def _generate_text( else: # filter function returned False, so element is filtered/skipped if debug: behaviour.stderr.write( - "debug: _generate_line_text: " + "debug: _generate_text_line: " f"{str(detail_check):<20} -> {str(filter_status):<5}" f" filtered '{detail}'\n" ) @@ -852,6 +854,9 @@ def _generate_text( if detail != "" ] + if debug: + behaviour.stderr.write(f"debug: {seen_names=}\n") + general_global_info: list[str] = [ str(location.get(detail, "")) for detail in st_line6_keys ] From f3a90f71e04b55a4e8ceab49740df7af67a8923f Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 10:57:26 +0000 Subject: [PATCH 33/41] tests: fix uq alt output and pass debug=True to s+ --- test.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test.py b/test.py index 38aa439..0b9dab5 100644 --- a/test.py +++ b/test.py @@ -91,7 +91,7 @@ tests: list[ContinuityTest] = [ ), ( "The University of Queensland\n" - "Macquarie Street\n" + "Eleanor Schonell Bridge\n" "St Lucia, Greater Brisbane, Dutton Park\n" "4072\n" "Queensland, Australia" @@ -141,10 +141,7 @@ def main() -> int: test_stderr = StringIO() output: str = "" - behaviour = surplus.Behaviour( - test.query, - stderr=test_stderr, - ) + behaviour = surplus.Behaviour(test.query, stderr=test_stderr, debug=True) try: query = surplus.parse_query(behaviour) @@ -186,12 +183,14 @@ def main() -> int: for expected_output in fail.test.expected: print( - indent(text=repr(expected_output), prefix=(2 * INDENT) * " ") + "\n" + indent(text=repr(expected_output), prefix=(2 * INDENT) * " ") + + "\n" + (indent(text=expected_output, prefix=(2 * INDENT) * " ") + "\n") ) print( - indent(text="Actual:", prefix=INDENT * " ") + "\n" + indent(text="Actual:", prefix=INDENT * " ") + + "\n" + (indent(text=repr(fail.output), prefix=(2 * INDENT) * " ") + "\n") + (indent(text=fail.output, prefix=(2 * INDENT) * " ") + "\n\n") + (indent(text="stderr:", prefix=INDENT * " ") + "\n") From d27b9158557f5706898c77d5c1247d783ced3697 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 11:29:55 +0000 Subject: [PATCH 34/41] ci(qc): do not run if no python files were changed --- .github/workflows/checks.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 34f39dd..6b47ae2 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -3,6 +3,8 @@ name: qc on: workflow_dispatch: push: + paths: + - '**.py' jobs: analyse: From fe48041039162576e3095b5d8c9709a9dc6b4354 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 13:42:21 +0000 Subject: [PATCH 35/41] s+: defaultable Behaviour and only-exception Results --- playground.ipynb | 14 +++--- surplus.py | 113 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 92 insertions(+), 35 deletions(-) diff --git a/playground.ipynb b/playground.ipynb index 9eb924c..06156dd 100644 --- a/playground.ipynb +++ b/playground.ipynb @@ -42,7 +42,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -60,7 +60,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 2, "metadata": {}, "outputs": [ { @@ -68,7 +68,6 @@ "output_type": "stream", "text": [ "True\tNone \t3\n", - "False\t'stest' \t-1\n", "False\tZeroDivisionError('division by zero') \tdivision by zero (ZeroDivisionError)\n" ] }, @@ -79,9 +78,9 @@ "traceback": [ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[0;31mZeroDivisionError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m/home/m/works/surplus/surplus.future.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\u001b[39mbool\u001b[39m(err_result), \u001b[39mrepr\u001b[39m(err_result\u001b[39m.\u001b[39merror), err_result\u001b[39m.\u001b[39mget()))\n\u001b[1;32m 12\u001b[0m \u001b[39mprint\u001b[39m(\n\u001b[1;32m 13\u001b[0m \u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\n\u001b[1;32m 14\u001b[0m \u001b[39mbool\u001b[39m(exc_result), \u001b[39mrepr\u001b[39m(exc_result\u001b[39m.\u001b[39merror), exc_result\u001b[39m.\u001b[39mcry(string\u001b[39m=\u001b[39m\u001b[39mTrue\u001b[39;00m)\n\u001b[1;32m 15\u001b[0m )\n\u001b[1;32m 16\u001b[0m )\n\u001b[0;32m---> 17\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\u001b[39mbool\u001b[39m(exc_result), \u001b[39mrepr\u001b[39m(exc_result\u001b[39m.\u001b[39merror), exc_result\u001b[39m.\u001b[39;49mget()))\n", - "File \u001b[0;32m~/works/surplus/surplus.py:202\u001b[0m, in \u001b[0;36mResult.get\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 200\u001b[0m \u001b[39m\"\"\"method that returns self.value if Result is non-erroneous else raises error\"\"\"\u001b[39;00m\n\u001b[1;32m 201\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39misinstance\u001b[39m(\u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror, \u001b[39mBaseException\u001b[39;00m):\n\u001b[0;32m--> 202\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror\n\u001b[1;32m 204\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mvalue\n", - "\u001b[1;32m/home/m/works/surplus/surplus.future.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 3\u001b[0m err_result \u001b[39m=\u001b[39m Result[\u001b[39mint\u001b[39m](\u001b[39m-\u001b[39m\u001b[39m1\u001b[39m, error\u001b[39m=\u001b[39m\u001b[39m\"\u001b[39m\u001b[39mstest\u001b[39m\u001b[39m\"\u001b[39m)\n\u001b[1;32m 5\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m----> 6\u001b[0m \u001b[39m1\u001b[39;49m \u001b[39m/\u001b[39;49m \u001b[39m0\u001b[39;49m\n\u001b[1;32m 7\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m \u001b[39mas\u001b[39;00m exc:\n\u001b[1;32m 8\u001b[0m exc_result \u001b[39m=\u001b[39m Result[\u001b[39mint\u001b[39m](\u001b[39m-\u001b[39m\u001b[39m1\u001b[39m, error\u001b[39m=\u001b[39mexc)\n", + "\u001b[1;32m/home/m/works/surplus/playground.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\u001b[39mbool\u001b[39m(nom_result), \u001b[39mrepr\u001b[39m(nom_result\u001b[39m.\u001b[39merror), nom_result\u001b[39m.\u001b[39mget()))\n\u001b[1;32m 9\u001b[0m \u001b[39mprint\u001b[39m(\n\u001b[1;32m 10\u001b[0m \u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\n\u001b[1;32m 11\u001b[0m \u001b[39mbool\u001b[39m(exc_result), \u001b[39mrepr\u001b[39m(exc_result\u001b[39m.\u001b[39merror), exc_result\u001b[39m.\u001b[39mcry(string\u001b[39m=\u001b[39m\u001b[39mTrue\u001b[39;00m)\n\u001b[1;32m 12\u001b[0m )\n\u001b[1;32m 13\u001b[0m )\n\u001b[0;32m---> 14\u001b[0m \u001b[39mprint\u001b[39m(\u001b[39m\"\u001b[39m\u001b[39m{}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{:<40}\u001b[39;00m\u001b[39m\\t\u001b[39;00m\u001b[39m{}\u001b[39;00m\u001b[39m\"\u001b[39m\u001b[39m.\u001b[39mformat(\u001b[39mbool\u001b[39m(exc_result), \u001b[39mrepr\u001b[39m(exc_result\u001b[39m.\u001b[39merror), exc_result\u001b[39m.\u001b[39;49mget()))\n", + "File \u001b[0;32m~/works/surplus/surplus.py:247\u001b[0m, in \u001b[0;36mResult.get\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 245\u001b[0m \u001b[39m\"\"\"method that returns self.value if Result is non-erroneous else raises error\"\"\"\u001b[39;00m\n\u001b[1;32m 246\u001b[0m \u001b[39mif\u001b[39;00m \u001b[39misinstance\u001b[39m(\u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror, \u001b[39mBaseException\u001b[39;00m):\n\u001b[0;32m--> 247\u001b[0m \u001b[39mraise\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39merror\n\u001b[1;32m 248\u001b[0m \u001b[39mreturn\u001b[39;00m \u001b[39mself\u001b[39m\u001b[39m.\u001b[39mvalue\n", + "\u001b[1;32m/home/m/works/surplus/playground.ipynb Cell 5\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 1\u001b[0m nom_result \u001b[39m=\u001b[39m Result[\u001b[39mint\u001b[39m](\u001b[39m3\u001b[39m)\n\u001b[1;32m 3\u001b[0m \u001b[39mtry\u001b[39;00m:\n\u001b[0;32m----> 4\u001b[0m \u001b[39m1\u001b[39;49m \u001b[39m/\u001b[39;49m \u001b[39m0\u001b[39;49m\n\u001b[1;32m 5\u001b[0m \u001b[39mexcept\u001b[39;00m \u001b[39mException\u001b[39;00m \u001b[39mas\u001b[39;00m exc:\n\u001b[1;32m 6\u001b[0m exc_result \u001b[39m=\u001b[39m Result[\u001b[39mint\u001b[39m](\u001b[39m-\u001b[39m\u001b[39m1\u001b[39m, error\u001b[39m=\u001b[39mexc)\n", "\u001b[0;31mZeroDivisionError\u001b[0m: division by zero" ] } @@ -89,15 +88,12 @@ "source": [ "nom_result = Result[int](3)\n", "\n", - "err_result = Result[int](-1, error=\"stest\")\n", - "\n", "try:\n", " 1 / 0\n", "except Exception as exc:\n", " exc_result = Result[int](-1, error=exc)\n", "\n", "print(\"{}\\t{:<40}\\t{}\".format(bool(nom_result), repr(nom_result.error), nom_result.get()))\n", - "print(\"{}\\t{:<40}\\t{}\".format(bool(err_result), repr(err_result.error), err_result.get()))\n", "print(\n", " \"{}\\t{:<40}\\t{}\".format(\n", " bool(exc_result), repr(exc_result.error), exc_result.cry(string=True)\n", diff --git a/surplus.py b/surplus.py index dfe7acc..d2b5a31 100644 --- a/surplus.py +++ b/surplus.py @@ -123,11 +123,33 @@ SHAREABLE_TEXT_NAMES: Final[tuple[str, ...]] = ( # exceptions -class NoSuitableLocationError(Exception): +class SurplusException(Exception): + """base skeleton exception for handling and typing surplus exception classes""" + ... -class IncompletePlusCodeError(Exception): +class NoSuitableLocationError(SurplusException): + ... + + +class IncompletePlusCodeError(SurplusException): + ... + + +class PlusCodeNotFoundError(SurplusException): + ... + + +class LatlongParseError(SurplusException): + ... + + +class EmptyQueryError(SurplusException): + ... + + +class UnavailableFeatureError(SurplusException): ... @@ -135,7 +157,15 @@ class IncompletePlusCodeError(Exception): class ConversionResultTypeEnum(Enum): - """enum representing what the result type of conversion should be""" + """ + enum representing what the result type of conversion should be + + values + PLUS_CODE: str = "pluscode" + LOCAL_CODE: str = "localcode" + LATLONG: str = "latlong" + SHAREABLE_TEXT: str = "shareabletext" + """ PLUS_CODE = "pluscode" LOCAL_CODE = "localcode" @@ -153,8 +183,8 @@ class Result(NamedTuple, Generic[ResultType]): arguments value: ResultType value to return or fallback value if erroneous - error: BaseException | str | None = None - exception if any, or an error message + error: BaseException | None = None + exception if any methods def __bool__(self) -> bool: ... @@ -181,7 +211,7 @@ class Result(NamedTuple, Generic[ResultType]): """ value: ResultType - error: BaseException | str | None = None + error: BaseException | None = None def __bool__(self) -> bool: """method that returns True if self.error is not None""" @@ -356,7 +386,7 @@ class LocalCodeQuery(NamedTuple): return Result[Latlong]( PlusCodeQuery(recovered_pluscode.get()) .to_lat_long_coord(geocoder=geocoder) - .get() # PlusCodeQuery can get latlong coord offline, so no need to handle + .get() # PlusCodeQuery can get latlong coord safely, so no need to handle ) @@ -467,13 +497,13 @@ class Behaviour(NamedTuple): typing.NamedTuple representing expected behaviour of surplus arguments - query: str | list[str] + query: str | list[str] = "" str: original user-passed query string list[str]: original user-passed query string split by spaces - geocoder: Callable[[str], Latlong] + geocoder: Callable[[str], Latlong] = default_geocoder name string to location function, must take in a string and return a Latlong. exceptions are handled by the caller. - reverser: Callable[[str], dict[str, Any]] + reverser: Callable[[str], dict[str, Any]] = default_reverser Latlong object to dictionary function, must take in a string and return a dict. keys found in SHAREABLE_TEXT_LINE_*_KEYS used to access address details are placed top-level in the dict. exceptions are handled by the caller. see @@ -490,7 +520,7 @@ class Behaviour(NamedTuple): what type to convert query to """ - query: str | list[str] + query: str | list[str] = "" geocoder: Callable[[str], Latlong] = default_geocoder reverser: Callable[[Latlong], dict[str, Any]] = default_reverser stderr: TextIO = stderr @@ -532,14 +562,14 @@ def parse_query( original_query: str = "" split_query: list[str] = [] - if isinstance(behaviour.query, str): - original_query = behaviour.query - split_query = behaviour.query.split(" ") - - else: + if isinstance(behaviour.query, list): original_query = " ".join(behaviour.query) split_query = behaviour.query + else: + original_query = str(behaviour.query) + split_query = behaviour.query.split(" ") + for word in split_query: if validator.is_valid(word): portion_plus_code = word @@ -553,7 +583,7 @@ def parse_query( if portion_plus_code == "": return Result[Query]( LatlongQuery(EMPTY_LATLONG), - error="unable to find a Plus Code", + error=PlusCodeNotFoundError("unable to find a Plus Code"), ) # found a plus code! @@ -601,7 +631,7 @@ def parse_query( if (behaviour.query == []) or (behaviour.query == ""): return Result[Query]( LatlongQuery(EMPTY_LATLONG), - error="query is empty", + error=EmptyQueryError("behaviour.query is empty"), ) # try to find a plus/local code @@ -644,7 +674,7 @@ def parse_query( if len(comma_split_single) > 2: return Result[Query]( LatlongQuery(EMPTY_LATLONG), - error="unable to parse latlong coord", + error=LatlongParseError("unable to parse latlong coord"), ) try: # try to type cast query @@ -883,21 +913,41 @@ def _generate_text( def surplus( - query: PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery, + query: Query | str, behaviour: Behaviour, ) -> Result[str]: """ query to shareable text conversion function - query: PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery - query object to convert, see respective docstrings for more information on each - type of query object + query: Query | str + Query: query object to convert, see respective docstrings for more information on + each type of query object + str: string to attempt to query for behaviour: Behaviour program behaviour namedtuple returns Result[str] """ + if not isinstance(query, (PlusCodeQuery, LocalCodeQuery, LatlongQuery, StringQuery)): + query_result = parse_query( + behaviour=Behaviour( + query=str(query), + geocoder=behaviour.geocoder, + reverser=behaviour.reverser, + stderr=behaviour.stderr, + stdout=behaviour.stdout, + debug=behaviour.debug, + version_header=behaviour.version_header, + convert_to_type=behaviour.convert_to_type, + ) + ) + + if not query_result: + return Result[str]("", error=query_result.error) + + query = query_result.get() + # operate on query text: str = "" @@ -943,18 +993,29 @@ def surplus( case ConversionResultTypeEnum.PLUS_CODE: # TODO: https://github.com/markjoshwel/surplus/issues/18 return Result[str]( - text, error="converting to Plus Code is not implemented yet" + text, + error=UnavailableFeatureError( + "converting to Plus Code is not implemented yet" + ), ) case ConversionResultTypeEnum.LOCAL_CODE: # TODO: https://github.com/markjoshwel/surplus/issues/18 return Result[str]( - text, error="converting to Plus Code is not implemented yet" + text, + error=UnavailableFeatureError( + "converting to Plus Code is not implemented yet" + ), ) case ConversionResultTypeEnum.LATLONG: # TODO: https://github.com/markjoshwel/surplus/issues/18 - return Result[str](text, error="converting to Latlong is not implemented yet") + return Result[str]( + text, + error=UnavailableFeatureError( + "converting to Latlong is not implemented yet" + ), + ) case _: return Result[str]( From 883a8ee5287b58dfaa0f599fb69dac8538977e3f Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 13:42:34 +0000 Subject: [PATCH 36/41] deps: update requirements.txt --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 29a14d2..f058c34 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -geographiclib==2.0 ; python_version >= "3.10" and python_version < "4.0" \ +geographiclib==2.0 ; python_version >= "3.11" and python_version < "4.0" \ --hash=sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734 \ --hash=sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859 -geopy==2.4.0 ; python_version >= "3.10" and python_version < "4.0" \ +geopy==2.4.0 ; python_version >= "3.11" and python_version < "4.0" \ --hash=sha256:a59392bf17adb486b25dbdd71fbed27733bdf24a2dac588047a619de56695e36 \ --hash=sha256:d2639a46d0ce4c091e9688b750ba94348a14b898a1e55c68f4b4a07e7d1afa20 -pluscodes==2022.1.3 ; python_version >= "3.10" and python_version < "4.0" \ +pluscodes==2022.1.3 ; python_version >= "3.11" and python_version < "4.0" \ --hash=sha256:50625f472f8d4e8822e005180c2eb41bf09e45e429f362d3cded346f1169dae8 From 901fb96b0fdf09cd4ec07a2ab6164fdb05001c3c Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 15:23:12 +0000 Subject: [PATCH 37/41] s+: use 'sharetext' for enum value --- surplus.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/surplus.py b/surplus.py index d2b5a31..dd64550 100644 --- a/surplus.py +++ b/surplus.py @@ -164,13 +164,13 @@ class ConversionResultTypeEnum(Enum): PLUS_CODE: str = "pluscode" LOCAL_CODE: str = "localcode" LATLONG: str = "latlong" - SHAREABLE_TEXT: str = "shareabletext" + SHAREABLE_TEXT: str = "sharetext" """ PLUS_CODE = "pluscode" LOCAL_CODE = "localcode" LATLONG = "latlong" - SHAREABLE_TEXT = "shareabletext" + SHAREABLE_TEXT = "sharetext" ResultType = TypeVar("ResultType") From aaa1747517f053828506a50be19a6a5a68b1da04 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 17:03:23 +0000 Subject: [PATCH 38/41] meta: switch to directory layout for py.typed i dont like it, but that's how it'll have to be https://github.com/python/typing/issues/1333 s+: also, add __str__ support for query types (#18) --- .github/workflows/checks.yml | 6 +-- poetry.lock | 18 ++++----- pyproject.toml | 13 ++++-- surplus/__init__.py | 69 ++++++++++++++++++++++++++++++++ surplus/py.typed | 0 surplus.py => surplus/surplus.py | 18 ++++++++- 6 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 surplus/__init__.py create mode 100644 surplus/py.typed rename surplus.py => surplus/surplus.py (98%) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 6b47ae2..b41378d 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,13 +24,13 @@ jobs: run: devbox run poetry build - name: analyse with mypy - run: devbox run poetry run mypy surplus.py test.py + run: devbox run poetry run mypy surplus.py **/*.py - name: check for black formatting compliance - run: devbox run poetry run "black --check surplus.py test.py" + run: devbox run poetry run "black --check surplus.py **/*.py" - name: analyse isort compliance - run: devbox run poetry run "isort --check surplus.py test.py" + run: devbox run poetry run "isort --check **/*.py" test: runs-on: ubuntu-latest diff --git a/poetry.lock b/poetry.lock index 4f2aa86..9debda9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -14,18 +14,18 @@ files = [ [[package]] name = "asttokens" -version = "2.2.1" +version = "2.3.0" description = "Annotate AST trees with source code positions" category = "dev" optional = false python-versions = "*" files = [ - {file = "asttokens-2.2.1-py2.py3-none-any.whl", hash = "sha256:6b0ac9e93fb0335014d382b8fa9b3afa7df546984258005da0b9e7095b3deb1c"}, - {file = "asttokens-2.2.1.tar.gz", hash = "sha256:4622110b2a6f30b77e1473affaa97e711bc2f07d3f10848420ff1898edbe94f3"}, + {file = "asttokens-2.3.0-py2.py3-none-any.whl", hash = "sha256:bef1a51bc256d349e9f94e7e40e44b705ed1162f55294220dd561d24583d9877"}, + {file = "asttokens-2.3.0.tar.gz", hash = "sha256:2552a88626aaa7f0f299f871479fc755bd4e7c11e89078965e928fb7bb9a6afe"}, ] [package.dependencies] -six = "*" +six = ">=1.12.0" [package.extras] test = ["astroid", "pytest"] @@ -181,14 +181,14 @@ timezone = ["pytz"] [[package]] name = "ipython" -version = "8.14.0" +version = "8.15.0" description = "IPython: Productive Interactive Computing" category = "dev" optional = false python-versions = ">=3.9" files = [ - {file = "ipython-8.14.0-py3-none-any.whl", hash = "sha256:248aca623f5c99a6635bc3857677b7320b9b8039f99f070ee0d20a5ca5a8e6bf"}, - {file = "ipython-8.14.0.tar.gz", hash = "sha256:1d197b907b6ba441b692c48cf2a3a2de280dc0ac91a3405b39349a50272ca0a1"}, + {file = "ipython-8.15.0-py3-none-any.whl", hash = "sha256:45a2c3a529296870a97b7de34eda4a31bee16bc7bf954e07d39abe49caf8f887"}, + {file = "ipython-8.15.0.tar.gz", hash = "sha256:2baeb5be6949eeebf532150f81746f8333e2ccce02de1c7eedde3f23ed5e9f1e"}, ] [package.dependencies] @@ -206,9 +206,9 @@ stack-data = "*" traitlets = ">=5" [package.extras] -all = ["black", "curio", "docrepr", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] +all = ["black", "curio", "docrepr", "exceptiongroup", "ipykernel", "ipyparallel", "ipywidgets", "matplotlib", "matplotlib (!=3.2.0)", "nbconvert", "nbformat", "notebook", "numpy (>=1.21)", "pandas", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "qtconsole", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "trio", "typing-extensions"] black = ["black"] -doc = ["docrepr", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "ipykernel", "matplotlib", "pytest (<7)", "pytest (<7.1)", "pytest-asyncio", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "stack-data", "testpath", "typing-extensions"] kernel = ["ipykernel"] nbconvert = ["nbconvert"] nbformat = ["nbformat"] diff --git a/pyproject.toml b/pyproject.toml index 550c371..3dc3675 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,9 +3,13 @@ name = "surplus" version = "2.0.0" description = "Python script to convert Google Maps Plus Codes to iOS Shortcuts-like shareable text." authors = ["Mark Joshwel "] -license = "Unlicence" +license = "Unlicense" readme = "README.md" -include = ["surplus.py"] +repository = "https://github.com/markjoshwel/surplus" +keywords = ["pluscodes", "openlocationcode"] +packages = [ + {include = "surplus"} +] [tool.poetry.dependencies] python = "^3.11" @@ -28,6 +32,9 @@ line-length = 90 line_length = 90 profile = "black" +[tool.setuptools.package-data] +"*" = ["py.typed"] + [build-system] -requires = ["poetry-core"] +requires = ["poetry-core",] build-backend = "poetry.core.masonry.api" diff --git a/surplus/__init__.py b/surplus/__init__.py new file mode 100644 index 0000000..c1983a1 --- /dev/null +++ b/surplus/__init__.py @@ -0,0 +1,69 @@ +""" +surplus: Google Maps Plus Code to iOS Shortcuts-like shareable text +------------------------------------------------------------------- +by mark and contributors + +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to +""" + +# surplus was and would've been a single-file module, but typing is in the way :( +# https://github.com/python/typing/issues/1333 + +from .surplus import ( + EMPTY_LATLONG, + SHAREABLE_TEXT_LINE_0_KEYS, + SHAREABLE_TEXT_LINE_1_KEYS, + SHAREABLE_TEXT_LINE_2_KEYS, + SHAREABLE_TEXT_LINE_3_KEYS, + SHAREABLE_TEXT_LINE_4_KEYS, + SHAREABLE_TEXT_LINE_5_KEYS, + SHAREABLE_TEXT_LINE_6_KEYS, + SHAREABLE_TEXT_NAMES, + USER_AGENT, + VERSION, + Behaviour, + ConversionResultTypeEnum, + EmptyQueryError, + IncompletePlusCodeError, + Latlong, + LatlongParseError, + LatlongQuery, + LocalCodeQuery, + NoSuitableLocationError, + PlusCodeNotFoundError, + PlusCodeQuery, + Query, + ResultType, + StringQuery, + SurplusException, + UnavailableFeatureError, + cli, + default_geocoder, + default_reverser, + handle_args, + parse_query, + surplus, +) diff --git a/surplus/py.typed b/surplus/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/surplus.py b/surplus/surplus.py similarity index 98% rename from surplus.py rename to surplus/surplus.py index dd64550..7420d19 100644 --- a/surplus.py +++ b/surplus/surplus.py @@ -283,6 +283,7 @@ class PlusCodeQuery(NamedTuple): methods def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + def __str__(self) -> str: ... """ code: str @@ -321,6 +322,9 @@ class PlusCodeQuery(NamedTuple): return Result[Latlong](Latlong(latitude=latitude, longitude=longitude)) + def __str__(self) -> str: + return f"{self.code}" + class LocalCodeQuery(NamedTuple): """ @@ -335,6 +339,7 @@ class LocalCodeQuery(NamedTuple): methods def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + def __str__(self) -> str: ... """ code: str @@ -389,6 +394,9 @@ class LocalCodeQuery(NamedTuple): .get() # PlusCodeQuery can get latlong coord safely, so no need to handle ) + def __str__(self) -> str: + return f"{self.code} {self.locality}" + class LatlongQuery(NamedTuple): """ @@ -399,6 +407,7 @@ class LatlongQuery(NamedTuple): methods def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + def __str__(self) -> str: ... """ latlong: Latlong @@ -417,6 +426,9 @@ class LatlongQuery(NamedTuple): return Result[Latlong](self.latlong) + def __str__(self) -> str: + return f"{self.latlong.latitude}, {self.latlong.longitude}" + class StringQuery(NamedTuple): """ @@ -427,6 +439,7 @@ class StringQuery(NamedTuple): methods def to_lat_long_coord(self, ...) -> Result[Latlong]: ... + def __str__(self) -> str: ... """ query: str @@ -449,6 +462,9 @@ class StringQuery(NamedTuple): except Exception as exc: return Result[Latlong](EMPTY_LATLONG, error=exc) + def __str__(self) -> str: + return self.query + Query: TypeAlias = PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery @@ -631,7 +647,7 @@ def parse_query( if (behaviour.query == []) or (behaviour.query == ""): return Result[Query]( LatlongQuery(EMPTY_LATLONG), - error=EmptyQueryError("behaviour.query is empty"), + error=EmptyQueryError("empty query string passed"), ) # try to find a plus/local code From 6591d9f47644c34a5dee746d1c28b31ba022a70b Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 17:41:32 +0000 Subject: [PATCH 39/41] ci(qc): fix analyse job invocations --- .github/workflows/checks.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index b41378d..05ea57c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,10 +24,10 @@ jobs: run: devbox run poetry build - name: analyse with mypy - run: devbox run poetry run mypy surplus.py **/*.py + run: devbox run poetry run mypy **/*.py - name: check for black formatting compliance - run: devbox run poetry run "black --check surplus.py **/*.py" + run: devbox run poetry run "black --check **/*.py" - name: analyse isort compliance run: devbox run poetry run "isort --check **/*.py" From 35f19b8bc969c125ee7524fc1f082e83e92652cc Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sat, 2 Sep 2023 20:05:37 +0000 Subject: [PATCH 40/41] docs,s+: add api documentation and update docstrings --- README.md | 611 +++++++++++++++++++++++++++++++++++++++++++-- surplus/surplus.py | 104 ++++---- 2 files changed, 652 insertions(+), 63 deletions(-) diff --git a/README.md b/README.md index 35e0fdc..bdeaebc 100644 --- a/README.md +++ b/README.md @@ -18,25 +18,26 @@ surplus is a Python script to convert to iOS Shortcuts-like shareable text. - [installation](#installation) -- [command-line usage](#command-line-usaage) +- [usage](#usage) + - [command-line usage](#command-line-usaage) + - [example api usage](#example-api-usage) - [developer's guide](#developers-guide) - - [api reference](#api-reference) - [contributor's guide](#contributors-guide) - [reporting incorrect output](#reporting-incorrect-output) - [the reporting process](#the-reporting-process) - [what counts as "incorrect"](#what-counts-as-incorrect) - [output technical details](#the-technical-details-of-surpluss-output) +- [api reference](#api-reference) - [licence](#licence) ```text $ surplus 9R3J+R9 Singapore -TODO CLI DEMO -``` - -```python ->>> from surplus import surplus, Localcode ->>> Localcode(code="8RPQ+JW", locality="Singapore").full_length() -TODO API DEMO +surplus version 2.0.0 +Thomson Plaza +301 Upper Thomson Road +Sin Ming, Bishan +574408 +Central, Singapore ``` ## installation @@ -51,7 +52,9 @@ install surplus directly from the repository using pip: pip install git+https://github.com/markjoshwel/surplus.git@future ``` -## command-line usage +## usage + +### command-line usage ```text usage: surplus [-h] [-d] [-v] [-c {pluscode,localcode,latlong,string}] @@ -70,12 +73,57 @@ options: -d, --debug prints lat, long and reverser response dict to stderr -v, --version prints version information to stderr and exits - -c {pluscode,localcode,latlong,shareabletext}, - --convert-to {pluscode,localcode,latlong,shareabletext} + -c {pluscode,localcode,latlong,sharetext}, + --convert-to {pluscode,localcode,latlong,sharetext} converts query a specific output type, defaults - to 'shareabletext' + to 'sharetext' ``` +### example api usage + +here are a few examples to get you quickly started using surplus in your own program: + +1. let surplus do the heavy lifiting + + ```python + >>> from surplus import surplus, Behaviour + >>> result = surplus("Ngee Ann Polytechnic, Singapore", Behaviour()) + >>> result.get() + 'Ngee Ann Polytechnic\n535 Clementi Road\nBukit Timah\n599489\nNorthwest, Singapore' + ``` + +2. handle queries seperately + + ```python + >>> import surplus + >>> behaviour = surplus.Behaviour("6PH58R3M+F8") + >>> query = surplus.parse_query(behaviour) + >>> result = surplus.surplus(query.get(), behaviour) + >>> result.get() + 'MacRitchie Nature Trail\nCentral Water Catchment\n574325\nCentral, Singapore' + ``` + +3. start from a Query object + + ```python + >>> import surplus + >>> localcode = surplus.LocalCodeQuery(code="8R3M+F8", locality="Singapore") + >>> pluscode_str = localcode.to_full_plus_code(geocoder=surplus.default_geocoder).get() + >>> pluscode = surplus.PlusCodeQuery(pluscode_str) + >>> result = surplus.surplus(pluscode, surplus.Behaviour()) + >>> result.get() + 'Wisma Atria\n435 Orchard Road\n238877\nCentral, Singapore' + ``` + +notes: + +- you can change what surplus does by passing in a custom `Behaviour` object. + +- most surplus functions return a `Result` object. while you can `.get()` the Result to + obtain the proper return value, this is dangerous and might raise an exception. + +see the [api reference](#api-reference) for more information. + ## developer's guide prerequisites: @@ -91,6 +139,8 @@ poetry install poetry shell ``` +for information on surplus's exposed api, see the [api reference](#api-reference). + ## contributor's guide 1. fork the repository and branch off from the `future` branch @@ -203,6 +253,10 @@ of incorrect outputs. ## the technical details of surplus's output +> **Note** +> this is a breakdown of surplus's output when converting to shareable text. +> when converting to other output types, n + ``` $ s+ --debug 8QJF+RP Singapore surplus version 2.0.0, debug mode @@ -256,7 +310,7 @@ variables represents the plus code and locality portions of a [shortened plus code](https://en.wikipedia.org/wiki/Open_Location_Code#Common_usage_and_shortening) - (_referred to as a "short/local code" in the codebase_) respectively. + (_referred to as a "local code" in the codebase_) respectively. - **variable `query`** @@ -458,7 +512,534 @@ breakdown of each output line, accompanied by their nominatim key: ## api reference -TODO API REF +- [constants](#constants) +- [exception classes](#exception-classes) +- [types](#types) +- [`class Behaviour`](#class-behaviour) +- [`class ConversionResultTypeEnum`](#class-conversionresulttypeenum) +- [`class Result`](#class-result) + - [`Result.__bool__()`](#result__bool__) + - [`Result.cry()`](#resultcry) + - [`Result.get()`](#resultget) +- [`class Latlong`](#class-latlong) +- [`class PlusCodeQuery`](#class-pluscodequery) +- [`class LocalCodeQuery`](#class-localcodequery) +- [`class LatlongQuery`](#class-latlongquery) +- [`class StringQuery`](#class-stringquery) +- [`def surplus()`](#def-surplus) +- [`def parse_query()`](#def-parse_query) +- [`def default_geocoder()`](#def-default_geocoder) +- [`def default_reverser()`](#def-default_reverser) + +### constants + +- `VERSION: tuple[int, int, int]` + + a tuple of integers representing the version of surplus, in the format + `[major, minor, patch]` + +- `SHAREABLE_TEXT_LINE_0_KEYS: tuple[str, ...]` + `SHAREABLE_TEXT_LINE_1_KEYS: tuple[str, ...]` + `SHAREABLE_TEXT_LINE_2_KEYS: tuple[str, ...]` + `SHAREABLE_TEXT_LINE_3_KEYS: tuple[str, ...]` + `SHAREABLE_TEXT_LINE_4_KEYS: tuple[str, ...]` + `SHAREABLE_TEXT_LINE_5_KEYS: tuple[str, ...]` + `SHAREABLE_TEXT_LINE_6_KEYS: tuple[str, ...]` + + a tuple of strings containing nominatim keys used in shareable text line 0-6 + +- `SHAREABLE_TEXT_NAMES: tuple[str, ...]` + + a tuple of strings containing nominatim keys used in shareable text line 0-2 and + special keys in line 3 + +- `EMPTY_LATLONG: Latlong` + a constant for an empty latlong coordinate, with latitude and longitude set to 0.0 + +### exception classes + +- `class SurplusException(Exception)` + base skeleton exception for handling and typing surplus exception classes +- `class NoSuitableLocationError(SurplusException)` +- `class IncompletePlusCodeError(SurplusException)` +- `class PlusCodeNotFoundError(SurplusException)` +- `class LatlongParseError(SurplusException)` +- `class EmptyQueryError(SurplusException)` +- `class UnavailableFeatureError(SurplusException)` + +### types + +#### `Query` + +```python +Query: typing.TypeAlias = PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery +``` + +[type alias](https://docs.python.org/3/library/typing.html#type-aliases) representing +either a +[`PlusCodeQuery`](#class-pluscodequery), +[`LocalCodeQuery`](#class-localcodequery), +[`LatlongQuery`](#class-latlongquery) or +[`StringQuery`](#class-stringquery) + +#### `ResultType` + +```python +ResultType = TypeVar("ResultType") +``` + +[generic type](https://docs.python.org/3/library/typing.html#generics) used by +[`Result`](#class-result) + +### `class Behaviour` + +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +representing how surplus operations should behave + +attributes + +- `query: str | list[str] = ""` + original user-passed query string or a list of strings from splitting user-passed query + string by spaces + +- `geocoder: typing.Callable[[str], Latlong] = default_geocoder` + name string to location function, must take in a string and return a + [`Latlong`](#class-latlong), exceptions are handled by the caller + +- `reverser: Callable[[Latlong], dict[str, Any]] = default_reverser` + [`Latlong`] object to dictionary function, must take in a string and return a dict. + keys found in SHAREABLE_TEXT_LINE_*_KEYS used to access address details are placed + top-level in the dict, exceptions are handled by the caller. + see the [playground notebook](playground.ipynb) for example output + +- `stderr: typing.TextIO = sys.stderr` + [TextIO-like object](https://docs.python.org/3/library/io.html#text-i-o) + representing a writeable file. + defaults to [`sys.stderr`](https://docs.python.org/3/library/sys.html#sys.stderr). + +- `stdout: typing.TextIO = sys.stdout` + [TextIO-like object](https://docs.python.org/3/library/io.html#text-i-o) + representing a writeable file. + defaults to [`sys.stdout`](https://docs.python.org/3/library/sys.html#sys.stdout). + +- `debug: bool = False` + whether to print debug information to stderr + +- `version_header: bool = False` + whether to print version information and exit + +- `convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT` + what type to convert the query to + +### `class ConversionResultTypeEnum` + +[enum.Enum](https://docs.python.org/3/library/enum.html) +representing what the result type of conversion should be + +values + +- `PLUS_CODE: str = "pluscode"` +- `LOCAL_CODE: str = "localcode"` +- `LATLONG: str = "latlong"` +- `SHAREABLE_TEXT: str = "sharetext"` + +### `class Result` + +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +representing the result for safe value retrieval + +attributes + +- `value: ResultType` + value to return or fallback value if erroneous + +- `error: BaseException | None = None` + exception if any + +example usage + +```python +# do something +def some_operation(path) -> Result[str]: + try: + file = open(path) + contents = file.read() + + except Exception as exc: + # must pass a default value + result = Result[str]("", error=exc) + + else: + result = Result[str](contents) + +# call function and handle result +result = some_operation("some_file.txt") + +if not result: # check if the result is erroneous + # .cry() raises the exception + # (or returns it as a string error message using string=True) + result.cry() + ... + +else: + # .get() raises exception or returns value, + # but since we checked for errors this is safe + print(result.get()) +``` + +methods + +- [`def __bool__(self) -> bool: ...`](#result__bool__) +- [`def get(self) -> ResultType: ...`](#resultcry) +- [`def cry(self, string: bool = False) -> str: ...`](#resultget) + +#### `Result.__bool__()` + +method that returns `True` if `self.error` is not `None` + +- signature + + ```python + def __bool__(self) -> bool: ... + ``` + +- returns `bool` + +#### `Result.cry()` + +method that raises `self.error` if is an instance of `BaseException`, returns +`self.error` if is an instance of str, or returns an empty string if `self.error` is None + +- signature + + ```python + def cry(self, string: bool = False) -> str: ... + ``` + +- arguments + + - `string: bool = False` + if `self.error` is an Exception, returns it as a string error message + +- returns `str` + +#### `Result.get()` + +method that returns `self.value` if Result is non-erroneous else raises error + +- signature + + ```python + def get(self) -> ResultType: ... + ``` + +- returns `self.value` + +### `class Latlong` + +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +representing a latitude-longitude coordinate pair + +attributes + +- `latitude: float` +- `longitude: float` + +methods + +- [`def __str__(self) -> str: ...`](#latlong__str__) + +#### `Latlong.__str__()` + +method that returns a comma-and-space-seperated string of `self.latitude` and +`self.longitude` + +- signature + + ```python + def __str__(self) -> str: ... + ``` + +- returns `str` + +### `class PlusCodeQuery` + +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +representing a full-length Plus Code (e.g., 6PH58QMF+FX) + +attributes + +- `code: str` + +methods + +- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#pluscodequeryto_lat_long_coord) +- [`def __str__(self) -> str: ...`](#pluscodequery__str__) + +#### `PlusCodeQuery.to_lat_long_coord()` + +- signature + + ```python + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + ... + ``` + +- arguments + + - `geocoder: typing.Callable[[str], Latlong]` + name string to location function, must take in a string and return a + [`Latlong`](#class-latlong), exceptions are handled by the caller + +- returns [`Result`](#class-result)[`[Latlong]`](#class-latlong) + +#### `PlusCodeQuery.__str__()` + +method that returns string representation of query + +- signature + + ```python + def __str__(self) -> str: ... + ``` + +- returns `str` + +### `class LocalCodeQuery` + +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +representing a +[shortened Plus Code](https://en.wikipedia.org/wiki/Open_Location_Code#Common_usage_and_shortening) +with locality, referred to by surplus as a "local code" + +attributes + +- `code: str` + Plus Code portion of local code, e.g., "8QMF+FX" + +- `locality: str` + remaining string of local code, e.g., "Singapore" + +methods + +- [`def to_full_plus_code(self, ...) -> Result[str]: ...`](#localcodequeryto_full_plus_code) +- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#localcodequeryto_lat_long_coord) +- [`def __str__(self) -> str: ...`](#localcodequery__str__) + +#### `LocalCodeQuery.to_full_plus_code()` + +exclusive method that returns a full-length Plus Code as a string + +- signature + + ```python + def to_full_plus_code(self, geocoder: Callable[[str], Latlong]) -> Result[str]: + ... + ``` + +- arguments + + - `geocoder: typing.Callable[[str], Latlong]` + name string to location function, must take in a string and return a + [`Latlong`](#class-latlong), exceptions are handled by the caller + +- returns [`Result`](#class-result)`[str]` + +#### `LocalCodeQuery.to_lat_long_coord()` + +method that returns a latitude-longitude coordinate pair + +- signature + + ```python + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + ... + ``` + +- arguments + + - `geocoder: typing.Callable[[str], Latlong]` + name string to location function, must take in a string and return a + [`Latlong`](#class-latlong), exceptions are handled by the caller + +- returns [`Result`](#class-result)[`[Latlong]`](#class-latlong) + +#### `LocalCodeQuery.__str__()` + +method that returns string representation of query + +- signature + + ```python + def __str__(self) -> str: ... + ``` + +- returns `str` + +### `class LatlongQuery` + +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +representing a latitude-longitude coordinate pair + +attributes + +- `latlong: Latlong` + +methods + +- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#latlongqueryto_lat_long_coord) +- [`def __str__(self) -> str: ...`](#latlongquery__str__) + +#### `LatlongQuery.to_lat_long_coord()` + +method that returns a latitude-longitude coordinate pair + +- signature + + ```python + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + ... + ``` + +- arguments + + - `geocoder: typing.Callable[[str], Latlong]` + name string to location function, must take in a string and return a + [`Latlong`](#class-latlong), exceptions are handled by the caller + +- returns [`Result`](#class-result)[`[Latlong]`](#class-latlong) + +#### `LatlongQuery.__str__()` + +method that returns string representation of query + +- signature + + ```python + def __str__(self) -> str: ... + ``` + +- returns `str` + +### `class StringQuery` + +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +representing a pure string query + +attributes + +- `query: str` + +methods + +- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#latlongqueryto_lat_long_coord) +- [`def __str__(self) -> str: ...`](#latlongquery__str__) + +#### `LatlongQuery.to_lat_long_coord()` + +method that returns a latitude-longitude coordinate pair + +- signature + + ```python + def to_lat_long_coord(self, geocoder: Callable[[str], Latlong]) -> Result[Latlong]: + ... + ``` + +- arguments + + - `geocoder: typing.Callable[[str], Latlong]` + name string to location function, must take in a string and return a + [`Latlong`](#class-latlong), exceptions are handled by the caller + +- returns [`Result`](#class-result)[`[Latlong]`](#class-latlong) + +#### `LatlongQuery.__str__()` + +method that returns string representation of query + +- signature + + ```python + def __str__(self) -> str: ... + ``` + +- returns `str` + +### `def surplus()` + +query to shareable text conversion function + +- signature + + ```python + def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: .. + ``` + +- arguments + + - `query: str | Query` + [query object](#query) to convert or string to attempt to query for then convert + + - `behaviour: Behaviour` + [surplus behaviour namedtuple](#class-behaviour) + +- returns [`Result`](#class-result)`[str]` + +### `def parse_query()` + +function that parses a query string into a query object + +- signature + + ```python + def parse_query(behaviour: Behaviour) -> Result[Query]: ... + ``` + +- arguments + + - `behaviour: Behaviour` + [surplus behaviour namedtuple](#class-behaviour) + +- returns [`Result`](#class-result)[`[Query]`](#query) + +### `def default_geocoder()` + +default geocoder for surplus, uses OpenStreetMap Nominatim + +> **Note** +> function is not used by surplus and not directly by the user, but is exposed for +> convenience +> +> pass in a custom function to [Behaviour](#class-behaviour) to override the default +> reverser +> +> see [Behaviour](#class-behaviour) for more information on what the function does + +- signature + + ```python + def default_geocoder(place: str) -> Latlong: + ``` + +### `def default_reverser()` + +default reverser for surplus, uses OpenStreetMap Nominatim + +> **Note** +> function is not used by surplus and not directly by the user, but is exposed for +> convenience +> +> pass in a custom function to [Behaviour](#class-behaviour) to override the default +> reverser +> +> see [Behaviour](#class-behaviour) for more information on what the function does + +- signature + + ```python + def default_reverser(latlong: Latlong) -> dict[str, Any]: + ``` ## licence diff --git a/surplus/surplus.py b/surplus/surplus.py index 7420d19..3df9388 100644 --- a/surplus/surplus.py +++ b/surplus/surplus.py @@ -178,7 +178,7 @@ ResultType = TypeVar("ResultType") class Result(NamedTuple, Generic[ResultType]): """ - typing.NamedTuple representing a result for safe value handling + typing.NamedTuple representing a result for safe value retrieval arguments value: ResultType @@ -193,21 +193,31 @@ class Result(NamedTuple, Generic[ResultType]): example # do something - try: - file_contents = Path(...).read_text() - except Exception as exc: - # must pass a default value - result = Result[str]("", error=exc) - else: - result = Result[str](file_contents) + def some_operation(path) -> Result[str]: + try: + file = open(path) + contents = file.read() - # handle result - if not result: - # .cry() either raises an exception or returns an error message - error_message = result.cry() + except Exception as exc: + # must pass a default value + result = Result[str]("", error=exc) + + else: + result = Result[str](contents) + + # call function and handle result + result = some_operation("some_file.txt") + + if not result: # check if the result is erroneous + # .cry() raises the exception + # (or returns it as a string error message using string=True) + result.cry() ... + else: - data = result.get() # raises exception or returns value + # .get() raises exception or returns value, + # but since we checked for errors this is safe + print(result.get()) """ value: ResultType @@ -221,11 +231,11 @@ class Result(NamedTuple, Generic[ResultType]): """ method that raises self.error if is an instance of BaseException, returns self.error if is an instance of str, or returns an empty string if - self.error is None. + self.error is None arguments string: bool = False - if self.error is an instance Exception, returns it as a string. + if self.error is an Exception, returns it as a string error message """ if isinstance(self.error, BaseException): @@ -276,7 +286,7 @@ EMPTY_LATLONG: Final[Latlong] = Latlong(latitude=0.0, longitude=0.0) class PlusCodeQuery(NamedTuple): """ - typing.NamedTuple representing a complete Plus Code + typing.NamedTuple representing a full-length Plus Code (e.g., 6PH58QMF+FX) arguments code: str @@ -295,7 +305,7 @@ class PlusCodeQuery(NamedTuple): arguments geocoder: typing.Callable[[str], Latlong] name string to location function, must take in a string and return a - Latlong. exceptions are handled. + Latlong, exceptions are handled by the caller returns Result[Latlong] """ @@ -323,13 +333,14 @@ class PlusCodeQuery(NamedTuple): return Result[Latlong](Latlong(latitude=latitude, longitude=longitude)) def __str__(self) -> str: + """method that returns string representation of query""" return f"{self.code}" class LocalCodeQuery(NamedTuple): """ - typing.NamedTuple representing a complete shortened Plus Code with locality, referred - to by surplus as a "local code" + typing.NamedTuple representing a shortened Plus Code with locality, referred to by + surplus as a "local code" arguments code: str @@ -338,6 +349,7 @@ class LocalCodeQuery(NamedTuple): remaining string of local code, e.g., "Singapore" methods + def to_full_plus_code(self, ...) -> Result[str]: ... def to_lat_long_coord(self, ...) -> Result[Latlong]: ... def __str__(self) -> str: ... """ @@ -347,12 +359,12 @@ class LocalCodeQuery(NamedTuple): def to_full_plus_code(self, geocoder: Callable[[str], Latlong]) -> Result[str]: """ - method that returns a full-length Plus Code + exclusive method that returns a full-length Plus Code as a string arguments geocoder: typing.Callable[[str], Latlong] name string to location function, must take in a string and return a - Latlong. exceptions are handled. + Latlong, exceptions are handled by the caller returns Result[str] """ @@ -378,7 +390,7 @@ class LocalCodeQuery(NamedTuple): arguments geocoder: typing.Callable[[str], Latlong] name string to location function, must take in a string and return a - Latlong. exceptions are handled. + Latlong, exceptions are handled by the caller returns Result[Latlong] """ @@ -395,6 +407,7 @@ class LocalCodeQuery(NamedTuple): ) def __str__(self) -> str: + """method that returns string representation of query""" return f"{self.code} {self.locality}" @@ -419,7 +432,7 @@ class LatlongQuery(NamedTuple): arguments geocoder: typing.Callable[[str], Latlong] name string to location function, must take in a string and return a - Latlong. exceptions are handled. + Latlong, exceptions are handled by the caller returns Result[Latlong] """ @@ -427,15 +440,16 @@ class LatlongQuery(NamedTuple): return Result[Latlong](self.latlong) def __str__(self) -> str: + """method that returns string representation of query""" return f"{self.latlong.latitude}, {self.latlong.longitude}" class StringQuery(NamedTuple): """ - typing.NamedTuple representing a complete Plus Code + typing.NamedTuple representing a pure string query arguments - code: str + query: str methods def to_lat_long_coord(self, ...) -> Result[Latlong]: ... @@ -451,7 +465,7 @@ class StringQuery(NamedTuple): arguments geocoder: typing.Callable[[str], Latlong] name string to location function, must take in a string and return a - Latlong. exceptions are handled. + Latlong, exceptions are handled by the caller returns Result[Latlong] """ @@ -463,6 +477,7 @@ class StringQuery(NamedTuple): return Result[Latlong](EMPTY_LATLONG, error=exc) def __str__(self) -> str: + """method that returns string representation of query""" return self.query @@ -488,7 +503,7 @@ def default_geocoder(place: str) -> Latlong: def default_reverser(latlong: Latlong) -> dict[str, Any]: - """default geocoder for surplus, uses OpenStreetMap Nominatim""" + """default reverser for surplus, uses OpenStreetMap Nominatim""" location: _geopy_Location | None = _geopy_Nominatim(user_agent=USER_AGENT).reverse( str(latlong) ) @@ -510,24 +525,24 @@ def default_reverser(latlong: Latlong) -> dict[str, Any]: class Behaviour(NamedTuple): """ - typing.NamedTuple representing expected behaviour of surplus + typing.NamedTuple representing how surplus operations should behave arguments query: str | list[str] = "" - str: original user-passed query string - list[str]: original user-passed query string split by spaces - geocoder: Callable[[str], Latlong] = default_geocoder - name string to location function, must take in a string and return a Latlong. - exceptions are handled by the caller. + original user-passed query string or a list of strings from splitting + user-passed query string by spaces + geocoder: Callable[[str], Latlong] = default_geocoderi + name string to location function, must take in a string and return a Latlong, + exceptions are handled by the caller reverser: Callable[[str], dict[str, Any]] = default_reverser Latlong object to dictionary function, must take in a string and return a dict. keys found in SHAREABLE_TEXT_LINE_*_KEYS used to access address details - are placed top-level in the dict. exceptions are handled by the caller. see - the playground notebook for example output. + are placed top-level in the dict, exceptions are handled by the caller. + see the playground notebook for example output stderr: TextIO = stderr - TextIO-like object representing a writeable file. defaults to sys.stderr. + TextIO-like object representing a writeable file. defaults to sys.stderr stdout: TextIO = stdout - TextIO-like object representing a writeable file. defaults to sys.stdout. + TextIO-like object representing a writeable file. defaults to sys.stdout debug: bool = False whether to print debug information to stderr version_header: bool = False @@ -549,9 +564,7 @@ class Behaviour(NamedTuple): # functions -def parse_query( - behaviour: Behaviour, -) -> Result[Query]: +def parse_query(behaviour: Behaviour) -> Result[Query]: """ function that parses a query string into a query object @@ -928,19 +941,14 @@ def _generate_text( return "".join(_unique(text)).rstrip() -def surplus( - query: Query | str, - behaviour: Behaviour, -) -> Result[str]: +def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: """ query to shareable text conversion function query: Query | str - Query: query object to convert, see respective docstrings for more information on - each type of query object - str: string to attempt to query for + query object to convert or string to attempt to query for then convert behaviour: Behaviour - program behaviour namedtuple + surplus behaviour namedtuple returns Result[str] """ From 9752710f8205438039fa495ff525031e5563b515 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Sun, 3 Sep 2023 14:23:40 +0000 Subject: [PATCH 41/41] s+,docs: future is now pyproj: remove py.typed include, poetry does this automatically ci(qc): fix typo ci(release): duplicate non-versioned wheels --- .github/workflows/checks.yml | 2 +- .github/workflows/publish-slsa3.yml | 3 + README.md | 253 +++++++++++++++------------- pyproject.toml | 5 +- surplus/surplus.py | 18 +- 5 files changed, 155 insertions(+), 126 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 05ea57c..264297b 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -19,7 +19,7 @@ jobs: - name: install dependencies run: devbox run poetry install - - name: buil wheel + - name: build wheel id: build run: devbox run poetry build diff --git a/.github/workflows/publish-slsa3.yml b/.github/workflows/publish-slsa3.yml index b7001ae..c71acf9 100644 --- a/.github/workflows/publish-slsa3.yml +++ b/.github/workflows/publish-slsa3.yml @@ -27,6 +27,9 @@ jobs: id: build run: devbox run poetry build + - name: duplicate non-versioned wheel + run: cp dist/surplus-*.whl dist/surplus-py3-none-any.whl + - name: generate provenance subjects id: hash run: | diff --git a/README.md b/README.md index bdeaebc..aadc8df 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,12 @@ # surplus -> **Warning** -> -> **this is surplus `2.0.0`.** -> surplus is being rewritten to better incorporate with -> [sandplus](https://github.com/markjoshwel/sandplus.git). -> sandplus is surplus's Android application accompaniment, written in Kotlin with Jetpack -> Compose. -> -> you are on the `future` branch. if you see this warning, that means code is not -> finalised and ready to be used. -> want the old, stable, working codebase? see the -> [`main`](https://github.com/markjoshwel/surplus/tree/main) branch. - surplus is a Python script to convert [Google Maps Plus Codes](https://maps.google.com/pluscodes/) to iOS Shortcuts-like shareable text. - [installation](#installation) - [usage](#usage) - - [command-line usage](#command-line-usaage) + - [command-line usage](#command-line-usage) - [example api usage](#example-api-usage) - [developer's guide](#developers-guide) - [contributor's guide](#contributors-guide) @@ -42,16 +29,27 @@ Central, Singapore ## installation -> **Note** +> [!IMPORTANT] > python 3.11 or later is required due to a bug in earlier versions. > [(python/cpython#88089)](https://github.com/python/cpython/issues/88089) -install surplus directly from the repository using pip: +for most, you can install surplus built from the latest stable release: ```text -pip install git+https://github.com/markjoshwel/surplus.git@future +pip install https://github.com/markjoshwel/surplus/releases/latest/download/surplus-py3-none-any.whl ``` +or directly from the repository using pip: + +```text +pip install git+https://github.com/markjoshwel/surplus.git@main +``` + +surplus is also a public domain dedicated [single python file](surplus/surplus.py), so +feel free to grab that and embed it into your own program as you see fit. + +see [licence](#licence) for licensing information. + ## usage ### command-line usage @@ -94,14 +92,14 @@ here are a few examples to get you quickly started using surplus in your own pro 2. handle queries seperately - ```python - >>> import surplus - >>> behaviour = surplus.Behaviour("6PH58R3M+F8") - >>> query = surplus.parse_query(behaviour) - >>> result = surplus.surplus(query.get(), behaviour) - >>> result.get() - 'MacRitchie Nature Trail\nCentral Water Catchment\n574325\nCentral, Singapore' - ``` + ```python + >>> import surplus + >>> behaviour = surplus.Behaviour("6PH58R3M+F8") + >>> query = surplus.parse_query(behaviour) + >>> result = surplus.surplus(query.get(), behaviour) + >>> result.get() + 'MacRitchie Nature Trail\nCentral Water Catchment\n574325\nCentral, Singapore' + ``` 3. start from a Query object @@ -117,10 +115,12 @@ here are a few examples to get you quickly started using surplus in your own pro notes: -- you can change what surplus does by passing in a custom `Behaviour` object. +- you can change what surplus does by passing in a custom [`Behaviour`](#class-behaviour) + object -- most surplus functions return a `Result` object. while you can `.get()` the Result to - obtain the proper return value, this is dangerous and might raise an exception. +- most surplus functions return a [`Result`](#class-result) object. while you can + call [`.get()`](#resultget) to obtain the proper return value, this is dangerous and + might raise an exception see the [api reference](#api-reference) for more information. @@ -152,7 +152,7 @@ for information on surplus's exposed api, see the [api reference](#api-reference when contributing your first changes, please include an empty commit for a copyright waiver using the following message (replace 'Your Name' with your name or nickname): -``` +```text Your Name Copyright Waiver I dedicate any and all copyright interest in this software to the @@ -166,7 +166,7 @@ the command to create an empty commit is `git commit --allow-empty` ### reporting incorrect output -> **Note** +> [!NOTE] > this section is independent from the rest of the contributing section. different output from the iOS Shortcuts app is expected, however incorrect output is not. @@ -191,7 +191,7 @@ and do the following: [`--debug` flag](#command-line-usage) passed to the surplus CLI or with `debug=True` set in function calls. - > **Note** + > [!NOTE] > if you are using the surplus API and have passed custom stdout and stderr parameters > to redirect output, include that instead. @@ -210,7 +210,7 @@ and do the following: - iOS Shortcuts Output - ``` + ```text Plaza Singapura 68 Orchard Rd 238839 @@ -219,7 +219,7 @@ and do the following: - surplus Output - ``` + ```text Plaza Singapura 68 Orchard Road Museum @@ -230,16 +230,12 @@ and do the following: this _should not_ be reported as incorrect, as the only difference between the two is that surplus displays more information. - note: for singaporean readers, "Musuem" here is correct as it refers to the - [Museum planning area](https://en.wikipedia.org/wiki/Museum_Planning_Area), - in which Plaza Singapura is located in. - other examples that _should not_ be reported are: - name of place is incorrect/different this may be due to incorrect data from the geolocator function, which is OpenStreetMap Nominatim by default. - in the case of Nominatim, it means that there the data on OpenStreetMap is incorrect. + in the case of Nominatim, it means that the data on OpenStreetMap is incorrect. (_if so, then consider updating OpenStreetMap to help not just you, but other surplus and OpenStreetMap users!_) @@ -253,19 +249,19 @@ of incorrect outputs. ## the technical details of surplus's output -> **Note** +> [!NOTE] > this is a breakdown of surplus's output when converting to shareable text. -> when converting to other output types, n +> when converting to other output types, output may be different. -``` +```text $ s+ --debug 8QJF+RP Singapore surplus version 2.0.0, debug mode -debug: behaviour.query=['8QJF+RP', 'Singapore'] -debug: portion_plus_code='8QJF+RP', portion_locality='Singapore' -debug: query=Result(value=LocalCodeQuery(code='8QJF+RP', locality='Singapore'), error=None) -debug: latlong.get()=Latlong(latitude=1.3320625, longitude=103.7743125) -debug: location={'amenity': 'Ngee Ann Polytechnic', 'house_number': '535', 'road': 'Clementi Road', 'suburb': 'Bukit Timah', 'city': 'Singapore', 'county': 'Northwest', 'ISO3166-2-lvl6': 'SG-03', 'postcode': '599489', 'country': 'Singapore', 'country_code': 'sg', 'raw': "{...}", 'latitude': '1.33318835', 'longitude': '103.77461234638255'} -debug: seen_names=['Ngee Ann Polytechnic', 'Clementi Road'] +debug: parse_query: behaviour.query=['8QJF+RP', 'Singapore'] +debug: _match_plus_code: portion_plus_code='8QJF+RP', portion_locality='Singapore' +debug: cli: query=Result(value=LocalCodeQuery(code='8QJF+RP', locality='Singapore'), error=None) +debug: cli: latlong.get()=Latlong(latitude=1.3320625, longitude=103.7743125) +debug: cli: location={'amenity': 'Ngee Ann Polytechnic', 'house_number': '535', 'road': 'Clementi Road', 'suburb': 'Bukit Timah', 'city': 'Singapore', 'county': 'Northwest', 'ISO3166-2-lvl6': 'SG-03', 'postcode': '599489', 'country': 'Singapore', 'country_code': 'sg', 'raw': "{...}", 'latitude': '1.33318835', 'longitude': '103.77461234638255'} +debug: _generate_text: seen_names=['Ngee Ann Polytechnic', 'Clementi Road'] debug: _generate_text_line: [True] -> True -------- 'Ngee Ann Polytechnic' debug: _generate_text_line: [True] -> True -------- '535' debug: _generate_text_line: [True] -> True -------- 'Clementi Road' @@ -292,8 +288,8 @@ variables - **variable `behaviour.query`** - query split by comma, comes from - [`argparse.ArgumentParser.parse_args`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.parse_args) + the original query string or a list of strings from space-splitting the original query + string passed to [`parse_query()`](#def-parse_query) for parsing ```text $ s+ 77Q4+7X Austin, Texas, USA @@ -302,6 +298,12 @@ variables behaviour.query -> ['77Q4+7X', 'Austin', 'Texas', 'USA'] ``` + + ```text + >>> surplus("77Q4+7X Austin, Texas, USA", surplus.Behaviour()) + + behaviour.query -> '77Q4+7X Austin, Texas, USA' + ``` - **variables `portion_plus_code` and `portion_locality`** @@ -310,36 +312,33 @@ variables represents the plus code and locality portions of a [shortened plus code](https://en.wikipedia.org/wiki/Open_Location_Code#Common_usage_and_shortening) - (_referred to as a "local code" in the codebase_) respectively. + (_referred to as a "local code" in the codebase_) respectively - **variable `query`** - query is a variable of type `surplus.Result[surplus.Query]`, where `surplus.Query` is - a TypeAlias of `PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery`. + query is a variable of type [`Result`](#class-result)[`[Query]`](#query) - this variable is displayed to show what query type - `surplus.parse_query` has recognised, and if there were any errors - during query parsing. + this variable is displayed to show what query type [`parse_query()`](#def-parse_query) has + recognised, and if there were any errors during query parsing - **expression `latlong.get()=`** (_only shown if the query is a plus code_) - the latitude longitude coordinates derived from the plus code. + the latitude longitude coordinates derived from the plus code - **variable `location`** - the response dictionary from the reverser passed to - [`surplus.surplus()`](#surplussurplus) + the response dictionary from the reverser function passed to + [`surplus()`](#def-surplus) - for more information on what the dictionary should contain or how it should look like, - see the [playground notebook](playground.ipynb), documentation on surplus.Behaviour or - the surplus's implementation of the reverser function in `surplus.default_reverser`. + for more information on the reverser function, see [`Behaviour`](#class-behaviour) and + [`default_reverser`](#def-default_reverser) - **variable `seen_names`** a list of unique important names found in certain nominatim keys used in final output - lines 0-3. + lines 0-3 - **`_generate_text_line` seen name checks** @@ -351,15 +350,15 @@ variables ``` a check is done on shareable text line 4 keys (`SHAREABLE_TEXT_LINE_4_KEYS` - general - regional location) to reduce repeated elements found in `seen_names`. + regional location) to reduce repeated elements found in `seen_names` reasoning is, if an element on line 4 (general regional location) is the exact same as - a previously seen name, there is no need to include the element. + a previously seen name, there is no need to include the element - **filter function boolean list** `_generate_text_line`, an internal function defined inside `_generate_text` can be - passed a filter function as a way to filter out certain elements on a line. + passed a filter function as a way to filter out certain elements on a line ```python # the filter used in _generate_text, for line 4's seen name checks @@ -383,9 +382,9 @@ variables the current iteration from iterating through a list of strings containing elements from line 4. (general regional location) -breakdown of each output line, accompanied by their nominatim key: +line breakdown of shareable text output, accompanied by their Nominatim keys: -``` +```text 0 name of a place 1 building name 2 highway name @@ -401,7 +400,7 @@ breakdown of each output line, accompanied by their nominatim key: - examples - ``` + ```text The University of Queensland Ngee Ann Polytechnic Botanic Gardens @@ -409,7 +408,7 @@ breakdown of each output line, accompanied by their nominatim key: - nominatim keys - ``` + ```text emergency, historic, military, natural, landuse, place, railway, man_made, aerialway, boundary, amenity, aeroway, club, craft, leisure, office, mountain_pass, shop, tourism, bridge, tunnel, waterway @@ -419,14 +418,14 @@ breakdown of each output line, accompanied by their nominatim key: - examples - ``` + ```text Novena Square Office Tower A Visitor Centre ``` - nominatim keys - ``` + ```text building ``` @@ -434,14 +433,14 @@ breakdown of each output line, accompanied by their nominatim key: - examples - ``` + ```text Marina Coastal Expressway Lornie Highway ``` - nominatim keys - ``` + ```text highway ``` @@ -449,7 +448,7 @@ breakdown of each output line, accompanied by their nominatim key: - examples - ``` + ```text 535 Clementi Road Macquarie Street Braddell Road @@ -457,7 +456,7 @@ breakdown of each output line, accompanied by their nominatim key: - nominatim keys - ``` + ```text house_number, house_name, road ``` @@ -465,7 +464,7 @@ breakdown of each output line, accompanied by their nominatim key: - examples - ``` + ```text St Lucia, Greater Brisbane The Drag, Austin Toa Payoh Crest @@ -473,7 +472,7 @@ breakdown of each output line, accompanied by their nominatim key: - nominatim keys - ``` + ```text residential, neighbourhood, allotments, quarter, city_district, district, borough, suburb, subdivision, municipality, city, town, village ``` @@ -482,7 +481,7 @@ breakdown of each output line, accompanied by their nominatim key: - examples - ``` + ```text 310131 78705 4066 @@ -490,7 +489,7 @@ breakdown of each output line, accompanied by their nominatim key: - nominatim key - ``` + ```text postcode ``` @@ -498,7 +497,7 @@ breakdown of each output line, accompanied by their nominatim key: - examples - ``` + ```text Travis County, Texas, United States Southeast, Singapore Queensland, Australia @@ -506,7 +505,7 @@ breakdown of each output line, accompanied by their nominatim key: - nominatim keys - ``` + ```text region, county, state, state_district, country, continent ``` @@ -515,6 +514,8 @@ breakdown of each output line, accompanied by their nominatim key: - [constants](#constants) - [exception classes](#exception-classes) - [types](#types) + - [`Query`](#query) + - [`ResultType`](#resulttype) - [`class Behaviour`](#class-behaviour) - [`class ConversionResultTypeEnum`](#class-conversionresulttypeenum) - [`class Result`](#class-result) @@ -522,10 +523,20 @@ breakdown of each output line, accompanied by their nominatim key: - [`Result.cry()`](#resultcry) - [`Result.get()`](#resultget) - [`class Latlong`](#class-latlong) + - [`Latlong.__str__()`](#latlong__str__) - [`class PlusCodeQuery`](#class-pluscodequery) + - [`PlusCodeQuery.to_lat_long_coord()`](#pluscodequeryto_lat_long_coord) + - [`PlusCodeQuery.__str__()`](#pluscodequery__str__) - [`class LocalCodeQuery`](#class-localcodequery) + - [`LocalCodeQuery.to_full_plus_code()`](#localcodequeryto_full_plus_code) + - [`LocalCodeQuery.to_lat_long_coord()`](#localcodequeryto_lat_long_coord) + - [`LocalCodeQuery.__str__()`](#localcodequery__str__) - [`class LatlongQuery`](#class-latlongquery) + - [`LatlongQuery.to_lat_long_coord()`](#latlongqueryto_lat_long_coord) + - [`LatlongQuery.__str__()`](#latlongquery__str__) - [`class StringQuery`](#class-stringquery) + - [`StringQuery.to_lat_long_coord()`](#stringqueryto_lat_long_coord) + - [`StringQuery.__str__()`](#stringquery__str__) - [`def surplus()`](#def-surplus) - [`def parse_query()`](#def-parse_query) - [`def default_geocoder()`](#def-default_geocoder) @@ -593,13 +604,13 @@ ResultType = TypeVar("ResultType") ### `class Behaviour` -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) representing how surplus operations should behave attributes - `query: str | list[str] = ""` - original user-passed query string or a list of strings from splitting user-passed query + original user-passed query string or a list of strings from splitting user-passed query string by spaces - `geocoder: typing.Callable[[str], Latlong] = default_geocoder` @@ -607,8 +618,8 @@ attributes [`Latlong`](#class-latlong), exceptions are handled by the caller - `reverser: Callable[[Latlong], dict[str, Any]] = default_reverser` - [`Latlong`] object to dictionary function, must take in a string and return a dict. - keys found in SHAREABLE_TEXT_LINE_*_KEYS used to access address details are placed + [`Latlong`](#class-latlong) object to dictionary function, must take in a string and return a + dict. keys found in SHAREABLE_TEXT_LINE_*_KEYS used to access address details are placed top-level in the dict, exceptions are handled by the caller. see the [playground notebook](playground.ipynb) for example output @@ -645,7 +656,7 @@ values ### `class Result` -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) representing the result for safe value retrieval attributes @@ -667,10 +678,10 @@ def some_operation(path) -> Result[str]: except Exception as exc: # must pass a default value - result = Result[str]("", error=exc) + return Result[str]("", error=exc) else: - result = Result[str](contents) + return Result[str](contents) # call function and handle result result = some_operation("some_file.txt") @@ -690,8 +701,8 @@ else: methods - [`def __bool__(self) -> bool: ...`](#result__bool__) -- [`def get(self) -> ResultType: ...`](#resultcry) -- [`def cry(self, string: bool = False) -> str: ...`](#resultget) +- [`def cry(self, string: bool = False) -> str: ...`](#resultcry) +- [`def get(self) -> ResultType: ...`](#resultget) #### `Result.__bool__()` @@ -737,7 +748,7 @@ method that returns `self.value` if Result is non-erroneous else raises error ### `class Latlong` -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) representing a latitude-longitude coordinate pair attributes @@ -764,7 +775,7 @@ method that returns a comma-and-space-seperated string of `self.latitude` and ### `class PlusCodeQuery` -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) representing a full-length Plus Code (e.g., 6PH58QMF+FX) attributes @@ -807,7 +818,7 @@ method that returns string representation of query ### `class LocalCodeQuery` -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) representing a [shortened Plus Code](https://en.wikipedia.org/wiki/Open_Location_Code#Common_usage_and_shortening) with locality, referred to by surplus as a "local code" @@ -878,7 +889,7 @@ method that returns string representation of query ### `class LatlongQuery` -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) representing a latitude-longitude coordinate pair attributes @@ -923,7 +934,7 @@ method that returns string representation of query ### `class StringQuery` -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) +[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) representing a pure string query attributes @@ -932,10 +943,10 @@ attributes methods -- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#latlongqueryto_lat_long_coord) -- [`def __str__(self) -> str: ...`](#latlongquery__str__) +- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#stringqueryto_lat_long_coord) +- [`def __str__(self) -> str: ...`](#stringquery__str__) -#### `LatlongQuery.to_lat_long_coord()` +#### `StringQuery.to_lat_long_coord()` method that returns a latitude-longitude coordinate pair @@ -954,7 +965,7 @@ method that returns a latitude-longitude coordinate pair - returns [`Result`](#class-result)[`[Latlong]`](#class-latlong) -#### `LatlongQuery.__str__()` +#### `StringQuery.__str__()` method that returns string representation of query @@ -1007,14 +1018,10 @@ function that parses a query string into a query object default geocoder for surplus, uses OpenStreetMap Nominatim -> **Note** +> [!NOTE] > function is not used by surplus and not directly by the user, but is exposed for -> convenience -> -> pass in a custom function to [Behaviour](#class-behaviour) to override the default -> reverser -> -> see [Behaviour](#class-behaviour) for more information on what the function does +> convenience being [Behaviour](#class-behaviour) objects. +> pass in a custom function to [Behaviour](#class-behaviour) to override the default reverser. - signature @@ -1026,14 +1033,10 @@ default geocoder for surplus, uses OpenStreetMap Nominatim default reverser for surplus, uses OpenStreetMap Nominatim -> **Note** +> [!NOTE] > function is not used by surplus and not directly by the user, but is exposed for -> convenience -> -> pass in a custom function to [Behaviour](#class-behaviour) to override the default -> reverser -> -> see [Behaviour](#class-behaviour) for more information on what the function does +> convenience being [Behaviour](#class-behaviour) objects. +> pass in a custom function to [Behaviour](#class-behaviour) to override the default reverser. - signature @@ -1046,3 +1049,27 @@ default reverser for surplus, uses OpenStreetMap Nominatim surplus is free and unencumbered software released into the public domain. for more information, please refer to the [UNLICENCE](/UNLICENCE), , or the python module docstring. + +however, direct dependencies of surplus are licensed under different, but still permissive +and open-source licences. + +```text +geopy 2.4.0 Python Geocoding Toolbox +└── geographiclib >=1.52,<3 +pluscodes 2022.1.3 Compute Plus Codes (Open Location Codes). +``` + +- [geopy](https://pypi.org/project/geopy/): + Python Geocoding Toolbox + + MIT License + + - [geographiclib](https://pypi.org/project/geographiclib/): + The geodesic routines from GeographicLib + + MIT License + +- [pluscodes](https://pypi.org/project/pluscodes/): + Compute Plus Codes (Open Location Codes) + + Apache 2.0 diff --git a/pyproject.toml b/pyproject.toml index 3dc3675..16b1adb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,6 @@ line-length = 90 line_length = 90 profile = "black" -[tool.setuptools.package-data] -"*" = ["py.typed"] - [build-system] -requires = ["poetry-core",] +requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" diff --git a/surplus/surplus.py b/surplus/surplus.py index 3df9388..41816a9 100644 --- a/surplus/surplus.py +++ b/surplus/surplus.py @@ -200,10 +200,10 @@ class Result(NamedTuple, Generic[ResultType]): except Exception as exc: # must pass a default value - result = Result[str]("", error=exc) + return Result[str]("", error=exc) else: - result = Result[str](contents) + return Result[str](contents) # call function and handle result result = some_operation("some_file.txt") @@ -629,7 +629,9 @@ def parse_query(behaviour: Behaviour) -> Result[Query]: ) if behaviour.debug: - behaviour.stderr.write(f"debug: {portion_plus_code=}, {portion_locality=}\n") + behaviour.stderr.write( + f"debug: _match_plus_code: {portion_plus_code=}, {portion_locality=}\n" + ) return Result[Query]( LocalCodeQuery( @@ -654,7 +656,7 @@ def parse_query(behaviour: Behaviour) -> Result[Query]: # Toa Payoh North (no commas) if behaviour.debug: - behaviour.stderr.write(f"debug: {behaviour.query=}\n") + behaviour.stderr.write(f"debug: parse_query: {behaviour.query=}\n") # check if empty if (behaviour.query == []) or (behaviour.query == ""): @@ -914,7 +916,7 @@ def _generate_text( ] if debug: - behaviour.stderr.write(f"debug: {seen_names=}\n") + behaviour.stderr.write(f"debug: _generate_text: {seen_names=}\n") general_global_info: list[str] = [ str(location.get(detail, "")) for detail in st_line6_keys @@ -984,7 +986,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: return Result[str]("", error=latlong.error) if behaviour.debug: - behaviour.stderr.write(f"debug: {latlong.get()=}\n") + behaviour.stderr.write(f"debug: cli: {latlong.get()=}\n") # reverse location and handle result try: @@ -994,7 +996,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: return Result[str]("", error=exc) if behaviour.debug: - behaviour.stderr.write(f"debug: {location=}\n") + behaviour.stderr.write(f"debug: cli: {location=}\n") # generate text if behaviour.debug: @@ -1069,7 +1071,7 @@ def cli() -> int: query = parse_query(behaviour=behaviour) if behaviour.debug: - behaviour.stderr.write(f"debug: {query=}\n") + behaviour.stderr.write(f"debug: cli: {query=}\n") if not query: behaviour.stderr.write(f"error: {query.cry(string=not behaviour.debug)}\n")