From c1cb938d26bd6dd0eefac070ed602207dfd4b903 Mon Sep 17 00:00:00 2001 From: KimgiaiiWuyi <444835641@qq.com> Date: Sun, 16 Oct 2022 01:11:57 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=89=20=E5=8E=9F=E7=A5=9E=E5=9C=B0?= =?UTF-8?q?=E5=9B=BEAPI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + .gitmodules | 3 + .../GetMapImage/GenshinMap/.editorconfig | 16 + .../GenshinMap/.github/workflows/codecov.yml | 53 ++ .../GetMapImage/GenshinMap/.gitignore | 663 ++++++++++++++ .../GenshinMap/.pre-commit-config.yaml | 22 + .../GetMapImage/GenshinMap/LICENSE | 19 + .../GetMapImage/GenshinMap/README.md | 65 ++ .../GenshinMap/genshinmap/__init__.py | 17 + .../GetMapImage/GenshinMap/genshinmap/exc.py | 13 + .../GetMapImage/GenshinMap/genshinmap/img.py | 85 ++ .../GenshinMap/genshinmap/models.py | 187 ++++ .../GenshinMap/genshinmap/request.py | 176 ++++ .../GenshinMap/genshinmap/utils.py | 220 +++++ .../GetMapImage/GenshinMap/poetry.lock | 819 ++++++++++++++++++ .../GetMapImage/GenshinMap/pyproject.toml | 51 ++ .../GetMapImage/GenshinMap/tests/anchors.json | 71 ++ .../GetMapImage/GenshinMap/tests/conftest.py | 13 + .../GetMapImage/GenshinMap/tests/labels.json | 96 ++ .../GetMapImage/GenshinMap/tests/maps.json | 13 + .../GetMapImage/GenshinMap/tests/page.json | 44 + .../GetMapImage/GenshinMap/tests/points.json | 31 + .../GenshinMap/tests/spots/kinds.json | 17 + .../GenshinMap/tests/spots/spots.json | 24 + .../GetMapImage/GenshinMap/tests/test_exc.py | 17 + .../GenshinMap/tests/test_kmeans.py | 21 + .../GenshinMap/tests/test_models.py | 17 + .../GenshinMap/tests/test_request.py | 245 ++++++ .../GenshinMap/tests/test_utils.py | 94 ++ fastapi_genshin_map/GetMapImage/__init__.py | 0 .../GetMapImage/get_map_image.py | 188 ++++ fastapi_genshin_map/GetMapImage/logger.py | 24 + .../GetMapImage/texture2d/mark_god.png | Bin 0 -> 6193 bytes .../GetMapImage/texture2d/mark_quest.png | Bin 0 -> 3695 bytes .../GetMapImage/texture2d/mark_trans.png | Bin 0 -> 4757 bytes fastapi_genshin_map/main.py | 26 + 36 files changed, 3355 insertions(+) create mode 100644 .gitmodules create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/.editorconfig create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/.github/workflows/codecov.yml create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/.gitignore create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/.pre-commit-config.yaml create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/LICENSE create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/README.md create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/__init__.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/exc.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/img.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/models.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/request.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/utils.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/poetry.lock create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/pyproject.toml create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/anchors.json create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/conftest.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/labels.json create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/maps.json create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/page.json create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/points.json create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/kinds.json create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/spots.json create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_exc.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_kmeans.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_models.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_request.py create mode 100644 fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_utils.py create mode 100644 fastapi_genshin_map/GetMapImage/__init__.py create mode 100644 fastapi_genshin_map/GetMapImage/get_map_image.py create mode 100644 fastapi_genshin_map/GetMapImage/logger.py create mode 100644 fastapi_genshin_map/GetMapImage/texture2d/mark_god.png create mode 100644 fastapi_genshin_map/GetMapImage/texture2d/mark_quest.png create mode 100644 fastapi_genshin_map/GetMapImage/texture2d/mark_trans.png create mode 100644 fastapi_genshin_map/main.py diff --git a/.gitignore b/.gitignore index b6e4761..76cb155 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,8 @@ dmypy.json # Pyre type checker .pyre/ + +# 地图资源 +fastapi_genshin_map/GetMapImage/map_data +fastapi_genshin_map/GetMapImage/resource_data +fastapi_genshin_map/GetMapImage/genshinmap.log \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9571502 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "fastapi_genshin_map/GenshinMap"] + path = fastapi_genshin_map/GetMapImage/GenshinMap + url = https://github.com/MingxuanGame/GenshinMap.git diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/.editorconfig b/fastapi_genshin_map/GetMapImage/GenshinMap/.editorconfig new file mode 100644 index 0000000..1a90675 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://EditorConfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{*.py,*.pyi}] +indent_size = 4 + +[*.md] +trim_trailing_whitespace = false diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/.github/workflows/codecov.yml b/fastapi_genshin_map/GetMapImage/GenshinMap/.github/workflows/codecov.yml new file mode 100644 index 0000000..51b2485 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/.github/workflows/codecov.yml @@ -0,0 +1,53 @@ +name: Unittest + +on: + push: + branches: + - master + pull_request: + +jobs: + test: + name: Coverage + strategy: + matrix: + version: ["3.8", "3.9", "3.10"] + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + env: + OS: ${{ matrix.os }} + PYTHON_VERSION: ${{ matrix.python-version }} + + steps: + - uses: actions/checkout@v3 + + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.version }} + architecture: "x64" + + - uses: Gr1N/setup-poetry@v7 + + - id: poetry-cache + run: echo "::set-output name=dir::$(poetry config virtualenvs.path)" + shell: bash + + - uses: actions/cache@v2 + with: + path: ${{ steps.poetry-cache.outputs.dir }} + key: ${{ runner.os }}-poetry-${{ steps.python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install dependencies + run: poetry install + shell: bash + + - name: Run Pytest + run: | + poetry run pytest -n auto --cov-report xml + + - name: Upload report + uses: codecov/codecov-action@v3 + with: + env_vars: OS,PYTHON_VERSION + files: ./coverage.xml + flags: unittests diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/.gitignore b/fastapi_genshin_map/GetMapImage/GenshinMap/.gitignore new file mode 100644 index 0000000..a919378 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/.gitignore @@ -0,0 +1,663 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,jetbrains+all,python +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudio,visualstudiocode,jetbrains+all,python + +### JetBrains+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### JetBrains+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### VisualStudioCode ### +.vscode/* + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +# Support for Project snippet scope +.vscode/*.code-snippets + +# Ignore code-workspaces +*.code-workspace + +### VisualStudio ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools + +# Local History for Visual Studio Code + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +### VisualStudio Patch ### +# Additional files built by Visual Studio + +# End of https://www.toptal.com/developers/gitignore/api/visualstudio,visualstudiocode,jetbrains+all,python,data diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/.pre-commit-config.yaml b/fastapi_genshin_map/GetMapImage/GenshinMap/.pre-commit-config.yaml new file mode 100644 index 0000000..90754e1 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +ci: + autofix_commit_msg: ":rotating_light: auto fix by pre-commit-ci" + autofix_prs: true + autoupdate_branch: master + autoupdate_schedule: monthly + autoupdate_commit_msg: ":arrow_up: auto update by pre-commit-ci" + +repos: + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + + - repo: https://github.com/psf/black + rev: 22.8.0 + hooks: + - id: black + + - repo: https://github.com/pycqa/flake8 + rev: 5.0.4 + hooks: + - id: flake8 diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/LICENSE b/fastapi_genshin_map/GetMapImage/GenshinMap/LICENSE new file mode 100644 index 0000000..f9e7462 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/LICENSE @@ -0,0 +1,19 @@ +The MIT License (MIT) +Copyright (c) 2022 MingxuanGame + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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 OR +COPYRIGHT HOLDERS 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. diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/README.md b/fastapi_genshin_map/GetMapImage/GenshinMap/README.md new file mode 100644 index 0000000..afd4b96 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/README.md @@ -0,0 +1,65 @@ +# GenshinMap + +[![License](https://img.shields.io/github/license/MingxuanGame/GenshinMap?style=flat-square)](https://github.com/MingxuanGame/GenshinMap/blob/master/LICENSE) +[![QQ群](https://img.shields.io/badge/QQ%E7%BE%A4-929275476-success?style=flat-square)](https://jq.qq.com/?_wv=1027&k=C7XY04F1) + +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) +[![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?&labelColor=ef8336)](https://pycqa.github.io/isort/) +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/MingxuanGame/GenshinMap/master.svg)](https://results.pre-commit.ci/latest/github/MingxuanGame/GenshinMap/master) +[![Codecov](https://codecov.io/gh/MingxuanGame/GenshinMap/branch/master/graph/badge.svg?token=SVSXXE6MBQ)](https://app.codecov.io/gh/MingxuanGame/GenshinMap) + +GenshinMap 是一个原神米游社大地图 API 的包装,用于简易获取大地图数据 + +## 快速开始 + +```python +import asyncio + +from genshinmap import utils, models, request + + +async def main(): + map_id = models.MapID.teyvat + # 获取地图数据 + maps = await request.get_maps(map_id) + # 获取资源列表 + labels = await request.get_labels(map_id) + # 获取坐标 + points = await request.get_points(map_id) + + # 获取单片地图 + map_image = await utils.get_map_by_pos(maps.detail, 1024) + # 获取传送锚点坐标 + transmittable = utils.get_points_by_id(3, points) + # 转换坐标 + transmittable_converted = utils.convert_pos( + transmittable, maps.detail.origin + ) + + # 获取地图锚点之一 + anchors = await request.get_anchors(map_id)[0] + # 转换地图锚点偏左坐标 + anchors_converted = utils.convert_pos( + anchors.get_children_all_left_point(), maps.detail.origin + ) + + +if __name__ == "__main__": + asyncio.run(main()) +``` + +## 致谢 + +[观测大地图](https://webstatic.mihoyo.com/app/ys-map-cn/index.html) —— 本项目所包装的地图 + +[H-K-Y/Genshin_Impact_bot](https://github.com/H-K-Y/Genshin_Impact_bot) —— 大地图数据的处理,本项目的蓝本 + +## 计划 + +* [ ] 补全模型相关文档 +* [ ] yuanshen.site 支持 +* *More...* + +## 许可 + +[MIT](./LICENSE) diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/__init__.py b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/__init__.py new file mode 100644 index 0000000..a6aa5ae --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/__init__.py @@ -0,0 +1,17 @@ +from .models import Maps as Maps # noqa: F401 +from .models import Tree as Tree # noqa: F401 +from .models import MapID as MapID # noqa: F401 +from .models import Point as Point # noqa: F401 +from .models import Slice as Slice # noqa: F401 +from .models import MapInfo as MapInfo # noqa: F401 +from .models import XYPoint as XYPoint # noqa: F401 +from .utils import make_map as make_map # noqa: F401 +from .request import get_maps as get_maps # noqa: F401 +from .exc import StatusError as StatusError # noqa: F401 +from .request import get_labels as get_labels # noqa: F401 +from .request import get_points as get_points # noqa: F401 +from .utils import convert_pos as convert_pos # noqa: F401 +from .utils import get_map_by_pos as get_map_by_pos # noqa: F401 +from .utils import get_points_by_id as get_points_by_id # noqa: F401 + +__all__ = ["utils", "request", "exc", "models", "imgs"] diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/exc.py b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/exc.py new file mode 100644 index 0000000..b28fb60 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/exc.py @@ -0,0 +1,13 @@ +class StatusError(ValueError): + """米游社状态异常""" + + def __init__(self, status: int, message: str, *args: object) -> None: + super().__init__(status, message, *args) + self.status = status + self.message = message + + def __str__(self) -> str: + return f"miHoYo API {self.status}: {self.message}" + + def __repr__(self) -> str: + return f"" diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/img.py b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/img.py new file mode 100644 index 0000000..4facad1 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/img.py @@ -0,0 +1,85 @@ +from __future__ import annotations + +from typing import List, Tuple + +import numpy as np +from sklearn.cluster import KMeans +from shapely.geometry import Point, Polygon + +from .models import XYPoint + +Pos = Tuple[float, float] +Poses = List[XYPoint] +Points = List[Point] + + +def k_means_points( + points: List[XYPoint], length: int = 500, clusters: int = 3 +) -> List[Tuple[XYPoint, XYPoint, Poses]]: + """ + 通过 K-Means 获取集群坐标列表 + + 参数: + points: `list[XYPoint]` + 坐标列表,建议预先使用 `convert_pos` 进行坐标转换 + + length: `int` (default: 500) + 区域大小,如果太大则可能一个点会在多个集群中 + + clusters: `int` (default: 3) + 集群数量 + + 返回: + `list[tuple[XYPoint, XYPoint, list[XYPoint]]]` + + tuple 中: + 第 1 个元素为集群最左上方的点 + 第 2 个元素为集群最右下方的点 + 第 3 个元素为集群内所有点 + + list 按照集群内点的数量降序排序 + + 提示: + length: + +---------------------+ + │ │ + │ │ + │ │ + |--length--|--length--│ + │ │ + │ │ + │ │ + +---------------------+ + """ + pos_array = np.array(points) + k_means = KMeans(n_clusters=clusters).fit(pos_array) + points_temp: List[Points] = [] + for k_means_pos in k_means.cluster_centers_: + x = ( + k_means_pos[0] - length if k_means_pos[0] > length else 0, + k_means_pos[0] + length, + ) + y = ( + k_means_pos[1] - length if k_means_pos[1] > length else 0, + k_means_pos[1] + length, + ) + path = Polygon( + [(x[0], y[0]), (x[0], y[1]), (x[1], y[1]), (x[1], y[0])] + ) + + points_temp.append( + [Point(i) for i in pos_array if path.contains(Point(i))] + ) + return_list = [] + for i in points_temp: + pos_array_ = np.array([p.xy for p in i]) + return_list.append( + ( + XYPoint(pos_array_[:, 0].min(), pos_array_[:, 1].min()), + XYPoint(pos_array_[:, 0].max(), pos_array_[:, 1].max()), + list(map(lambda p: XYPoint(p.x, p.y), i)), + ) + ) + return sorted( + return_list, key=lambda pos_tuple: len(pos_tuple[2]), reverse=True + ) diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/models.py b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/models.py new file mode 100644 index 0000000..5e97c07 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/models.py @@ -0,0 +1,187 @@ +from __future__ import annotations + +from enum import IntEnum +from typing import List, Tuple, Optional, NamedTuple + +from pydantic import HttpUrl, BaseModel, validator + + +class MapID(IntEnum): + """地图 ID""" + + teyvat = 2 + """提瓦特""" + enkanomiya = 7 + """渊下宫""" + chasm = 9 + """层岩巨渊·地下矿区""" + # golden_apple_archipelago = 12 + """金苹果群岛""" + + +class Label(BaseModel): + id: int + name: str + icon: HttpUrl + parent_id: int + depth: int + node_type: int + jump_type: int + jump_target_id: int + display_priority: int + children: list + activity_page_label: int + area_page_label: List[int] + is_all_area: bool + + +class Tree(BaseModel): + id: int + name: str + icon: str + parent_id: int + depth: int + node_type: int + jump_type: int + jump_target_id: int + display_priority: int + children: List[Label] + activity_page_label: int + area_page_label: List + is_all_area: bool + + +class Point(BaseModel): + id: int + label_id: int + x_pos: float + y_pos: float + author_name: str + ctime: str + display_state: int + + +class Slice(BaseModel): + url: HttpUrl + + +class Maps(BaseModel): + slices: List[HttpUrl] + origin: List[int] + total_size: List[int] + padding: List[int] + + @validator("slices", pre=True) + def slices_to_list(cls, v): + urls: List[str] = [] + for i in v: + urls.extend(j["url"] for j in i) + return urls + + +class MapInfo(BaseModel): + id: int + name: str + parent_id: int + depth: int + detail: Maps + node_type: int + children: list + icon: Optional[HttpUrl] + ch_ext: Optional[str] + + @validator("detail", pre=True) + def detail_str_to_maps(cls, v): + return Maps.parse_raw(v) + + +class XYPoint(NamedTuple): + x: float + y: float + + +class Kind(BaseModel): + id: int + name: str + icon_id: int + icon_url: HttpUrl + is_game: int + + +class SpotKinds(BaseModel): + list: List[Kind] + is_sync: bool + already_share: bool + + +class Spot(BaseModel): + id: int + name: str + content: str + kind_id: int + spot_icon: str + x_pos: float + y_pos: float + nick_name: str + avatar_url: HttpUrl + status: int + + +class SubAnchor(BaseModel): + id: int + name: str + l_x: int + l_y: int + r_x: int + r_y: int + app_sn: str + parent_id: str + map_id: str + sort: int + + +class Anchor(BaseModel): + id: int + name: str + l_x: int + l_y: int + r_x: int + r_y: int + app_sn: str + parent_id: str + map_id: str + children: List[SubAnchor] + sort: int + + def get_children_all_left_point(self) -> List[XYPoint]: + """获取所有子锚点偏左的 `XYPoint` 坐标""" + return [XYPoint(x=i.l_x, y=i.l_y) for i in self.children] + + def get_children_all_right_point(self) -> List[XYPoint]: + """获取所有子锚点偏右的 `XYPoint` 坐标""" + return [XYPoint(x=i.r_x, y=i.r_y) for i in self.children] + + +class PageLabel(BaseModel): + id: int + name: str + type: int + pc_icon_url: str + mobile_icon_url: str + sort: int + pc_icon_url2: str + map_id: int + jump_url: str + jump_type: str + center: Optional[Tuple[float, float]] + zoom: Optional[float] + + @validator("center", pre=True) + def center_str_to_tuple(cls, v: str) -> Optional[Tuple[float, float]]: + if v and (splitted := v.split(",")): + return tuple(map(float, splitted)) + + @validator("zoom", pre=True) + def zoom_str_to_float(cls, v: str): + if v: + return float(v) diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/request.py b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/request.py new file mode 100644 index 0000000..6b0c2a4 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/request.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +from typing import Any, Dict, List, Tuple + +from httpx import Response, AsyncClient + +from .exc import StatusError +from .models import ( + Spot, + Tree, + MapID, + Point, + Anchor, + MapInfo, + PageLabel, + SpotKinds, +) + +API_CLIENT = AsyncClient( + base_url="https://api-takumi.mihoyo.com/common/map_user/ys_obc/v1/map" +) +Spots = Dict[int, List[Spot]] + + +async def _request( + endpoint: str, client: AsyncClient = API_CLIENT +) -> Dict[str, Any]: + resp = await client.get(endpoint) + resp.raise_for_status() + data: Dict[str, Any] = resp.json() + if data["retcode"] != 0: + raise StatusError(data["retcode"], data["message"]) + return data["data"] + + +async def get_labels(map_id: MapID) -> List[Tree]: + """ + 获取米游社资源列表 + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[Tree]` + """ + data = await _request(f"/label/tree?map_id={map_id}&app_sn=ys_obc") + return [Tree.parse_obj(i) for i in data["tree"]] + + +async def get_points(map_id: MapID) -> List[Point]: + """ + 获取米游社坐标列表 + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[Point]` + """ + data = await _request(f"/point/list?map_id={map_id}&app_sn=ys_obc") + return [Point.parse_obj(i) for i in data["point_list"]] + + +async def get_maps(map_id: MapID) -> MapInfo: + """ + 获取米游社地图 + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `MapInfo` + """ + data = await _request(f"/info?map_id={map_id}&app_sn=ys_obc&lang=zh-cn") + return MapInfo.parse_obj(data["info"]) + + +async def get_spot_from_game( + map_id: MapID, cookie: str +) -> Tuple[Spots, SpotKinds]: + """ + 获取游戏内标点 + + 注意:每十分钟只能获取一次,否则会 -2000 错误 + + 参数: + map_id: `MapID` + 地图 ID + + cookie: `str` + 米游社 Cookie + + 返回: + `tuple[Spots, SpotKinds]` + """ + + def _raise_for_retcode(resp: Response) -> Dict[str, Any]: + resp.raise_for_status() + data: dict[str, Any] = resp.json() + if data["retcode"] != 0: + raise StatusError(data["retcode"], data["message"]) + return data["data"] + + # 1. 申请刷新 + resp = await API_CLIENT.post( + "/spot_kind/sync_game_spot", + json={ + "map_id": str(map_id.value), + "app_sn": "ys_obc", + "lang": "zh-cn", + }, + headers={"Cookie": cookie}, + ) + _raise_for_retcode(resp) + + # 2. 获取类别 + resp = await API_CLIENT.get( + "/spot_kind/get_spot_kinds?map_id=2&app_sn=ys_obc&lang=zh-cn", + headers={"Cookie": cookie}, + ) + data = _raise_for_retcode(resp) + spot_kinds_data = SpotKinds.parse_obj(data) + ids = [kind.id for kind in spot_kinds_data.list] + + # 3.获取坐标 + resp = await API_CLIENT.post( + "/spot/get_map_spots_by_kinds", + json={ + "map_id": str(map_id.value), + "app_sn": "ys_obc", + "lang": "zh-cn", + "kind_ids": ids, + }, + ) + data = _raise_for_retcode(resp) + spots: Spots = {} + for k, v in data["spots"].items(): + spots[int(k)] = [Spot.parse_obj(i) for i in v["list"]] + return spots, spot_kinds_data + + +async def get_page_label(map_id: MapID) -> List[PageLabel]: + """ + 获取米游社大地图标签(例如蒙德,龙脊雪山等) + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[PageLabel]` + """ + data = await _request( + f"/get_map_pageLabel?map_id={map_id}&app_sn=ys_obc&lang=zh-cn", + ) + return [PageLabel.parse_obj(i) for i in data["list"]] + + +async def get_anchors(map_id: MapID) -> List[Anchor]: + """ + 获取米游社地图锚点,含子锚点(例如珉林-庆云顶等) + + 参数: + map_id: `MapID` + 地图 ID + + 返回: + `list[Anchor]` + """ + data = await _request( + f"/map_anchor/list?map_id={map_id}&app_sn=ys_obc&lang=zh-cn", + ) + return [Anchor.parse_obj(i) for i in data["list"]] diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/utils.py b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/utils.py new file mode 100644 index 0000000..d747225 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/genshinmap/utils.py @@ -0,0 +1,220 @@ +from __future__ import annotations + +from math import ceil +from io import BytesIO +from typing import List, Tuple, Union +from asyncio import gather, create_task + +from PIL import Image +from httpx import AsyncClient + +from .models import Maps, Point, XYPoint + +CLIENT = AsyncClient() + + +async def get_img(url: str) -> Image.Image: + resp = await CLIENT.get(url) + resp.raise_for_status() + return Image.open(BytesIO(resp.read())) + + +async def make_map(map: Maps) -> Image.Image: + """ + 获取所有地图并拼接 + + 警告:可能导致内存溢出 + + 在测试中,获取并合成「提瓦特」地图时占用了约 1.4 GiB + + 建议使用 `genshinmap.utils.get_map_by_pos` 获取地图单片 + + 参数: + map: `Maps` + 地图数据,可通过 `get_maps` 获取 + + 返回: + `PIL.Image.Image` 对象 + + 另见: + `get_map_by_pos` + """ + img = Image.new("RGBA", tuple(map.total_size)) + x = 0 + y = 0 + maps: List[Image.Image] = await gather( + *[create_task(get_img(url)) for url in map.slices] + ) + for m in maps: + img.paste(m, (x, y)) + x += 4096 + if x >= map.total_size[0]: + x = 0 + y += 4096 + return img + + +async def get_map_by_pos( + map: Maps, x: Union[int, float], y: Union[int, float] = 0 +) -> Image.Image: + """ + 根据横坐标获取地图单片 + + 参数: + map: `Maps` + 地图数据,可通过 `get_maps` 获取 + + x: `int | float` + 横坐标 + + y: `int | float` (default: 0) + 纵坐标 + + 返回: + `PIL.Image.Image` 对象 + """ + return await get_img(map.slices[_pos_to_index(x, y)]) + + +def get_points_by_id(id_: int, points: List[Point]) -> List[XYPoint]: + """ + 根据 Label ID 获取坐标点 + + 参数: + id_: `int` + Label ID + + points: `list[Point]` + 米游社坐标点列表,可通过 `get_points` 获取 + + 返回: + `list[XYPoint]` + """ + return [ + XYPoint(point.x_pos, point.y_pos) + for point in points + if point.label_id == id_ + ] + + +def convert_pos(points: List[XYPoint], origin: List[int]) -> List[XYPoint]: + """ + 将米游社资源坐标转换为以左上角为原点的坐标系的坐标 + + 参数: + points: `list[XYPoint]` + 米游社资源坐标 + + origin: `list[Point]` + 米游社地图 Origin,可通过 `get_maps` 获取 + + 返回: + `list[XYPoint]` + + 示例: + >>> from genshinmap.models import XYPoint + >>> points = [XYPoint(1200, 5000), XYPoint(-4200, 1800)] + >>> origin = [4844,4335] + >>> convert_pos(points, origin) + [XYPoint(x=6044, y=9335), XYPoint(x=644, y=6135)] + """ + return [XYPoint(x + origin[0], y + origin[1]) for x, y in points] + + +def convert_pos_crop( + top_left_index: int, points: List[XYPoint] +) -> List[XYPoint]: + """ + 根据左上角地图切片的索引转换坐标(已经通过 `convert_pos` 转换) + + 参数: + top_left_index: `int` + 左上角地切片图的索引 + + points: `list[XYPoint]` + 米游社资源坐标(已经通过 `convert_pos` 转换) + + 返回: + `list[XYPoint]` + + 示例: + >>> from genshinmap.models import XYPoint + >>> points = [XYPoint(0, 0), XYPoint(20, 20)] + >>> convert_pos_crop(0, points) + [XYPoint(x=0, y=0), XYPoint(x=20, y=20)] + >>> convert_pos_crop(1, points) + [XYPoint(x=-4096, y=0), XYPoint(x=-4076, y=20)] + >>> convert_pos_crop(4, points) + [XYPoint(x=0, y=-4096), XYPoint(x=20, y=-4076)] + >>> convert_pos_crop(5, points) + [XYPoint(x=-4096, y=-4096),XYPoint(x=-4076, y=-4076)] + """ + y, x = divmod(top_left_index, 4) + if x == y == 0: + return points + x *= 4096 + y *= 4096 + result = [] + for point in points: + px, py = point + result.append(XYPoint(px - x, py - y)) + return result + + +def _pos_to_index(x: Union[int, float], y: Union[int, float]) -> int: + # 4 * (y // 4096) {0,4,8} + # x // 4096 {0,1,2,3} + return 4 * (int(y // 4096)) + int(x // 4096) + + +def _generate_matrix( + top_left: int, top_right: int, bottom_left: int +) -> List[int]: + result = [] + while True: + result.extend(iter(range(top_left, top_right + 1))) + if top_left == bottom_left: + break + top_left_copy = top_left + top_left += 4 + top_right = top_left + (top_right - top_left_copy) + return result + + +def crop_image_and_points( + points: List[XYPoint], +) -> Tuple[List[int], int, List[XYPoint]]: + """ + 根据坐标(需通过 `convert_pos` 转换)计算地图切片索引,间隔(即贴完一张图片后还需要贴几张才换行)和转换后的坐标 + + 参数: + points: `list[XYPoint]` + 米游社资源坐标(已经通过 `convert_pos` 转换) + + 返回: + `tuple[list[int], int, list[XYPoint]]` + + 第 1 个元素为地图切片索引的列表 + 第 2 个元素为间隔 + 第 3 个元素为使用 `convert_pos_crop` 转换后的坐标 + + 示例: + >>> points = [XYPoint(x=4200, y=8000), XYPoint(x=4150, y=10240)] + >>> crop_image_and_points(points) + ([5, 9], 0, [XYPoint(x=104, y=3904), XYPoint(x=54, y=6144)]) + """ + xs = [p.x for p in points] + ys = [p.y for p in points] + x1, y1 = min(xs), min(ys) + x2, y2 = max(xs), max(ys) + + x1 = int(x1 // 4096 * 4096) + x2 = x1 if x1 + 4096 >= x2 else ceil(x2 / 4096) * 4096 - 4096 + y1 = int(y1 // 4096 * 4096) + y2 = y1 if y1 + 4096 >= y2 else ceil(y2 / 4096) * 4096 - 4096 + index_x1, index_x2 = _pos_to_index(x1, y1), _pos_to_index(x2, y1) + return ( + _generate_matrix(index_x1, index_x2, _pos_to_index(x1, y2)), + index_x2 - index_x1, + convert_pos_crop(index_x1, points), + ) diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/poetry.lock b/fastapi_genshin_map/GetMapImage/GenshinMap/poetry.lock new file mode 100644 index 0000000..a7f9a84 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/poetry.lock @@ -0,0 +1,819 @@ +[[package]] +name = "anyio" +version = "3.6.1" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] +test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16)"] + +[[package]] +name = "attrs" +version = "22.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] + +[[package]] +name = "black" +version = "22.6.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2022.6.15" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.5" +description = "Cross-platform colored terminal text." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "6.5.0" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "distlib" +version = "0.3.5" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "execnet" +version = "1.9.0" +description = "execnet: rapid multi-Python deployment" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +testing = ["pre-commit"] + +[[package]] +name = "filelock" +version = "3.7.1" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + +[[package]] +name = "flake8" +version = "5.0.4" +description = "the modular source code checker: pep8 pyflakes and co" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +mccabe = ">=0.7.0,<0.8.0" +pycodestyle = ">=2.9.0,<2.10.0" +pyflakes = ">=2.5.0,<2.6.0" + +[[package]] +name = "h11" +version = "0.12.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "httpcore" +version = "0.15.0" +description = "A minimal low-level HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0.0,<4.0.0" +certifi = "*" +h11 = ">=0.11,<0.13" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.0" +description = "The next generation HTTP client." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.16.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotlicffi", "brotli"] +cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "identify" +version = "2.5.3" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.3" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.10.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "joblib" +version = "1.1.0" +description = "Lightweight pipelining with Python functions" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nodeenv" +version = "1.7.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" + +[[package]] +name = "numpy" +version = "1.23.1" +description = "NumPy is the fundamental package for array computing with Python." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "packaging" +version = "21.3" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" + +[[package]] +name = "pathspec" +version = "0.9.0" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[[package]] +name = "pillow" +version = "9.2.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-issues (>=3.0.1)", "sphinx-removed-in", "sphinxext-opengraph"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "platformdirs" +version = "2.5.2" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] +test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] + +[[package]] +name = "pluggy" +version = "1.0.0" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "2.20.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pycodestyle" +version = "2.9.1" +description = "Python style guide checker" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pydantic" +version = "1.9.1" +description = "Data validation and settings management using python type hints" +category = "main" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +typing-extensions = ">=3.7.4.3" + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] + +[[package]] +name = "pyflakes" +version = "2.5.0" +description = "passive checker of Python programs" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "pyparsing" +version = "3.0.9" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +category = "dev" +optional = false +python-versions = ">=3.6.8" + +[package.extras] +diagrams = ["railroad-diagrams", "jinja2"] + +[[package]] +name = "pytest" +version = "7.1.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +py = ">=1.8.2" +tomli = ">=1.0.0" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.19.0" +description = "Pytest support for asyncio" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pytest = ">=6.1.0" + +[package.extras] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] + +[[package]] +name = "pytest-cov" +version = "4.0.0" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "pytest-forked" +version = "1.4.0" +description = "run tests in isolated forked subprocesses" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +py = "*" +pytest = ">=3.10" + +[[package]] +name = "pytest-xdist" +version = "2.5.0" +description = "pytest xdist plugin for distributed testing and loop-on-failing modes" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +execnet = ">=1.1" +pytest = ">=6.2.0" +pytest-forked = "*" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "scikit-learn" +version = "1.1.2" +description = "A set of python modules for machine learning and data mining" +category = "main" +optional = false +python-versions = ">=3.8" + +[package.dependencies] +joblib = ">=1.0.0" +numpy = ">=1.17.3" +scipy = ">=1.3.2" +threadpoolctl = ">=2.0.0" + +[package.extras] +tests = ["numpydoc (>=1.2.0)", "pyamg (>=4.0.0)", "mypy (>=0.961)", "black (>=22.3.0)", "flake8 (>=3.8.2)", "pytest-cov (>=2.9.0)", "pytest (>=5.0.1)", "pandas (>=1.0.5)", "scikit-image (>=0.16.2)", "matplotlib (>=3.1.2)"] +examples = ["seaborn (>=0.9.0)", "pandas (>=1.0.5)", "scikit-image (>=0.16.2)", "matplotlib (>=3.1.2)"] +docs = ["sphinxext-opengraph (>=0.4.2)", "sphinx-prompt (>=1.3.0)", "Pillow (>=7.1.2)", "numpydoc (>=1.2.0)", "sphinx-gallery (>=0.7.0)", "sphinx (>=4.0.1)", "memory-profiler (>=0.57.0)", "seaborn (>=0.9.0)", "pandas (>=1.0.5)", "scikit-image (>=0.16.2)", "matplotlib (>=3.1.2)"] +benchmark = ["memory-profiler (>=0.57.0)", "pandas (>=1.0.5)", "matplotlib (>=3.1.2)"] + +[[package]] +name = "scipy" +version = "1.9.0" +description = "SciPy: Scientific Library for Python" +category = "main" +optional = false +python-versions = ">=3.8,<3.12" + +[package.dependencies] +numpy = ">=1.18.5,<1.25.0" + +[[package]] +name = "shapely" +version = "1.8.2" +description = "Geometric objects, predicates, and operations" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +all = ["pytest", "pytest-cov", "numpy"] +test = ["pytest", "pytest-cov"] +vectorized = ["numpy"] + +[[package]] +name = "sniffio" +version = "1.2.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.5" + +[[package]] +name = "threadpoolctl" +version = "3.1.0" +description = "threadpoolctl" +category = "main" +optional = false +python-versions = ">=3.6" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "typing-extensions" +version = "4.3.0" +description = "Backported and Experimental Type Hints for Python 3.7+" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "virtualenv" +version = "20.16.3" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +platformdirs = ">=2.4,<3" + +[package.extras] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] + +[metadata] +lock-version = "1.1" +python-versions = ">=3.8,<3.12" # for scipy +content-hash = "22a50ccf4320b985e2dd4138e2ca946816e0d4dd31c0d3058382072d31bafe6d" + +[metadata.files] +anyio = [ + {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, + {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, +] +attrs = [] +black = [ + {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, + {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, + {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, + {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, + {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, + {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, + {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, + {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, + {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, + {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, + {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, + {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, + {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, + {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, + {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, + {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, + {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, + {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, +] +certifi = [ + {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, + {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, +] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] +click = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] +colorama = [ + {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, + {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, +] +coverage = [] +distlib = [] +execnet = [] +filelock = [ + {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, + {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, +] +flake8 = [] +h11 = [ + {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, + {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, +] +httpcore = [ + {file = "httpcore-0.15.0-py3-none-any.whl", hash = "sha256:1105b8b73c025f23ff7c36468e4432226cbb959176eab66864b8e31c4ee27fa6"}, + {file = "httpcore-0.15.0.tar.gz", hash = "sha256:18b68ab86a3ccf3e7dc0f43598eaddcf472b602aba29f9aa6ab85fe2ada3980b"}, +] +httpx = [ + {file = "httpx-0.23.0-py3-none-any.whl", hash = "sha256:42974f577483e1e932c3cdc3cd2303e883cbfba17fe228b0f63589764d7b9c4b"}, + {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, +] +identify = [] +idna = [ + {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, + {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, + {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, +] +joblib = [] +mccabe = [] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nodeenv = [ + {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, + {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, +] +numpy = [] +packaging = [ + {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, + {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, +] +pathspec = [ + {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, + {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, +] +pillow = [] +platformdirs = [ + {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, + {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, +] +pluggy = [ + {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, + {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, +] +pre-commit = [] +py = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] +pycodestyle = [] +pydantic = [ + {file = "pydantic-1.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c8098a724c2784bf03e8070993f6d46aa2eeca031f8d8a048dff277703e6e193"}, + {file = "pydantic-1.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c320c64dd876e45254bdd350f0179da737463eea41c43bacbee9d8c9d1021f11"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18f3e912f9ad1bdec27fb06b8198a2ccc32f201e24174cec1b3424dda605a310"}, + {file = "pydantic-1.9.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c11951b404e08b01b151222a1cb1a9f0a860a8153ce8334149ab9199cd198131"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8bc541a405423ce0e51c19f637050acdbdf8feca34150e0d17f675e72d119580"}, + {file = "pydantic-1.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e565a785233c2d03724c4dc55464559639b1ba9ecf091288dd47ad9c629433bd"}, + {file = "pydantic-1.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:a4a88dcd6ff8fd47c18b3a3709a89adb39a6373f4482e04c1b765045c7e282fd"}, + {file = "pydantic-1.9.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:447d5521575f18e18240906beadc58551e97ec98142266e521c34968c76c8761"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:985ceb5d0a86fcaa61e45781e567a59baa0da292d5ed2e490d612d0de5796918"}, + {file = "pydantic-1.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059b6c1795170809103a1538255883e1983e5b831faea6558ef873d4955b4a74"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:d12f96b5b64bec3f43c8e82b4aab7599d0157f11c798c9f9c528a72b9e0b339a"}, + {file = "pydantic-1.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:ae72f8098acb368d877b210ebe02ba12585e77bd0db78ac04a1ee9b9f5dd2166"}, + {file = "pydantic-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:79b485767c13788ee314669008d01f9ef3bc05db9ea3298f6a50d3ef596a154b"}, + {file = "pydantic-1.9.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:494f7c8537f0c02b740c229af4cb47c0d39840b829ecdcfc93d91dcbb0779892"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0f047e11febe5c3198ed346b507e1d010330d56ad615a7e0a89fae604065a0e"}, + {file = "pydantic-1.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:969dd06110cb780da01336b281f53e2e7eb3a482831df441fb65dd30403f4608"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:177071dfc0df6248fd22b43036f936cfe2508077a72af0933d0c1fa269b18537"}, + {file = "pydantic-1.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:9bcf8b6e011be08fb729d110f3e22e654a50f8a826b0575c7196616780683380"}, + {file = "pydantic-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a955260d47f03df08acf45689bd163ed9df82c0e0124beb4251b1290fa7ae728"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9ce157d979f742a915b75f792dbd6aa63b8eccaf46a1005ba03aa8a986bde34a"}, + {file = "pydantic-1.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0bf07cab5b279859c253d26a9194a8906e6f4a210063b84b433cf90a569de0c1"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d93d4e95eacd313d2c765ebe40d49ca9dd2ed90e5b37d0d421c597af830c195"}, + {file = "pydantic-1.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1542636a39c4892c4f4fa6270696902acb186a9aaeac6f6cf92ce6ae2e88564b"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a9af62e9b5b9bc67b2a195ebc2c2662fdf498a822d62f902bf27cccb52dbbf49"}, + {file = "pydantic-1.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fe4670cb32ea98ffbf5a1262f14c3e102cccd92b1869df3bb09538158ba90fe6"}, + {file = "pydantic-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:9f659a5ee95c8baa2436d392267988fd0f43eb774e5eb8739252e5a7e9cf07e0"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b83ba3825bc91dfa989d4eed76865e71aea3a6ca1388b59fc801ee04c4d8d0d6"}, + {file = "pydantic-1.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1dd8fecbad028cd89d04a46688d2fcc14423e8a196d5b0a5c65105664901f810"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02eefd7087268b711a3ff4db528e9916ac9aa18616da7bca69c1871d0b7a091f"}, + {file = "pydantic-1.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7eb57ba90929bac0b6cc2af2373893d80ac559adda6933e562dcfb375029acee"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4ce9ae9e91f46c344bec3b03d6ee9612802682c1551aaf627ad24045ce090761"}, + {file = "pydantic-1.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:72ccb318bf0c9ab97fc04c10c37683d9eea952ed526707fabf9ac5ae59b701fd"}, + {file = "pydantic-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:61b6760b08b7c395975d893e0b814a11cf011ebb24f7d869e7118f5a339a82e1"}, + {file = "pydantic-1.9.1-py3-none-any.whl", hash = "sha256:4988c0f13c42bfa9ddd2fe2f569c9d54646ce84adc5de84228cfe83396f3bd58"}, + {file = "pydantic-1.9.1.tar.gz", hash = "sha256:1ed987c3ff29fff7fd8c3ea3a3ea877ad310aae2ef9889a119e22d3f2db0691a"}, +] +pyflakes = [] +pyparsing = [ + {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, + {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, +] +pytest = [] +pytest-asyncio = [] +pytest-cov = [] +pytest-forked = [] +pytest-xdist = [] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +scikit-learn = [] +scipy = [] +shapely = [] +sniffio = [ + {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, + {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, +] +threadpoolctl = [] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tomli = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] +typing-extensions = [] +virtualenv = [] diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/pyproject.toml b/fastapi_genshin_map/GetMapImage/GenshinMap/pyproject.toml new file mode 100644 index 0000000..552012e --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/pyproject.toml @@ -0,0 +1,51 @@ +[tool.poetry] +name = "genshinmap" +version = "0.1.0" +description = "GenshinMap 是一个米游社大地图 API 的包装,用于简易获取大地图数据" +authors = ["MingxuanGame "] +license = "MIT" + +[tool.poetry.dependencies] +python = ">=3.8,<3.12" # for scipy +scipy = "^1.9.0" +Pillow = "^9.2.0" +httpx = "^0.23.0" +pydantic = "^1.9.1" +numpy = "^1.23.1" +scikit-learn = "^1.1.2" +shapely = "^1.8.2" + +[tool.poetry.dev-dependencies] +black = "^22.6.0" +flake8 = "^5.0.4" +isort = "^5.10.1" +pre-commit = "^2.20.0" +pytest = "^7.1.3" +pytest-asyncio = "^0.19.0" +pytest-cov = "^4.0.0" +pytest-xdist = "^2.5.0" +coverage = "^6.5.0" + +[tool.black] +line-length = 79 +target-version = ["py38", "py39", "py310", "py311"] +include = '\.pyi?$' +extend-exclude = ''' +''' + +[tool.isort] +profile = "black" +line_length = 79 +length_sort = true +skip_gitignore = true +force_sort_within_sections = true +extra_standard_library = ["typing_extensions"] + +[tool.pytest.ini_options] +asyncio_mode = "auto" +addopts = "--cov=genshinmap --cov-report=term-missing" + + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/anchors.json b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/anchors.json new file mode 100644 index 0000000..71172ec --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/anchors.json @@ -0,0 +1,71 @@ +{ + "list": [ + { + "id": "178", + "name": "列柱沙原", + "l_x": -6845, + "l_y": 4014, + "r_x": -5689, + "r_y": 5306, + "app_sn": "", + "parent_id": "0", + "map_id": "2", + "children": [ + { + "id": "191", + "name": "秘仪圣殿", + "l_x": -5897, + "l_y": 4570, + "r_x": -5617, + "r_y": 4750, + "app_sn": "", + "parent_id": "178", + "map_id": "2", + "children": [], + "sort": 0 + }, + { + "id": "179", + "name": "赤王陵", + "l_x": -6461, + "l_y": 4482, + "r_x": -6081, + "r_y": 4848, + "app_sn": "", + "parent_id": "178", + "map_id": "2", + "children": [], + "sort": 0 + } + ], + "sort": 0 + }, + { + "id": "184", + "name": "上风蚀地", + "l_x": -5709, + "l_y": 4848, + "r_x": -4901, + "r_y": 5906, + "app_sn": "", + "parent_id": "0", + "map_id": "2", + "children": [ + { + "id": "185", + "name": "荼诃落谷", + "l_x": -5367, + "l_y": 5164, + "r_x": -5053, + "r_y": 5780, + "app_sn": "", + "parent_id": "184", + "map_id": "2", + "children": [], + "sort": 0 + } + ], + "sort": 0 + } + ] +} diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/conftest.py b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/conftest.py new file mode 100644 index 0000000..21c6085 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/conftest.py @@ -0,0 +1,13 @@ +import asyncio + +import pytest + + +@pytest.fixture(scope="session") +def event_loop(): + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + yield loop + loop.close() diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/labels.json b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/labels.json new file mode 100644 index 0000000..e74f2dc --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/labels.json @@ -0,0 +1,96 @@ +{ + "tree": [ + { + "id": 1, + "name": "传送点", + "icon": "", + "parent_id": 0, + "depth": 1, + "node_type": 1, + "jump_type": 0, + "jump_target_id": 0, + "display_priority": 0, + "children": [ + { + "id": 2, + "name": "七天神像", + "icon": "https://uploadstatic.mihoyo.com/ys-obc/2020/09/08/75276545/c59585d1fabc9c22ad3fcf94e1622aa8_357413506633071859.png", + "parent_id": 1, + "depth": 2, + "node_type": 2, + "jump_type": 0, + "jump_target_id": 0, + "display_priority": 0, + "children": [], + "activity_page_label": 27, + "area_page_label": [], + "is_all_area": true + }, + { + "id": 3, + "name": "传送锚点", + "icon": "https://uploadstatic.mihoyo.com/ys-obc/2020/09/08/75276545/0cc42d15134cbb724304050fd0bbcaac_8799482478853097434.png", + "parent_id": 1, + "depth": 2, + "node_type": 2, + "jump_type": 0, + "jump_target_id": 0, + "display_priority": 0, + "children": [], + "activity_page_label": 27, + "area_page_label": [], + "is_all_area": true + } + ], + "activity_page_label": 0, + "area_page_label": [], + "is_all_area": false + }, + { + "id": 426, + "name": "地标", + "icon": "", + "parent_id": 0, + "depth": 1, + "node_type": 1, + "jump_type": 0, + "jump_target_id": 0, + "display_priority": 0, + "children": [ + { + "id": 410, + "name": "洞口", + "icon": "https://uploadstatic.mihoyo.com/ys-obc/2022/08/10/75379475/d06eb3e4e17ad63d822a5065ee6f002e_2513888037572049765.png", + "parent_id": 426, + "depth": 2, + "node_type": 2, + "jump_type": 0, + "jump_target_id": 0, + "display_priority": 0, + "children": [], + "activity_page_label": 27, + "area_page_label": [25], + "is_all_area": false + }, + { + "id": 190, + "name": "浪船锚点", + "icon": "https://uploadstatic.mihoyo.com/ys-obc/2021/06/10/16314655/6db2133bf163989dbff9c6a1e0c814a0_7631310718225262758.png", + "parent_id": 426, + "depth": 2, + "node_type": 2, + "jump_type": 0, + "jump_target_id": 0, + "display_priority": 0, + "children": [], + "activity_page_label": 24, + "area_page_label": [8], + "is_all_area": false + } + ], + "activity_page_label": 0, + "area_page_label": [], + "is_all_area": false + } + ] +} diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/maps.json b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/maps.json new file mode 100644 index 0000000..0dac854 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/maps.json @@ -0,0 +1,13 @@ +{ + "info": { + "id": 2, + "name": "提瓦特大地图", + "parent_id": 1, + "depth": 2, + "detail": "{\"slices\":[[{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/0eec74713b864e6f639c9090484f8870_3432819210174318714.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/a56832ba1fe0fedd3b9a3c634df22d25_8286951540042186828.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/3c6dbcbfb72d21a87c3b886a8310067e_7249020933999162032.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/28971171ff5e668c45189c013bae1fdc_4601440331303570504.png\"}],[{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/b5b9903d30037a728395b4c24e7a3998_4062420627465094766.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/aa19c8ddb19294bc2994d83456bd3b07_8029045233523466417.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/171d53d698db8625bd8270de3a2c04c6_6971423124470409050.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/5787c113e4c746352dd8bcb72418a344_7040357371920568328.png\"}],[{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/47e448278ac5e80a1f3272085f78d3db_767778978113016811.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/2a5a4c3d9f3ba1d2b0781e50ee63fb71_7932745916637713891.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/b563941d22abfe9085dd61c2de711e32_5711770985239944753.png\"},{\"url\":\"https://uploadstatic.mihoyo.com/ys-obc/2022/09/15/75379475/2895d7ef091682cb9c1f75fdde5b891f_7629364203486646958.png\"}]],\"origin\":[8939,2286],\"total_size\":[16384,12288],\"padding\":[1024,512]}", + "node_type": 2, + "children": [], + "icon": "https://uploadstatic.mihoyo.com/ys-obc/2022/03/29/75379475/191a621844575ee5b8e44035834d051f_3328806497146477608.png", + "ch_ext": "" + } + } diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/page.json b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/page.json new file mode 100644 index 0000000..5861e3d --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/page.json @@ -0,0 +1,44 @@ +{ + "list": [ + { + "id": 1, + "name": "蒙德", + "type": 1, + "pc_icon_url": "https://uploadstatic.mihoyo.com/ys-obc/2022/03/29/75379475/fa4770a33c59b94f2ba0d6e3608a9a47_196000061405650070.png", + "mobile_icon_url": "", + "sort": 5, + "pc_icon_url2": "https://uploadstatic.mihoyo.com/ys-obc/2022/03/29/75379475/15f8175ad96ab5f0a38bf14b9e359666_3990014784054466810.png", + "map_id": 2, + "jump_url": "", + "jump_type": "coordinate", + "center": "-111.96,-200.87", + "zoom": "-1.00" + }, + { + "id": 4, + "name": "龙脊雪山", + "type": 1, + "pc_icon_url": "https://uploadstatic.mihoyo.com/ys-obc/2022/03/29/75379475/e60eb5568a5c8122d566bd40decc5371_9191303240951212319.png", + "mobile_icon_url": "", + "sort": 4, + "pc_icon_url2": "https://uploadstatic.mihoyo.com/ys-obc/2022/03/29/75379475/0ba7ceadf1a8a04bd698674371e6813d_6227119471395520312.png", + "map_id": 2, + "jump_url": "", + "jump_type": "coordinate", + "center": "1183.49,-116.41", + "zoom": "0.00" + }, + { + "id": 3, + "name": "璃月", + "type": 1, + "pc_icon_url": "https://uploadstatic.mihoyo.com/ys-obc/2022/03/29/75379475/49985f158ed6060b98538fd3b62f6d0f_5835606737467298557.png", + "mobile_icon_url": "", + "sort": 3, + "pc_icon_url2": "https://uploadstatic.mihoyo.com/ys-obc/2022/03/29/75379475/fb1f9ec4eb36cd09bfb412f40a0af864_9071064856381496516.png", + "map_id": 2, + "jump_url": "", + "jump_type": "coordinate", + "center": "2602.08,-1612.55", + "zoom": "-1.00" + }]} diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/points.json b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/points.json new file mode 100644 index 0000000..b95570b --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/points.json @@ -0,0 +1,31 @@ +{ + "point_list": [ + { + "id": 26831, + "label_id": 298, + "x_pos": 114, + "y_pos": 514, + "author_name": "悦弥", + "ctime": "2022-06-16 10:26:12", + "display_state": 1 + }, + { + "id": 26832, + "label_id": 298, + "x_pos": 1919, + "y_pos": 810, + "author_name": "悦弥", + "ctime": "2022-06-16 10:26:12", + "display_state": 1 + }, + { + "id": 26831, + "label_id": 297, + "x_pos": -1287.5, + "y_pos": 716, + "author_name": "悦弥", + "ctime": "2022-06-16 10:26:12", + "display_state": 1 + } + ] +} diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/kinds.json b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/kinds.json new file mode 100644 index 0000000..2d62bcf --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/kinds.json @@ -0,0 +1,17 @@ +{ + "retcode": 0, + "message": "OK", + "data": { + "list": [ + { + "id": 581061, + "name": "", + "icon_id": 101, + "icon_url": "https://uploadstatic.mihoyo.com/hk4e/upload/officialsites/202011/wiki-ys-map-in-game-5_1605084458_2681.png", + "is_game": 2 + } + ], + "is_sync": false, + "already_share": false + } +} diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/spots.json b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/spots.json new file mode 100644 index 0000000..5dd4c15 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/spots/spots.json @@ -0,0 +1,24 @@ +{ + "retcode": 0, + "message": "OK", + "data": { + "spots": { + "581061": { + "list": [ + { + "id": 2267179, + "name": "", + "content": "", + "kind_id": 581061, + "spot_icon": "", + "x_pos": 416.3067626953125, + "y_pos": 57.92724609375, + "nick_name": "MingxuanGame1", + "avatar_url": "https://img-static.mihoyo.com/avatar/avatar40004.png", + "status": 1 + } + ] + } + } + } +} diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_exc.py b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_exc.py new file mode 100644 index 0000000..759e944 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_exc.py @@ -0,0 +1,17 @@ +import pytest + + +def test_exc() -> None: + from genshinmap.exc import StatusError + + def _raise() -> None: + raise StatusError(1, "error") + + with pytest.raises(StatusError) as exc_info: + _raise() + exc = exc_info.value + assert exc.status == 1 + assert exc.message == "err" + + assert str(exc) == "miHoYo API 1:1 err" + assert repr(exc) == "" diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_kmeans.py b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_kmeans.py new file mode 100644 index 0000000..0d58de3 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_kmeans.py @@ -0,0 +1,21 @@ +def test_k_means() -> None: + from genshinmap.models import XYPoint + from genshinmap.img import k_means_points + + points = [ + # Cluster 1 + XYPoint(9, 9), + XYPoint(10, 10), + XYPoint(11, 11), + XYPoint(12, 12), + XYPoint(13, 13), + # Cluster 2 + XYPoint(100, 100), + XYPoint(101, 101), + XYPoint(102, 102), + ] + clusters = k_means_points(points, 15, 2) + top_left, bottom_right, cluster_points = clusters[0] + assert top_left == XYPoint(9, 9) + assert bottom_right == XYPoint(13, 13) + assert len(cluster_points) == 5 diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_models.py b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_models.py new file mode 100644 index 0000000..4e3d259 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_models.py @@ -0,0 +1,17 @@ +import json +from pathlib import Path + + +def test_anchor_points() -> None: + from genshinmap.models import Anchor, XYPoint + + with open(Path(__file__).parent / "anchors.json", encoding="utf-8") as f: + anchor = Anchor.parse_obj(json.load(f)["list"][0]) + assert anchor.get_children_all_left_point() == [ + XYPoint(-5897, 4570), + XYPoint(-6461, 4482), + ] + assert anchor.get_children_all_right_point() == [ + XYPoint(-5617, 4750), + XYPoint(-6081, 4848), + ] diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_request.py b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_request.py new file mode 100644 index 0000000..5376079 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_request.py @@ -0,0 +1,245 @@ +import json +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, Optional + +import pytest + +DIR = Path(__file__).parent + +if TYPE_CHECKING: + from httpx import Response + + +@pytest.mark.asyncio +async def test_labels(monkeypatch: pytest.MonkeyPatch) -> None: + with open(DIR / "labels.json", encoding="utf-8") as f: + data = json.load(f) + + async def _fake_request(endpoint: str) -> Dict[str, Any]: + return data + + monkeypatch.setattr("genshinmap.request._request", _fake_request) + + from genshinmap.models import Tree + from genshinmap.request import MapID, get_labels + + assert await get_labels(MapID.teyvat) == [ + Tree.parse_obj(i) for i in data["tree"] + ] + + +@pytest.mark.asyncio +async def test_points(monkeypatch: pytest.MonkeyPatch) -> None: + with open(DIR / "points.json", encoding="utf-8") as f: + data = json.load(f) + + async def _fake_request(endpoint: str) -> Dict[str, Any]: + return data + + monkeypatch.setattr("genshinmap.request._request", _fake_request) + + from genshinmap.models import Point + from genshinmap.request import MapID, get_points + + assert await get_points(MapID.teyvat) == [ + Point.parse_obj(i) for i in data["point_list"] + ] + + +@pytest.mark.asyncio +async def test_maps(monkeypatch: pytest.MonkeyPatch) -> None: + with open(DIR / "maps.json", encoding="utf-8") as f: + data = json.load(f) + + async def _fake_request(endpoint: str) -> Dict[str, Any]: + return data + + monkeypatch.setattr("genshinmap.request._request", _fake_request) + + from genshinmap.models import MapInfo + from genshinmap.request import MapID, get_maps + + assert await get_maps(MapID.teyvat) == MapInfo.parse_obj(data["info"]) + + +@pytest.mark.asyncio +async def test_page_label(monkeypatch: pytest.MonkeyPatch) -> None: + with open(DIR / "page.json", encoding="utf-8") as f: + data = json.load(f) + + async def _fake_request(endpoint: str) -> Dict[str, Any]: + return data + + monkeypatch.setattr("genshinmap.request._request", _fake_request) + + from genshinmap.models import PageLabel + from genshinmap.request import MapID, get_page_label + + assert await get_page_label(MapID.teyvat) == [ + PageLabel.parse_obj(i) for i in data["list"] + ] + + +@pytest.mark.asyncio +async def test_anchor(monkeypatch: pytest.MonkeyPatch) -> None: + with open(DIR / "anchors.json", encoding="utf-8") as f: + data = json.load(f) + + async def _fake_request(endpoint: str) -> Dict[str, Any]: + return data + + monkeypatch.setattr("genshinmap.request._request", _fake_request) + + from genshinmap.models import Anchor + from genshinmap.request import MapID, get_anchors + + assert await get_anchors(MapID.teyvat) == [ + Anchor.parse_obj(i) for i in data["list"] + ] + + +@pytest.mark.asyncio +async def test_get_spot_from_game(monkeypatch: pytest.MonkeyPatch) -> None: + with open(DIR / "spots" / "spots.json", encoding="utf-8") as f: + spots = f.read() + with open(DIR / "spots" / "kinds.json", encoding="utf-8") as f: + kinds = f.read() + + async def _post( + self, + url: str, + json: Dict[str, Any], + headers: Optional[Dict[str, Any]] = None, + ) -> "Response": + from httpx import Request, Response + + if url == "/spot_kind/sync_game_spot": + # 1. 申请刷新 + return Response( + 200, + text='{"retcode":0,"message":"OK","data":{}}', + request=Request("POST", url), + ) + else: + # 3.获取坐标 + return Response(200, text=spots, request=Request("POST", url)) + + async def _get(self, url: str, headers: Dict[str, Any]) -> "Response": + from httpx import Request, Response + + return Response(200, text=kinds, request=Request("GET", url)) + + monkeypatch.setattr("httpx._client.AsyncClient.post", _post) + monkeypatch.setattr("httpx._client.AsyncClient.get", _get) + + from genshinmap.models import SpotKinds + from genshinmap.request import MapID, get_spot_from_game + + spot, kind = await get_spot_from_game(MapID.teyvat, "") + assert spot == { + 581061: [ + { + "id": 2267179, + "name": "", + "content": "", + "kind_id": 581061, + "spot_icon": "", + "x_pos": 416.3067626953125, + "y_pos": 57.92724609375, + "nick_name": "MingxuanGame1", + "avatar_url": ( + "https://img-static.mihoyo.com/avatar/avatar40004.png" + ), + "status": 1, + } + ] + } + assert kind == SpotKinds.parse_obj(json.loads(kinds)["data"]) + + +@pytest.mark.asyncio +async def test_spot_status_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _post( + self, + url: str, + json: Dict[str, Any], + headers: Optional[Dict[str, Any]] = None, + ) -> "Response": + from httpx import Request, Response + + return Response( + 200, + text='{"data":null,"message":"10分钟内只能操作一次","retcode":-2000}', + request=Request("POST", url), + ) + + monkeypatch.setattr("httpx._client.AsyncClient.post", _post) + from genshinmap.exc import StatusError + from genshinmap.request import MapID, get_spot_from_game + + with pytest.raises(StatusError) as exc_info: + await get_spot_from_game(MapID.teyvat, "") + exc = exc_info.value + assert exc.status == -2000 + assert exc.message == "10分钟内只能操作一次" + + +@pytest.mark.asyncio +async def test_internal_request_status_error( + monkeypatch: pytest.MonkeyPatch, +) -> None: + async def _get(self, url: str) -> "Response": + from httpx import Request, Response + + return Response( + 200, + text='{"retcode":0,"message":"OK","data":{"test": 1}}', + request=Request("GET", url), + ) + + monkeypatch.setattr("httpx._client.AsyncClient.get", _get) + from genshinmap.request import _request + + assert await _request("") == {"test": 1} + + +@pytest.mark.asyncio +async def test_internal_request(monkeypatch: pytest.MonkeyPatch) -> None: + async def _get(self, url: str) -> "Response": + from httpx import Request, Response + + return Response( + 200, + text='{"retcode":1,"message":"err","data":null}', + request=Request("GET", url), + ) + + monkeypatch.setattr("httpx._client.AsyncClient.get", _get) + from genshinmap.exc import StatusError + from genshinmap.request import _request + + with pytest.raises(StatusError) as exc_info: + await _request("") + exc = exc_info.value + assert exc.status == 1 + assert exc.message == "err" + + +@pytest.mark.asyncio +@pytest.mark.parametrize(argnames="map_id", argvalues=[2, 7, 9]) +async def test_connection(map_id) -> None: + from genshinmap.request import ( + get_maps, + get_labels, + get_points, + get_anchors, + get_page_label, + ) + + assert await get_labels(map_id) + assert await get_anchors(map_id) + assert await get_page_label(map_id) + assert await get_maps(map_id) + assert await get_points(map_id) diff --git a/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_utils.py b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_utils.py new file mode 100644 index 0000000..a2c3c22 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/GenshinMap/tests/test_utils.py @@ -0,0 +1,94 @@ +import json +from pathlib import Path + +DIR = Path(__file__).parent + + +def test_get_points_by_id() -> None: + from genshinmap.models import Point, XYPoint + from genshinmap.utils import get_points_by_id + + with open(DIR / "points.json") as f: + points = [Point.parse_obj(i) for i in json.load(f)["point_list"]] + assert get_points_by_id(298, points) == [ + XYPoint(114, 514), + XYPoint(1919, 810), + ] + + +def test_convert_pos() -> None: + from genshinmap.models import XYPoint + from genshinmap.utils import convert_pos + + points = [XYPoint(1200, 5000), XYPoint(-4200, 1800)] + origin = [4844, 4335] + assert convert_pos(points, origin) == [ + XYPoint(x=6044, y=9335), + XYPoint(x=644, y=6135), + ] + + +def test_convert_pos_crop() -> None: + from genshinmap.models import XYPoint + from genshinmap.utils import convert_pos_crop + + points = [XYPoint(0, 0), XYPoint(20, 20)] + assert convert_pos_crop(0, points) == points + assert convert_pos_crop(1, points) == [ + XYPoint(-4096, 0), + XYPoint(-4076, 20), + ] + assert convert_pos_crop(4, points) == [ + XYPoint(0, -4096), + XYPoint(20, -4076), + ] + assert convert_pos_crop(5, points) == [ + XYPoint(-4096, -4096), + XYPoint(-4076, -4076), + ] + + +def test_internal_generate_matrix() -> None: + from genshinmap.utils import _generate_matrix + + assert _generate_matrix(0, 3, 8) == list(range(12)) + assert _generate_matrix(0, 1, 4) == [0, 1, 4, 5] + assert _generate_matrix(0, 2, 4) == [0, 1, 2, 4, 5, 6] + assert _generate_matrix(0, 0, 4) == [0, 4] + assert _generate_matrix(0, 0, 8) == [0, 4, 8] + assert _generate_matrix(0, 2, 0) == [0, 1, 2] + assert _generate_matrix(0, 2, 8) == [0, 1, 2, 4, 5, 6, 8, 9, 10] + assert _generate_matrix(1, 3, 5) == [1, 2, 3, 5, 6, 7] + + +def test_internal_pos_to_index() -> None: + from genshinmap.utils import _pos_to_index + + assert _pos_to_index(0, 0) == 0 + assert _pos_to_index(4096, 0) == 1 + assert _pos_to_index(0, 4096) == 4 + assert _pos_to_index(4096, 4096) == 5 + + +def test_crop_image_and_points() -> None: + from genshinmap.models import XYPoint + from genshinmap.utils import crop_image_and_points + + assert crop_image_and_points( + [XYPoint(x=4200, y=8000), XYPoint(x=4150, y=10240)] + ) == ([5, 9], 0, [XYPoint(x=104, y=3904), XYPoint(x=54, y=6144)]) + points = [ + XYPoint(x=0, y=0), + XYPoint(x=20, y=20), + XYPoint(x=4096, y=0), + XYPoint(x=4116, y=20), + XYPoint(x=0, y=4096), + XYPoint(x=20, y=4116), + XYPoint(x=4096, y=4096), + XYPoint(x=4116, y=4116), + ] + assert crop_image_and_points(points) == ( + [0, 1, 4, 5], + 1, + points, + ) diff --git a/fastapi_genshin_map/GetMapImage/__init__.py b/fastapi_genshin_map/GetMapImage/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/fastapi_genshin_map/GetMapImage/get_map_image.py b/fastapi_genshin_map/GetMapImage/get_map_image.py new file mode 100644 index 0000000..73a295f --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/get_map_image.py @@ -0,0 +1,188 @@ +import random +from io import BytesIO +from pathlib import Path +from time import time + +from fastapi import APIRouter, HTTPException, Query +from fastapi.responses import FileResponse, StreamingResponse +from PIL import Image + +from .GenshinMap.genshinmap import img, models, request, utils +from .logger import logger + +Image.MAX_IMAGE_PIXELS = 333120000 +router = APIRouter(prefix='/get_map') +TEXT_PATH = Path(__file__).parent / 'texture2d' +mark_quest = Image.open(TEXT_PATH / 'mark_quest.png').resize((32, 32)) +MAP = Path(__file__).parent / 'map_data' +RESOURCE_PATH = Path(__file__).parent / 'resource_data' +CHASM_PATH = MAP / 'chasm.png' +ENKANOMIYA_PATH = MAP / 'enkanomiya.png' +TEYVAT_PATH = MAP / 'teyvat.png' + + +MAP_ID_DICT = { + '2': models.MapID.teyvat, # 提瓦特 + '9': models.MapID.chasm, # 层岩巨渊 + '7': models.MapID.enkanomiya, # 渊下宫 + # MapID.golden_apple_archipelago, # 金苹果群岛 +} + + +@router.on_event('startup') +async def create_genshin_map(): + if CHASM_PATH.exists() and ENKANOMIYA_PATH.exists() and TEYVAT_PATH.exists(): + logger.info('****************** 开始地图API服务 *****************') + return + logger.info('****************** 地图API服务进行初始化 *****************') + mark_god_pic = Image.open(TEXT_PATH / 'mark_god.png') + mark_trans_pic = Image.open(TEXT_PATH / 'mark_trans.png') + for map_id in models.MapID: + maps = await request.get_maps(map_id) + points = await request.get_points(map_id) + # 获取七天神像锚点 + mark_god = utils.get_points_by_id(2, points) + # 获取传送锚点 + mark_trans = utils.get_points_by_id(3, points) + # 转换两个锚点为标准坐标 + mark_god_converted = utils.convert_pos(mark_god, maps.detail.origin) + mark_trans_converted = utils.convert_pos(mark_trans, maps.detail.origin) + maps = await request.get_maps(map_id) + map_img = await utils.make_map(maps.detail) + for mark_god_point in mark_god_converted: + map_img.paste( + mark_god_pic, + (int(mark_god_point.x) - 32, int(mark_god_point.y) - 64), + mark_god_pic, + ) + for mark_trans_point in mark_trans_converted: + map_img.paste( + mark_trans_pic, + (int(mark_trans_point.x) - 32, int(mark_trans_point.y) - 64), + mark_trans_pic, + ) + if not MAP.exists(): + MAP.mkdir() + map_img.save(MAP / f'{map_id.name}.png') + logger.info('****************** 开始地图API服务 *****************') + + +@router.get('') +async def get_map_by_point(resource_name=Query(None), map_id=Query(str)): + req_id = random.randint(10000, 99999) + prefix = f'>> [请求序列:{req_id}]' + logger.info(f'{prefix} 收到资源点访问请求! [资源名称] {resource_name} [地图ID] {map_id}') + ERROR = { + 'retcode': -1, + 'message': f'该资源点 - {resource_name} 不存在!', + } + # 校验map_id有效性 + if map_id not in MAP_ID_DICT: + logger.warning(f'{prefix} 请求失败! 原因: 该地图ID [{map_id}] 不存在!') + return { + 'retcode': -1, + 'message': f'该地图ID - {map_id} 不存在!', + } + + # 寻找主地图的缓存 + map_data = MAP_ID_DICT[map_id] + map_path = MAP / f'{map_data.name}.png' + + # 寻找保存点 + if not RESOURCE_PATH.exists(): + RESOURCE_PATH.mkdir() + save_path = RESOURCE_PATH / f'{map_data.name}_{resource_name}.jpg' + + # 如果存在缓存,直接回复 + if save_path.exists(): + logger.info(f'{prefix} [成功] [资源名称] {resource_name} 已有缓存, 直接发送!') + return FileResponse(save_path) + + logger.info(f'{prefix} [资源名称] {resource_name} 暂无缓存, 开始执行绘制...') + maps = await request.get_maps(map_id) + labels = await request.get_labels(map_id) + + # 请求资源ID + resource_id = 0 + for label in labels: + for child in label.children: + if resource_name == child.name: + resource_id = child.id + resource_name = child.name + break + + if resource_id == 0: + logger.warning(f'{prefix} 请求失败! 原因: 该资源点 [{resource_name}] 不存在!') + return ERROR + + # 请求坐标点 + points = await request.get_points(map_id) + transmittable = utils.get_points_by_id(resource_id, points) + + # 转换坐标 + transmittable_converted = utils.convert_pos(transmittable, maps.detail.origin) + + # 进行最密点获取,暂时废弃 + # if len(transmittable_converted) >= 3: + # group_point = img.k_means_points(transmittable_converted) + + # 如果资源点不存在,返回错误 + if len(transmittable_converted) == 0: + return ERROR + else: + # 计算极限坐标 + up = 20000 + down = 0 + left = 20000 + right = 0 + for point in transmittable_converted: + if point.x >= right: + right = point.x + if point.x <= left: + left = point.x + if point.y >= down: + down = point.y + if point.y <= up: + up = point.y + offset = 100 + group_point = [ + [ + models.XYPoint(left - offset, up - offset), + models.XYPoint(right + offset, down + offset), + transmittable_converted, + ] + ] + + # 打开主地图 + genshin_map = Image.open(map_path) + + # 计算裁切点 + lt_point = group_point[0][0] + rb_point = group_point[0][1] + + # 开始裁切 + genshin_map = genshin_map.crop( + (int(lt_point.x), int(lt_point.y), int(rb_point.x), int(rb_point.y)) + ) + + # 在地图上绘制资源点 + for point in group_point[0][2]: + point_trans = ( + int(point.x) - int(lt_point.x), + int(point.y) - int(lt_point.y), + ) + genshin_map.paste( + mark_quest, (point_trans[0] - 16, point_trans[1] - 16), mark_quest + ) + + # 转换RGB图 + genshin_map = genshin_map.convert('RGB') + + # 转为Bytes,暂时废弃 + # result_buffer = BytesIO() + # genshin_map.save(result_buffer, format='PNG', quality=80, subsampling=0) + + # 进行保存 + genshin_map.save(save_path, 'JPEG', quality=85) + logger.info(f'{prefix} [成功] [资源名称] {resource_name} 绘制完成!') + return FileResponse(save_path) diff --git a/fastapi_genshin_map/GetMapImage/logger.py b/fastapi_genshin_map/GetMapImage/logger.py new file mode 100644 index 0000000..d355861 --- /dev/null +++ b/fastapi_genshin_map/GetMapImage/logger.py @@ -0,0 +1,24 @@ +import logging + +import fastapi +import rollbar +from rollbar.contrib.fastapi import LoggerMiddleware +from rollbar.logger import RollbarHandler + +# Initialize Rollbar SDK with your server-side access token +rollbar.init( + 'ACCESS_TOKEN', + environment='staging', + handler='async', +) + +# Set root logger to log DEBUG and above +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +# Report ERROR and above to Rollbar +rollbar_handler = RollbarHandler() +rollbar_handler.setLevel(logging.ERROR) + +# Attach Rollbar handler to the root logger +logger.addHandler(rollbar_handler) diff --git a/fastapi_genshin_map/GetMapImage/texture2d/mark_god.png b/fastapi_genshin_map/GetMapImage/texture2d/mark_god.png new file mode 100644 index 0000000000000000000000000000000000000000..0ef365efc37306bbbdf08e998b832781c2e95e1b GIT binary patch literal 6193 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 P00000NkvXXu0mjf;1Q(Q literal 0 HcmV?d00001 diff --git a/fastapi_genshin_map/GetMapImage/texture2d/mark_quest.png b/fastapi_genshin_map/GetMapImage/texture2d/mark_quest.png new file mode 100644 index 0000000000000000000000000000000000000000..621cb00c46a268e94138187b5cbc539746eac5b1 GIT binary patch literal 3695 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 bool: + return record.getMessage().find('/') == -1 + + +# Filter out /endpoint +logging.getLogger("uvicorn.access").addFilter(EndpointFilter()) + +if __name__ == '__main__': + import uvicorn + + uvicorn.run(app=gsm, host='0.0.0.0', port=5000)