diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 671bec0..d8a7a2c 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v3 - name: install devbox - uses: jetpack-io/devbox-install-action@v0.3.0 + uses: jetpack-io/devbox-install-action@v0.7.0 - name: install dependencies run: devbox run poetry install @@ -28,10 +28,10 @@ jobs: run: devbox run poetry run mypy . - name: check for black formatting compliance - run: devbox run poetry run "black --check ." + run: devbox run poetry run black --check . - name: analyse isort compliance - run: devbox run poetry run "isort --check *.py **/*.py" + run: devbox run poetry run isort --check *.py **/*.py test: runs-on: ubuntu-latest @@ -40,7 +40,7 @@ jobs: uses: actions/checkout@v3 - name: install devbox - uses: jetpack-io/devbox-install-action@v0.3.0 + uses: jetpack-io/devbox-install-action@v0.7.0 - name: install dependencies run: devbox run poetry install diff --git a/.github/workflows/publish-slsa3-auto.yml b/.github/workflows/publish-slsa3-auto.yml index d70617b..aed9859 100644 --- a/.github/workflows/publish-slsa3-auto.yml +++ b/.github/workflows/publish-slsa3-auto.yml @@ -24,7 +24,7 @@ jobs: uses: tj-actions/branch-names@v7 - name: install devbox - uses: jetpack-io/devbox-install-action@v0.6.1 + uses: jetpack-io/devbox-install-action@v0.7.0 - name: install dependencies run: devbox run poetry install diff --git a/.github/workflows/publish-slsa3-manual.yml b/.github/workflows/publish-slsa3-manual.yml index e662d7e..1dbf952 100644 --- a/.github/workflows/publish-slsa3-manual.yml +++ b/.github/workflows/publish-slsa3-manual.yml @@ -22,7 +22,7 @@ jobs: uses: tj-actions/branch-names@v7 - name: install devbox - uses: jetpack-io/devbox-install-action@v0.6.1 + uses: jetpack-io/devbox-install-action@v0.7.0 - name: install dependencies run: devbox run poetry install diff --git a/README.md b/README.md index 45a55f1..7aa0b78 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ to iOS Shortcuts-like shareable text. ```text $ surplus 9R3J+R9 Singapore -surplus version 2.1.1 +surplus version 2.2.0 Thomson Plaza 301 Upper Thomson Road Sin Ming, Bishan @@ -57,7 +57,7 @@ see [licence](#licence) for licensing information. ```text usage: surplus [-h] [-d] [-v] [-c {pluscode,localcode,latlong,sharetext}] - [-u USER_AGENT] + [-u USER_AGENT] [-t] [query ...] Google Maps Plus Code to iOS Shortcuts-like shareable text @@ -79,6 +79,9 @@ options: -u USER_AGENT, --user-agent USER_AGENT user agent string to use for geocoding service, defaults to fingerprinted user agent string + -t, --using-termux-location + treats input as a termux-location output json + string, and parses it accordingly ``` ### example api usage @@ -260,7 +263,7 @@ of incorrect outputs. ```text $ s+ --debug 8QJF+RP Singapore -surplus version 2.1.1, debug mode (latest@future, Tue 05 Sep 2023 23:38:59 +0800) +surplus version 2.2.0, debug mode (latest@future, Tue 05 Sep 2023 23:38:59 +0800) 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) @@ -602,24 +605,62 @@ line breakdown of shareable text output, accompanied by their Nominatim keys: > this constant only affects the default surplus Nominatim geocoding functions. custom > functions do not read from this, unless deliberately programmed to do so -- `SHAREABLE_TEXT_LINE_0_KEYS: typing.Final[tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_1_KEYS: typing.Final[tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_2_KEYS: typing.Final[tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_3_KEYS: typing.Final[tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_4_KEYS: typing.Final[tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_5_KEYS: typing.Final[tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_6_KEYS: typing.Final[tuple[str, ...]]` +- `SHAREABLE_TEXT_LINE_0_KEYS: dict[str, tuple[str, ...]]` + `SHAREABLE_TEXT_LINE_1_KEYS: dict[str, tuple[str, ...]]` + `SHAREABLE_TEXT_LINE_2_KEYS: dict[str, tuple[str, ...]]` + `SHAREABLE_TEXT_LINE_3_KEYS: dict[str, tuple[str, ...]]` + `SHAREABLE_TEXT_LINE_4_KEYS: dict[str, tuple[str, ...]]` + `SHAREABLE_TEXT_LINE_5_KEYS: dict[str, tuple[str, ...]]` + `SHAREABLE_TEXT_LINE_6_KEYS: dict[str, tuple[str, ...]]` - a tuple of strings containing Nominatim keys used in shareable text line 0-6 + a dictionary of iso3166-2 country-portion string keys with a tuple of Nominatim keys + used in shareable text line 0-6 as their values -- `SHAREABLE_TEXT_NAMES: typing.Final[tuple[str, ...]]` + ```python + { + "default": (...), + "SG": (...,), + ... + } + ``` + +- `SHAREABLE_TEXT_LINE_SETTINGS: dict[str, dict[int, tuple[str, bool]]]` + + a dictionary of iso3166-2 country-portion string keys with a dictionary as their values + + the dictionary values are dictionaries with integers as keys, and a tuple of two strings + + the first string is the separator string to use, and the second string is a boolean flag + that if `True` will check the line for seen names + + ```python + { + "default": { + 0: (", ", False), + ... + 6: (", ", False), + }, + "IT": { + 0: (", ", False), + ... + 6: (", ", False), + }, + ... + } + ``` + +- `SHAREABLE_TEXT_NAMES: dict[str, tuple[str, ...]]` + a dictionary of iso3166-2 country-portion string keys with a tuple of strings as their + values a tuple of strings containing Nominatim keys used in shareable text line 0-2 and special keys in line 3 + used for seen name checks + - `SHAREABLE_TEXT_LOCALITY: dict[str, tuple[str, ...]]` - a dictionary of iso3166-2 country-portion strings with a tuples of strings as their + a dictionary of iso3166-2 country-portion string keys with a tuple of strings as their values used when generating the locality portions of shortened Plus Codes/local codes @@ -632,6 +673,9 @@ line breakdown of shareable text output, accompanied by their Nominatim keys: } ``` +- `SHAREABLE_TEXT_DEFAULT: typing.Final[str]` + constant for what is the "default" key in the `SHAREABLE*` constants + - `EMPTY_LATLONG: typing.Final[Latlong]` a constant for an empty latlong coordinate, with latitude and longitude set to 0.0 @@ -793,6 +837,9 @@ attributes - `convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT` what type to convert the query to +- `using_termux_location: bool = False` + treats query as a termux-location output json string, and parses it accordingly + ### `class SurplusDefaultGeocoding` > [!IMPORTANT] @@ -1288,7 +1335,7 @@ it contains the following, in order, alongside an example: 1. `version` - the surplus version alongside a suffix, if any ```text - 2.1.1-local + 2.2.0-local ``` 2. `system_info` - generic machine and operating system information @@ -1312,7 +1359,7 @@ it contains the following, in order, alongside an example: after hashing, this string becomes a 12 character hexadecimal string, as shown below: ```text -surplus/2.1.1-local (1fdbfa0b0cfb) +surplus/2.2.0-local (1fdbfa0b0cfb) ^^^^^^^^^^^^ this is the hashed result of unique_info ``` diff --git a/devbox.json b/devbox.json index 63a0c48..9928084 100644 --- a/devbox.json +++ b/devbox.json @@ -1,14 +1,11 @@ { "packages": [ - "python311", - "python311Packages.ipykernel", - "poetry" + "python311@latest", + "python311Packages.ipykernel@latest", + "poetry@latest" ], "shell": { - "init_hook": [ - "poetry env use $(which python)", - "poetry shell" - ] + "init_hook": ["poetry install"] }, "nixpkgs": { "commit": "f80ac848e3d6f0c12c52758c0f25c10c97ca3b62" diff --git a/devbox.lock b/devbox.lock index 7e3b009..0bdffda 100644 --- a/devbox.lock +++ b/devbox.lock @@ -1,15 +1,67 @@ { "lockfile_version": "1", "packages": { - "poetry": { - "resolved": "github:NixOS/nixpkgs/f80ac848e3d6f0c12c52758c0f25c10c97ca3b62#poetry" + "poetry@latest": { + "last_modified": "2023-10-04T02:19:08Z", + "plugin_version": "0.0.3", + "resolved": "github:NixOS/nixpkgs/d1c9180c6d1f8fce9469436f48c1cb8180d7087d#poetry", + "source": "devbox-search", + "version": "1.6.1", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/i7q6kxa3ac7c57zalr4vwa04c1bll3xd-python3.10-poetry-1.6.1" + }, + "aarch64-linux": { + "store_path": "/nix/store/b0gmqd0y5pich25mk5p649g1bbbyxp0v-python3.10-poetry-1.6.1" + }, + "x86_64-darwin": { + "store_path": "/nix/store/q5zqqp3k58z46q4imgmj5pxbinmzjyqf-python3.10-poetry-1.6.1" + }, + "x86_64-linux": { + "store_path": "/nix/store/6v2zvmag2il359sbd8j1q7b3c20gs6yd-python3.10-poetry-1.6.1" + } + } }, - "python311": { + "python311@latest": { + "last_modified": "2023-09-27T18:02:17Z", "plugin_version": "0.0.1", - "resolved": "github:NixOS/nixpkgs/f80ac848e3d6f0c12c52758c0f25c10c97ca3b62#python311" + "resolved": "github:NixOS/nixpkgs/517501bcf14ae6ec47efd6a17dda0ca8e6d866f9#python311", + "source": "devbox-search", + "version": "3.11.4", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/rycxjkclx801wrhwrgllak0302xzjdvx-python3-3.11.4" + }, + "aarch64-linux": { + "store_path": "/nix/store/h4xi1djsmhk7bjdipz58xkfnf8lc9mpm-python3-3.11.4" + }, + "x86_64-darwin": { + "store_path": "/nix/store/yqs2zxkxn61xzimdaz1pbgawk2lnm0d8-python3-3.11.4" + }, + "x86_64-linux": { + "store_path": "/nix/store/3k7is7nc2xbav8a48vx7arad522d8czx-python3-3.11.4" + } + } }, - "python311Packages.ipykernel": { - "resolved": "github:NixOS/nixpkgs/f80ac848e3d6f0c12c52758c0f25c10c97ca3b62#python311Packages.ipykernel" + "python311Packages.ipykernel@latest": { + "last_modified": "2023-09-29T09:08:59Z", + "resolved": "github:NixOS/nixpkgs/bd9b686c0168041aea600222be0805a0de6e6ab8#python311Packages.ipykernel", + "source": "devbox-search", + "version": "6.21.2", + "systems": { + "aarch64-darwin": { + "store_path": "/nix/store/78gi0qk9lmrb4rk5ignww29p5j0l79gs-python3.11-ipykernel-6.21.2" + }, + "aarch64-linux": { + "store_path": "/nix/store/74m55prblwmsyx1ylv135iqw2wqj4c6a-python3.11-ipykernel-6.21.2" + }, + "x86_64-darwin": { + "store_path": "/nix/store/j0wpnwkdcqfp2bmp2b9pgzdngnmxzjj9-python3.11-ipykernel-6.21.2" + }, + "x86_64-linux": { + "store_path": "/nix/store/3kqcf56q2q46hj0g5n4rxzzy1c9vdiqa-python3.11-ipykernel-6.21.2" + } + } } } -} \ No newline at end of file +} diff --git a/poetry.lock b/poetry.lock index 8099786..323cf90 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 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 = [ @@ -16,7 +15,6 @@ files = [ name = "asttokens" version = "2.4.0" description = "Annotate AST trees with source code positions" -category = "dev" optional = false python-versions = "*" files = [ @@ -34,7 +32,6 @@ test = ["astroid", "pytest"] name = "backcall" version = "0.2.0" description = "Specifications for callback functions passed in to an API" -category = "dev" optional = false python-versions = "*" files = [ @@ -44,34 +41,33 @@ files = [ [[package]] name = "black" -version = "23.7.0" +version = "23.9.1" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.8" files = [ - {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"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:d6bc09188020c9ac2555a498949401ab35bb6bf76d4e0f8ee251694664df6301"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:13ef033794029b85dfea8032c9d3b92b42b526f1ff4bf13b2182ce4e917f5100"}, + {file = "black-23.9.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:75a2dc41b183d4872d3a500d2b9c9016e67ed95738a3624f4751a0cb4818fe71"}, + {file = "black-23.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13a2e4a93bb8ca74a749b6974925c27219bb3df4d42fc45e948a5d9feb5122b7"}, + {file = "black-23.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:adc3e4442eef57f99b5590b245a328aad19c99552e0bdc7f0b04db6656debd80"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:8431445bf62d2a914b541da7ab3e2b4f3bc052d2ccbf157ebad18ea126efb91f"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:8fc1ddcf83f996247505db6b715294eba56ea9372e107fd54963c7553f2b6dfe"}, + {file = "black-23.9.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:7d30ec46de88091e4316b17ae58bbbfc12b2de05e069030f6b747dfc649ad186"}, + {file = "black-23.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:031e8c69f3d3b09e1aa471a926a1eeb0b9071f80b17689a655f7885ac9325a6f"}, + {file = "black-23.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:538efb451cd50f43aba394e9ec7ad55a37598faae3348d723b59ea8e91616300"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:638619a559280de0c2aa4d76f504891c9860bb8fa214267358f0a20f27c12948"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:a732b82747235e0542c03bf352c126052c0fbc458d8a239a94701175b17d4855"}, + {file = "black-23.9.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:cf3a4d00e4cdb6734b64bf23cd4341421e8953615cba6b3670453737a72ec204"}, + {file = "black-23.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf99f3de8b3273a8317681d8194ea222f10e0133a24a7548c73ce44ea1679377"}, + {file = "black-23.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:14f04c990259576acd093871e7e9b14918eb28f1866f91968ff5524293f9c573"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:c619f063c2d68f19b2d7270f4cf3192cb81c9ec5bc5ba02df91471d0b88c4c5c"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:6a3b50e4b93f43b34a9d3ef00d9b6728b4a722c997c99ab09102fd5efdb88325"}, + {file = "black-23.9.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c46767e8df1b7beefb0899c4a95fb43058fa8500b6db144f4ff3ca38eb2f6393"}, + {file = "black-23.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50254ebfa56aa46a9fdd5d651f9637485068a1adf42270148cd101cdf56e0ad9"}, + {file = "black-23.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:403397c033adbc45c2bd41747da1f7fc7eaa44efbee256b53842470d4ac5a70f"}, + {file = "black-23.9.1-py3-none-any.whl", hash = "sha256:6ccd59584cc834b6d127628713e4b6b968e5f79572da66284532525a042549f9"}, + {file = "black-23.9.1.tar.gz", hash = "sha256:24b6b3ff5c6d9ea08a8888f6977eae858e1f340d7260cf56d70a49823236b62d"}, ] [package.dependencies] @@ -93,7 +89,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -108,7 +103,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -120,7 +114,6 @@ files = [ name = "decorator" version = "5.1.1" description = "Decorators for Humans" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -132,7 +125,6 @@ files = [ 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 = [ @@ -147,7 +139,6 @@ tests = ["asttokens", "littleutils", "pytest", "rich"] name = "geographiclib" version = "2.0" description = "The geodesic routines from GeographicLib" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -159,7 +150,6 @@ files = [ name = "geopy" version = "2.4.0" description = "Python Geocoding Toolbox" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -183,7 +173,6 @@ timezone = ["pytz"] name = "ipython" version = "8.15.0" description = "IPython: Productive Interactive Computing" -category = "dev" optional = false python-versions = ">=3.9" files = [ @@ -222,7 +211,6 @@ test-extra = ["curio", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.21)", "pa name = "isort" version = "5.12.0" description = "A Python utility / library to sort Python imports." -category = "dev" optional = false python-versions = ">=3.8.0" files = [ @@ -240,7 +228,6 @@ requirements-deprecated-finder = ["pip-api", "pipreqs"] 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 = [ @@ -260,7 +247,6 @@ testing = ["Django (<3.1)", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] name = "matplotlib-inline" version = "0.1.6" description = "Inline Matplotlib backend for Jupyter" -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -275,7 +261,6 @@ traitlets = "*" name = "mypy" version = "1.5.1" description = "Optional static typing for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -321,7 +306,6 @@ reports = ["lxml"] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -333,7 +317,6 @@ files = [ name = "packaging" version = "23.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -345,7 +328,6 @@ files = [ name = "parso" version = "0.8.3" description = "A Python Parser" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -361,7 +343,6 @@ testing = ["docopt", "pytest (<6.0.0)"] name = "pathspec" version = "0.11.2" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -373,7 +354,6 @@ files = [ name = "pexpect" version = "4.8.0" description = "Pexpect allows easy control of interactive console applications." -category = "dev" optional = false python-versions = "*" files = [ @@ -388,7 +368,6 @@ ptyprocess = ">=0.5" name = "pickleshare" version = "0.7.5" description = "Tiny 'shelve'-like database with concurrency support" -category = "dev" optional = false python-versions = "*" files = [ @@ -400,7 +379,6 @@ files = [ name = "platformdirs" 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 = [ @@ -416,7 +394,6 @@ test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-co name = "pluscodes" version = "2022.1.3" description = "Compute Plus Codes (Open Location Codes)." -category = "main" optional = false python-versions = ">=3.10" files = [ @@ -430,7 +407,6 @@ dev = ["black (==22.3.0)", "build (==0.8.0)", "coverage (==6.4)", "isort (==5.8. 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 = [ @@ -445,7 +421,6 @@ wcwidth = "*" name = "ptyprocess" version = "0.7.0" description = "Run a subprocess in a pseudo terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -457,7 +432,6 @@ files = [ name = "pure-eval" version = "0.2.2" description = "Safely evaluate AST nodes without side effects" -category = "dev" optional = false python-versions = "*" files = [ @@ -472,7 +446,6 @@ tests = ["pytest"] 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 = [ @@ -487,7 +460,6 @@ plugins = ["importlib-metadata"] 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 = [ @@ -499,7 +471,6 @@ files = [ 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 = [ @@ -519,7 +490,6 @@ tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] 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 = [ @@ -531,7 +501,6 @@ files = [ name = "traitlets" version = "5.9.0" description = "Traitlets Python configuration system" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -547,7 +516,6 @@ test = ["argcomplete (>=2.0)", "pre-commit", "pytest", "pytest-mock"] name = "typing-extensions" version = "4.7.1" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -559,7 +527,6 @@ files = [ name = "wcwidth" version = "0.2.6" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" files = [ @@ -570,4 +537,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "f9270d7fb45c708e923f4afe041b26f77becb8e4adfcd517742e67f6b8f9d6b9" +content-hash = "3f2847d76a104121389f12d4f82841153b114d479c8ddba0c5a78994b544d0d6" diff --git a/pyproject.toml b/pyproject.toml index 8aeb854..59c1298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "surplus" -version = "2.1.1" +version = "2.2.0" description = "Python script to convert Google Maps Plus Codes to iOS Shortcuts-like shareable text." authors = ["Mark Joshwel "] license = "Unlicense" @@ -17,7 +17,7 @@ pluscodes = "^2022.1.3" geopy = "^2.3.0" [tool.poetry.group.dev.dependencies] -black = {extras = ["jupyter"], version = "^23.7.0"} +black = {extras = ["jupyter"], version = "^23.9.1"} mypy = "^1.5.1" isort = "^5.12.0" diff --git a/surplus/surplus.py b/surplus/surplus.py index 7ebfd0e..cf9e4fd 100644 --- a/surplus/surplus.py +++ b/surplus/surplus.py @@ -31,11 +31,14 @@ For more information, please refer to from argparse import ArgumentParser from collections import OrderedDict +from copy import deepcopy from dataclasses import dataclass from datetime import datetime, timedelta, timezone from enum import Enum from functools import lru_cache from hashlib import shake_256 +from json import loads as json_loads +from json.decoder import JSONDecodeError from platform import platform from socket import gethostname from sys import stderr, stdin, stdout @@ -45,6 +48,7 @@ from typing import ( Final, Generic, NamedTuple, + Protocol, Sequence, TextIO, TypeAlias, @@ -55,12 +59,9 @@ from uuid import getnode from geopy import Location as _geopy_Location # type: ignore from geopy.extra.rate_limiter import RateLimiter as _geopy_RateLimiter # type: ignore from geopy.geocoders import Nominatim as _geopy_Nominatim # type: ignore -from pluscodes import Area as _PlusCode_Area # type: ignore from pluscodes import PlusCode as _PlusCode # type: ignore -from pluscodes import decode as _PlusCode_decode # type: ignore from pluscodes import encode as _PlusCode_encode # type: ignore from pluscodes.validator import Validator as _PlusCode_Validator # type: ignore -from typing_extensions import Protocol from pluscodes.openlocationcode import ( # type: ignore # isort: skip recoverNearest as _PlusCode_recoverNearest, @@ -68,89 +69,177 @@ from pluscodes.openlocationcode import ( # type: ignore # isort: skip # constants -VERSION: Final[tuple[int, int, int]] = (2, 1, 1) +VERSION: Final[tuple[int, int, int]] = (2, 2, 0) VERSION_SUFFIX: Final[str] = "-local" BUILD_BRANCH: Final[str] = "future" BUILD_COMMIT: Final[str] = "latest" BUILD_DATETIME: Final[datetime] = datetime.now(timezone(timedelta(hours=8))) # using SGT CONNECTION_MAX_RETRIES: int = 9 CONNECTION_WAIT_SECONDS: int = 10 -SHAREABLE_TEXT_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", -) -SHAREABLE_TEXT_LINE_1_KEYS: Final[tuple[str, ...]] = ("building",) -SHAREABLE_TEXT_LINE_2_KEYS: Final[tuple[str, ...]] = ("highway",) +LOCALITY_GEOCODER_LEVEL: int = 13 # adjusts geocoder zoom level when +# geocoding latlong into an address -SHAREABLE_TEXT_LINE_3_KEYS: Final[tuple[str, ...]] = ( - "house_number", - "house_name", - "road", -) -# special line 3 keys for Italian addresses (IT) -SHAREABLE_TEXT_LINE_3_KEYS_IT: Final[tuple[str, ...]] = ( - "road", - "house_number", - "house_name", -) - -SHAREABLE_TEXT_LINE_4_KEYS: Final[tuple[str, ...]] = ( - "residential", - "neighbourhood", - "allotments", - "quarter", - "city_district", - "district", - "borough", - "suburb", - "subdivision", - "municipality", - "city", - "town", - "village", -) -SHAREABLE_TEXT_LINE_5_KEYS: Final[tuple[str, ...]] = ("postcode",) -SHAREABLE_TEXT_LINE_6_KEYS: Final[tuple[str, ...]] = ( - "region", - "county", - "state", - "state_district", - "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") -) -SHAREABLE_TEXT_LOCALITY: dict[str, tuple[str, ...]] = { - "default": ("city_district", "district", "city", *SHAREABLE_TEXT_LINE_6_KEYS), - "SG": ("country",), +# default shareable text line keys +SHAREABLE_TEXT_LINE_0_KEYS: dict[str, tuple[str, ...]] = { + "default": ( + "emergency", + "historic", + "military", + "natural", + "landuse", + "place", + "railway", + "man_made", + "aerialway", + "boundary", + "amenity", + "aeroway", + "club", + "craft", + "leisure", + "office", + "mountain_pass", + "shop", + "tourism", + "bridge", + "tunnel", + "waterway", + ), } +SHAREABLE_TEXT_LINE_1_KEYS: dict[str, tuple[str, ...]] = { + "default": ("building",), +} +SHAREABLE_TEXT_LINE_2_KEYS: dict[str, tuple[str, ...]] = { + "default": ("highway",), +} +SHAREABLE_TEXT_LINE_3_KEYS: dict[str, tuple[str, ...]] = { + "default": ( + "house_number", + "house_name", + "road", + ), +} +SHAREABLE_TEXT_LINE_4_KEYS: dict[str, tuple[str, ...]] = { + "default": ( + "residential", + "neighbourhood", + "allotments", + "quarter", + "city_district", + "district", + "borough", + "suburb", + "subdivision", + "municipality", + "city", + "town", + "village", + ), +} +SHAREABLE_TEXT_LINE_5_KEYS: dict[str, tuple[str, ...]] = { + "default": ("postcode",), +} +SHAREABLE_TEXT_LINE_6_KEYS: dict[str, tuple[str, ...]] = { + "default": ( + "region", + "county", + "state", + "state_district", + "country", + "continent", + ), +} +SHAREABLE_TEXT_LINE_SETTINGS: dict[str, dict[int, tuple[str, bool]]] = { + # line number: (string separator to use, whether to check for seen names) + "default": { + 0: (", ", False), + 1: (", ", False), + 2: (", ", False), + 3: (" ", False), + 4: (", ", True), + 5: (", ", False), + 6: (", ", False), + }, +} +SHAREABLE_TEXT_NAMES: dict[str, tuple[str, ...]] = { + "default": ( + SHAREABLE_TEXT_LINE_0_KEYS["default"] + + SHAREABLE_TEXT_LINE_1_KEYS["default"] + + SHAREABLE_TEXT_LINE_2_KEYS["default"] + + ("house_name", "road") + ), +} +SHAREABLE_TEXT_LOCALITY: dict[str, tuple[str, ...]] = { + "default": ( + "city_district", + "district", + "city", + *SHAREABLE_TEXT_LINE_6_KEYS["default"], + ), +} +SHAREABLE_TEXT_DEFAULT: Final[str] = "default" + +# special per-country key arrangements for SG/Singapore +SHAREABLE_TEXT_LOCALITY.update( + { + "SG": ("country",), + } +) + +# special per-country key arrangements for IT/Italy +SHAREABLE_TEXT_LINE_3_KEYS.update( + { + "IT": ( + "road", + "house_number", + "house_name", + ), + } +) +SHAREABLE_TEXT_LINE_5_KEYS.update( + { + "IT": ( + "postcode", + "region", + "county", + "state", + "state_district", + ), + }, +) +SHAREABLE_TEXT_LINE_6_KEYS.update( + { + "IT": ( + "country", + "continent", + ), + } +) +SHAREABLE_TEXT_LINE_SETTINGS.update( + {"IT": deepcopy(SHAREABLE_TEXT_LINE_SETTINGS)["default"]} +) +SHAREABLE_TEXT_LINE_SETTINGS["IT"][5] = (" ", False) + +# special per-country key arrangements for MY/Malaysia +SHAREABLE_TEXT_LINE_4_KEYS.update( + { + "MY": tuple(), + }, +) +SHAREABLE_TEXT_LINE_5_KEYS.update( + { + "MY": ( + "postcode", + *SHAREABLE_TEXT_LINE_4_KEYS["default"], + ), + }, +) +SHAREABLE_TEXT_LINE_SETTINGS.update( + {"MY": deepcopy(SHAREABLE_TEXT_LINE_SETTINGS)["default"]} +) +SHAREABLE_TEXT_LINE_SETTINGS["MY"][4] = (" ", False) +SHAREABLE_TEXT_LINE_SETTINGS["MY"][5] = (" ", True) -# adjusts geocoder zoom level when geocoding latlong into an address -LOCALITY_GEOCODER_LEVEL: int = 13 # exceptions @@ -819,6 +908,8 @@ class Behaviour(NamedTuple): whether to print version information and exit convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT what type to convert query to + using_termux_location: bool = False + treats query as a termux-location output json string, and parses it accordingly """ query: str | list[str] = "" @@ -829,6 +920,7 @@ class Behaviour(NamedTuple): debug: bool = False version_header: bool = False convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT + using_termux_location: bool = False # functions @@ -967,7 +1059,46 @@ def parse_query(behaviour: Behaviour) -> Result[Query]: file=behaviour.stderr, ) - # not a plus/local code, try to match for latlong or string query + # check if termux-location json + if behaviour.using_termux_location: + try: + termux_location_json = json_loads(original_query) + if not isinstance(termux_location_json, dict): + raise ValueError("parsed termux-location json is not a dict") + + return Result[Query]( + LatlongQuery( + Latlong( + latitude=termux_location_json["latitude"], + longitude=termux_location_json["longitude"], + ) + ) + ) + + except (JSONDecodeError, TypeError) as exc: + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error=ValueError("could not parse termux-location json"), + ) + + except KeyError as exc: + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error=ValueError( + "could not get 'latitude' or 'longitude' keys from termux-location json" + ), + ) + + except Exception as exc: + return Result[Query]( + LatlongQuery(EMPTY_LATLONG), + error=exc, + ) + + # unreachable + + # not a plus/local code/termux-location json, + # try to match for latlong or string query match split_query: case [single]: # possibly a: @@ -1081,11 +1212,19 @@ def handle_args() -> Behaviour: help=f"user agent string to use for geocoding service, defaults to fingerprinted user agent string", default=default_fingerprint, ) + parser.add_argument( + "-t", + "--using-termux-location", + action="store_true", + default=False, + help="treats input as a termux-location output json string, and parses it accordingly", + ) + # initialisation args = parser.parse_args() - query: str | list[str] = "" + # "-" stdin check if args.query == ["-"]: stdin_query: list[str] = [] @@ -1097,8 +1236,8 @@ def handle_args() -> Behaviour: else: query = args.query + # setup structures and return geocoding = SurplusDefaultGeocoding(args.user_agent) - behaviour = Behaviour( query=query, geocoder=geocoding.geocoder, @@ -1108,8 +1247,8 @@ def handle_args() -> Behaviour: debug=args.debug, version_header=args.version, convert_to_type=ConversionResultTypeEnum(args.convert_to), + using_termux_location=args.using_termux_location, ) - return behaviour @@ -1147,7 +1286,7 @@ def _generate_text( def _generate_text_line( line_number: int, line_keys: Sequence[str], - seperator: str = ", ", + separator: str = ", ", filter: Callable[[str], list[bool]] = lambda e: [True], ) -> str: """ @@ -1158,8 +1297,8 @@ def _generate_text( line number to prefix with line_keys: Sequence[str] list of keys to .get() from location dict - seperator: str = ", " - seperator to join elements with + separator: str = ", " + separator to join elements with 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 @@ -1199,9 +1338,37 @@ def _generate_text( ) continue - line = line_prefix + seperator.join(basket) + line = line_prefix + separator.join(basket) return (line + "\n") if (line != "") else "" + C = TypeVar("C") + + def stget(split_iso3166_2: list[str], line_keys: dict[str, C]) -> tuple[bool, C]: + """ + (internal function) + + arguments: + split_iso3166_2: list[str] + the dash-split iso 3166-2 country code + + line_keys: + the shareable text line keys dict to use + + returns tuple[bool, C] + bool: whether the a special key arrangement was used + C: dict content + """ + + DEFAULT = "default" + country: str = DEFAULT + if len(iso3166_2) >= 1: + country = split_iso3166_2[0] + + if country not in line_keys: + return False, line_keys[DEFAULT] + else: + return True, line_keys[country] + # 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) @@ -1220,48 +1387,45 @@ def _generate_text( file=behaviour.stderr, ) + n_used_special: int = 0 # number of special key arrangements used + # 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 - st_locality: tuple[str, ...] = () + used_special, st_line0_keys = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_0_KEYS) + n_used_special += used_special - # special key arrangements for edge cases in local/regional address formats - match split_iso3166_2: - case ["SG", *_]: # Singapore - if debug: - print( - "debug: _generate_text: " - f"using special key arrangements for '{iso3166_2}' (Singapore)", - file=behaviour.stderr, - ) + used_special, st_line1_keys = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_1_KEYS) + n_used_special += used_special - st_locality = SHAREABLE_TEXT_LOCALITY[split_iso3166_2[0]] + used_special, st_line2_keys = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_2_KEYS) + n_used_special += used_special - case ["IT", *_]: # Italy - if debug: - print( - "debug: _generate_text: " - f"using special key arrangements for '{iso3166_2}' (Italy)", - file=behaviour.stderr, - ) + used_special, st_line3_keys = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_3_KEYS) + n_used_special += used_special - st_line3_keys = SHAREABLE_TEXT_LINE_3_KEYS_IT + used_special, st_line4_keys = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_4_KEYS) + n_used_special += used_special - case _: # default - if debug: - print( - "debug: _generate_text: " - f"using default key arrangements for '{iso3166_2}'", - file=behaviour.stderr, - ) + used_special, st_line5_keys = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_5_KEYS) + n_used_special += used_special - st_locality = SHAREABLE_TEXT_LOCALITY["default"] + used_special, st_line6_keys = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_6_KEYS) + n_used_special += used_special + + used_special, st_names = stget(split_iso3166_2, SHAREABLE_TEXT_NAMES) + n_used_special += used_special + + used_special, st_locality = stget(split_iso3166_2, SHAREABLE_TEXT_LOCALITY) + n_used_special += used_special + + used_special, st_line_settings = stget(split_iso3166_2, SHAREABLE_TEXT_LINE_SETTINGS) + n_used_special += used_special + + if n_used_special and debug: + print( + "debug: _generate_text: " + f"using special key arrangements for '{iso3166_2}' (Singapore)", + file=behaviour.stderr, + ) # start generating text match mode: @@ -1283,54 +1447,39 @@ def _generate_text( str(location.get(detail, "")) for detail in st_line6_keys ] - text.append( - _generate_text_line( - line_number=0, - line_keys=st_line0_keys, - ) - ) - text.append( - _generate_text_line( - line_number=1, - line_keys=st_line1_keys, - ) - ) - text.append( - _generate_text_line( - line_number=2, - line_keys=st_line2_keys, - ) - ) - text.append( - _generate_text_line( - line_number=3, - line_keys=st_line3_keys, - seperator=" ", - ) - ) - text.append( - _generate_text_line( - line_number=4, - line_keys=st_line4_keys, - filter=lambda ak: [ - # everything here should be True if the element is to be kept + for ( + line_number, + line_keys, + ) in enumerate( + [ + st_line0_keys, + st_line1_keys, + st_line2_keys, + st_line3_keys, + st_line4_keys, + st_line5_keys, + st_line6_keys, + ] + ): + line_separator, line_filter = st_line_settings[line_number] + + # filter: everything here should be True if the element is to be kept + if line_filter is False: + _filter = lambda e: [True] + else: + _filter = lambda ak: [ 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( + line_number=line_number, + line_keys=line_keys, + separator=line_separator, + filter=_filter, + ) ) - ) - text.append( - _generate_text_line( - line_number=5, - line_keys=st_line5_keys, - ) - ) - text.append( - _generate_text_line( - line_number=6, - line_keys=st_line6_keys, - ) - ) return "".join(_unique(text)).rstrip() @@ -1602,9 +1751,9 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: def cli() -> int: """command-line entry point, returns an exit code int""" - # handle arguments and print version header behaviour = handle_args() + # handle arguments and print version header print( f"surplus version {'.'.join([str(v) for v in VERSION])}{VERSION_SUFFIX}" + (f", debug mode" if behaviour.debug else "") diff --git a/test.py b/test.py index 875d626..79871b0 100644 --- a/test.py +++ b/test.py @@ -1,5 +1,3 @@ -# type: ignore - """ surplus test runner ------------------- @@ -58,7 +56,7 @@ class TestFailure(NamedTuple): tests: list[ContinuityTest] = [ ContinuityTest( query="8R3M+F8 Singapore", - expected=("Wisma Atria\n" "435 Orchard Road\n" "238877\n" "Central, Singapore"), + expected=[("Wisma Atria\n" "435 Orchard Road\n" "238877\n" "Central, Singapore")], ), ContinuityTest( query="9R3J+R9 Singapore", @@ -96,6 +94,13 @@ tests: list[ContinuityTest] = [ "4072\n" "Queensland, Australia" ), + ( + "The University of Queensland\n" + "Hawken Drive\n" + "St Lucia, Greater Brisbane\n" + "4072\n" + "Queensland, Australia" + ), ], ), ContinuityTest( @@ -107,7 +112,14 @@ tests: list[ContinuityTest] = [ "Bukit Timah\n" "599489\n" "Northwest, Singapore" - ) + ), + ( + "Ngee Ann Polytechnic\n" + "535 Clementi Road\n" + "Ewart Park, Bukit Timah\n" + "599489\n" + "Northwest, Singapore" + ), ], ), ContinuityTest( @@ -139,6 +151,28 @@ tests: list[ContinuityTest] = [ ), ], ), + ContinuityTest( + query="J286+WV San Cesario sul Panaro, Modena, Italy", + expected=[ + ( + "Via Emilia 1193a\n" + "Unione dei comuni del Sorbara, Sant'Anna\n" + "41018 Modena Emilia-Romagna\n" + "Italia" + ), + ], + ), + ContinuityTest( + query="GQ2G+GX Johor Bahru, Johor, Malaysia", + expected=[ + ( + "The Mall, Mid Valley Southkey\n" + "Jalan Bakar Batu\n" + "81100 Taman Sentosa Johor Bahru\n" + "Iskandar Malaysia, Johor, Malaysia" + ), + ], + ), ]